reamkit 1.33.2 → 1.34.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 (71) hide show
  1. package/README.md +13 -10
  2. package/dist/esm/core/converter/facade.js +1 -0
  3. package/dist/esm/core/converter/ream.d.ts +6 -6
  4. package/dist/esm/core/converter/ream.js +11 -5
  5. package/dist/esm/core/document-model/index.d.ts +1 -1
  6. package/dist/esm/core/document-model/types.d.ts +15 -0
  7. package/dist/esm/core/drawingml/text-warp.d.ts +34 -0
  8. package/dist/esm/core/drawingml/text-warp.js +49 -1
  9. package/dist/esm/core/font/index.d.ts +1 -1
  10. package/dist/esm/core/font/index.js +8 -0
  11. package/dist/esm/core/font/indic-shaper.d.ts +47 -0
  12. package/dist/esm/core/font/indic-shaper.js +1146 -0
  13. package/dist/esm/core/font/indic-table.d.ts +12 -0
  14. package/dist/esm/core/font/indic-table.js +463 -0
  15. package/dist/esm/core/font/measure.d.ts +45 -4
  16. package/dist/esm/core/font/measure.js +253 -14
  17. package/dist/esm/core/font/opentype-lookups.d.ts +214 -0
  18. package/dist/esm/core/font/opentype-lookups.js +1112 -0
  19. package/dist/esm/core/fonts/families.d.ts +4 -0
  20. package/dist/esm/core/fonts/families.js +43 -12
  21. package/dist/esm/core/fonts/provider.js +1 -0
  22. package/dist/esm/core/fonts/remote-fonts.d.ts +4 -2
  23. package/dist/esm/core/fonts/remote-fonts.js +36 -0
  24. package/dist/esm/core/fonts/scripts.d.ts +19 -0
  25. package/dist/esm/core/fonts/scripts.js +96 -1
  26. package/dist/esm/core/numbering/apply.js +12 -8
  27. package/dist/esm/core/style-cascade/index.d.ts +1 -1
  28. package/dist/esm/core/style-cascade/index.js +2 -2
  29. package/dist/esm/core/style-cascade/resolver.d.ts +9 -0
  30. package/dist/esm/core/style-cascade/resolver.js +31 -3
  31. package/dist/esm/core/style-cascade/table.js +3 -8
  32. package/dist/esm/excel/xlsx-to-pdf.js +1 -0
  33. package/dist/esm/index.js +1 -0
  34. package/dist/esm/layout/line-paint.d.ts +5 -4
  35. package/dist/esm/layout/line-paint.js +6 -5
  36. package/dist/esm/layout/page-doc.d.ts +26 -0
  37. package/dist/esm/layout/styled-layout.d.ts +8 -1
  38. package/dist/esm/layout/styled-layout.js +459 -112
  39. package/dist/esm/layout/undrawn.d.ts +18 -0
  40. package/dist/esm/layout/undrawn.js +59 -0
  41. package/dist/esm/pdf/cid-font.d.ts +5 -0
  42. package/dist/esm/pdf/cid-font.js +8 -2
  43. package/dist/esm/pdf/styled-page-emitter.js +117 -28
  44. package/dist/esm/pdf/text-page-renderer.js +1 -0
  45. package/dist/esm/pdf-reader/content.d.ts +1 -1
  46. package/dist/esm/pdf-reader/content.js +48 -13
  47. package/dist/esm/pdf-reader/embedded-fonts.js +1 -0
  48. package/dist/esm/pdf-reader/encodings.d.ts +9 -0
  49. package/dist/esm/pdf-reader/encodings.js +66 -1
  50. package/dist/esm/pdf-reader/face-outlines.js +1 -0
  51. package/dist/esm/pdf-reader/text.d.ts +10 -0
  52. package/dist/esm/pdf-reader/text.js +25 -3
  53. package/dist/esm/pdf-reader/vector.js +2 -2
  54. package/dist/esm/pptx/embedded-fonts.js +1 -0
  55. package/dist/esm/pptx/sp-helpers.js +4 -1
  56. package/dist/esm/svg/svg-text.js +30 -7
  57. package/dist/esm/word/doc/doc-reader.js +148 -57
  58. package/dist/esm/word/doc/doc-text.d.ts +80 -11
  59. package/dist/esm/word/doc/doc-text.js +438 -75
  60. package/dist/esm/word/document-parser.js +48 -28
  61. package/dist/esm/word/docx-to-pdf.d.ts +5 -5
  62. package/dist/esm/word/docx-to-pdf.js +9 -7
  63. package/dist/esm/word/docx-writer.js +3 -5
  64. package/dist/esm/word/font-embed.js +1 -0
  65. package/dist/esm/word/font-table.js +1 -0
  66. package/dist/esm/word/numbering-parser.js +3 -0
  67. package/dist/esm/word/paragraph-properties.js +2 -2
  68. package/dist/esm/word/run-properties.js +2 -2
  69. package/dist/esm/word/xml-helpers.d.ts +18 -0
  70. package/dist/esm/word/xml-helpers.js +48 -1
  71. package/package.json +7 -2
@@ -1,4 +1,5 @@
1
- import { PDF_NULL, PdfName, PdfStream } from "../pdf/objects.js";
1
+ import { PDF_NULL, PdfHexString, PdfName, PdfStream } from "../pdf/objects.js";
2
+ import { pdfTextString } from "./encodings.js";
2
3
  import { buildColorSpaceMap, buildShadingMap } from "./shading.js";
3
4
  import { IDENTITY, interpretContent, multiply } from "./content.js";
4
5
  import { textMarkupOf } from "./annot-draw.js";
@@ -275,8 +276,29 @@ function spacesOf(file, resources) {
275
276
  spaceCache.set(resources, made);
276
277
  return made;
277
278
  }
279
+ /**
280
+ * §14.9.4 — the replacement text of each property list the page's
281
+ * `/Properties` names: the content says `/Span /P1 BDC` and the `/ActualText`
282
+ * is in the resources, not in the stream.
283
+ *
284
+ * @param file The owning file.
285
+ * @param resources The resource dictionary in force.
286
+ * @returns Name → the text, for the lists that hold one.
287
+ */
288
+ function actualTextsOf(file, resources) {
289
+ const out = /* @__PURE__ */ new Map();
290
+ const props = resources ? file.get(resources, "Properties") : void 0;
291
+ if (!(props instanceof Map)) return out;
292
+ for (const [name, value] of props) {
293
+ const list = file.resolve(value);
294
+ const text = list instanceof Map ? file.get(list, "ActualText") : void 0;
295
+ if (text instanceof PdfHexString) out.set(name, pdfTextString(text.bytes));
296
+ else if (typeof text === "string") out.set(name, pdfTextString(Uint8Array.from(text, (c) => c.charCodeAt(0) & 255)));
297
+ }
298
+ return out;
299
+ }
278
300
  function collectRuns(file, resources, content, baseCtm, depth, visiting, out, shown) {
279
- const result = interpretContent(content, buildFonts(file, resources), baseCtm, shadingsOf(file, resources), void 0, spacesOf(file, resources), hiddenProperties(file, resources));
301
+ const result = interpretContent(content, buildFonts(file, resources), baseCtm, shadingsOf(file, resources), void 0, spacesOf(file, resources), hiddenProperties(file, resources), actualTextsOf(file, resources));
280
302
  out.push(...result.texts.map((r) => withPatternColour(file, resources, r, visiting)));
281
303
  if (shown) addShown(shown, result.shown);
282
304
  if (depth >= MAX_FORM_DEPTH) return;
@@ -411,4 +433,4 @@ function inRect(x, y, r) {
411
433
  return x >= r[0] - 1 && x <= r[2] + 1 && y >= r[1] - 2 && y <= r[3] + 2;
412
434
  }
413
435
  //#endregion
414
- export { buildFonts, extractPageText };
436
+ export { actualTextsOf, buildFonts, extractPageText };
@@ -4,7 +4,7 @@ import { buildAlphaMap, buildColorSpaceMap, buildShadingMap, gradientShading, sh
4
4
  import { IDENTITY, interpretContent, multiply } from "./content.js";
5
5
  import { appearanceContent, collectPageAppearances } from "./annots.js";
6
6
  import { hiddenProperties, hiddenXObject } from "./optional-content.js";
7
- import { buildFonts } from "./text.js";
7
+ import { actualTextsOf, buildFonts } from "./text.js";
8
8
  import { patternTint, tintedHex } from "./pattern-tint.js";
9
9
  //#region src/pdf-reader/vector.ts
10
10
  function paintedVectors(file, page) {
@@ -53,7 +53,7 @@ function paintedVectors(file, page) {
53
53
  const xobjects = resources ? file.get(resources, "XObject") : PDF_NULL;
54
54
  const xobjDict = xobjects instanceof Map ? xobjects : void 0;
55
55
  const maps = mapsOf(resources);
56
- const result = interpretContent(content, buildFonts(file, resources), baseCtm, maps.shadings, maps.alphas, maps.spaces, hiddenProperties(file, resources));
56
+ const result = interpretContent(content, buildFonts(file, resources), baseCtm, maps.shadings, maps.alphas, maps.spaces, hiddenProperties(file, resources), actualTextsOf(file, resources));
57
57
  for (const paint of result.shadings) {
58
58
  if (out.length >= MAX_VECTORS) break;
59
59
  const dict = resources ? file.get(resources, "Shading") : PDF_NULL;
@@ -1,4 +1,5 @@
1
1
  import { FontRegistry } from "../core/font/font-registry.js";
2
+ import "../core/font/index.js";
2
3
  import { poAttr, poChildren, poIs } from "../core/po-helpers.js";
3
4
  //#region src/pptx/embedded-fonts.ts
4
5
  var REL_FONT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/font";
@@ -85,7 +85,10 @@ function rPrToRunProps(rPr, colors, themeFonts) {
85
85
  ...u !== void 0 ? { underline: u === "none" ? "none" : "single" } : {},
86
86
  ...sz !== void 0 ? { fontSizePt: pt(sz / 100) } : {},
87
87
  ...colorHex ? { colorHex } : {},
88
- ...typeface ? { fontFamily: { ascii: typeface } } : {},
88
+ ...typeface ? { fontFamily: {
89
+ ascii: typeface,
90
+ hAnsi: typeface
91
+ } } : {},
89
92
  ...capsOf(poAttr(rPr, "cap"))
90
93
  };
91
94
  }
@@ -1,7 +1,7 @@
1
1
  import { svgPathData } from "../core/vector.js";
2
2
  import { reverseByCodePoint } from "../core/bidi/index.js";
3
3
  import { FAUX_ITALIC_SHEAR, computeJustifyExtra, lineVisualOrder } from "../layout/line-paint.js";
4
- import { warpGlyphMatrix } from "../core/drawingml/text-warp.js";
4
+ import { warpGlyphMatrix, warpGlyphRun } from "../core/drawingml/text-warp.js";
5
5
  import { sanitizeHref } from "../core/links.js";
6
6
  import { cffOutlineSource, openTypeCff } from "../pdf-reader/cff-outline.js";
7
7
  import { outlineSource } from "../pdf-reader/glyf-outline.js";
@@ -128,10 +128,16 @@ function emitTextToken(out, tok, x, baseline, ctx) {
128
128
  const scale = (tok.synthetic?.widthScale ?? 1) * (tok.resolvedRun.widthScale ?? 1);
129
129
  const slant = tok.synthetic?.italic === true ? FAUX_ITALIC_SHEAR : 0;
130
130
  const rise = tok.risePt ?? 0;
131
- const text = tok.bidiLevel % 2 === 1 ? reverseByCodePoint(tok.text) : tok.text;
131
+ const reversed = tok.bidiLevel % 2 === 1;
132
+ const text = reversed ? reverseByCodePoint(tok.text) : tok.text;
132
133
  const run = tok.font.measure.glyphRun(text);
133
134
  const upem = tok.font.parsed.unitsPerEm;
134
135
  const spacingEm = (tok.resolvedRun.letterSpacingPt ?? 0) / (size * scale);
136
+ const outline = tok.resolvedRun.textOutline;
137
+ const strokeWidth = outline !== void 0 ? outline.widthPt : tok.synthetic?.bold === true ? size * .03 : 0;
138
+ const colours = reversed ? tok.charColorsHex?.slice().reverse() : tok.charColorsHex;
139
+ const sources = colours ? tok.font.measure.glyphSources(text) : void 0;
140
+ const paint = (hex) => hex === void 0 || hex === tok.resolvedRun.colorHex ? "" : ` fill="#${hex}"${strokeWidth > 0 && outline === void 0 ? ` stroke="#${hex}"` : ""}`;
135
141
  const uses = [];
136
142
  let pen = 0;
137
143
  for (let g = 0; g < run.gids.length; g++) {
@@ -140,12 +146,16 @@ function emitTextToken(out, tok, x, baseline, ctx) {
140
146
  emitAsText(out, tok, x, baseline);
141
147
  return;
142
148
  }
143
- if (id !== null) uses.push(`<use href="#${id}"${pen !== 0 ? ` x="${fmtEm(pen)}"` : ""}/>`);
144
- pen += (run.advances[g] ?? 0) / upem + spacingEm;
149
+ const gx = pen + (run.xOffsets?.[g] ?? 0) / upem;
150
+ const gy = (run.yOffsets?.[g] ?? 0) / upem;
151
+ if (id !== null) {
152
+ const fill = paint(sources ? colours?.[sources[g]] : void 0);
153
+ uses.push(`<use href="#${id}"${gx !== 0 ? ` x="${fmtEm(gx)}"` : ""}${gy !== 0 ? ` y="${fmtEm(gy)}"` : ""}${fill}/>`);
154
+ }
155
+ const spaced = run.clusterEnds ? run.clusterEnds[g] === true : true;
156
+ pen += (run.advances[g] ?? 0) / upem + (spaced ? spacingEm : 0);
145
157
  }
146
158
  if (uses.length === 0) return;
147
- const outline = tok.resolvedRun.textOutline;
148
- const strokeWidth = outline !== void 0 ? outline.widthPt : tok.synthetic?.bold === true ? size * .03 : 0;
149
159
  const stroke = strokeWidth > 0 ? ` stroke="#${outline?.colorHex ?? tok.resolvedRun.colorHex}" stroke-width="${fmtEm(strokeWidth / size)}"` : "";
150
160
  const group = `<g transform="matrix(${fmt(size * scale)} 0 ${fmt(slant * size)} ${fmt(-size)} ${fmt(x)} ${fmt(baseline - rise)})" fill="#${tok.resolvedRun.colorHex}"${stroke}>${uses.join("")}</g>`;
151
161
  const href = tok.href !== void 0 ? sanitizeHref(tok.href) : void 0;
@@ -282,6 +292,19 @@ function emitWarped(out, item, ctx) {
282
292
  continue;
283
293
  }
284
294
  const size = tok.fontSizePt;
295
+ const group = (m, uses, hex = tok.resolvedRun.colorHex) => `<g transform="matrix(${fmt(m[0] * size)} ${fmt(m[1] * size)} ${fmt(m[2] * size)} ${fmt(m[3] * size)} ${fmt(m[4])} ${fmt(m[5])})" fill="#${hex}">${uses}</g>`;
296
+ if (tok.font.measure.clustered(tok.text)) {
297
+ const glyphs = warpGlyphRun(warp, tok.font.measure.glyphRun(tok.text), cursor, tok.widthPt, size / tok.font.parsed.unitsPerEm, item.baselineY);
298
+ const colours = tok.charColorsHex;
299
+ const sources = colours ? tok.font.measure.glyphSources(tok.text) : void 0;
300
+ for (const g of glyphs) {
301
+ const id = ctx.glyphs.use(tok.font, g.gid);
302
+ const hex = sources ? colours?.[sources[g.index]] : void 0;
303
+ if (id) out.push(group(g.matrix, `<use href="#${id}"/>`, hex));
304
+ }
305
+ cursor += tok.widthPt;
306
+ continue;
307
+ }
285
308
  const chars = [...tok.text];
286
309
  const raw = chars.map((c) => tok.font.measure.textWidthPt(c, size));
287
310
  const total = raw.reduce((a, b) => a + b, 0);
@@ -301,7 +324,7 @@ function emitWarped(out, item, ctx) {
301
324
  pen += (run.advances[g] ?? 0) / upem;
302
325
  }
303
326
  if (uses.length === 0) continue;
304
- out.push(`<g transform="matrix(${fmt(m[0] * size)} ${fmt(m[1] * size)} ${fmt(m[2] * size)} ${fmt(m[3] * size)} ${fmt(m[4])} ${fmt(m[5])})" fill="#${tok.resolvedRun.colorHex}">${uses.join("")}</g>`);
327
+ out.push(group(m, uses.join("")));
305
328
  }
306
329
  }
307
330
  }
@@ -27,6 +27,11 @@ var TABLE_BORDERS = {
27
27
  insideH: TABLE_BORDER,
28
28
  insideV: TABLE_BORDER
29
29
  };
30
+ var LVL_FOLLOW = [
31
+ " ",
32
+ " ",
33
+ ""
34
+ ];
30
35
  function createListMarkers(tables) {
31
36
  const counters = /* @__PURE__ */ new Map();
32
37
  const resolve = (ilfo, ilvl) => {
@@ -43,7 +48,8 @@ function createListMarkers(tables) {
43
48
  const r = resolve(ilfo, ilvl);
44
49
  if (!r) return void 0;
45
50
  const { lsid, lvl } = r;
46
- if (lvl.nfc === 23 || lvl.nfc === 255 || !lvl.xst.some((cu) => cu <= 8)) return lvl.xst.map((cu) => String.fromCharCode(cu)).join("");
51
+ const follow = LVL_FOLLOW[lvl.follow] ?? " ";
52
+ if (lvl.nfc === 23 || lvl.nfc === 255 || !lvl.xst.some((cu) => cu <= 8)) return lvl.xst.map((cu) => String.fromCharCode(cu)).join("") + follow;
47
53
  const col = counters.get(lsid) ?? [];
48
54
  col[ilvl] = (col[ilvl] ?? lvl.iStartAt - 1) + 1;
49
55
  for (let l = ilvl + 1; l < col.length; l++) col[l] = void 0;
@@ -53,7 +59,7 @@ function createListMarkers(tables) {
53
59
  const other = resolve(ilfo, cu);
54
60
  out += formatListNumber(other?.lvl.nfc ?? 0, col[cu] ?? other?.lvl.iStartAt ?? 1);
55
61
  } else out += String.fromCharCode(cu);
56
- return out;
62
+ return out + follow;
57
63
  } };
58
64
  }
59
65
  function formatListNumber(nfc, n) {
@@ -128,54 +134,79 @@ var DOC_ENCRYPTED_LOSS = {
128
134
  function readDoc(bytes) {
129
135
  const content = extractDocContent(bytes);
130
136
  const resources = new ResourceStore();
131
- const body = buildBody(content, resources);
137
+ const { body, sectionEnds } = buildBody(content, resources);
132
138
  const headersFooters = /* @__PURE__ */ new Map();
133
- const headers = [];
134
- const footers = [];
135
- const addStory = (story, kind, type) => {
136
- if (!story) return;
137
- const id = `${kind}-${type}`;
138
- const els = resolveBodyStyles(story.flatMap((p) => mapParagraph(p, resources)), EMPTY_STYLE_SHEET);
139
- headersFooters.set(id, els);
140
- (kind === "header" ? headers : footers).push({
141
- type,
142
- relationshipId: id
143
- });
139
+ const bandIds = /* @__PURE__ */ new Map();
140
+ const bands = (index) => {
141
+ const headers = [];
142
+ const footers = [];
143
+ const hf = content.headerFooters?.[index];
144
+ const add = (story, kind, type) => {
145
+ if (!story) return;
146
+ let id = bandIds.get(story);
147
+ if (id === void 0) {
148
+ id = `${kind}-${type}-${index}`;
149
+ bandIds.set(story, id);
150
+ headersFooters.set(id, resolveBodyStyles(story.flatMap((p) => mapParagraph(p, resources)), EMPTY_STYLE_SHEET));
151
+ }
152
+ (kind === "header" ? headers : footers).push({
153
+ type,
154
+ relationshipId: id
155
+ });
156
+ };
157
+ add(hf?.defaultHeader, "header", "default");
158
+ add(hf?.firstHeader, "header", "first");
159
+ add(hf?.evenHeader, "header", "even");
160
+ add(hf?.defaultFooter, "footer", "default");
161
+ add(hf?.firstFooter, "footer", "first");
162
+ add(hf?.evenFooter, "footer", "even");
163
+ return {
164
+ headers,
165
+ footers
166
+ };
144
167
  };
145
- const hf = content.headerFooters;
146
- if (hf) {
147
- addStory(hf.defaultHeader, "header", "default");
148
- addStory(hf.firstHeader, "header", "first");
149
- addStory(hf.evenHeader, "header", "even");
150
- addStory(hf.defaultFooter, "footer", "default");
151
- addStory(hf.firstFooter, "footer", "first");
152
- addStory(hf.evenFooter, "footer", "even");
153
- }
154
- const ps = content.pageSize;
155
- const pageSize = ps ? {
156
- width: pt(ps.widthPt),
157
- height: pt(ps.heightPt),
158
- orientation: ps.widthPt > ps.heightPt ? "landscape" : "portrait"
159
- } : {
160
- width: pt(612),
161
- height: pt(792)
168
+ const sectionProperties = (sec, index) => {
169
+ const ps = sec?.pageSize;
170
+ const sectionStart = sec?.bkc !== void 0 ? sectionStartOf(sec.bkc) : void 0;
171
+ const pageNumberFormat = sec?.pageNumberNfc !== void 0 ? pageNumberFormatOf(sec.pageNumberNfc) : void 0;
172
+ return {
173
+ pageSize: ps ? {
174
+ width: pt(ps.widthPt),
175
+ height: pt(ps.heightPt),
176
+ orientation: ps.widthPt > ps.heightPt ? "landscape" : "portrait"
177
+ } : {
178
+ width: pt(612),
179
+ height: pt(792)
180
+ },
181
+ margins: toMargins(sec?.margins),
182
+ ...bands(index),
183
+ ...sec?.titlePage ? { titlePg: true } : {},
184
+ ...content.facingPages ? { evenAndOddHeaders: true } : {},
185
+ ...sectionStart ? { sectionStart } : {},
186
+ ...sec?.pageNumberStart !== void 0 ? { pageNumberStart: sec.pageNumberStart } : {},
187
+ ...pageNumberFormat ? { pageNumberFormat } : {}
188
+ };
162
189
  };
190
+ const docSections = content.sections ?? [];
191
+ const sections = [];
192
+ for (const end of sectionEnds) {
193
+ const last = sections[sections.length - 1]?.endIndex ?? 0;
194
+ if (end.endIndex <= last || end.endIndex >= body.length) continue;
195
+ sections.push({
196
+ properties: sectionProperties(docSections[end.section], end.section),
197
+ endIndex: end.endIndex
198
+ });
199
+ }
200
+ if (sections.length > 0) sections.push({
201
+ properties: sectionProperties(docSections[docSections.length - 1], docSections.length - 1),
202
+ endIndex: body.length
203
+ });
163
204
  return {
164
205
  doc: {
165
206
  kind: "flow",
166
207
  body: resolveBodyStyles(body, EMPTY_STYLE_SHEET),
167
- sections: [],
168
- section: {
169
- pageSize,
170
- margins: {
171
- top: pt(72),
172
- right: pt(72),
173
- bottom: pt(72),
174
- left: pt(72)
175
- },
176
- headers,
177
- footers
178
- },
208
+ sections,
209
+ section: sectionProperties(docSections[0], 0),
179
210
  styles: EMPTY_STYLE_SHEET,
180
211
  resources,
181
212
  ...headersFooters.size > 0 ? { headersFooters } : {},
@@ -186,12 +217,16 @@ function readDoc(bytes) {
186
217
  }
187
218
  function buildBody(content, resources) {
188
219
  const markers = createListMarkers(content.listTables);
189
- const body = groupTables(content.paragraphs, resources, markers);
220
+ const sectionEnds = [];
221
+ const body = groupTables(content.paragraphs, resources, markers, sectionEnds);
190
222
  while (body.length > 1 && isEmptyParagraph(body[body.length - 1])) body.pop();
191
223
  if (body.length === 0) body.push(emptyParagraph());
192
- return body;
224
+ return {
225
+ body,
226
+ sectionEnds
227
+ };
193
228
  }
194
- function groupTables(paras, resources, markers) {
229
+ function groupTables(paras, resources, markers, sectionEnds) {
195
230
  const out = [];
196
231
  let i = 0;
197
232
  while (i < paras.length) if (paras[i].props.inTable) {
@@ -201,7 +236,14 @@ function groupTables(paras, resources, markers) {
201
236
  kind: "table",
202
237
  table: buildTable(group, resources, markers)
203
238
  });
204
- } else out.push(...mapParagraph(paras[i++], resources, markers));
239
+ } else {
240
+ const p = paras[i++];
241
+ out.push(...mapParagraph(p, resources, markers));
242
+ if (p.sectionEnd !== void 0) sectionEnds.push({
243
+ section: p.sectionEnd,
244
+ endIndex: out.length
245
+ });
246
+ }
205
247
  return out;
206
248
  }
207
249
  function buildTable(group, resources, markers) {
@@ -331,16 +373,24 @@ function mapParagraph(p, resources, markers) {
331
373
  const textRuns = p.runs.filter((r) => r.picture === void 0);
332
374
  const ilvl = p.props.listIlvl ?? 0;
333
375
  const isList = (p.props.listIlfo ?? 0) > 0;
334
- if (textRuns.some((r) => r.text.length > 0) || isList) {
376
+ if (textRuns.some((r) => r.text.length > 0 || r.pageBreak || r.field) || isList) {
335
377
  const runs = textRuns.map((r) => ({
336
378
  text: r.text,
337
- properties: toRunProperties(r.props)
379
+ properties: toRunProperties(r.props),
380
+ ...r.pageBreak ? { pageBreak: true } : {},
381
+ ...r.columnBreak ? { columnBreak: true } : {},
382
+ ...r.field ? { field: r.field } : {}
338
383
  }));
384
+ const last = textRuns[textRuns.length - 1];
385
+ if (last?.pageBreak || last?.columnBreak) runs.push({
386
+ text: ZERO_WIDTH_SPACE,
387
+ properties: p.mark ? toRunProperties(p.mark) : {}
388
+ });
339
389
  let properties = toParaProperties(p.props);
340
390
  if (isList) {
341
- const marker = markers?.marker(p.props.listIlfo, ilvl) ?? LIST_BULLETS[ilvl % LIST_BULLETS.length];
391
+ const marker = markers?.marker(p.props.listIlfo, ilvl) ?? `${LIST_BULLETS[ilvl % LIST_BULLETS.length]} `;
342
392
  runs.unshift({
343
- text: `${marker} `,
393
+ text: marker,
344
394
  properties: {}
345
395
  });
346
396
  properties = {
@@ -360,7 +410,7 @@ function mapParagraph(p, resources, markers) {
360
410
  });
361
411
  }
362
412
  for (const r of p.runs) if (r.picture) out.push(imageBlock(r.picture, resources, toParaProperties(p.props)));
363
- if (out.length === 0) out.push(emptyParagraph());
413
+ if (out.length === 0) out.push(emptyParagraph(toParaProperties(p.props), p.mark));
364
414
  return out;
365
415
  }
366
416
  function imageBlock(pic, resources, paragraphProperties) {
@@ -374,14 +424,14 @@ function imageBlock(pic, resources, paragraphProperties) {
374
424
  }
375
425
  };
376
426
  }
377
- function emptyParagraph() {
427
+ function emptyParagraph(properties = {}, mark = void 0) {
378
428
  return {
379
429
  kind: "paragraph",
380
430
  paragraph: {
381
- properties: {},
431
+ properties,
382
432
  runs: [{
383
433
  text: ZERO_WIDTH_SPACE,
384
- properties: {}
434
+ properties: mark ? toRunProperties(mark) : {}
385
435
  }]
386
436
  }
387
437
  };
@@ -392,13 +442,24 @@ function isEmptyParagraph(el) {
392
442
  function toRunProperties(p) {
393
443
  const underline = p.underlineKul ? underlineStyle(p.underlineKul) : void 0;
394
444
  const size = p.sizeHalfPts && p.sizeHalfPts > 0 ? pt(p.sizeHalfPts / 2) : void 0;
445
+ const fontFamily = toFontFamily(p);
395
446
  return {
396
447
  ...p.bold ? { bold: true } : {},
397
448
  ...p.italic ? { italic: true } : {},
398
449
  ...underline ? { underline } : {},
399
- ...size ? { fontSizePt: size } : {}
450
+ ...size ? { fontSizePt: size } : {},
451
+ ...fontFamily ? { fontFamily } : {}
400
452
  };
401
453
  }
454
+ function toFontFamily(p) {
455
+ const fonts = {
456
+ ...p.fontAscii ? { ascii: p.fontAscii } : {},
457
+ ...p.fontOther ? { hAnsi: p.fontOther } : {},
458
+ ...p.fontEastAsia ? { eastAsia: p.fontEastAsia } : {},
459
+ ...p.fontBi ? { cs: p.fontBi } : {}
460
+ };
461
+ return Object.keys(fonts).length > 0 ? fonts : void 0;
462
+ }
402
463
  function toParaProperties(p) {
403
464
  const alignment = p.jc !== void 0 ? alignmentFromJc(p.jc) : void 0;
404
465
  return {
@@ -410,7 +471,8 @@ function toParaProperties(p) {
410
471
  ...p.spaceAfterTwips !== void 0 ? { spacingAfter: pt(p.spaceAfterTwips / 20) } : {},
411
472
  ...p.widowControl !== void 0 ? { widowControl: p.widowControl } : {},
412
473
  ...p.keepNext !== void 0 ? { keepNext: p.keepNext } : {},
413
- ...p.keepLines !== void 0 ? { keepLines: p.keepLines } : {}
474
+ ...p.keepLines !== void 0 ? { keepLines: p.keepLines } : {},
475
+ ...p.pageBreakBefore !== void 0 ? { pageBreakBefore: p.pageBreakBefore } : {}
414
476
  };
415
477
  }
416
478
  function underlineStyle(kul) {
@@ -422,6 +484,35 @@ function underlineStyle(kul) {
422
484
  default: return "single";
423
485
  }
424
486
  }
487
+ function toMargins(m) {
488
+ const side = (twips) => twipsToPt(twips ?? 1440);
489
+ return {
490
+ top: side(m?.topTwips),
491
+ right: side(m?.rightTwips),
492
+ bottom: side(m?.bottomTwips),
493
+ left: side(m?.leftTwips),
494
+ ...m?.headerTwips !== void 0 ? { header: twipsToPt(m.headerTwips) } : {},
495
+ ...m?.footerTwips !== void 0 ? { footer: twipsToPt(m.footerTwips) } : {},
496
+ ...m?.gutterTwips ? { gutter: twipsToPt(m.gutterTwips) } : {}
497
+ };
498
+ }
499
+ function sectionStartOf(bkc) {
500
+ switch (bkc) {
501
+ case 0: return "continuous";
502
+ case 3: return "evenPage";
503
+ case 4: return "oddPage";
504
+ default: return "nextPage";
505
+ }
506
+ }
507
+ function pageNumberFormatOf(nfc) {
508
+ switch (nfc) {
509
+ case 1: return "upperRoman";
510
+ case 2: return "lowerRoman";
511
+ case 3: return "upperLetter";
512
+ case 4: return "lowerLetter";
513
+ default: return;
514
+ }
515
+ }
425
516
  function alignmentFromJc(jc) {
426
517
  switch (jc) {
427
518
  case 1: return "center";
@@ -6,6 +6,14 @@ export interface DocCharProps {
6
6
  readonly underlineKul?: number;
7
7
  /** Font size in half-points. */
8
8
  readonly sizeHalfPts?: number;
9
+ /** The font of ASCII characters, by name — `sprmCRgFtc0`. */
10
+ readonly fontAscii?: string;
11
+ /** The font of East Asian characters — `sprmCRgFtc1`. */
12
+ readonly fontEastAsia?: string;
13
+ /** The font of the characters neither ASCII nor East Asian — `sprmCRgFtc2`. */
14
+ readonly fontOther?: string;
15
+ /** The font of complex-script characters — `sprmCFtcBi`. */
16
+ readonly fontBi?: string;
9
17
  /** The character is special — `sprmCFSpec` (e.g. a picture placeholder). */
10
18
  readonly fSpec?: boolean;
11
19
  /** `sprmCPicLocation` — offset into the `Data` stream of the picture's PICF. */
@@ -80,6 +88,8 @@ export interface DocParaProps {
80
88
  readonly keepNext?: boolean;
81
89
  /** `sprmPFKeep` — every line on one page. */
82
90
  readonly keepLines?: boolean;
91
+ /** `sprmPFPageBreakBefore` — the paragraph starts a new page. */
92
+ readonly pageBreakBefore?: boolean;
83
93
  }
84
94
  /** One maximal same-formatting run of text, or an embedded picture. */
85
95
  export interface DocRun {
@@ -87,6 +97,15 @@ export interface DocRun {
87
97
  readonly props: DocCharProps;
88
98
  /** Set instead of `text` when the run is an embedded picture. */
89
99
  readonly picture?: DocPicture;
100
+ /** The run is a manual page break (0x0C), alone in a run with no text. */
101
+ readonly pageBreak?: boolean;
102
+ /** The run is a manual column break (0x0E), alone in a run whose text is `\n`. */
103
+ readonly columnBreak?: boolean;
104
+ /**
105
+ * The run is the result of a PAGE or NUMPAGES field, whose `text` is the
106
+ * number Word cached when it last saved.
107
+ */
108
+ readonly field?: 'PAGE' | 'NUMPAGES';
90
109
  }
91
110
  /** One paragraph: its runs plus the decoded paragraph properties. */
92
111
  export interface DocParagraph {
@@ -97,11 +116,59 @@ export interface DocParagraph {
97
116
  * closes a table cell. With `props.rowEnd` it also closes the row.
98
117
  */
99
118
  readonly cellMark?: boolean;
119
+ /**
120
+ * The paragraph was ended by the section mark of the (zero-based) section it
121
+ * is the last of — the index into {@link DocContent.sections}.
122
+ */
123
+ readonly sectionEnd?: number;
124
+ /** The paragraph mark's own character properties: what an empty line is set in. */
125
+ readonly mark?: DocCharProps;
126
+ }
127
+ /** One section of the main document (§2.8.26 PlcfSed): what its SEP says. */
128
+ export interface DocSection {
129
+ /**
130
+ * `sprmSBkc` — where the section starts: 0 on the page in hand, 1 in the next
131
+ * column, 2 on a new page, 3 on an even page, 4 on an odd page. Absent when
132
+ * the SEP says nothing, which is a new page.
133
+ */
134
+ readonly bkc?: number;
135
+ /** The page size in points (xaPage/yaPage), when present and sane. */
136
+ readonly pageSize?: {
137
+ readonly widthPt: number;
138
+ readonly heightPt: number;
139
+ };
140
+ /** `sprmSFTitlePage` — the first page shows the first-page header and footer. */
141
+ readonly titlePage?: boolean;
142
+ /** The margins the SEP states, in twips; each absent one is Word's default. */
143
+ readonly margins?: DocMargins;
144
+ /**
145
+ * `sprmSFPgnRestart` with `sprmSPgnStart97` — the number the section's first
146
+ * page is printed with, when the section starts its count again; absent, the
147
+ * count runs on from the section before.
148
+ */
149
+ readonly pageNumberStart?: number;
150
+ /** `sprmSNfcPgn` — the MSONFC the section's page numbers print in; absent, arabic. */
151
+ readonly pageNumberNfc?: number;
152
+ }
153
+ /**
154
+ * A section's margins in twips (§2.6.4 sprmSDxaLeft … sprmSDzaGutter). A top or
155
+ * bottom margin is stored signed, negative for one the header or footer may not
156
+ * push; its size is the same either way.
157
+ */
158
+ export interface DocMargins {
159
+ readonly leftTwips?: number;
160
+ readonly rightTwips?: number;
161
+ readonly topTwips?: number;
162
+ readonly bottomTwips?: number;
163
+ readonly headerTwips?: number;
164
+ readonly footerTwips?: number;
165
+ readonly gutterTwips?: number;
100
166
  }
101
167
  /**
102
- * The section's header/footer stories (best-effort: the binary story ordering
103
- * cannot be validated against a real file, so only well-formed, non-empty stories
104
- * are surfaced). `default` is the odd-page (primary) header/footer.
168
+ * One section's header/footer stories ([MS-DOC] Plcfhdd: six per section, after
169
+ * the six separator stories). `default` is the odd-page (primary) one. A story
170
+ * the section leaves empty is the previous section's, the very same array; one
171
+ * that holds no text is absent.
105
172
  */
106
173
  export interface DocHeaderFooters {
107
174
  readonly defaultHeader?: ReadonlyArray<DocParagraph>;
@@ -121,6 +188,8 @@ export interface DocLvl {
121
188
  readonly nfc: number;
122
189
  readonly iStartAt: number;
123
190
  readonly xst: ReadonlyArray<number>;
191
+ /** `ixchFollow` — what follows the number text: 0 a tab, 1 a space, 2 nothing. */
192
+ readonly follow: number;
124
193
  }
125
194
  /**
126
195
  * The document's list tables (DOC-10): an LFO → lsid map (a paragraph's 1-based
@@ -136,23 +205,23 @@ export interface DocListTables {
136
205
  /** Everything extracted from a `.doc`'s `WordDocument` stream. */
137
206
  export interface DocContent {
138
207
  readonly paragraphs: ReadonlyArray<DocParagraph>;
139
- readonly headerFooters?: DocHeaderFooters;
208
+ /** Each section's header/footer stories, in section order. */
209
+ readonly headerFooters?: ReadonlyArray<DocHeaderFooters>;
210
+ /** `Dop.fFacingPages` — even pages show the even header and footer. */
211
+ readonly facingPages?: boolean;
140
212
  /** The list tables resolving a paragraph's (ilfo, ilvl) to its number format. */
141
213
  readonly listTables?: DocListTables;
142
214
  /**
143
- * The first section's page size in points (xaPage/yaPage from its SEP), when
144
- * present and sane; absent ⇒ the reader's default (US Letter) applies.
215
+ * Every section of the main document, in order, when the PlcfSed is usable. A
216
+ * section with no page size takes the reader's default (US Letter).
145
217
  */
146
- readonly pageSize?: {
147
- readonly widthPt: number;
148
- readonly heightPt: number;
149
- };
218
+ readonly sections?: ReadonlyArray<DocSection>;
150
219
  /** The file is encrypted/obfuscated — text cannot be read without the key. */
151
220
  readonly encrypted: boolean;
152
221
  }
153
222
  /**
154
223
  * Extract a `.doc`'s `WordDocument` stream into its main-document paragraphs plus
155
- * the section's header/footer stories, list tables and first-section page size.
224
+ * the section's header/footer stories, list tables and sections.
156
225
  * Joins the piece table (CLX → CPs/FCs), CHPX run formatting and PAPX paragraph
157
226
  * formatting, splitting at the CR / cell marks. Every offset is bounds-checked, so
158
227
  * a structurally odd file yields no text rather than throwing.