@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.
- package/README.md +180 -43
- package/dist/{chunk-33YRFXZZ.js → chunk-46FMDOWZ.js} +260 -14
- package/dist/chunk-46FMDOWZ.js.map +1 -0
- package/dist/{chunk-ERZ627GR.js → chunk-5LSSWZPU.js} +12 -27
- package/dist/chunk-5LSSWZPU.js.map +1 -0
- package/dist/chunk-6M7Z25LA.js +46 -0
- package/dist/chunk-6M7Z25LA.js.map +1 -0
- package/dist/{chunk-S3Y7H2BK.js → chunk-D7IFPWNZ.js} +95 -50
- package/dist/chunk-D7IFPWNZ.js.map +1 -0
- package/dist/chunk-DQAZR57U.js +33 -0
- package/dist/chunk-DQAZR57U.js.map +1 -0
- package/dist/chunk-EHLGMSTE.js +273 -0
- package/dist/chunk-EHLGMSTE.js.map +1 -0
- package/dist/{chunk-A3FHLTY5.js → chunk-MJGRI6XR.js} +6 -19
- package/dist/chunk-MJGRI6XR.js.map +1 -0
- package/dist/chunk-PN52A5AA.js +32 -0
- package/dist/chunk-PN52A5AA.js.map +1 -0
- package/dist/chunk-RFAPOKHJ.js +123 -0
- package/dist/chunk-RFAPOKHJ.js.map +1 -0
- package/dist/chunk-RTS5XBZ7.js +526 -0
- package/dist/chunk-RTS5XBZ7.js.map +1 -0
- package/dist/{chunk-7DEUGIOO.js → chunk-SHLPSADL.js} +120 -2
- package/dist/chunk-SHLPSADL.js.map +1 -0
- package/dist/chunk-SSUPBUF5.js +15 -0
- package/dist/chunk-SSUPBUF5.js.map +1 -0
- package/dist/{chunk-UDS45KUJ.js → chunk-WWBIKRNP.js} +40 -80
- package/dist/chunk-WWBIKRNP.js.map +1 -0
- package/dist/{chunk-VN2KEOYB.js → chunk-YESS7VY3.js} +379 -87
- package/dist/chunk-YESS7VY3.js.map +1 -0
- package/dist/csv/index.d.ts +50 -0
- package/dist/csv/index.js +13 -0
- package/dist/csv/index.js.map +1 -0
- package/dist/docx/index.js +6 -3
- package/dist/epub/index.js +4 -2
- package/dist/html/index.d.ts +42 -2
- package/dist/html/index.js +9 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +47 -14
- package/dist/ooxml/index.d.ts +5 -1
- package/dist/ooxml/index.js +13 -9
- package/dist/pdf/index.js +2 -1
- package/dist/pptx/index.d.ts +36 -19
- package/dist/pptx/index.js +9 -2
- package/dist/registry/index.d.ts +167 -0
- package/dist/registry/index.js +17 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/xlsx/index.d.ts +60 -29
- package/dist/xlsx/index.js +4 -1
- package/package.json +12 -2
- package/src/__tests__/convert.test.ts +186 -0
- package/src/__tests__/csvImport.test.ts +84 -0
- package/src/__tests__/exportThemeReconciliation.test.ts +87 -0
- package/src/__tests__/formatRegistry.test.ts +174 -0
- package/src/__tests__/htmlImport.test.ts +57 -0
- package/src/__tests__/lossyWarnings.test.ts +105 -0
- package/src/__tests__/pdfImport.test.ts +95 -1
- package/src/__tests__/plainHtml.test.ts +31 -0
- package/src/__tests__/pptxExport.test.ts +138 -0
- package/src/__tests__/pptxImport.test.ts +115 -0
- package/src/__tests__/roundTripAssets.test.ts +50 -0
- package/src/__tests__/roundTripMatrix.fixtures.ts +86 -0
- package/src/__tests__/roundTripMatrix.helpers.ts +154 -0
- package/src/__tests__/roundTripMatrix.test.ts +142 -0
- package/src/__tests__/sharedContainer.test.ts +41 -0
- package/src/__tests__/sharedImages.test.ts +61 -0
- package/src/__tests__/xlsxExport.test.ts +164 -0
- package/src/__tests__/xlsxImport.test.ts +80 -0
- package/src/csv/index.ts +188 -0
- package/src/docx/export.ts +27 -44
- package/src/docx/import.ts +7 -39
- package/src/epub/export.ts +10 -27
- package/src/html/imageUtils.ts +5 -19
- package/src/html/import.ts +297 -0
- package/src/html/index.ts +4 -0
- package/src/html/plainHtml.ts +66 -16
- package/src/index.ts +36 -5
- package/src/ooxml/index.ts +3 -0
- package/src/ooxml/namespaces.ts +13 -0
- package/src/pdf/export.ts +10 -5
- package/src/pdf/import.ts +146 -51
- package/src/pptx/export.ts +225 -85
- package/src/pptx/import.ts +273 -0
- package/src/pptx/index.ts +10 -28
- package/src/registry/convert.ts +318 -0
- package/src/registry/defaultFormats.ts +323 -0
- package/src/registry/errors.ts +46 -0
- package/src/registry/index.ts +38 -0
- package/src/registry/registry.ts +48 -0
- package/src/registry/types.ts +107 -0
- package/src/shared/container.ts +28 -0
- package/src/shared/images.ts +44 -0
- package/src/shared/inlineRuns.ts +99 -0
- package/src/shared/text.ts +41 -0
- package/src/xlsx/export.ts +253 -0
- package/src/xlsx/import.ts +159 -0
- package/src/xlsx/index.ts +18 -61
- package/dist/chunk-33YRFXZZ.js.map +0 -1
- package/dist/chunk-67KIJHV2.js +0 -21
- package/dist/chunk-67KIJHV2.js.map +0 -1
- package/dist/chunk-7DEUGIOO.js.map +0 -1
- package/dist/chunk-A3FHLTY5.js.map +0 -1
- package/dist/chunk-ERZ627GR.js.map +0 -1
- package/dist/chunk-S3Y7H2BK.js.map +0 -1
- package/dist/chunk-UDS45KUJ.js.map +0 -1
- package/dist/chunk-VN2KEOYB.js.map +0 -1
- package/dist/chunk-YN5HFCEW.js +0 -120
- 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
|
|
11
|
-
* - **XLSX** — Microsoft Excel (
|
|
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 (
|
|
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 (
|
|
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';
|
package/src/ooxml/index.ts
CHANGED
package/src/ooxml/namespaces.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
217
|
-
|
|
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
|
-
//
|
|
165
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
/**
|
|
186
|
-
|
|
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
|
|
333
|
-
*
|
|
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
|
-
//
|
|
342
|
-
|
|
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
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
816
|
+
pushBlock(
|
|
817
|
+
{
|
|
818
|
+
type: 'paragraph',
|
|
819
|
+
children: mergeAdjacentText(allInlines),
|
|
820
|
+
} as MarkdownParagraph,
|
|
821
|
+
line.page,
|
|
822
|
+
);
|
|
728
823
|
}
|
|
729
824
|
}
|
|
730
825
|
|