Skip to content

Commit d2c8849

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(files): address PDF renderer edge cases
1 parent 0a6aa50 commit d2c8849

6 files changed

Lines changed: 50 additions & 33 deletions

File tree

apps/sim/app/api/files/export/[id]/markdown-pdf.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,17 @@ ${repeatedParagraphs}`
120120
expect(tablePages.every((page) => page.includes('Name') && page.includes('Value'))).toBe(true)
121121
})
122122

123+
it('renders a table that contains only a header', async () => {
124+
const buffer = await renderMarkdownPdf({
125+
markdown: '| Name | Value |\n| --- | --- |',
126+
title: 'Header-only table',
127+
})
128+
129+
const text = (await pdfPagesText(buffer)).join(' ')
130+
expect(text).toContain('Name')
131+
expect(text).toContain('Value')
132+
})
133+
123134
it('falls back instead of decoding an image above the pixel ceiling', async () => {
124135
const oversizedSvg = Buffer.from(
125136
'<svg xmlns="http://www.w3.org/2000/svg" width="20000" height="20000"><rect width="100%" height="100%" fill="red"/></svg>'

apps/sim/app/api/files/export/[id]/markdown-pdf.tsx

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -267,19 +267,31 @@ function fontRuns(value: string): FontRun[] {
267267
}
268268
})
269269

270-
for (const [index, character] of characters.entries()) {
271-
if (character.family !== 'Geist' || !character.neutral) continue
272-
273-
let previous = index - 1
274-
while (previous >= 0 && characters[previous].neutral) previous -= 1
275-
let next = index + 1
276-
while (next < characters.length && characters[next].neutral) next += 1
277-
const previousFamily = characters[previous]?.family
278-
const nextFamily = characters[next]?.family
279-
if (previousFamily && previousFamily !== 'Geist' && previousFamily === nextFamily) {
280-
character.family = previousFamily
281-
} else if (previousFamily && previousFamily !== 'Geist' && next >= characters.length) {
282-
character.family = previousFamily
270+
let index = 0
271+
while (index < characters.length) {
272+
if (!characters[index].neutral) {
273+
index += 1
274+
continue
275+
}
276+
277+
const start = index
278+
while (index < characters.length && characters[index].neutral) index += 1
279+
280+
const previousFamily = characters[start - 1]?.family
281+
const nextFamily = characters[index]?.family
282+
const inheritedFamily =
283+
previousFamily &&
284+
previousFamily !== 'Geist' &&
285+
(previousFamily === nextFamily || index === characters.length)
286+
? previousFamily
287+
: undefined
288+
289+
if (inheritedFamily) {
290+
for (let neutralIndex = start; neutralIndex < index; neutralIndex += 1) {
291+
if (characters[neutralIndex].family === 'Geist') {
292+
characters[neutralIndex].family = inheritedFamily
293+
}
294+
}
283295
}
284296
}
285297

@@ -489,6 +501,10 @@ function estimateTableRowHeight(row: JSONContent, columnCount: number): number {
489501
function chunkTableRows(header: JSONContent, rows: JSONContent[]): TableChunk[] {
490502
const columnCount = header.content?.length ?? rows[0]?.content?.length ?? 1
491503
const headerHeight = estimateTableRowHeight(header, columnCount)
504+
if (rows.length === 0) {
505+
return [{ rows: [], unbreakable: headerHeight <= MAX_UNBREAKABLE_TABLE_HEIGHT }]
506+
}
507+
492508
const chunks: TableChunk[] = []
493509
let current: JSONContent[] = []
494510
let currentHeight = headerHeight

apps/sim/app/api/files/export/[id]/route.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
type ResolvedEmbeddedFileRef,
2626
replaceEmbeddedFileRefs,
2727
} from '@/lib/uploads/utils/embedded-image-ref'
28-
import { formatFileSize } from '@/lib/uploads/utils/file-utils'
28+
import { formatFileSize, isMarkdownFile } from '@/lib/uploads/utils/file-utils'
2929
import { verifyFileAccess } from '@/app/api/files/authorization'
3030
import { encodeFilenameForHeader } from '@/app/api/files/utils'
3131

@@ -54,15 +54,6 @@ const PDF_EXPORT_RATE_LIMIT: TokenBucketConfig = {
5454
refillIntervalMs: 60_000,
5555
}
5656

57-
const MARKDOWN_MIME_TYPES = new Set(['text/markdown', 'text/x-markdown'])
58-
const MARKDOWN_EXTENSIONS = new Set(['md', 'markdown'])
59-
60-
function isMarkdown(originalName: string, contentType: string): boolean {
61-
if (MARKDOWN_MIME_TYPES.has(contentType)) return true
62-
const ext = originalName.split('.').pop()?.toLowerCase() ?? ''
63-
return MARKDOWN_EXTENSIONS.has(ext)
64-
}
65-
6657
function safeFilename(name: string): string {
6758
return path
6859
.basename(name)
@@ -141,7 +132,7 @@ export const GET = withRouteHandler(
141132
)
142133
}
143134

144-
if (!isMarkdown(record.originalName, record.contentType)) {
135+
if (!isMarkdownFile({ name: record.originalName, type: record.contentType })) {
145136
if (format === 'pdf') {
146137
return NextResponse.json(
147138
{ error: 'PDF export is only available for Markdown files.' },

apps/sim/lib/uploads/client/download.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { requestRaw } from '@/lib/api/client/request'
22
import { fileExportContract } from '@/lib/api/contracts/storage-transfer'
33
import { downloadWorkspaceFileItemsContract } from '@/lib/api/contracts/workspace-file-folders'
44
import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
5+
import { isMarkdownFile } from '@/lib/uploads/utils/file-utils'
56

67
export function saveBlob(blob: Blob, fileName: string): void {
78
const objectUrl = URL.createObjectURL(blob)
@@ -35,10 +36,7 @@ export async function triggerFileDownload(
3536
record: WorkspaceFileRecord,
3637
options?: { format?: 'pdf' }
3738
): Promise<void> {
38-
const isMarkdown =
39-
record.type === 'text/markdown' ||
40-
record.type === 'text/x-markdown' ||
41-
/\.(?:md|markdown)$/i.test(record.name)
39+
const isMarkdown = isMarkdownFile(record)
4240

4341
if (options?.format === 'pdf' && !isMarkdown) {
4442
throw new Error('PDF export is only available for Markdown files')

apps/sim/lib/uploads/utils/file-utils.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@ describe('isMarkdownFile', () => {
2727
expect(isMarkdownFile({ name: 'doc.markdown' })).toBe(true)
2828
})
2929

30-
it('is true for a text/markdown MIME even without a .md name', () => {
30+
it('is true for Markdown MIME types even without a .md name', () => {
3131
expect(isMarkdownFile({ type: 'text/markdown', name: 'notes' })).toBe(true)
3232
expect(isMarkdownFile({ type: 'text/markdown', name: 'doc.txt' })).toBe(true)
33+
expect(isMarkdownFile({ type: 'text/x-markdown', name: 'legacy' })).toBe(true)
3334
})
3435

3536
it('is false for non-markdown files', () => {

apps/sim/lib/uploads/utils/file-utils.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,12 +213,12 @@ export function getFileExtension(filename: string): string {
213213
/**
214214
* Whether a file renders in the collaborative rich markdown editor. Server-safe counterpart to the
215215
* client's `isMarkdownFile` (which uses `resolvePreviewType`): the editor treats a file as markdown by
216-
* its `text/markdown` MIME *or* a `.md`/`.markdown` extension — MIME first, matching the client — so a
217-
* `text/markdown` file with a non-`.md` name still counts. Used to gate server work (e.g. the live-doc
218-
* merge) to exactly the files that can be open in that editor.
216+
* its Markdown MIME *or* a `.md`/`.markdown` extension — MIME first, matching the client — so a
217+
* Markdown file with a non-`.md` name still counts. Used to gate server work (e.g. the live-doc merge)
218+
* to exactly the files that can be open in that editor.
219219
*/
220220
export function isMarkdownFile(file: { type?: string | null; name: string }): boolean {
221-
if (file.type === 'text/markdown') return true
221+
if (file.type === 'text/markdown' || file.type === 'text/x-markdown') return true
222222
const ext = getFileExtension(file.name)
223223
return ext === 'md' || ext === 'markdown'
224224
}

0 commit comments

Comments
 (0)