@bendyline/squisq-formats 0.1.1 → 1.0.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 (109) hide show
  1. package/dist/__tests__/docxExport.test.d.ts +8 -0
  2. package/dist/__tests__/docxExport.test.d.ts.map +1 -0
  3. package/dist/__tests__/docxExport.test.js +383 -0
  4. package/dist/__tests__/docxExport.test.js.map +1 -0
  5. package/dist/__tests__/docxImport.test.d.ts +8 -0
  6. package/dist/__tests__/docxImport.test.d.ts.map +1 -0
  7. package/dist/__tests__/docxImport.test.js +309 -0
  8. package/dist/__tests__/docxImport.test.js.map +1 -0
  9. package/dist/__tests__/html.test.d.ts +8 -0
  10. package/dist/__tests__/html.test.d.ts.map +1 -0
  11. package/dist/__tests__/html.test.js +243 -0
  12. package/dist/__tests__/html.test.js.map +1 -0
  13. package/dist/__tests__/ooxml.test.d.ts +5 -0
  14. package/dist/__tests__/ooxml.test.d.ts.map +1 -0
  15. package/dist/__tests__/ooxml.test.js +152 -0
  16. package/dist/__tests__/ooxml.test.js.map +1 -0
  17. package/dist/__tests__/pdfExport.test.d.ts +8 -0
  18. package/dist/__tests__/pdfExport.test.d.ts.map +1 -0
  19. package/dist/__tests__/pdfExport.test.js +275 -0
  20. package/dist/__tests__/pdfExport.test.js.map +1 -0
  21. package/dist/__tests__/pdfImport.test.d.ts +11 -0
  22. package/dist/__tests__/pdfImport.test.d.ts.map +1 -0
  23. package/dist/__tests__/pdfImport.test.js +254 -0
  24. package/dist/__tests__/pdfImport.test.js.map +1 -0
  25. package/dist/__tests__/roundTrip.test.d.ts +7 -0
  26. package/dist/__tests__/roundTrip.test.d.ts.map +1 -0
  27. package/dist/__tests__/roundTrip.test.js +173 -0
  28. package/dist/__tests__/roundTrip.test.js.map +1 -0
  29. package/dist/docx/export.d.ts +54 -0
  30. package/dist/docx/export.d.ts.map +1 -0
  31. package/dist/docx/export.js +729 -0
  32. package/dist/docx/export.js.map +1 -0
  33. package/dist/docx/import.d.ts +52 -0
  34. package/dist/docx/import.d.ts.map +1 -0
  35. package/dist/docx/import.js +687 -0
  36. package/dist/docx/import.js.map +1 -0
  37. package/dist/docx/index.d.ts +15 -100
  38. package/dist/docx/index.d.ts.map +1 -0
  39. package/dist/docx/index.js +22 -13
  40. package/dist/docx/index.js.map +1 -1
  41. package/dist/docx/styles.d.ts +68 -0
  42. package/dist/docx/styles.d.ts.map +1 -0
  43. package/dist/docx/styles.js +125 -0
  44. package/dist/docx/styles.js.map +1 -0
  45. package/dist/html/htmlTemplate.d.ts +64 -0
  46. package/dist/html/htmlTemplate.d.ts.map +1 -0
  47. package/dist/html/htmlTemplate.js +235 -0
  48. package/dist/html/htmlTemplate.js.map +1 -0
  49. package/dist/html/imageUtils.d.ts +29 -0
  50. package/dist/html/imageUtils.d.ts.map +1 -0
  51. package/dist/html/imageUtils.js +63 -0
  52. package/dist/html/imageUtils.js.map +1 -0
  53. package/dist/html/index.d.ts +9 -77
  54. package/dist/html/index.d.ts.map +1 -0
  55. package/dist/html/index.js +137 -16
  56. package/dist/html/index.js.map +1 -1
  57. package/dist/index.d.ts +36 -7
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +35 -53
  60. package/dist/index.js.map +1 -1
  61. package/dist/ooxml/index.d.ts +14 -343
  62. package/dist/ooxml/index.d.ts.map +1 -0
  63. package/dist/ooxml/index.js +22 -98
  64. package/dist/ooxml/index.js.map +1 -1
  65. package/dist/ooxml/namespaces.d.ts +67 -0
  66. package/dist/ooxml/namespaces.d.ts.map +1 -0
  67. package/dist/ooxml/namespaces.js +94 -0
  68. package/dist/ooxml/namespaces.js.map +1 -0
  69. package/dist/ooxml/reader.d.ts +52 -0
  70. package/dist/ooxml/reader.d.ts.map +1 -0
  71. package/dist/ooxml/reader.js +190 -0
  72. package/dist/ooxml/reader.js.map +1 -0
  73. package/dist/ooxml/types.d.ts +82 -0
  74. package/dist/ooxml/types.d.ts.map +1 -0
  75. package/dist/ooxml/types.js +9 -0
  76. package/dist/ooxml/types.js.map +1 -0
  77. package/dist/ooxml/writer.d.ts +71 -0
  78. package/dist/ooxml/writer.d.ts.map +1 -0
  79. package/dist/ooxml/writer.js +200 -0
  80. package/dist/ooxml/writer.js.map +1 -0
  81. package/dist/ooxml/xmlUtils.d.ts +76 -0
  82. package/dist/ooxml/xmlUtils.d.ts.map +1 -0
  83. package/dist/ooxml/xmlUtils.js +108 -0
  84. package/dist/ooxml/xmlUtils.js.map +1 -0
  85. package/dist/pdf/export.d.ts +50 -0
  86. package/dist/pdf/export.d.ts.map +1 -0
  87. package/dist/pdf/export.js +740 -0
  88. package/dist/pdf/export.js.map +1 -0
  89. package/dist/pdf/import.d.ts +78 -0
  90. package/dist/pdf/import.d.ts.map +1 -0
  91. package/dist/pdf/import.js +619 -0
  92. package/dist/pdf/import.js.map +1 -0
  93. package/dist/pdf/index.d.ts +17 -121
  94. package/dist/pdf/index.d.ts.map +1 -0
  95. package/dist/pdf/index.js +25 -14
  96. package/dist/pdf/index.js.map +1 -1
  97. package/dist/pdf/styles.d.ts +142 -0
  98. package/dist/pdf/styles.d.ts.map +1 -0
  99. package/dist/pdf/styles.js +145 -0
  100. package/dist/pdf/styles.js.map +1 -0
  101. package/dist/pptx/index.d.ts +9 -12
  102. package/dist/pptx/index.d.ts.map +1 -0
  103. package/dist/pptx/index.js +44 -12
  104. package/dist/pptx/index.js.map +1 -1
  105. package/dist/xlsx/index.d.ts +9 -12
  106. package/dist/xlsx/index.d.ts.map +1 -0
  107. package/dist/xlsx/index.js +44 -12
  108. package/dist/xlsx/index.js.map +1 -1
  109. package/package.json +3 -4
@@ -0,0 +1,687 @@
1
+ /**
2
+ * DOCX Import
3
+ *
4
+ * Parses a .docx file (Office Open XML WordprocessingML) and converts
5
+ * its content into a squisq MarkdownDocument (or Doc).
6
+ *
7
+ * Uses JSZip + DOMParser to read the archive and parse the XML — no
8
+ * third-party docx library. Handles headings, paragraphs, inline
9
+ * formatting (bold, italic, strikethrough), hyperlinks, lists, tables,
10
+ * blockquotes, code blocks, images, and footnotes.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * import { docxToMarkdownDoc } from '@bendyline/squisq-formats/docx';
15
+ *
16
+ * const response = await fetch('document.docx');
17
+ * const data = await response.arrayBuffer();
18
+ * const doc = await docxToMarkdownDoc(data);
19
+ * ```
20
+ */
21
+ import { markdownToDoc } from '@bendyline/squisq/doc';
22
+ import { openPackage, getPartXml, getPartRelationships } from '../ooxml/reader.js';
23
+ import { NS_WML, NS_R } from '../ooxml/namespaces.js';
24
+ import { HEADING_STYLE_MAP, QUOTE_STYLE_IDS, CODE_STYLE_IDS, INLINE_CODE_STYLE_IDS, BULLET_NUM_FORMATS, } from './styles.js';
25
+ /**
26
+ * Convert a .docx file to a MarkdownDocument.
27
+ *
28
+ * @param data - The raw .docx file as ArrayBuffer or Blob
29
+ * @param options - Import options
30
+ * @returns A MarkdownDocument representing the document content
31
+ */
32
+ export async function docxToMarkdownDoc(data, options = {}) {
33
+ const pkg = await openPackage(data);
34
+ const ctx = await buildImportContext(pkg, options);
35
+ const documentXml = await getPartXml(pkg, 'word/document.xml');
36
+ if (!documentXml) {
37
+ return { type: 'document', children: [] };
38
+ }
39
+ const body = getFirstElement(documentXml, 'body');
40
+ if (!body) {
41
+ return { type: 'document', children: [] };
42
+ }
43
+ const blocks = await convertBody(body, ctx);
44
+ return { type: 'document', children: blocks };
45
+ }
46
+ /**
47
+ * Convert a .docx file to a squisq Doc.
48
+ *
49
+ * Convenience wrapper: DOCX → MarkdownDocument → Doc.
50
+ *
51
+ * @param data - The raw .docx file as ArrayBuffer or Blob
52
+ * @param options - Import options
53
+ * @returns A squisq Doc
54
+ */
55
+ export async function docxToDoc(data, options = {}) {
56
+ const markdownDoc = await docxToMarkdownDoc(data, options);
57
+ return markdownToDoc(markdownDoc);
58
+ }
59
+ async function buildImportContext(pkg, options) {
60
+ const ctx = {
61
+ headingStyles: new Map(),
62
+ quoteStyles: new Set(),
63
+ codeStyles: new Set(),
64
+ inlineCodeStyles: new Set(),
65
+ documentRels: new Map(),
66
+ numbering: new Map(),
67
+ footnotes: new Map(),
68
+ pkg,
69
+ options,
70
+ };
71
+ // Initialize with built-in defaults
72
+ for (const [id, depth] of Object.entries(HEADING_STYLE_MAP)) {
73
+ ctx.headingStyles.set(id, depth);
74
+ }
75
+ for (const id of QUOTE_STYLE_IDS) {
76
+ ctx.quoteStyles.add(id);
77
+ }
78
+ for (const id of CODE_STYLE_IDS) {
79
+ ctx.codeStyles.add(id);
80
+ }
81
+ for (const id of INLINE_CODE_STYLE_IDS) {
82
+ ctx.inlineCodeStyles.add(id);
83
+ }
84
+ // Parse styles.xml for custom heading/quote/code mappings
85
+ await parseStyles(pkg, ctx);
86
+ // Parse document relationships
87
+ const rels = await getPartRelationships(pkg, 'word/document.xml');
88
+ for (const rel of rels) {
89
+ ctx.documentRels.set(rel.id, rel);
90
+ }
91
+ // Parse numbering.xml
92
+ await parseNumbering(pkg, ctx);
93
+ // Parse footnotes.xml
94
+ await parseFootnotes(pkg, ctx);
95
+ return ctx;
96
+ }
97
+ // ============================================
98
+ // Styles Parsing
99
+ // ============================================
100
+ async function parseStyles(pkg, ctx) {
101
+ const doc = await getPartXml(pkg, 'word/styles.xml');
102
+ if (!doc)
103
+ return;
104
+ const styles = doc.getElementsByTagNameNS(NS_WML, 'style');
105
+ // Fallback for documents that don't use namespace prefixes properly
106
+ const stylesList = styles.length > 0 ? styles : doc.getElementsByTagName('style');
107
+ for (let i = 0; i < stylesList.length; i++) {
108
+ const style = stylesList[i];
109
+ const styleId = style.getAttributeNS(NS_WML, 'styleId') ?? style.getAttribute('w:styleId');
110
+ if (!styleId)
111
+ continue;
112
+ const nameEl = getFirstChildElement(style, 'name');
113
+ const styleName = nameEl?.getAttributeNS(NS_WML, 'val') ?? nameEl?.getAttribute('w:val') ?? '';
114
+ // Check if this is a heading style by name
115
+ const headingMatch = styleName.match(/^heading\s+(\d+)$/i);
116
+ if (headingMatch) {
117
+ const depth = parseInt(headingMatch[1], 10);
118
+ if (depth >= 1 && depth <= 6) {
119
+ ctx.headingStyles.set(styleId, depth);
120
+ }
121
+ }
122
+ // Check pPr > outlineLvl for heading detection
123
+ const pPr = getFirstChildElement(style, 'pPr');
124
+ if (pPr) {
125
+ const outlineLvl = getFirstChildElement(pPr, 'outlineLvl');
126
+ if (outlineLvl) {
127
+ const val = outlineLvl.getAttributeNS(NS_WML, 'val') ?? outlineLvl.getAttribute('w:val');
128
+ if (val !== null) {
129
+ const depth = parseInt(val, 10) + 1;
130
+ if (depth >= 1 && depth <= 6) {
131
+ ctx.headingStyles.set(styleId, depth);
132
+ }
133
+ }
134
+ }
135
+ }
136
+ }
137
+ }
138
+ // ============================================
139
+ // Numbering Parsing
140
+ // ============================================
141
+ async function parseNumbering(pkg, ctx) {
142
+ const doc = await getPartXml(pkg, 'word/numbering.xml');
143
+ if (!doc)
144
+ return;
145
+ // Parse abstract numbering definitions
146
+ const abstractNums = new Map(); // abstractNumId → levels(ilvl → isOrdered)
147
+ const abstractNumEls = getAllElements(doc, 'abstractNum');
148
+ for (const absNum of abstractNumEls) {
149
+ const absId = getAttr(absNum, 'abstractNumId');
150
+ if (!absId)
151
+ continue;
152
+ const levels = new Map();
153
+ const lvlEls = getAllChildElements(absNum, 'lvl');
154
+ for (const lvl of lvlEls) {
155
+ const ilvlStr = getAttr(lvl, 'ilvl');
156
+ if (ilvlStr === null)
157
+ continue;
158
+ const ilvl = parseInt(ilvlStr, 10);
159
+ const numFmtEl = getFirstChildElement(lvl, 'numFmt');
160
+ const numFmt = numFmtEl ? getAttr(numFmtEl, 'val') : null;
161
+ const isOrdered = numFmt !== null && !BULLET_NUM_FORMATS.has(numFmt);
162
+ levels.set(ilvl, isOrdered);
163
+ }
164
+ abstractNums.set(absId, levels);
165
+ }
166
+ // Parse concrete num → abstractNum mappings
167
+ const numEls = getAllElements(doc, 'num');
168
+ for (const num of numEls) {
169
+ const numId = getAttr(num, 'numId');
170
+ if (!numId)
171
+ continue;
172
+ const abstractNumIdEl = getFirstChildElement(num, 'abstractNumId');
173
+ const absId = abstractNumIdEl ? getAttr(abstractNumIdEl, 'val') : null;
174
+ if (!absId)
175
+ continue;
176
+ const levels = abstractNums.get(absId);
177
+ if (levels) {
178
+ ctx.numbering.set(numId, { levels });
179
+ }
180
+ }
181
+ }
182
+ // ============================================
183
+ // Footnotes Parsing
184
+ // ============================================
185
+ async function parseFootnotes(pkg, ctx) {
186
+ const doc = await getPartXml(pkg, 'word/footnotes.xml');
187
+ if (!doc)
188
+ return;
189
+ const footnoteEls = getAllElements(doc, 'footnote');
190
+ for (const fn of footnoteEls) {
191
+ const id = getAttr(fn, 'id');
192
+ const type = getAttr(fn, 'type');
193
+ // Skip separator and continuation separator footnotes
194
+ if (!id || type === 'separator' || type === 'continuationSeparator')
195
+ continue;
196
+ ctx.footnotes.set(id, fn);
197
+ }
198
+ }
199
+ // ============================================
200
+ // Body Conversion
201
+ // ============================================
202
+ async function convertBody(body, ctx) {
203
+ const result = [];
204
+ const children = Array.from(body.children);
205
+ let i = 0;
206
+ while (i < children.length) {
207
+ const el = children[i];
208
+ const localName = el.localName;
209
+ if (localName === 'p') {
210
+ // Check if this is part of a list
211
+ const numPr = getNumPr(el);
212
+ if (numPr) {
213
+ // Collect consecutive list paragraphs
214
+ const { node, consumed } = await collectList(children, i, ctx);
215
+ result.push(node);
216
+ i += consumed;
217
+ continue;
218
+ }
219
+ const block = await convertParagraph(el, ctx);
220
+ if (block) {
221
+ result.push(block);
222
+ }
223
+ i++;
224
+ }
225
+ else if (localName === 'tbl') {
226
+ const table = await convertTable(el, ctx);
227
+ if (table)
228
+ result.push(table);
229
+ i++;
230
+ }
231
+ else {
232
+ // Skip unknown elements (sectPr, bookmarkStart, etc.)
233
+ i++;
234
+ }
235
+ }
236
+ // Append footnote definitions at the end
237
+ const footnoteNodes = await convertFootnoteDefinitions(ctx);
238
+ result.push(...footnoteNodes);
239
+ return result;
240
+ }
241
+ // ============================================
242
+ // Paragraph Conversion
243
+ // ============================================
244
+ async function convertParagraph(el, ctx) {
245
+ const pPr = getFirstChildElement(el, 'pPr');
246
+ const styleId = getParagraphStyleId(pPr);
247
+ // Check for heading
248
+ if (styleId && ctx.headingStyles.has(styleId)) {
249
+ const depth = ctx.headingStyles.get(styleId);
250
+ const inlines = await convertRuns(el, ctx);
251
+ if (inlines.length === 0)
252
+ return null;
253
+ return {
254
+ type: 'heading',
255
+ depth: Math.min(Math.max(depth, 1), 6),
256
+ children: inlines,
257
+ };
258
+ }
259
+ // Check for blockquote
260
+ if (styleId && ctx.quoteStyles.has(styleId)) {
261
+ const inlines = await convertRuns(el, ctx);
262
+ if (inlines.length === 0)
263
+ return null;
264
+ const paragraph = { type: 'paragraph', children: inlines };
265
+ return { type: 'blockquote', children: [paragraph] };
266
+ }
267
+ // Check for code block
268
+ if (styleId && ctx.codeStyles.has(styleId)) {
269
+ const text = getElementTextContent(el);
270
+ return { type: 'code', value: text };
271
+ }
272
+ // Regular paragraph
273
+ const inlines = await convertRuns(el, ctx);
274
+ if (inlines.length === 0)
275
+ return null;
276
+ return { type: 'paragraph', children: inlines };
277
+ }
278
+ // ============================================
279
+ // Run (Inline) Conversion
280
+ // ============================================
281
+ async function convertRuns(paragraphEl, ctx) {
282
+ const result = [];
283
+ const children = Array.from(paragraphEl.children);
284
+ for (const child of children) {
285
+ const localName = child.localName;
286
+ if (localName === 'r') {
287
+ const inlines = await convertRun(child, ctx);
288
+ result.push(...inlines);
289
+ }
290
+ else if (localName === 'hyperlink') {
291
+ const link = await convertHyperlink(child, ctx);
292
+ if (link)
293
+ result.push(link);
294
+ }
295
+ // Skip pPr, bookmarkStart, bookmarkEnd, etc.
296
+ }
297
+ return mergeAdjacentText(result);
298
+ }
299
+ async function convertRun(runEl, ctx) {
300
+ const result = [];
301
+ const rPr = getFirstChildElement(runEl, 'rPr');
302
+ const format = parseRunFormat(rPr, ctx);
303
+ for (const child of Array.from(runEl.children)) {
304
+ const localName = child.localName;
305
+ if (localName === 't') {
306
+ const text = child.textContent ?? '';
307
+ if (!text)
308
+ continue;
309
+ if (format.code) {
310
+ result.push({ type: 'inlineCode', value: text });
311
+ }
312
+ else {
313
+ let node = { type: 'text', value: text };
314
+ if (format.strike) {
315
+ node = { type: 'delete', children: [node] };
316
+ }
317
+ if (format.italic) {
318
+ node = { type: 'emphasis', children: [node] };
319
+ }
320
+ if (format.bold) {
321
+ node = { type: 'strong', children: [node] };
322
+ }
323
+ result.push(node);
324
+ }
325
+ }
326
+ else if (localName === 'br') {
327
+ result.push({ type: 'break' });
328
+ }
329
+ else if (localName === 'footnoteReference') {
330
+ const fnId = getAttr(child, 'id');
331
+ if (fnId && fnId !== '0' && fnId !== '-1') {
332
+ result.push({
333
+ type: 'footnoteReference',
334
+ identifier: `fn${fnId}`,
335
+ });
336
+ }
337
+ }
338
+ else if (localName === 'drawing' || localName === 'pict') {
339
+ const img = await extractImage(child, ctx);
340
+ if (img)
341
+ result.push(img);
342
+ }
343
+ }
344
+ return result;
345
+ }
346
+ function parseRunFormat(rPr, ctx) {
347
+ if (!rPr)
348
+ return { bold: false, italic: false, strike: false, code: false };
349
+ const bold = hasChildElement(rPr, 'b') && !isFalseToggle(getFirstChildElement(rPr, 'b'));
350
+ const italic = hasChildElement(rPr, 'i') && !isFalseToggle(getFirstChildElement(rPr, 'i'));
351
+ const strike = hasChildElement(rPr, 'strike') && !isFalseToggle(getFirstChildElement(rPr, 'strike'));
352
+ // Check for inline code via character style
353
+ const rStyle = getFirstChildElement(rPr, 'rStyle');
354
+ const charStyleId = rStyle ? getAttr(rStyle, 'val') : null;
355
+ const isCodeStyle = charStyleId ? ctx.inlineCodeStyles.has(charStyleId) : false;
356
+ // Check for monospace font as a code indicator
357
+ const rFonts = getFirstChildElement(rPr, 'rFonts');
358
+ const fontName = rFonts ? (getAttr(rFonts, 'ascii') ?? getAttr(rFonts, 'hAnsi') ?? '') : '';
359
+ const isMonospace = /consolas|courier|mono/i.test(fontName);
360
+ return { bold, italic, strike, code: isCodeStyle || isMonospace };
361
+ }
362
+ function isFalseToggle(el) {
363
+ const val = getAttr(el, 'val');
364
+ return val === '0' || val === 'false';
365
+ }
366
+ // ============================================
367
+ // Hyperlink Conversion
368
+ // ============================================
369
+ async function convertHyperlink(el, ctx) {
370
+ const rId = el.getAttributeNS(NS_R, 'id') ?? el.getAttribute('r:id');
371
+ let url = '';
372
+ if (rId) {
373
+ const rel = ctx.documentRels.get(rId);
374
+ if (rel)
375
+ url = rel.target;
376
+ }
377
+ // Also check for w:anchor (internal bookmarks)
378
+ if (!url) {
379
+ const anchor = el.getAttributeNS(NS_WML, 'anchor') ?? el.getAttribute('w:anchor');
380
+ if (anchor)
381
+ url = `#${anchor}`;
382
+ }
383
+ const inlines = [];
384
+ for (const child of Array.from(el.children)) {
385
+ if (child.localName === 'r') {
386
+ inlines.push(...(await convertRun(child, ctx)));
387
+ }
388
+ }
389
+ if (inlines.length === 0)
390
+ return null;
391
+ return {
392
+ type: 'link',
393
+ url,
394
+ children: mergeAdjacentText(inlines),
395
+ };
396
+ }
397
+ // ============================================
398
+ // Image Extraction
399
+ // ============================================
400
+ async function extractImage(_el, _ctx) {
401
+ // Image extraction is complex (DrawingML with blip references).
402
+ // For v1, emit a placeholder. Full implementation requires:
403
+ // 1. Find <a:blip r:embed="rId_"/> in the drawing tree
404
+ // 2. Resolve the rId to a media path
405
+ // 3. Extract the binary data and encode as data URI
406
+ // This is planned for a future enhancement.
407
+ return {
408
+ type: 'image',
409
+ url: '',
410
+ alt: 'Image',
411
+ };
412
+ }
413
+ async function collectList(elements, startIdx, ctx) {
414
+ const firstNumPr = getNumPr(elements[startIdx]);
415
+ const { numId } = firstNumPr;
416
+ // Determine if ordered from numbering definition
417
+ const numInfo = ctx.numbering.get(numId);
418
+ const isOrdered = numInfo?.levels.get(0) ?? false;
419
+ const items = [];
420
+ let consumed = 0;
421
+ let i = startIdx;
422
+ while (i < elements.length) {
423
+ const el = elements[i];
424
+ if (el.localName !== 'p')
425
+ break;
426
+ const numPr = getNumPr(el);
427
+ if (!numPr || numPr.numId !== numId)
428
+ break;
429
+ // Convert this paragraph's inline content
430
+ const inlines = await convertRuns(el, ctx);
431
+ if (inlines.length > 0) {
432
+ const paragraph = { type: 'paragraph', children: inlines };
433
+ // Check if this is a nested list item
434
+ if (numPr.ilvl > firstNumPr.ilvl) {
435
+ // Collect nested list items
436
+ const nested = await collectNestedList(elements, i, ctx, firstNumPr.ilvl);
437
+ if (items.length > 0) {
438
+ // Attach nested list to the last item
439
+ const lastItem = items[items.length - 1];
440
+ const nestedIsOrdered = numInfo?.levels.get(numPr.ilvl) ?? false;
441
+ const nestedList = {
442
+ type: 'list',
443
+ ordered: nestedIsOrdered,
444
+ children: nested.items,
445
+ };
446
+ lastItem.children.push(nestedList);
447
+ }
448
+ i += nested.consumed;
449
+ consumed += nested.consumed;
450
+ continue;
451
+ }
452
+ items.push({
453
+ type: 'listItem',
454
+ children: [paragraph],
455
+ });
456
+ }
457
+ i++;
458
+ consumed++;
459
+ }
460
+ return {
461
+ node: {
462
+ type: 'list',
463
+ ordered: isOrdered,
464
+ children: items,
465
+ },
466
+ consumed,
467
+ };
468
+ }
469
+ async function collectNestedList(elements, startIdx, ctx, parentIlvl) {
470
+ const items = [];
471
+ let consumed = 0;
472
+ let i = startIdx;
473
+ while (i < elements.length) {
474
+ const el = elements[i];
475
+ if (el.localName !== 'p')
476
+ break;
477
+ const numPr = getNumPr(el);
478
+ if (!numPr)
479
+ break;
480
+ if (numPr.ilvl <= parentIlvl)
481
+ break;
482
+ const inlines = await convertRuns(el, ctx);
483
+ if (inlines.length > 0) {
484
+ const paragraph = { type: 'paragraph', children: inlines };
485
+ items.push({ type: 'listItem', children: [paragraph] });
486
+ }
487
+ i++;
488
+ consumed++;
489
+ }
490
+ return { items, consumed };
491
+ }
492
+ function getNumPr(el) {
493
+ const pPr = getFirstChildElement(el, 'pPr');
494
+ if (!pPr)
495
+ return null;
496
+ const numPr = getFirstChildElement(pPr, 'numPr');
497
+ if (!numPr)
498
+ return null;
499
+ const ilvlEl = getFirstChildElement(numPr, 'ilvl');
500
+ const numIdEl = getFirstChildElement(numPr, 'numId');
501
+ const ilvlVal = ilvlEl ? getAttr(ilvlEl, 'val') : null;
502
+ const numIdVal = numIdEl ? getAttr(numIdEl, 'val') : null;
503
+ if (!numIdVal || numIdVal === '0')
504
+ return null; // numId 0 means "no list"
505
+ return {
506
+ numId: numIdVal,
507
+ ilvl: ilvlVal ? parseInt(ilvlVal, 10) : 0,
508
+ };
509
+ }
510
+ // ============================================
511
+ // Table Conversion
512
+ // ============================================
513
+ async function convertTable(tblEl, ctx) {
514
+ const rows = [];
515
+ const trEls = getAllChildElements(tblEl, 'tr');
516
+ for (let ri = 0; ri < trEls.length; ri++) {
517
+ const row = await convertTableRow(trEls[ri], ctx, ri === 0);
518
+ rows.push(row);
519
+ }
520
+ if (rows.length === 0)
521
+ return null;
522
+ // If first row isn't explicitly a header, treat it as one anyway
523
+ // (Markdown tables require a header row)
524
+ return {
525
+ type: 'table',
526
+ children: rows,
527
+ };
528
+ }
529
+ async function convertTableRow(trEl, ctx, isHeader) {
530
+ const cells = [];
531
+ const tcEls = getAllChildElements(trEl, 'tc');
532
+ for (const tc of tcEls) {
533
+ const cell = await convertTableCell(tc, ctx, isHeader);
534
+ cells.push(cell);
535
+ }
536
+ return { type: 'tableRow', children: cells };
537
+ }
538
+ async function convertTableCell(tcEl, ctx, isHeader) {
539
+ const inlines = [];
540
+ // A cell can contain multiple paragraphs; flatten them with breaks
541
+ const paragraphs = getAllChildElements(tcEl, 'p');
542
+ for (let pi = 0; pi < paragraphs.length; pi++) {
543
+ if (pi > 0) {
544
+ inlines.push({ type: 'break' });
545
+ }
546
+ const runs = await convertRuns(paragraphs[pi], ctx);
547
+ inlines.push(...runs);
548
+ }
549
+ return {
550
+ type: 'tableCell',
551
+ isHeader,
552
+ children: mergeAdjacentText(inlines),
553
+ };
554
+ }
555
+ // ============================================
556
+ // Footnote Definition Conversion
557
+ // ============================================
558
+ async function convertFootnoteDefinitions(ctx) {
559
+ const results = [];
560
+ for (const [id, el] of ctx.footnotes) {
561
+ const children = [];
562
+ const paragraphs = getAllChildElements(el, 'p');
563
+ for (const p of paragraphs) {
564
+ const inlines = await convertRuns(p, ctx);
565
+ if (inlines.length > 0) {
566
+ children.push({
567
+ type: 'paragraph',
568
+ children: inlines,
569
+ });
570
+ }
571
+ }
572
+ if (children.length > 0) {
573
+ results.push({
574
+ type: 'footnoteDefinition',
575
+ identifier: `fn${id}`,
576
+ children,
577
+ });
578
+ }
579
+ }
580
+ return results;
581
+ }
582
+ // ============================================
583
+ // XML Helper Utilities
584
+ // ============================================
585
+ /**
586
+ * Get the first element child with a given local name.
587
+ * Handles both namespaced and non-namespaced elements.
588
+ */
589
+ function getFirstChildElement(parent, localName) {
590
+ for (const child of Array.from(parent.children ?? [])) {
591
+ if (child.localName === localName)
592
+ return child;
593
+ }
594
+ return null;
595
+ }
596
+ /**
597
+ * Get all direct child elements with a given local name.
598
+ */
599
+ function getAllChildElements(parent, localName) {
600
+ const result = [];
601
+ for (const child of Array.from(parent.children)) {
602
+ if (child.localName === localName)
603
+ result.push(child);
604
+ }
605
+ return result;
606
+ }
607
+ /**
608
+ * Get all elements with a given local name in the document.
609
+ */
610
+ function getAllElements(doc, localName) {
611
+ // Try namespace-aware first
612
+ const nsEls = 'getElementsByTagNameNS' in doc ? doc.getElementsByTagNameNS(NS_WML, localName) : null;
613
+ if (nsEls && nsEls.length > 0)
614
+ return Array.from(nsEls);
615
+ // Fallback: try with w: prefix
616
+ const prefixed = doc.getElementsByTagName(`w:${localName}`);
617
+ if (prefixed.length > 0)
618
+ return Array.from(prefixed);
619
+ // Final fallback: bare name
620
+ return Array.from(doc.getElementsByTagName(localName));
621
+ }
622
+ /**
623
+ * Get the first element with given local name in the document or subtree.
624
+ */
625
+ function getFirstElement(doc, localName) {
626
+ const els = getAllElements(doc, localName);
627
+ return els.length > 0 ? els[0] : null;
628
+ }
629
+ /**
630
+ * Get a w:-prefixed attribute, trying namespace-aware first then fallback.
631
+ */
632
+ function getAttr(el, localName) {
633
+ return el.getAttributeNS(NS_WML, localName) || el.getAttribute(`w:${localName}`) || null;
634
+ }
635
+ /**
636
+ * Check if an element has a direct child with the given local name.
637
+ */
638
+ function hasChildElement(parent, localName) {
639
+ return getFirstChildElement(parent, localName) !== null;
640
+ }
641
+ /**
642
+ * Get the paragraph style ID from a pPr element.
643
+ */
644
+ function getParagraphStyleId(pPr) {
645
+ if (!pPr)
646
+ return null;
647
+ const pStyle = getFirstChildElement(pPr, 'pStyle');
648
+ if (!pStyle)
649
+ return null;
650
+ return getAttr(pStyle, 'val');
651
+ }
652
+ /**
653
+ * Get all text content from an element (concatenating all w:t descendants).
654
+ */
655
+ function getElementTextContent(el) {
656
+ const parts = [];
657
+ function walk(node) {
658
+ if (node.localName === 't') {
659
+ parts.push(node.textContent ?? '');
660
+ }
661
+ for (const child of Array.from(node.children)) {
662
+ walk(child);
663
+ }
664
+ }
665
+ walk(el);
666
+ return parts.join('');
667
+ }
668
+ /**
669
+ * Merge adjacent MarkdownText nodes to reduce fragmentation.
670
+ */
671
+ function mergeAdjacentText(nodes) {
672
+ if (nodes.length <= 1)
673
+ return nodes;
674
+ const result = [];
675
+ for (const node of nodes) {
676
+ const prev = result[result.length - 1];
677
+ if (node.type === 'text' && prev?.type === 'text') {
678
+ // Merge into previous text node
679
+ prev.value += node.value;
680
+ }
681
+ else {
682
+ result.push(node);
683
+ }
684
+ }
685
+ return result;
686
+ }
687
+ //# sourceMappingURL=import.js.map