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,8 +1,9 @@
1
1
  import { resolveFamilyStyle } from "../core/fonts/remote-fonts.js";
2
- import { scriptForCodepoint } from "../core/fonts/scripts.js";
3
- import { shapeText } from "../core/font/opentype-layout.js";
2
+ import { fontSlotOf, scriptForCodepoint } from "../core/fonts/scripts.js";
4
3
  import { hasSubstitutable, substituteLetters } from "../core/font/ligatures.js";
5
- import { createFontMeasure } from "../core/font/measure.js";
4
+ import { hasIndic, syllableStarts } from "../core/font/indic-shaper.js";
5
+ import { createFontMeasure, shapeGlyphs } from "../core/font/measure.js";
6
+ import "../core/font/index.js";
6
7
  import { pageItemBounds } from "./page-doc.js";
7
8
  import { halfPtToPt, pt } from "../core/ir/units.js";
8
9
  import { ResourceStore } from "../core/ir/resources.js";
@@ -31,6 +32,7 @@ import { rectPath } from "../core/drawingml/preset-geometry.js";
31
32
  import { buildShapePaths, buildShapeTransform, buildStroke, gradientToSolid, lineEndPaths } from "../core/drawingml/shape-render.js";
32
33
  import { StructTreeBuilder } from "../pdf/struct-tree.js";
33
34
  import { floatIntoFrame, turnOntoSheet } from "./turned-section.js";
35
+ import { undrawnLoss } from "./undrawn.js";
34
36
  //#region src/layout/styled-layout.ts
35
37
  function parsePdfAProfile(pdfA) {
36
38
  const part = Number(pdfA.charAt(6));
@@ -584,7 +586,7 @@ function layoutStyledDocument(body, options) {
584
586
  const group = bodyFrames.starts.get(idx);
585
587
  if (group) return layoutFrameBlock(group, ctx.options ?? options, fontResources, imageResources, width);
586
588
  if (bodyFrames.continued.has(idx)) return emptyBlock(options);
587
- return layoutBodyElement(own, ctx.options ?? options, fontResources, imageResources, width, ctx.pageContentHeight, box);
589
+ return layoutBodyElement(own, ctx.options ?? options, fontResources, imageResources, width, ctx.pageContentHeight, box, true);
588
590
  });
589
591
  for (let i = 0; i < blocks.length; i++) {
590
592
  const here = blocks[i];
@@ -696,9 +698,14 @@ function layoutStyledDocument(body, options) {
696
698
  }
697
699
  const bookmarks = /* @__PURE__ */ new Map();
698
700
  const commentNotes = options.commentAnnotations && options.comments ? buildCommentNotes(noteAssigned.comments, options.comments) : void 0;
699
- const reflowParagraph = (paragraph, width, widths) => layoutParagraphBlock(paragraph, options, fontResources, imageResources, width, widths);
701
+ const reflowParagraph = (paragraph, width, widths) => layoutParagraphBlock(paragraph, options, fontResources, imageResources, width, widths, true);
702
+ const pages = paginateSections(blocks, sectionCtxs, structBuilder, options.language ?? "en-US", notePlan, bookmarks, reflowParagraph, backgroundImageOf(options, imageResources), collapsesSpacing(options), options.compatibilityMode);
703
+ if (options.losses) {
704
+ const undrawn = undrawnLoss(pages);
705
+ if (undrawn) options.losses.push(undrawn);
706
+ }
700
707
  return {
701
- pages: paginateSections(blocks, sectionCtxs, structBuilder, options.language ?? "en-US", notePlan, bookmarks, reflowParagraph, backgroundImageOf(options, imageResources), collapsesSpacing(options), options.compatibilityMode),
708
+ pages,
702
709
  resources: options.resources ?? new ResourceStore(),
703
710
  fontResources,
704
711
  imageResources,
@@ -1328,8 +1335,8 @@ function laidOutBlocksFor(elements, options, fontResources, contentWidth, imageR
1328
1335
  return layoutBodyElement(el, options, fontResources, imageResources, contentWidth, void 0, box);
1329
1336
  });
1330
1337
  }
1331
- function layoutBodyElement(el, options, fontResources, imageResources, contentWidth, maxHeight, box) {
1332
- if (el.kind === "paragraph") return layoutParagraphBlock(el.paragraph, options, fontResources, imageResources, contentWidth);
1338
+ function layoutBodyElement(el, options, fontResources, imageResources, contentWidth, maxHeight, box, pageFlow = false) {
1339
+ if (el.kind === "paragraph") return layoutParagraphBlock(el.paragraph, options, fontResources, imageResources, contentWidth, void 0, pageFlow);
1333
1340
  if (el.kind === "table") return layoutTableBlock(el.table, options, fontResources, imageResources, contentWidth);
1334
1341
  if (el.kind === "image") {
1335
1342
  const res = el.image.resource ? imageResources?.get(el.image.resource) : void 0;
@@ -2891,13 +2898,13 @@ function runFontKeyAndParsed(options, ascii, bold, italic) {
2891
2898
  /**
2892
2899
  * The face a CHARACTER falls back to when the run's own has no glyph for it.
2893
2900
  *
2894
- * The curated substitutes are Latin families: Han, Kana, Hangul, Arabic, Thai
2895
- * and the geometric symbols are a notdef box in every one of them, and 2145
2896
- * characters of the corpus are exactly those. A face for the character's
2897
- * writing system is fetched when the document holds one (see `addScriptFonts`),
2898
- * and this is where a run reaches it — followed by every other family already
2899
- * loaded, because a glyph one of them happens to carry is still better than a
2900
- * box.
2901
+ * The curated substitutes are Latin families: Han, Kana, Hangul, Arabic, Thai,
2902
+ * the scripts of India and the geometric symbols are a notdef box in every one
2903
+ * of them, and 2145 characters of the corpus are exactly those. A face for the
2904
+ * character's writing system is fetched when the document holds one (see
2905
+ * `addScriptFonts`), and this is where a run reaches it — followed by every
2906
+ * other family already loaded, because a glyph one of them happens to carry is
2907
+ * still better than a box.
2901
2908
  *
2902
2909
  * Called from BOTH walks that need it — the glyph-subset collection and the
2903
2910
  * tokenizer — so the face a character is measured, subset and drawn with is one
@@ -2943,19 +2950,17 @@ function fallbackFaceKey(options, cp, bold, italic) {
2943
2950
  * Only where nobody can draw the character itself: one a face DOES carry is
2944
2951
  * drawn as written.
2945
2952
  *
2946
- * @param options The render options (the loaded registries).
2947
- * @param primary The run's own face.
2948
- * @param resolved Its resolved properties (weight and slant).
2949
- * @param text The run's text.
2953
+ * @param faces The face each character of the run is drawn with ({@link runFaces}).
2954
+ * @param text The run's text.
2950
2955
  * @returns The text to set, which is `text` itself unless something was undrawable.
2951
2956
  */
2952
- function drawableText(options, primary, resolved, text) {
2957
+ function drawableText(faces, text) {
2953
2958
  if (!hasSubstitutable(text)) return text;
2954
2959
  let out = "";
2955
2960
  for (const ch of text) {
2956
2961
  const cp = ch.codePointAt(0);
2957
2962
  const letters = substituteLetters(cp);
2958
- const drawable = letters === void 0 || primary.glyphForCodepoint(cp) !== 0 || fallbackFaceKey(options, cp, resolved.bold, resolved.italic) !== void 0;
2963
+ const drawable = letters === void 0 || faces(cp).parsed.glyphForCodepoint(cp) !== 0;
2959
2964
  out += drawable ? ch : letters;
2960
2965
  }
2961
2966
  return out;
@@ -2994,7 +2999,7 @@ function syntheticFace(variant, bold, italic, widthScale) {
2994
2999
  */
2995
3000
  function splitByFace(plan, text) {
2996
3001
  const first = faceOf(plan, text.codePointAt(0) ?? 0);
2997
- if (!plan.faceFor || plan.faceFor.size === 0) return [{
3002
+ if (text === "" || !plan.faceFor || plan.faceFor.size === 0) return [{
2998
3003
  text,
2999
3004
  face: first
3000
3005
  }];
@@ -3029,24 +3034,120 @@ function faceOf(plan, cp) {
3029
3034
  ...plan.synthetic ? { synthetic: plan.synthetic } : {}
3030
3035
  };
3031
3036
  }
3037
+ var INDIC_JOINS = /* @__PURE__ */ new Set([
3038
+ 8204,
3039
+ 8205,
3040
+ 9676,
3041
+ 160,
3042
+ 2404,
3043
+ 2405
3044
+ ]);
3045
+ /**
3046
+ * Whether a family has a face of its OWN on hand — embedded, or a substitute
3047
+ * fetched for it — rather than the first registry, which is where
3048
+ * {@link runFontKeyAndParsed} sends a family nothing was fetched for.
3049
+ *
3050
+ * @param options The render options (the loaded faces).
3051
+ * @param family The family's name.
3052
+ */
3053
+ function hasOwnFace(options, family) {
3054
+ if (options.embeddedFonts?.has(family.trim().toLowerCase()) === true) return true;
3055
+ return options.registriesByFamily?.has(resolveFamilyStyle(family).key) === true;
3056
+ }
3057
+ /**
3058
+ * §17.3.2.26 — the faces a run's other slots name, where one is on hand and is
3059
+ * not the face its ascii slot names.
3060
+ *
3061
+ * @param options The render options (the loaded faces).
3062
+ * @param resolved The run's resolved properties.
3063
+ * @param primary The run's own face, its ascii slot's.
3064
+ * @returns The face per slot, or `undefined` when every slot draws in `primary`.
3065
+ */
3066
+ function slotFaces(options, resolved, primary) {
3067
+ let out;
3068
+ for (const slot of [
3069
+ "hAnsi",
3070
+ "eastAsia",
3071
+ "cs"
3072
+ ]) {
3073
+ const family = resolved.fontFamily[slot];
3074
+ if (family === void 0 || !hasOwnFace(options, family)) continue;
3075
+ const face = runFontKeyAndParsed(options, family, resolved.bold, resolved.italic);
3076
+ if (face.fontKey !== primary.fontKey) (out ??= /* @__PURE__ */ new Map()).set(slot, face);
3077
+ }
3078
+ return out;
3079
+ }
3080
+ /**
3081
+ * {@link fallbackFaceKey}, with what the face lacks of the run's weight and
3082
+ * slant: a script face is fetched in the regular weight only, and a bold run in
3083
+ * it is stroked, which is what F3's synthesis is for.
3084
+ */
3085
+ function fallbackFace(options, cp, resolved) {
3086
+ const face = fallbackFaceKey(options, cp, resolved.bold, resolved.italic);
3087
+ if (!face) return void 0;
3088
+ const variant = face.fontKey.split(":")[1];
3089
+ const got = variant === "bold" || variant === "boldItalic" || variant === "italic" ? variant : "regular";
3090
+ return {
3091
+ ...face,
3092
+ ...syntheticFace(got, resolved.bold, resolved.italic)
3093
+ };
3094
+ }
3095
+ /**
3096
+ * §17.3.2.26 — the face each character of a run is drawn with: the one its
3097
+ * slot names ({@link fontSlotOf}), else the run's own, else whichever face on
3098
+ * hand has the glyph ({@link fallbackFaceKey}).
3099
+ *
3100
+ * A run names a face per slot, and Word sets each character in its slot's:
3101
+ * "split" in a run's ascii Verdana and "Кириллица" beside it in its hAnsi
3102
+ * Courier New. We drew both in Verdana's substitute, and the hAnsi face was
3103
+ * read for nothing but the line's height. A slot whose family has no face on
3104
+ * hand leaves its characters to the run's own, as before.
3105
+ *
3106
+ * Called from every walk that needs it — the glyph-subset collection, the
3107
+ * tokenizer, a table cell's measure — so the face a character is measured,
3108
+ * subset and drawn with is one decision made once.
3109
+ *
3110
+ * @param options The render options (the loaded faces).
3111
+ * @param resolved The run's resolved properties (its faces, weight, slant, direction).
3112
+ * @param primary The run's own face, the one its ascii slot names.
3113
+ * @param text The run's text, for the scripts of India (see {@link INDIC_JOINS}).
3114
+ * @returns The face a code point of the run is drawn with.
3115
+ */
3116
+ function runFaces(options, resolved, primary, text) {
3117
+ const slots = slotFaces(options, resolved, primary);
3118
+ const drawing = (cp) => {
3119
+ const own = slots?.get(fontSlotOf(cp, resolved.rtl)) ?? primary;
3120
+ if (own.parsed.glyphForCodepoint(cp) !== 0) return own;
3121
+ if (own !== primary && primary.parsed.glyphForCodepoint(cp) !== 0) return primary;
3122
+ return fallbackFace(options, cp, resolved) ?? own;
3123
+ };
3124
+ const indic = indicFace(drawing, text);
3125
+ if (!indic) return drawing;
3126
+ return (cp) => INDIC_JOINS.has(cp) && indic.parsed.glyphForCodepoint(cp) !== 0 ? indic : drawing(cp);
3127
+ }
3128
+ /** The face that draws the first letter of the scripts of India in `text`. */
3129
+ function indicFace(drawing, text) {
3130
+ for (const ch of text) {
3131
+ const cp = ch.codePointAt(0);
3132
+ if (cp < 2304 || cp > 3455 || cp === 2404 || cp === 2405) continue;
3133
+ return drawing(cp);
3134
+ }
3135
+ }
3032
3136
  /**
3033
3137
  * `text` cut into the stretches ONE face draws, by the same decision the
3034
- * tokenizer makes — the whole string as a single span when the run's own face
3035
- * covers it, which is nearly always.
3138
+ * tokenizer makes — the whole string as a single span when one face draws all
3139
+ * of it, which is nearly always.
3036
3140
  *
3037
- * @param options The render options (the loaded registries).
3038
- * @param primary The run's own face.
3039
- * @param resolved Its resolved properties (weight and slant).
3040
- * @param text The run's text.
3141
+ * @param faces The face each character of the run is drawn with ({@link runFaces}).
3142
+ * @param text The run's text.
3041
3143
  */
3042
- function faceSpans(options, primary, resolved, text) {
3144
+ function faceSpans(faces, text) {
3043
3145
  const out = [];
3044
- let cur = primary;
3146
+ let cur;
3045
3147
  let buf = "";
3046
3148
  for (const ch of text) {
3047
- const cp = ch.codePointAt(0);
3048
- const face = primary.parsed.glyphForCodepoint(cp) !== 0 ? primary : fallbackFaceKey(options, cp, resolved.bold, resolved.italic) ?? primary;
3049
- if (buf !== "" && face.fontKey !== cur.fontKey) {
3149
+ const face = faces(ch.codePointAt(0));
3150
+ if (cur && face.fontKey !== cur.fontKey) {
3050
3151
  out.push({
3051
3152
  ...cur,
3052
3153
  text: buf
@@ -3056,37 +3157,34 @@ function faceSpans(options, primary, resolved, text) {
3056
3157
  cur = face;
3057
3158
  buf += ch;
3058
3159
  }
3059
- if (buf !== "") out.push({
3160
+ if (cur) out.push({
3060
3161
  ...cur,
3061
3162
  text: buf
3062
3163
  });
3063
3164
  return out;
3064
3165
  }
3065
3166
  /**
3066
- * The face for every character of `text` the run's own cannot draw.
3167
+ * The face for every character of `text` the run's own does not draw.
3067
3168
  *
3068
- * @param options The render options (the loaded registries).
3069
3169
  * @param fontResources The faces the document embeds, by key.
3070
3170
  * @param primary The run's own face.
3071
- * @param resolved Its resolved properties (weight and slant).
3171
+ * @param faces The face each character is drawn with ({@link runFaces}).
3072
3172
  * @param text The run's text.
3073
3173
  * @returns A map from code point to face, or `undefined` when the run's own
3074
3174
  * face draws all of it.
3075
3175
  */
3076
- function facesForRun(options, fontResources, primary, resolved, text) {
3176
+ function facesForRun(fontResources, primary, faces, text) {
3077
3177
  let out;
3078
3178
  for (const ch of text) {
3079
3179
  const cp = ch.codePointAt(0);
3080
- if (out?.has(cp) === true || primary.glyphForCodepoint(cp) !== 0) continue;
3081
- const fallback = fallbackFaceKey(options, cp, resolved.bold, resolved.italic);
3082
- if (!fallback) continue;
3083
- const font = fontResources.get(fallback.fontKey);
3180
+ if (out?.has(cp) === true) continue;
3181
+ const face = faces(cp);
3182
+ if (face.fontKey === primary.fontKey) continue;
3183
+ const font = fontResources.get(face.fontKey);
3084
3184
  if (!font) continue;
3085
- out ??= /* @__PURE__ */ new Map();
3086
- const { variant } = { variant: fallback.fontKey.split(":")[1] ?? "" };
3087
- out.set(cp, {
3185
+ (out ??= /* @__PURE__ */ new Map()).set(cp, {
3088
3186
  font,
3089
- ...syntheticFace(variant === "bold" || variant === "boldItalic" || variant === "italic" ? variant : "regular", resolved.bold, resolved.italic)
3187
+ ...face.synthetic ? { synthetic: face.synthetic } : {}
3090
3188
  });
3091
3189
  }
3092
3190
  return out;
@@ -3099,7 +3197,8 @@ function collectFontResources(body, headersFooters, options, imageResources) {
3099
3197
  const numerals = [...new Set([options.section, ...(options.sections ?? []).map((sec) => sec.properties)].flatMap((p) => p?.pageNumberFormat !== void 0 ? [p.pageNumberFormat] : []))].map((fmt) => Array.from({ length: 200 }, (_, i) => formatCounter(fmt, i + 1)).join("")).join("");
3100
3198
  const addRun = (run, para) => {
3101
3199
  const resolved = resolveRunProperties(run.properties, para.properties, options.styles);
3102
- const { fontKey, parsed } = runFontKeyAndParsed(options, resolved.fontFamily.ascii, resolved.bold, resolved.italic);
3200
+ const primary = runFontKeyAndParsed(options, resolved.fontFamily.ascii, resolved.bold, resolved.italic);
3201
+ const { fontKey, parsed } = primary;
3103
3202
  let bucket = used.get(fontKey);
3104
3203
  if (!bucket) {
3105
3204
  bucket = {
@@ -3108,11 +3207,9 @@ function collectFontResources(body, headersFooters, options, imageResources) {
3108
3207
  };
3109
3208
  used.set(fontKey, bucket);
3110
3209
  }
3111
- const text = drawableText(options, parsed, resolved, run.listMarker === true ? markerText(run.text, parsed) : run.text);
3112
- for (const span of faceSpans(options, {
3113
- fontKey,
3114
- parsed
3115
- }, resolved, text)) {
3210
+ const faces = runFaces(options, resolved, primary, run.text);
3211
+ const text = drawableText(faces, run.text);
3212
+ for (const span of faceSpans(faces, text)) {
3116
3213
  let sink = used.get(span.fontKey);
3117
3214
  if (!sink) {
3118
3215
  sink = {
@@ -3121,8 +3218,13 @@ function collectFontResources(body, headersFooters, options, imageResources) {
3121
3218
  };
3122
3219
  used.set(span.fontKey, sink);
3123
3220
  }
3124
- const shaped = shapeText(span.text, span.parsed.glyphForCodepoint, span.parsed.advanceWidths, span.parsed.ligatures, span.parsed.kerning, span.parsed.joiningForms);
3221
+ const shown = run.listMarker === true ? markerText(span.text, span.parsed) : span.text;
3222
+ const shaped = shapeGlyphs(span.parsed, shown);
3125
3223
  for (const g of shaped.gids) sink.gids.add(g);
3224
+ for (const [g, t] of shaped.glyphText ?? []) {
3225
+ sink.glyphText ??= /* @__PURE__ */ new Map();
3226
+ if (!sink.glyphText.has(g)) sink.glyphText.set(g, t);
3227
+ }
3126
3228
  }
3127
3229
  if (run.text.includes(" ")) for (const stop of resolveParagraphProperties(para.properties, options.styles).tabs) {
3128
3230
  const ch = stop.leader === void 0 ? void 0 : TAB_LEADER_CHARS.get(stop.leader);
@@ -3287,7 +3389,8 @@ function collectFontResources(body, headersFooters, options, imageResources) {
3287
3389
  resourceName,
3288
3390
  parsed: info.parsed,
3289
3391
  measure: createFontMeasure(info.parsed, options.layoutProfile !== "word"),
3290
- gids: info.gids
3392
+ gids: info.gids,
3393
+ ...info.glyphText ? { glyphText: info.glyphText } : {}
3291
3394
  });
3292
3395
  }
3293
3396
  return out;
@@ -3342,7 +3445,7 @@ function paragraphLeading(pp, options) {
3342
3445
  markLinePt: m.ascentPt + m.descentPt + m.lineGapPt
3343
3446
  };
3344
3447
  }
3345
- function layoutParagraphBlock(paragraph, options, fontResources, imageResources, contentWidth, lineWidths) {
3448
+ function layoutParagraphBlock(paragraph, options, fontResources, imageResources, contentWidth, lineWidths, pageFlow = false) {
3346
3449
  const cascaded = resolveParagraphProperties(paragraph.properties, options.styles);
3347
3450
  const baseResolved = options.gridLinePitchPt !== void 0 && cascaded.snapToGrid !== false ? {
3348
3451
  ...cascaded,
@@ -3367,20 +3470,24 @@ function layoutParagraphBlock(paragraph, options, fontResources, imageResources,
3367
3470
  ...baseResolved,
3368
3471
  alignment: "right"
3369
3472
  } : baseResolved;
3370
- const tokens = tokenizeParagraph(paragraph, options, fontResources, imageResources, contentWidth, baseDir);
3473
+ const all = tokenizeParagraph(paragraph, options, fontResources, imageResources, contentWidth, baseDir);
3474
+ const tokens = pageFlow ? all : all.filter((t) => !(t.kind === "text" && t.pageBreak));
3371
3475
  const firstLineWidth = paragraphMaxWidth(resolved, contentWidth, true);
3372
3476
  const otherWidth = paragraphMaxWidth(resolved, contentWidth, false);
3373
3477
  const leading = paragraphLeading(paragraph.properties, options);
3374
3478
  const runWrap = (part, firstWidth, widths) => wrap(part, firstWidth, otherWidth, resolved, options.hyphenator, options.layoutProfile ?? "ream", leading, options.doNotExpandShiftReturn === true, widths);
3375
- const parts = splitAtColumnBreaks(tokens);
3479
+ const flow = splitAtBreaks(tokens);
3376
3480
  const columnBreakLines = /* @__PURE__ */ new Set();
3481
+ const pageBreakLines = /* @__PURE__ */ new Set();
3377
3482
  let wrapped;
3378
- if (parts.length === 1) wrapped = runWrap(tokens, firstLineWidth, lineWidths);
3483
+ if (flow.parts.length === 1) wrapped = runWrap(flow.parts[0].tokens, firstLineWidth, lineWidths);
3379
3484
  else {
3380
3485
  wrapped = [];
3381
- for (const [i, part] of parts.entries()) {
3382
- const lines = runWrap(part, i === 0 ? firstLineWidth : otherWidth, lineWidths?.slice(wrapped.length));
3383
- if (i > 0 && lines.length > 0) columnBreakLines.add(wrapped.length);
3486
+ for (const [i, part] of flow.parts.entries()) {
3487
+ const width = i === 0 ? firstLineWidth : otherWidth;
3488
+ const lines = runWrap(part.tokens, width, lineWidths?.slice(wrapped.length));
3489
+ if (lines.length === 0 && (part.opens === "page" || part.endsPage)) lines.push(emptyLine(paragraph, options, resolved, Math.max(width, 0)));
3490
+ if (i > 0 && lines.length > 0) (part.opens === "page" ? pageBreakLines : columnBreakLines).add(wrapped.length);
3384
3491
  wrapped.push(...i === 0 ? lines : lines.map((l) => ({
3385
3492
  ...l,
3386
3493
  firstLine: false
@@ -3391,10 +3498,6 @@ function layoutParagraphBlock(paragraph, options, fontResources, imageResources,
3391
3498
  let heightPt = 0;
3392
3499
  for (const line of lines) heightPt += computeLineHeight(line, resolved);
3393
3500
  const numbering = paragraph.properties.numbering;
3394
- const breakIdx = paragraph.runs.findIndex((r) => r.pageBreak === true);
3395
- const holds = (r) => r.text !== "" || r.inlineImage !== void 0 || r.math !== void 0;
3396
- const leadingPageBreak = breakIdx >= 0 && paragraph.runs.slice(0, breakIdx + 1).every((r) => r.text === "" && r.inlineImage === void 0) && paragraph.runs.slice(breakIdx + 1).some(holds);
3397
- const trailingPageBreak = paragraph.runs.some((r, i) => r.pageBreak === true && (!leadingPageBreak || i > breakIdx));
3398
3501
  return {
3399
3502
  kind: "paragraph",
3400
3503
  resolved,
@@ -3406,9 +3509,10 @@ function layoutParagraphBlock(paragraph, options, fontResources, imageResources,
3406
3509
  numId: numbering.numId,
3407
3510
  level: numbering.ilvl
3408
3511
  } } : {},
3409
- ...leadingPageBreak ? { pageBreakBefore: true } : {},
3410
- ...trailingPageBreak ? { pageBreakAfter: true } : {},
3512
+ ...flow.pageBreakBefore ? { pageBreakBefore: true } : {},
3513
+ ...flow.pageBreakAfter ? { pageBreakAfter: true } : {},
3411
3514
  ...columnBreakLines.size > 0 ? { columnBreakLines } : {},
3515
+ ...pageBreakLines.size > 0 ? { pageBreakLines } : {},
3412
3516
  ...paragraph.bookmarks && paragraph.bookmarks.length > 0 ? { bookmarks: paragraph.bookmarks } : {},
3413
3517
  source: paragraph
3414
3518
  };
@@ -3500,7 +3604,132 @@ function displayCapsRuns(paragraph, options) {
3500
3604
  start = i;
3501
3605
  }
3502
3606
  }
3503
- return any ? out : paragraph.runs;
3607
+ return mendSplitSyllables(any ? out : paragraph.runs);
3608
+ }
3609
+ /** A run of plain text: no picture, equation, field, note mark or list marker. */
3610
+ function plainTextRun(run) {
3611
+ return run.text.length > 0 && run.inlineImage === void 0 && run.math === void 0 && run.field === void 0 && run.footnoteRef === void 0 && run.endnoteRef === void 0 && run.noteNumber !== true && run.listMarker !== true;
3612
+ }
3613
+ var SYLLABLE_REACH = 32;
3614
+ /** Per code point of a run's text, the properties of the run each character was typed in. */
3615
+ function charPropertiesOf(run) {
3616
+ return run.charProperties ?? [...run.text].map(() => run.properties);
3617
+ }
3618
+ /**
3619
+ * A syllable of the scripts of India that a run boundary cuts, given whole to
3620
+ * the run it begins in. Word sets runs apart for a colour, a weight, a tracked
3621
+ * change or a spelling mark, and nothing stops one from starting at a vowel
3622
+ * sign: font issues.docx colours each sign of कृष्ण apart, and
3623
+ * Untitled 2.docx ends a bold run on the र् of a reph. Shaped run by run,
3624
+ * each sign stood alone on a dotted circle and the reph stayed a Ra and a
3625
+ * virama. The syllable now takes the formatting of the run it begins in…
3626
+ *
3627
+ * …all but its colour. Word draws each sign of कृष्ण in the colour of its
3628
+ * own run, on the one shaped syllable, so each character brought along keeps
3629
+ * the properties of the run it came from (`charProperties`), and the glyphs
3630
+ * are painted from them.
3631
+ */
3632
+ function mendSplitSyllables(runs) {
3633
+ if (!runs.some((r) => hasIndic(r.text))) return runs;
3634
+ const out = [...runs];
3635
+ let prev = -1;
3636
+ for (let k = 0; k < out.length; k++) {
3637
+ const run = out[k];
3638
+ if (!plainTextRun(run)) {
3639
+ if (run.text.length > 0 || run.inlineImage !== void 0 || run.math !== void 0) prev = -1;
3640
+ continue;
3641
+ }
3642
+ const before = prev >= 0 ? out[prev] : void 0;
3643
+ if (before && hasIndic(before.text + run.text)) {
3644
+ const tail = [...before.text].slice(-32);
3645
+ const head = [...run.text].slice(0, SYLLABLE_REACH);
3646
+ const starts = syllableStarts([...tail, ...head].join(""));
3647
+ let moved = 0;
3648
+ while (moved < head.length && !starts[tail.length + moved]) moved++;
3649
+ if (moved > 0) {
3650
+ const take = head.slice(0, moved).join("");
3651
+ const brought = charPropertiesOf(run);
3652
+ out[prev] = {
3653
+ ...before,
3654
+ text: before.text + take,
3655
+ charProperties: [...charPropertiesOf(before), ...brought.slice(0, moved)]
3656
+ };
3657
+ out[k] = {
3658
+ ...run,
3659
+ text: run.text.slice(take.length),
3660
+ ...run.charProperties ? { charProperties: brought.slice(moved) } : {}
3661
+ };
3662
+ }
3663
+ }
3664
+ if (out[k].text.length > 0) prev = k;
3665
+ }
3666
+ return out;
3667
+ }
3668
+ /**
3669
+ * The colour each character of a run is drawn in, where a split syllable
3670
+ * brought characters of other runs into it: each takes its own run's colour,
3671
+ * resolved through the styles as that run's would be.
3672
+ *
3673
+ * @param options The render options (for the style cascade).
3674
+ * @param paragraph The paragraph the run is in.
3675
+ * @param faces The face each character of the run is drawn with ({@link runFaces}).
3676
+ * @param resolved The run's resolved properties.
3677
+ * @param run The run, as {@link mendSplitSyllables} left it.
3678
+ * @param drawn Its text as drawn ({@link drawableText}).
3679
+ * @returns Per code point of `drawn`, its colour — or nothing when every
3680
+ * character is in the run's own.
3681
+ */
3682
+ function charColours(options, paragraph, faces, resolved, run, drawn) {
3683
+ const from = run.charProperties;
3684
+ if (!from) return void 0;
3685
+ const hexOf = /* @__PURE__ */ new Map([[run.properties, resolved.colorHex]]);
3686
+ const out = [];
3687
+ let i = 0;
3688
+ for (const ch of run.text) {
3689
+ const props = from[i++] ?? run.properties;
3690
+ let hex = hexOf.get(props);
3691
+ if (hex === void 0) {
3692
+ hex = resolveRunProperties(props, paragraph.properties, options.styles).colorHex;
3693
+ hexOf.set(props, hex);
3694
+ }
3695
+ const letters = drawn === run.text ? 1 : [...drawableText(faces, ch)].length;
3696
+ for (let k = 0; k < letters; k++) out.push(hex);
3697
+ }
3698
+ return out.some((hex) => hex !== resolved.colorHex) ? out : void 0;
3699
+ }
3700
+ /**
3701
+ * A token's share of the per-character colours it was cut from: the code
3702
+ * points [from, to) of them, kept only when one differs from the token's own.
3703
+ *
3704
+ * @param colours The colours of what the token was cut from, if any.
3705
+ * @param ownHex The token's own colour.
3706
+ * @param from The first code point the token takes.
3707
+ * @param to The code point after its last.
3708
+ * @returns `{ charColorsHex }` when the token needs them, `{}` when not.
3709
+ */
3710
+ function charColoursOf(colours, ownHex, from, to) {
3711
+ if (!colours) return {};
3712
+ const own = colours.slice(from, to);
3713
+ return own.some((hex) => hex !== ownHex) ? { charColorsHex: own } : {};
3714
+ }
3715
+ /**
3716
+ * A piece cut out of a text token — its code points [from, from + its text's
3717
+ * length) — with the token's formatting and its own share of the per-character
3718
+ * colours.
3719
+ */
3720
+ function tokenPiece(tok, text, from, widthPt) {
3721
+ const { charColorsHex, ...rest } = tok;
3722
+ if (!charColorsHex) return {
3723
+ ...tok,
3724
+ text,
3725
+ widthPt
3726
+ };
3727
+ return {
3728
+ ...rest,
3729
+ text,
3730
+ widthPt,
3731
+ ...charColoursOf(charColorsHex, tok.resolvedRun.colorHex, from, from + [...text].length)
3732
+ };
3504
3733
  }
3505
3734
  function tokenizeParagraph(paragraph, options, fontResources, imageResources, contentWidth, baseDir) {
3506
3735
  const plans = displayCapsRuns(paragraph, options).map((run) => {
@@ -3570,22 +3799,26 @@ function tokenizeParagraph(paragraph, options, fontResources, imageResources, co
3570
3799
  };
3571
3800
  }
3572
3801
  const resolvedRun = resolveRunProperties(run.properties, paragraph.properties, options.styles);
3573
- const { fontKey, synthetic } = runFontKeyAndParsed(options, resolvedRun.fontFamily.ascii, resolvedRun.bold, resolvedRun.italic);
3802
+ const primary = runFontKeyAndParsed(options, resolvedRun.fontFamily.ascii, resolvedRun.bold, resolvedRun.italic);
3803
+ const { fontKey, synthetic } = primary;
3574
3804
  const script = SCRIPT_OFFSET[resolvedRun.verticalAlign] ?? 0;
3575
3805
  const own = lookupFont(fontResources, fontKey);
3806
+ const faces = runFaces(options, resolvedRun, primary, run.text);
3576
3807
  const drawn = (() => {
3577
- const text = drawableText(options, own.parsed, resolvedRun, run.text);
3808
+ const text = drawableText(faces, run.text);
3578
3809
  return text === run.text ? run : {
3579
3810
  ...run,
3580
3811
  text
3581
3812
  };
3582
3813
  })();
3583
- const faceFor = facesForRun(options, fontResources, own.parsed, resolvedRun, drawn.text);
3814
+ const faceFor = facesForRun(fontResources, primary, faces, drawn.text);
3815
+ const charColorsHex = charColours(options, paragraph, faces, resolvedRun, run, drawn.text);
3584
3816
  return {
3585
3817
  run: drawn,
3586
3818
  resolvedRun,
3587
3819
  font: own,
3588
3820
  ...faceFor ? { faceFor } : {},
3821
+ ...charColorsHex ? { charColorsHex } : {},
3589
3822
  ...synthetic ? { synthetic } : {},
3590
3823
  fontSizePt: script === 0 ? resolvedRun.fontSizePt : resolvedRun.fontSizePt * SCRIPT_SCALE,
3591
3824
  ...script === 0 ? {} : { risePt: resolvedRun.fontSizePt * script },
@@ -3605,17 +3838,62 @@ function tokenizeParagraph(paragraph, options, fontResources, imageResources, co
3605
3838
  })), baseDir));
3606
3839
  }
3607
3840
  /**
3608
- * Split a paragraph's tokens at every §17.3.3.1 column break, dropping the
3609
- * break token itself (its line ends there anyway). One part ⇒ no break.
3841
+ * Split a paragraph's tokens at every §17.3.3.1 column or page break, dropping
3842
+ * the break token itself (its line ends there anyway). One part ⇒ no break.
3843
+ *
3844
+ * A forced page break happens WHERE IT STANDS. One that precedes every word of
3845
+ * the paragraph moves the paragraph itself to the next page, one that follows
3846
+ * every word sends what FOLLOWS the paragraph there, and neither splits it. One
3847
+ * between two of its words splits it there: Word sets "Latin", a page break and
3848
+ * "Second" on two pages, and read as a break after the paragraph, both words
3849
+ * stood on the first.
3850
+ *
3851
+ * A PICTURE is not nothing, however empty its run's text: the break after it
3852
+ * sends what follows to the next page, it does not move the paragraph. Counted
3853
+ * as empty, tdf125657.docx — a full-page image and then a break — kept the
3854
+ * break's page to itself and lost the one the mark after it stands on.
3855
+ *
3856
+ * …and a break is before the paragraph only when something FOLLOWS it. A
3857
+ * paragraph that is nothing but the break — a page break typed on a line of its
3858
+ * own — keeps its mark with the break, on the page the break ends, and what
3859
+ * follows starts the next page at its very top. Word for Mac, in every mode: a
3860
+ * document ending in one is one page, two in a row leave the second alone at
3861
+ * the top of page 2, and the paragraph after the break stands at the page's
3862
+ * first line. Moved to the next page with its mark, it stood an empty line down
3863
+ * every such page.
3610
3864
  *
3611
3865
  * @param tokens The paragraph's tokens in order.
3612
- * @returns The parts, in order; always at least one.
3866
+ * @returns The parts, in order (always at least one), and whether the
3867
+ * paragraph breaks the page before itself and after itself.
3613
3868
  */
3614
- function splitAtColumnBreaks(tokens) {
3615
- const parts = [[]];
3616
- for (const t of tokens) if (t.kind === "text" && t.columnBreak) parts.push([]);
3617
- else parts[parts.length - 1].push(t);
3618
- return parts.filter((p, i) => i === 0 || p.length > 0);
3869
+ function splitAtBreaks(tokens) {
3870
+ const isPage = (t) => t.kind === "text" && t.pageBreak === true;
3871
+ const pageBreakBefore = tokens.length > 0 && isPage(tokens[0]) && tokens.some((t) => !isPage(t));
3872
+ const pageBreakAfter = tokens.length > 0 && isPage(tokens[tokens.length - 1]);
3873
+ const parts = [{
3874
+ tokens: [],
3875
+ endsPage: false
3876
+ }];
3877
+ const end = pageBreakAfter ? tokens.length - 1 : tokens.length;
3878
+ for (const t of tokens.slice(pageBreakBefore ? 1 : 0, end)) if (isPage(t)) {
3879
+ parts[parts.length - 1].endsPage = true;
3880
+ parts.push({
3881
+ tokens: [],
3882
+ opens: "page",
3883
+ endsPage: false
3884
+ });
3885
+ } else if (t.kind === "text" && t.columnBreak) parts.push({
3886
+ tokens: [],
3887
+ opens: "column",
3888
+ endsPage: false
3889
+ });
3890
+ else parts[parts.length - 1].tokens.push(t);
3891
+ if (pageBreakAfter) parts[parts.length - 1].endsPage = true;
3892
+ return {
3893
+ parts: parts.filter((p, i) => i === 0 || p.tokens.length > 0 || p.opens === "page" || p.endsPage),
3894
+ pageBreakBefore,
3895
+ pageBreakAfter
3896
+ };
3619
3897
  }
3620
3898
  function tokenizePlansLtr(plans) {
3621
3899
  const tokens = [];
@@ -3652,13 +3930,21 @@ function tokenizePlansLtr(plans) {
3652
3930
  });
3653
3931
  continue;
3654
3932
  }
3933
+ if (plan.run.listMarkerSpace === true) {
3934
+ tokens.push(markerSpaceToken(plan, 0));
3935
+ continue;
3936
+ }
3655
3937
  const highlight = (plan.run.commentRangeRefs?.length ?? 0) > 0;
3938
+ let at = 0;
3656
3939
  for (const t of tokenizeText(plan.run.text)) for (const piece of splitByFace(plan, t.text)) {
3657
- const text = plan.run.listMarker === true ? markerText(piece.text, plan.font.parsed) : piece.text;
3940
+ const text = plan.run.listMarker === true ? markerText(piece.text, piece.face.font.parsed) : piece.text;
3941
+ const from = at;
3942
+ if (plan.charColorsHex) at += t.tab ? 1 : [...piece.text].length;
3658
3943
  tokens.push({
3659
3944
  kind: "text",
3660
3945
  text,
3661
3946
  isSpace: t.isSpace,
3947
+ ...charColoursOf(plan.charColorsHex, plan.resolvedRun.colorHex, from, at),
3662
3948
  ...t.tab ? { tab: true } : {},
3663
3949
  ...plan.run.href !== void 0 ? { href: plan.run.href } : {},
3664
3950
  ...plan.run.footnoteRef !== void 0 ? { footnoteRef: plan.run.footnoteRef } : {},
@@ -3675,10 +3961,58 @@ function tokenizePlansLtr(plans) {
3675
3961
  bidiLevel: 0
3676
3962
  });
3677
3963
  }
3964
+ if (plan.run.pageBreak) tokens.push(pageBreakToken(plan));
3678
3965
  }
3679
3966
  return tokens;
3680
3967
  }
3681
3968
  /**
3969
+ * §17.3.3.1 — the mark a `w:br w:type="page"` leaves among its paragraph's
3970
+ * tokens: nothing to draw, only where the break stands. A run that holds text
3971
+ * as well breaks after it, the way the writer writes one. It never reaches a
3972
+ * line: the paragraph is split at it before it is wrapped.
3973
+ *
3974
+ * @param plan The break's run.
3975
+ * @returns An empty text token flagged `pageBreak`.
3976
+ */
3977
+ function pageBreakToken(plan) {
3978
+ return {
3979
+ kind: "text",
3980
+ text: "",
3981
+ isSpace: false,
3982
+ pageBreak: true,
3983
+ resolvedRun: plan.resolvedRun,
3984
+ font: plan.font,
3985
+ fontSizePt: plan.fontSizePt,
3986
+ widthPt: 0,
3987
+ bidiLevel: 0
3988
+ };
3989
+ }
3990
+ /** Arial's space, as a fraction of the em: 569 of its 2048 units. */
3991
+ var MARKER_SPACE_EM = 569 / 2048;
3992
+ /**
3993
+ * §17.9.29 — the space a list level's `w:suff="space"` puts after its marker,
3994
+ * as Word sets it: a gap of 0.278 em of the marker's size, whatever the
3995
+ * marker's face — Arial's space, though the marker is in none of it. Measured
3996
+ * in Word for Mac at 48pt, Courier New, Verdana, Times New Roman, Georgia,
3997
+ * Calibri, Tahoma, Impact and Songti markers all leave the same 13.4pt before
3998
+ * the text; a space of each face would have ranged from 8.5 to 28.8. Nothing is
3999
+ * drawn in it, and a justified line does not stretch it.
4000
+ */
4001
+ function markerSpaceToken(plan, bidiLevel) {
4002
+ return {
4003
+ kind: "text",
4004
+ text: "",
4005
+ isSpace: false,
4006
+ listMarker: true,
4007
+ listMarkerSpace: true,
4008
+ resolvedRun: plan.resolvedRun,
4009
+ font: plan.font,
4010
+ fontSizePt: plan.fontSizePt,
4011
+ widthPt: MARKER_SPACE_EM * plan.fontSizePt,
4012
+ bidiLevel
4013
+ };
4014
+ }
4015
+ /**
3682
4016
  * A LIST MARKER the font has no glyph for, swapped for the dot every text font
3683
4017
  * carries.
3684
4018
  *
@@ -3689,8 +4023,15 @@ function tokenizePlansLtr(plans) {
3689
4023
  * eleven slides. The marker is the one place where the shape matters more than
3690
4024
  * the code point, so it takes the nearest thing the font can draw.
3691
4025
  *
3692
- * @param text The marker's text.
3693
- * @param font The font it is set in.
4026
+ * Asked of the face that DRAWS the character, not of the run's own: a marker
4027
+ * may be set in two faces, and a character another face draws is no business
4028
+ * of the run's. chtoutline.docx numbers its chapters "第 1 章" in a Latin
4029
+ * heading face with the Han in a face of their own; asked of the Latin one, 第
4030
+ * and 章 became dots set in the Han face's subset, which had reserved neither,
4031
+ * and the heading printed nothing but its "1".
4032
+ *
4033
+ * @param text The marker's text.
4034
+ * @param parsed The face that draws it.
3694
4035
  * @returns The text to draw.
3695
4036
  */
3696
4037
  function markerText(text, parsed) {
@@ -3718,7 +4059,7 @@ function measureRunText(plan, text, face) {
3718
4059
  };
3719
4060
  const base = drawn.font.measure.textWidthPt(text, plan.fontSizePt) * (drawn.synthetic?.widthScale ?? 1) * (plan.resolvedRun.widthScale ?? 1);
3720
4061
  const extra = plan.resolvedRun.letterSpacingPt;
3721
- return extra === void 0 || extra === 0 ? base : base + extra * [...text].length;
4062
+ return extra === void 0 || extra === 0 ? base : base + extra * drawn.font.measure.spacingSlots(text);
3722
4063
  }
3723
4064
  function tokenizePlansBidi(plans, realLevels) {
3724
4065
  const tokens = [];
@@ -3759,6 +4100,11 @@ function tokenizePlansBidi(plans, realLevels) {
3759
4100
  realIdx++;
3760
4101
  continue;
3761
4102
  }
4103
+ if (plan.run.listMarkerSpace === true) {
4104
+ tokens.push(markerSpaceToken(plan, realLevels[realIdx] ?? 0));
4105
+ realIdx += [...plan.run.text].length;
4106
+ continue;
4107
+ }
3762
4108
  const chars = [...plan.run.text];
3763
4109
  let bufStart = 0;
3764
4110
  let curSpace = false;
@@ -3767,11 +4113,12 @@ function tokenizePlansBidi(plans, realLevels) {
3767
4113
  const flush = (endExclusive) => {
3768
4114
  if (endExclusive <= bufStart) return;
3769
4115
  const raw = chars.slice(bufStart, endExclusive).join("");
3770
- const text = plan.run.listMarker === true ? markerText(raw, plan.font.parsed) : raw;
4116
+ const text = plan.run.listMarker === true ? markerText(raw, curFace.font.parsed) : raw;
3771
4117
  tokens.push({
3772
4118
  kind: "text",
3773
4119
  text,
3774
4120
  isSpace: curSpace,
4121
+ ...charColoursOf(plan.charColorsHex, plan.resolvedRun.colorHex, bufStart, endExclusive),
3775
4122
  ...plan.run.href !== void 0 ? { href: plan.run.href } : {},
3776
4123
  ...plan.run.footnoteRef !== void 0 ? { footnoteRef: plan.run.footnoteRef } : {},
3777
4124
  ...plan.run.anchor !== void 0 ? { anchor: plan.run.anchor } : {},
@@ -3806,6 +4153,7 @@ function tokenizePlansBidi(plans, realLevels) {
3806
4153
  realIdx++;
3807
4154
  }
3808
4155
  flush(chars.length);
4156
+ if (plan.run.pageBreak) tokens.push(pageBreakToken(plan));
3809
4157
  }
3810
4158
  return tokens;
3811
4159
  }
@@ -3976,7 +4324,8 @@ function paragraphItemStream(tokens, hyphenator, maxLineWidthPt) {
3976
4324
  font: tok.font,
3977
4325
  fontSizePt: tok.fontSizePt,
3978
4326
  widthPt: fragWidth,
3979
- bidiLevel: tok.bidiLevel
4327
+ bidiLevel: tok.bidiLevel,
4328
+ ...tok.charColorsHex ? charColoursOf(tok.charColorsHex, tok.resolvedRun.colorHex, [...tok.text.slice(0, prev)].length, [...tok.text.slice(0, end)].length) : {}
3980
4329
  };
3981
4330
  entries.push({
3982
4331
  item: {
@@ -4128,11 +4477,12 @@ function splitAtHyphens(tok) {
4128
4477
  }
4129
4478
  if (parts.length === 0) return void 0;
4130
4479
  parts.push(tok.text.slice(start));
4131
- return parts.map((text) => ({
4132
- ...tok,
4133
- text,
4134
- widthPt: tok.font.measure.textWidthPt(text, tok.fontSizePt)
4135
- }));
4480
+ let from = 0;
4481
+ return parts.map((text) => {
4482
+ const piece = tokenPiece(tok, text, from, tok.font.measure.textWidthPt(text, tok.fontSizePt));
4483
+ if (tok.charColorsHex) from += [...text].length;
4484
+ return piece;
4485
+ });
4136
4486
  }
4137
4487
  function splitToWidth(tok, widthPt) {
4138
4488
  if (tok.kind !== "text") return void 0;
@@ -4153,11 +4503,7 @@ function splitToWidth(tok, widthPt) {
4153
4503
  width = nextWidth;
4154
4504
  end++;
4155
4505
  }
4156
- out.push({
4157
- ...tok,
4158
- text,
4159
- widthPt: width
4160
- });
4506
+ out.push(tokenPiece(tok, text, start, width));
4161
4507
  start = end;
4162
4508
  }
4163
4509
  return out.length > 1 ? out : void 0;
@@ -4293,10 +4639,11 @@ function wrap(tokens, firstLineWidth, otherWidth, resolved, hyphenator, profile,
4293
4639
  /**
4294
4640
  * §17.3.2.26 — the family Word sets a token's characters in: East Asian text
4295
4641
  * in the run's `eastAsia` font, complex-script text (and a `w:rtl` run) in its
4296
- * `cs` one, the rest in `ascii` — `hAnsi` above ASCII. The layout draws a run
4297
- * with its ascii face and hands what that face lacks to another, so the name
4298
- * the run gives for THESE characters is the one Word's line is looked up by:
4299
- * a Chinese sentence in a Calibri run does not stand on Calibri's line.
4642
+ * `cs` one, the rest in `ascii` — `hAnsi` above ASCII (see {@link fontSlotOf}).
4643
+ * The layout draws each character in its slot's face where that face is
4644
+ * loaded, and in what it falls back to where not, so the name the run gives for
4645
+ * THESE characters is the one Word's line is looked up by: a Chinese sentence
4646
+ * in a Calibri run does not stand on Calibri's line.
4300
4647
  *
4301
4648
  * @param t The text token.
4302
4649
  * @returns The family's name, when the run gives one for that slot.
@@ -4306,12 +4653,9 @@ function wordFamilyOf(t) {
4306
4653
  if (t.resolvedRun.rtl) return fonts.cs;
4307
4654
  let high = false;
4308
4655
  for (const ch of t.text) {
4309
- const cp = ch.codePointAt(0) ?? 0;
4310
- if (cp <= 127) continue;
4311
- high = true;
4312
- const script = scriptForCodepoint(cp);
4313
- if (script === "hebrew" || script === "arabic" || script === "thai") return fonts.cs;
4314
- if (script === "kr" || script === "jp" || script === "sc") return fonts.eastAsia;
4656
+ const slot = fontSlotOf(ch.codePointAt(0) ?? 0);
4657
+ if (slot === "cs" || slot === "eastAsia") return fonts[slot];
4658
+ if (slot === "hAnsi") high = true;
4315
4659
  }
4316
4660
  return high ? fonts.hAnsi ?? fonts.ascii : fonts.ascii;
4317
4661
  }
@@ -4765,10 +5109,8 @@ function measureSingleLine(paragraph, options, fontResources) {
4765
5109
  let total = 0;
4766
5110
  for (const run of paragraph.runs) {
4767
5111
  const resolved = resolveRunProperties(run.properties, paragraph.properties, options.styles);
4768
- const { fontKey, synthetic } = runFontKeyAndParsed(options, resolved.fontFamily.ascii, resolved.bold, resolved.italic);
4769
- const font = lookupFont(fontResources, fontKey);
4770
- const fontSizePt = resolved.fontSizePt;
4771
- total += font.measure.textWidthPt(run.text, fontSizePt) * (synthetic?.widthScale ?? 1) * (resolved.widthScale ?? 1);
5112
+ const primary = runFontKeyAndParsed(options, resolved.fontFamily.ascii, resolved.bold, resolved.italic);
5113
+ for (const span of faceSpans(runFaces(options, resolved, primary, run.text), run.text)) total += lookupFont(fontResources, span.fontKey).measure.textWidthPt(span.text, resolved.fontSizePt) * (span.synthetic?.widthScale ?? 1) * (resolved.widthScale ?? 1);
4772
5114
  }
4773
5115
  return total;
4774
5116
  }
@@ -5633,8 +5975,8 @@ var PageAssembler = class PageAssembler {
5633
5975
  /**
5634
5976
  * The first of a paragraph's lines, from `from` on, that the current column
5635
5977
  * no longer holds: each line is counted with the notes it brings onto the
5636
- * page, the way the line loop places it, and an explicit column break ends
5637
- * the column where it stands.
5978
+ * page, the way the line loop places it, and an explicit column or page break
5979
+ * ends the column where it stands.
5638
5980
  *
5639
5981
  * @param pb The paragraph being placed.
5640
5982
  * @param from The first line not yet placed.
@@ -5647,7 +5989,7 @@ var PageAssembler = class PageAssembler {
5647
5989
  let notesOnPage = this.pageNotes.length > 0;
5648
5990
  const counted = /* @__PURE__ */ new Set();
5649
5991
  for (let i = from; i < pb.lines.length; i++) {
5650
- if (i > from && pb.columnBreakLines?.has(i)) return i;
5992
+ if (i > from && (pb.columnBreakLines?.has(i) || pb.pageBreakLines?.has(i))) return i;
5651
5993
  const line = pb.lines[i];
5652
5994
  const h = computeLineHeight(line, pb.resolved);
5653
5995
  const notes = this.lineFootnotes(line).filter((x) => !counted.has(x.id));
@@ -6183,6 +6525,7 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
6183
6525
  if (!pb.resolved.widowControl || n < 2 || asm.exclusions.length > 0) return void 0;
6184
6526
  let end = asm.firstLineOverflowing(pb, from);
6185
6527
  if (end >= n) return void 0;
6528
+ if (pb.columnBreakLines?.has(end) || pb.pageBreakLines?.has(end)) return void 0;
6186
6529
  if (n - end === 1) end--;
6187
6530
  if (from === 0 && end === 1) end = 0;
6188
6531
  if (end <= from) return asm.colHasContent() ? from : from + 1;
@@ -6212,6 +6555,10 @@ function paginateSections(blocks, sectionCtxs, builder, defaultLang = "en-US", n
6212
6555
  let firstLineOfBlock = true;
6213
6556
  let pairPending;
6214
6557
  for (const [lineIdx, line] of pb.lines.entries()) {
6558
+ if (pb.pageBreakLines?.has(lineIdx) && asm.pageHasContent()) {
6559
+ asm.flushPage();
6560
+ columnEnd = keptColumnEnd(lineIdx);
6561
+ }
6215
6562
  if (pb.columnBreakLines?.has(lineIdx) && asm.colHasContent()) {
6216
6563
  advanceBefore(lineIdx);
6217
6564
  columnEnd = keptColumnEnd(lineIdx);