@bendyline/squisq-formats 1.3.1 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +180 -43
  2. package/dist/{chunk-33YRFXZZ.js → chunk-46FMDOWZ.js} +260 -14
  3. package/dist/chunk-46FMDOWZ.js.map +1 -0
  4. package/dist/{chunk-ERZ627GR.js → chunk-5LSSWZPU.js} +12 -27
  5. package/dist/chunk-5LSSWZPU.js.map +1 -0
  6. package/dist/chunk-6M7Z25LA.js +46 -0
  7. package/dist/chunk-6M7Z25LA.js.map +1 -0
  8. package/dist/{chunk-S3Y7H2BK.js → chunk-D7IFPWNZ.js} +95 -50
  9. package/dist/chunk-D7IFPWNZ.js.map +1 -0
  10. package/dist/chunk-DQAZR57U.js +33 -0
  11. package/dist/chunk-DQAZR57U.js.map +1 -0
  12. package/dist/chunk-EHLGMSTE.js +273 -0
  13. package/dist/chunk-EHLGMSTE.js.map +1 -0
  14. package/dist/{chunk-A3FHLTY5.js → chunk-MJGRI6XR.js} +6 -19
  15. package/dist/chunk-MJGRI6XR.js.map +1 -0
  16. package/dist/chunk-PN52A5AA.js +32 -0
  17. package/dist/chunk-PN52A5AA.js.map +1 -0
  18. package/dist/chunk-RFAPOKHJ.js +123 -0
  19. package/dist/chunk-RFAPOKHJ.js.map +1 -0
  20. package/dist/chunk-RTS5XBZ7.js +526 -0
  21. package/dist/chunk-RTS5XBZ7.js.map +1 -0
  22. package/dist/{chunk-7DEUGIOO.js → chunk-SHLPSADL.js} +120 -2
  23. package/dist/chunk-SHLPSADL.js.map +1 -0
  24. package/dist/chunk-SSUPBUF5.js +15 -0
  25. package/dist/chunk-SSUPBUF5.js.map +1 -0
  26. package/dist/{chunk-UDS45KUJ.js → chunk-WWBIKRNP.js} +40 -80
  27. package/dist/chunk-WWBIKRNP.js.map +1 -0
  28. package/dist/{chunk-VN2KEOYB.js → chunk-YESS7VY3.js} +379 -87
  29. package/dist/chunk-YESS7VY3.js.map +1 -0
  30. package/dist/csv/index.d.ts +50 -0
  31. package/dist/csv/index.js +13 -0
  32. package/dist/csv/index.js.map +1 -0
  33. package/dist/docx/index.js +6 -3
  34. package/dist/epub/index.js +4 -2
  35. package/dist/html/index.d.ts +42 -2
  36. package/dist/html/index.js +9 -2
  37. package/dist/index.d.ts +3 -1
  38. package/dist/index.js +47 -14
  39. package/dist/ooxml/index.d.ts +5 -1
  40. package/dist/ooxml/index.js +13 -9
  41. package/dist/pdf/index.js +2 -1
  42. package/dist/pptx/index.d.ts +36 -19
  43. package/dist/pptx/index.js +9 -2
  44. package/dist/registry/index.d.ts +167 -0
  45. package/dist/registry/index.js +17 -0
  46. package/dist/registry/index.js.map +1 -0
  47. package/dist/xlsx/index.d.ts +60 -29
  48. package/dist/xlsx/index.js +4 -1
  49. package/package.json +12 -2
  50. package/src/__tests__/convert.test.ts +186 -0
  51. package/src/__tests__/csvImport.test.ts +84 -0
  52. package/src/__tests__/exportThemeReconciliation.test.ts +87 -0
  53. package/src/__tests__/formatRegistry.test.ts +174 -0
  54. package/src/__tests__/htmlImport.test.ts +57 -0
  55. package/src/__tests__/lossyWarnings.test.ts +105 -0
  56. package/src/__tests__/pdfImport.test.ts +95 -1
  57. package/src/__tests__/plainHtml.test.ts +31 -0
  58. package/src/__tests__/pptxExport.test.ts +138 -0
  59. package/src/__tests__/pptxImport.test.ts +115 -0
  60. package/src/__tests__/roundTripAssets.test.ts +50 -0
  61. package/src/__tests__/roundTripMatrix.fixtures.ts +86 -0
  62. package/src/__tests__/roundTripMatrix.helpers.ts +154 -0
  63. package/src/__tests__/roundTripMatrix.test.ts +142 -0
  64. package/src/__tests__/sharedContainer.test.ts +41 -0
  65. package/src/__tests__/sharedImages.test.ts +61 -0
  66. package/src/__tests__/xlsxExport.test.ts +164 -0
  67. package/src/__tests__/xlsxImport.test.ts +80 -0
  68. package/src/csv/index.ts +188 -0
  69. package/src/docx/export.ts +27 -44
  70. package/src/docx/import.ts +7 -39
  71. package/src/epub/export.ts +10 -27
  72. package/src/html/imageUtils.ts +5 -19
  73. package/src/html/import.ts +297 -0
  74. package/src/html/index.ts +4 -0
  75. package/src/html/plainHtml.ts +66 -16
  76. package/src/index.ts +36 -5
  77. package/src/ooxml/index.ts +3 -0
  78. package/src/ooxml/namespaces.ts +13 -0
  79. package/src/pdf/export.ts +10 -5
  80. package/src/pdf/import.ts +146 -51
  81. package/src/pptx/export.ts +225 -85
  82. package/src/pptx/import.ts +273 -0
  83. package/src/pptx/index.ts +10 -28
  84. package/src/registry/convert.ts +318 -0
  85. package/src/registry/defaultFormats.ts +323 -0
  86. package/src/registry/errors.ts +46 -0
  87. package/src/registry/index.ts +38 -0
  88. package/src/registry/registry.ts +48 -0
  89. package/src/registry/types.ts +107 -0
  90. package/src/shared/container.ts +28 -0
  91. package/src/shared/images.ts +44 -0
  92. package/src/shared/inlineRuns.ts +99 -0
  93. package/src/shared/text.ts +41 -0
  94. package/src/xlsx/export.ts +253 -0
  95. package/src/xlsx/import.ts +159 -0
  96. package/src/xlsx/index.ts +18 -61
  97. package/dist/chunk-33YRFXZZ.js.map +0 -1
  98. package/dist/chunk-67KIJHV2.js +0 -21
  99. package/dist/chunk-67KIJHV2.js.map +0 -1
  100. package/dist/chunk-7DEUGIOO.js.map +0 -1
  101. package/dist/chunk-A3FHLTY5.js.map +0 -1
  102. package/dist/chunk-ERZ627GR.js.map +0 -1
  103. package/dist/chunk-S3Y7H2BK.js.map +0 -1
  104. package/dist/chunk-UDS45KUJ.js.map +0 -1
  105. package/dist/chunk-VN2KEOYB.js.map +0 -1
  106. package/dist/chunk-YN5HFCEW.js +0 -120
  107. package/dist/chunk-YN5HFCEW.js.map +0 -1
package/src/index.ts CHANGED
@@ -7,11 +7,15 @@
7
7
  * Supported formats:
8
8
  * - **DOCX** — Microsoft Word (import + export) ✅
9
9
  * - **PDF** — Portable Document Format (import + export) ✅
10
- * - **PPTX** — Microsoft PowerPoint (export ✅, import planned)
11
- * - **XLSX** — Microsoft Excel (planned)
10
+ * - **PPTX** — Microsoft PowerPoint (import + export ✅; import extracts slide-level embedded images)
11
+ * - **XLSX** — Microsoft Excel (import ✅, export ✅ tables-only)
12
+ * - **CSV** — Comma-separated values (import ✅, export ✅)
13
+ * - **HTML** — import + export (single-file, ZIP, plain, and player-embedding)
14
+ * - **EPUB** — export ✅
12
15
  *
13
16
  * All converters run in the browser — no server or native binaries required.
14
- * The shared `ooxml/` subpath export provides reusable OOXML infrastructure.
17
+ * The shared `ooxml/` subpath export provides reusable OOXML infrastructure, and the
18
+ * `registry` subpath exposes a format registry + `convert()` pipeline over all of the above.
15
19
  *
16
20
  * @example
17
21
  * ```ts
@@ -28,14 +32,18 @@
28
32
  export { markdownDocToDocx, docToDocx, docxToMarkdownDoc, docxToDoc } from './docx/index.js';
29
33
  export type { DocxExportOptions, DocxImportOptions } from './docx/index.js';
30
34
 
31
- // PPTX (export implemented, import stub)
35
+ // PPTX (import + export)
32
36
  export { markdownDocToPptx, docToPptx, pptxToMarkdownDoc, pptxToDoc } from './pptx/index.js';
33
37
  export type { PptxExportOptions, PptxImportOptions } from './pptx/index.js';
34
38
 
35
- // XLSX (stub)
39
+ // XLSX (import + export; export is tables-only → one worksheet per markdown table)
36
40
  export { markdownDocToXlsx, docToXlsx, xlsxToMarkdownDoc, xlsxToDoc } from './xlsx/index.js';
37
41
  export type { XlsxExportOptions, XlsxImportOptions } from './xlsx/index.js';
38
42
 
43
+ // CSV (import + export)
44
+ export { csvToMarkdownDoc, csvToDoc, markdownDocToCsv, parseCsv } from './csv/index.js';
45
+ export type { CsvImportOptions, CsvExportOptions } from './csv/index.js';
46
+
39
47
  // PDF (fully implemented)
40
48
  export {
41
49
  markdownDocToPdf,
@@ -49,7 +57,30 @@ export type { PdfExportOptions, PdfImportOptions } from './pdf/index.js';
49
57
  // HTML (fully implemented)
50
58
  export { docToHtml, docToHtmlZip, collectImagePaths } from './html/index.js';
51
59
  export type { HtmlExportOptions, HtmlZipExportOptions } from './html/index.js';
60
+ export { htmlToMarkdown, htmlToMarkdownDoc, htmlToMarkdownDocSync } from './html/index.js';
61
+ export type { HtmlImportOptions } from './html/index.js';
52
62
 
53
63
  // EPUB (export)
54
64
  export { markdownDocToEpub, docToEpub } from './epub/index.js';
55
65
  export type { EpubExportOptions } from './epub/index.js';
66
+
67
+ // Format registry + programmatic convert()
68
+ export {
69
+ convert,
70
+ createRegistry,
71
+ defaultRegistry,
72
+ defaultFormats,
73
+ ConversionError,
74
+ BUILTIN_FORMAT_IDS,
75
+ } from './registry/index.js';
76
+ export type {
77
+ FormatId,
78
+ ConversionResult,
79
+ NormalizedInput,
80
+ ConvertOptions,
81
+ FormatDefinition,
82
+ FormatRegistry,
83
+ ConvertSource,
84
+ ConversionErrorCode,
85
+ ConversionErrorOptions,
86
+ } from './registry/index.js';
@@ -91,4 +91,7 @@ export {
91
91
  REL_SLIDE_LAYOUT,
92
92
  REL_SLIDE_MASTER,
93
93
  CONTENT_TYPE_XLSX_WORKBOOK,
94
+ CONTENT_TYPE_XLSX_WORKSHEET,
95
+ CONTENT_TYPE_XLSX_STYLES,
96
+ REL_WORKSHEET,
94
97
  } from './namespaces.js';
@@ -77,6 +77,9 @@ export const NS_WML = 'http://schemas.openxmlformats.org/wordprocessingml/2006/m
77
77
  /** PresentationML main namespace (p:) */
78
78
  export const NS_PML = 'http://schemas.openxmlformats.org/presentationml/2006/main';
79
79
 
80
+ /** PowerPoint 2010 extension namespace (p14:) */
81
+ export const NS_PML_2010 = 'http://schemas.microsoft.com/office/powerpoint/2010/main';
82
+
80
83
  // ============================================
81
84
  // SpreadsheetML (XLSX)
82
85
  // ============================================
@@ -181,3 +184,13 @@ export const REL_SLIDE_MASTER =
181
184
 
182
185
  export const CONTENT_TYPE_XLSX_WORKBOOK =
183
186
  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml';
187
+
188
+ export const CONTENT_TYPE_XLSX_WORKSHEET =
189
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml';
190
+
191
+ export const CONTENT_TYPE_XLSX_STYLES =
192
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml';
193
+
194
+ /** Relationship type: Worksheet */
195
+ export const REL_WORKSHEET =
196
+ 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet';
package/src/pdf/export.ts CHANGED
@@ -22,8 +22,7 @@
22
22
  import { PDFDocument, StandardFonts, rgb, PDFFont, PDFPage } from 'pdf-lib';
23
23
 
24
24
  import type { Doc } from '@bendyline/squisq/schemas';
25
- import { resolveTheme } from '@bendyline/squisq/schemas';
26
- import { docToMarkdown } from '@bendyline/squisq/doc';
25
+ import { docToMarkdown, resolveThemeForDoc } from '@bendyline/squisq/doc';
27
26
  import type {
28
27
  MarkdownDocument,
29
28
  MarkdownBlockNode,
@@ -51,6 +50,7 @@ import type {
51
50
  MarkdownInlineMath,
52
51
  MarkdownFootnoteReference,
53
52
  } from '@bendyline/squisq/markdown';
53
+ import { readFrontmatterThemeId } from '@bendyline/squisq/markdown';
54
54
 
55
55
  import {
56
56
  PAGE_WIDTH_LETTER,
@@ -127,7 +127,7 @@ export async function markdownDocToPdf(
127
127
  pdfDoc.setCreationDate(new Date());
128
128
  pdfDoc.setModificationDate(new Date());
129
129
 
130
- const ctx = await createExportContext(pdfDoc, options);
130
+ const ctx = await createExportContext(pdfDoc, options, doc);
131
131
 
132
132
  renderBlocks(doc.children, ctx, 0);
133
133
 
@@ -190,6 +190,7 @@ interface ExportContext {
190
190
  async function createExportContext(
191
191
  pdfDoc: PDFDocument,
192
192
  options: PdfExportOptions,
193
+ doc: MarkdownDocument,
193
194
  ): Promise<ExportContext> {
194
195
  const [regular, bold, italic, boldItalic, mono, monoBold] = await Promise.all([
195
196
  pdfDoc.embedFont(StandardFonts.Helvetica),
@@ -213,8 +214,12 @@ async function createExportContext(
213
214
  let colorHeading = COLOR_HEADING;
214
215
  let colorLink = COLOR_LINK;
215
216
 
216
- if (options.themeId) {
217
- const theme = resolveTheme(options.themeId);
217
+ // Honor an explicit themeId, else the doc's frontmatter theme (`squisq-theme`
218
+ // / legacy) — mirroring PPTX/DOCX/HTML so every format themes consistently,
219
+ // including inline custom themes (resolved doc-scoped).
220
+ const themeId = options.themeId ?? readFrontmatterThemeId(doc.frontmatter);
221
+ if (themeId) {
222
+ const theme = resolveThemeForDoc(doc, themeId);
218
223
  if (theme.colors) {
219
224
  colorText = hexToRgb(theme.colors.text) ?? COLOR_TEXT;
220
225
  colorHeading = hexToRgb(theme.colors.primary) ?? COLOR_HEADING;
package/src/pdf/import.ts CHANGED
@@ -22,6 +22,7 @@
22
22
 
23
23
  import type { Doc } from '@bendyline/squisq/schemas';
24
24
  import { markdownToDoc } from '@bendyline/squisq/doc';
25
+ import { stringifyMarkdown } from '@bendyline/squisq/markdown';
25
26
  import type {
26
27
  MarkdownDocument,
27
28
  MarkdownBlockNode,
@@ -43,8 +44,8 @@ import type {
43
44
  MarkdownImage,
44
45
  } from '@bendyline/squisq/markdown';
45
46
 
46
- import { MemoryContentContainer } from '@bendyline/squisq/storage';
47
47
  import type { ContentContainer } from '@bendyline/squisq/storage';
48
+ import { buildContainer } from '../shared/container.js';
48
49
 
49
50
  import {
50
51
  DEFAULT_FONT_SIZE,
@@ -159,31 +160,44 @@ export async function pdfToContainer(
159
160
  const textLines = await extractTextLines(bytes);
160
161
  const images = await extractImages(bytes);
161
162
 
163
+ // Under Node there is no DOM/canvas, so extractImages() returns [] via its
164
+ // environment guard and embedded images can't be decoded. The registry's
165
+ // `importContainer` wrapper surfaces this (typeof document === 'undefined')
166
+ // as a ConversionResult warning; direct callers of pdfToContainer get no
167
+ // signal, which is an accepted trade-off for the degraded Node path.
168
+
162
169
  const bodySize = options.bodyFontSize ?? detectBodyFontSize(textLines);
163
170
 
164
- // If we have images, insert image references into the block list
165
- let blocks = classifyLines(textLines, bodySize, options);
171
+ // Classify text into blocks, capturing the originating page of each block
172
+ // (parallel array) so images can be placed on their own page rather than
173
+ // dumped at the document end.
174
+ const blockPages: number[] = [];
175
+ let blocks = classifyLines(textLines, bodySize, options, blockPages);
166
176
  if (images.length > 0) {
167
- blocks = insertImageBlocks(blocks, images);
177
+ blocks = insertImageBlocks(blocks, blockPages, images);
168
178
  }
169
179
 
170
180
  const markdownDoc: MarkdownDocument = { type: 'document', children: blocks };
171
181
 
172
- const { stringifyMarkdown } = await import('@bendyline/squisq/markdown');
173
- const markdown = stringifyMarkdown(markdownDoc);
174
-
175
- const container = new MemoryContentContainer();
176
- await container.writeDocument(markdown);
177
-
178
- for (const img of images) {
179
- await container.writeFile(img.path, new Uint8Array(img.data), 'image/png');
180
- }
181
-
182
- return container;
182
+ // pdf image extraction only ever produces PNG (canvas re-encode), so every
183
+ // entry gets image/png.
184
+ return buildContainer(
185
+ stringifyMarkdown(markdownDoc),
186
+ images.map((img) => [img.path, { data: img.data, mimeType: 'image/png' }] as const),
187
+ );
183
188
  }
184
189
 
185
- /** Extracted image with position info for placement. */
186
- interface ExtractedImage {
190
+ /**
191
+ * Extracted image with position info for placement.
192
+ *
193
+ * `page` is captured reliably from the operator-list walk. `y` is currently
194
+ * always 0 — recovering a real y within the page would require tracking the
195
+ * current transformation matrix (CTM) during the paint operator, which is out
196
+ * of scope here (see the placement note in `insertImageBlocks`).
197
+ *
198
+ * Exported for direct unit testing of `insertImageBlocks`.
199
+ */
200
+ export interface ExtractedImage {
187
201
  path: string;
188
202
  data: ArrayBuffer;
189
203
  page: number;
@@ -328,29 +342,86 @@ function imageDataToPng(img: PdfjsImageData): ArrayBuffer | null {
328
342
  }
329
343
  }
330
344
 
345
+ /** Wrap an extracted image as a paragraph containing an image node. */
346
+ function imageParagraph(img: ExtractedImage): MarkdownParagraph {
347
+ const imgNode: MarkdownImage = {
348
+ type: 'image',
349
+ url: img.path,
350
+ alt: `Image ${img.path.replace('images/image', '').replace('.png', '')}`,
351
+ };
352
+ return { type: 'paragraph', children: [imgNode] };
353
+ }
354
+
331
355
  /**
332
- * Insert image reference blocks between text blocks.
333
- * Places each image after the last text block on the same page (or at the end).
356
+ * Insert image reference blocks among the text blocks, page by page.
357
+ *
358
+ * Each image is placed immediately after the LAST content block that
359
+ * originated from the same page (per the parallel `blockPages` array).
360
+ * Images on a page that produced no text blocks (e.g. an image-only page)
361
+ * fall back to the last block of the nearest preceding page that did; if no
362
+ * such page exists, they are appended at the document end.
363
+ *
364
+ * Placement is intentionally page-level only. Ordering *within* a page follows
365
+ * the extraction order of the images (roughly the paint-operator order) — real
366
+ * vertical (y) ordering within a page is future work, since `ExtractedImage.y`
367
+ * is not yet populated (it needs CTM tracking during the paint operator).
368
+ *
369
+ * @param blocks - The classified content blocks, in document order.
370
+ * @param blockPages - Parallel array: `blockPages[i]` is the 0-based page that
371
+ * `blocks[i]` came from. Must be the same length as `blocks`.
372
+ * @param images - Extracted images with a reliable `page` field.
334
373
  */
335
- function insertImageBlocks(
374
+ export function insertImageBlocks(
336
375
  blocks: MarkdownBlockNode[],
376
+ blockPages: number[],
337
377
  images: ExtractedImage[],
338
378
  ): MarkdownBlockNode[] {
339
379
  if (images.length === 0) return blocks;
340
380
 
341
- // Simple strategy: append all images at the end as paragraphs
342
- const result = [...blocks];
381
+ // No text blocks at all every image simply appends in order.
382
+ if (blocks.length === 0) {
383
+ return images.map(imageParagraph);
384
+ }
385
+
386
+ // Map each page → index of its LAST block, and remember which pages have
387
+ // blocks (sorted ascending) for the image-only-page fallback lookup.
388
+ const lastBlockIndexByPage = new Map<number, number>();
389
+ for (let i = 0; i < blocks.length; i++) {
390
+ lastBlockIndexByPage.set(blockPages[i], i);
391
+ }
392
+ const pagesWithBlocks = [...lastBlockIndexByPage.keys()].sort((a, b) => a - b);
393
+
394
+ const lastIndex = blocks.length - 1;
395
+
396
+ /** Resolve the block index after which an image on `page` should be inserted. */
397
+ const anchorFor = (page: number): number => {
398
+ const direct = lastBlockIndexByPage.get(page);
399
+ if (direct !== undefined) return direct;
400
+ // Image-only page: fall back to the nearest preceding page with blocks.
401
+ let anchor = -1;
402
+ for (const p of pagesWithBlocks) {
403
+ if (p < page) anchor = lastBlockIndexByPage.get(p)!;
404
+ else break;
405
+ }
406
+ // No preceding page with blocks → append at the document end.
407
+ return anchor === -1 ? lastIndex : anchor;
408
+ };
409
+
410
+ // Group image paragraphs by the block index they should follow, preserving
411
+ // image order within each group.
412
+ const insertAfter = new Map<number, MarkdownParagraph[]>();
343
413
  for (const img of images) {
344
- const imgNode: MarkdownImage = {
345
- type: 'image',
346
- url: img.path,
347
- alt: `Image ${img.path.replace('images/image', '').replace('.png', '')}`,
348
- };
349
- const para: MarkdownParagraph = {
350
- type: 'paragraph',
351
- children: [imgNode],
352
- };
353
- result.push(para);
414
+ const anchor = anchorFor(img.page);
415
+ const group = insertAfter.get(anchor);
416
+ if (group) group.push(imageParagraph(img));
417
+ else insertAfter.set(anchor, [imageParagraph(img)]);
418
+ }
419
+
420
+ const result: MarkdownBlockNode[] = [];
421
+ for (let i = 0; i < blocks.length; i++) {
422
+ result.push(blocks[i]);
423
+ const imgs = insertAfter.get(i);
424
+ if (imgs) result.push(...imgs);
354
425
  }
355
426
  return result;
356
427
  }
@@ -592,8 +663,20 @@ function classifyLines(
592
663
  lines: TextLine[],
593
664
  bodySize: number,
594
665
  options: PdfImportOptions,
666
+ /**
667
+ * Optional out-parameter: when provided, receives one entry per produced
668
+ * block giving the 0-based page that block originated from. Kept parallel
669
+ * to the returned block array so image placement can be page-aware without
670
+ * mutating the (strictly-typed) block nodes themselves.
671
+ */
672
+ blockPages?: number[],
595
673
  ): MarkdownBlockNode[] {
596
674
  const blocks: MarkdownBlockNode[] = [];
675
+ /** Push a block and record its originating page in the parallel array. */
676
+ const pushBlock = (block: MarkdownBlockNode, page: number): void => {
677
+ blocks.push(block);
678
+ if (blockPages) blockPages.push(page);
679
+ };
597
680
  const detectTables = options.detectTables !== false;
598
681
  const detectCodeBlocks = options.detectCodeBlocks !== false;
599
682
  const detectBlockquotes = options.detectBlockquotes !== false;
@@ -610,11 +693,14 @@ function classifyLines(
610
693
  // --- Heading detection ---
611
694
  if (line.fontSize >= IMPORT_HEADING_MIN_SIZE && line.fontSize > bodySize + 1) {
612
695
  const depth = sizeToHeadingDepth(line.fontSize);
613
- blocks.push({
614
- type: 'heading',
615
- depth,
616
- children: buildInlineNodes(line, options),
617
- } as MarkdownHeading);
696
+ pushBlock(
697
+ {
698
+ type: 'heading',
699
+ depth,
700
+ children: buildInlineNodes(line, options),
701
+ } as MarkdownHeading,
702
+ line.page,
703
+ );
618
704
  i++;
619
705
  continue;
620
706
  }
@@ -626,10 +712,13 @@ function classifyLines(
626
712
  codeLines.push(lines[i].text);
627
713
  i++;
628
714
  }
629
- blocks.push({
630
- type: 'code',
631
- value: codeLines.join('\n'),
632
- } as MarkdownCodeBlock);
715
+ pushBlock(
716
+ {
717
+ type: 'code',
718
+ value: codeLines.join('\n'),
719
+ } as MarkdownCodeBlock,
720
+ line.page,
721
+ );
633
722
  continue;
634
723
  }
635
724
 
@@ -639,7 +728,7 @@ function classifyLines(
639
728
  if (tableLines > 0) {
640
729
  const table = buildTable(lines.slice(i, i + tableLines), options);
641
730
  if (table) {
642
- blocks.push(table);
731
+ pushBlock(table, line.page);
643
732
  i += tableLines;
644
733
  continue;
645
734
  }
@@ -651,7 +740,7 @@ function classifyLines(
651
740
  const orderedMatch = line.text.match(IMPORT_ORDERED_PREFIX);
652
741
  if (bulletMatch || orderedMatch) {
653
742
  const listResult = consumeList(lines, i, typicalLeftMargin, bodySize, options);
654
- blocks.push(listResult.list);
743
+ pushBlock(listResult.list, line.page);
655
744
  i = listResult.nextIndex;
656
745
  continue;
657
746
  }
@@ -675,10 +764,13 @@ function classifyLines(
675
764
  children: buildInlineNodes(ql, options),
676
765
  }) as MarkdownParagraph,
677
766
  );
678
- blocks.push({
679
- type: 'blockquote',
680
- children: quoteBlocks,
681
- } as MarkdownBlockquote);
767
+ pushBlock(
768
+ {
769
+ type: 'blockquote',
770
+ children: quoteBlocks,
771
+ } as MarkdownBlockquote,
772
+ line.page,
773
+ );
682
774
  continue;
683
775
  }
684
776
 
@@ -721,10 +813,13 @@ function classifyLines(
721
813
  }
722
814
 
723
815
  if (allInlines.length > 0) {
724
- blocks.push({
725
- type: 'paragraph',
726
- children: mergeAdjacentText(allInlines),
727
- } as MarkdownParagraph);
816
+ pushBlock(
817
+ {
818
+ type: 'paragraph',
819
+ children: mergeAdjacentText(allInlines),
820
+ } as MarkdownParagraph,
821
+ line.page,
822
+ );
728
823
  }
729
824
  }
730
825