@stll/folio-core 0.49.0 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/dist/ai-edits/headless.d.ts +37 -1
  2. package/dist/ai-edits/headless.js +47 -16
  3. package/dist/ai-edits/word-diff.js +134 -1
  4. package/dist/compare/content-alignment.js +220 -5
  5. package/dist/compare/content.d.ts +1 -0
  6. package/dist/compare/content.js +51 -20
  7. package/dist/display-list/dom/renderDisplayListToDom.d.ts +10 -1
  8. package/dist/display-list/dom/renderDisplayListToDom.js +8 -7
  9. package/dist/display-list/html/renderDisplayListToHtml.d.ts +21 -0
  10. package/dist/display-list/html/renderDisplayListToHtml.js +126 -0
  11. package/dist/display-list/selectDisplayPages.d.ts +6 -0
  12. package/dist/display-list/selectDisplayPages.js +44 -0
  13. package/dist/docx/selectiveSave.d.ts +1 -1
  14. package/dist/docx/selectiveSave.js +16 -6
  15. package/dist/docx/structuralXmlPatch.d.ts +14 -0
  16. package/dist/docx/structuralXmlPatch.js +209 -0
  17. package/dist/export-pdf.d.ts +35 -2
  18. package/dist/export-pdf.js +51 -20
  19. package/dist/fonts/fontsourceFaces.d.ts +47 -0
  20. package/dist/fonts/fontsourceFaces.js +230 -0
  21. package/dist/layout-bridge/convert/flowBlockPostprocessing.d.ts +8 -0
  22. package/dist/layout-bridge/convert/flowBlockPostprocessing.js +106 -0
  23. package/dist/layout-bridge/convert/flowBorders.d.ts +17 -0
  24. package/dist/layout-bridge/convert/flowBorders.js +31 -0
  25. package/dist/layout-bridge/convert/flowConversionShared.d.ts +111 -0
  26. package/dist/layout-bridge/convert/flowConversionShared.js +23 -0
  27. package/dist/layout-bridge/convert/footnoteLayout.d.ts +2 -1
  28. package/dist/layout-bridge/convert/footnoteLayout.js +2 -1
  29. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +2 -1
  30. package/dist/layout-bridge/convert/imageConversion.d.ts +33 -0
  31. package/dist/layout-bridge/convert/imageConversion.js +118 -0
  32. package/dist/layout-bridge/convert/listMarkers.d.ts +28 -0
  33. package/dist/layout-bridge/convert/listMarkers.js +98 -0
  34. package/dist/layout-bridge/convert/noteReferences.d.ts +14 -0
  35. package/dist/layout-bridge/convert/noteReferences.js +29 -0
  36. package/dist/layout-bridge/convert/pageBreakSplitting.d.ts +16 -0
  37. package/dist/layout-bridge/convert/pageBreakSplitting.js +140 -0
  38. package/dist/layout-bridge/convert/paragraphAttrs.d.ts +20 -0
  39. package/dist/layout-bridge/convert/paragraphAttrs.js +234 -0
  40. package/dist/layout-bridge/convert/paragraphConversion.d.ts +12 -0
  41. package/dist/layout-bridge/convert/paragraphConversion.js +112 -0
  42. package/dist/layout-bridge/convert/paragraphRuns.d.ts +15 -0
  43. package/dist/layout-bridge/convert/paragraphRuns.js +281 -0
  44. package/dist/layout-bridge/convert/runFormattingMerge.d.ts +19 -0
  45. package/dist/layout-bridge/convert/runFormattingMerge.js +167 -0
  46. package/dist/layout-bridge/convert/runMarkFormatting.d.ts +11 -0
  47. package/dist/layout-bridge/convert/runMarkFormatting.js +291 -0
  48. package/dist/layout-bridge/convert/sectionBoundaries.d.ts +35 -0
  49. package/dist/layout-bridge/convert/sectionBoundaries.js +114 -0
  50. package/dist/layout-bridge/convert/tableConversion.d.ts +12 -0
  51. package/dist/layout-bridge/convert/tableConversion.js +303 -0
  52. package/dist/layout-bridge/convert/textBoxFill.d.ts +20 -0
  53. package/dist/layout-bridge/convert/textBoxFill.js +42 -0
  54. package/dist/layout-bridge/convert/textFormattingConversion.d.ts +22 -0
  55. package/dist/layout-bridge/convert/textFormattingConversion.js +120 -0
  56. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +5 -113
  57. package/dist/layout-bridge/convert/toFlowBlocks.js +11 -2082
  58. package/dist/layout-bridge/engine/hitTest.js +1 -1
  59. package/dist/layout-bridge/engine/measuring/index.d.ts +2 -1
  60. package/dist/layout-bridge/engine/measuring/index.js +2 -1
  61. package/dist/layout-bridge/engine/selectionRects.js +1 -1
  62. package/dist/layout-engine/imageLayout.d.ts +9 -0
  63. package/dist/layout-engine/imageLayout.js +48 -0
  64. package/dist/layout-engine/index.d.ts +3 -18
  65. package/dist/layout-engine/index.js +12 -937
  66. package/dist/layout-engine/layoutFlowShared.d.ts +6 -0
  67. package/dist/layout-engine/layoutFlowShared.js +8 -0
  68. package/dist/layout-engine/measure/crossRunWords.d.ts +45 -0
  69. package/dist/layout-engine/measure/crossRunWords.js +172 -0
  70. package/dist/layout-engine/measure/floatingLineClearance.d.ts +24 -0
  71. package/dist/layout-engine/measure/floatingLineClearance.js +44 -0
  72. package/dist/layout-engine/measure/inlineObjectMetrics.d.ts +6 -0
  73. package/dist/layout-engine/measure/inlineObjectMetrics.js +52 -0
  74. package/dist/layout-engine/measure/justification.d.ts +32 -0
  75. package/dist/layout-engine/measure/justification.js +83 -0
  76. package/dist/layout-engine/measure/lineFitting.d.ts +48 -0
  77. package/dist/layout-engine/measure/lineFitting.js +145 -0
  78. package/dist/layout-engine/measure/lineTypography.d.ts +44 -0
  79. package/dist/layout-engine/measure/lineTypography.js +124 -0
  80. package/dist/layout-engine/measure/measureBlocks.js +2 -1
  81. package/dist/layout-engine/measure/measureParagraph.d.ts +1 -20
  82. package/dist/layout-engine/measure/measureParagraph.js +32 -734
  83. package/dist/layout-engine/measure/paragraphMeasureShared.d.ts +109 -0
  84. package/dist/layout-engine/measure/paragraphMeasureShared.js +82 -0
  85. package/dist/layout-engine/measure/paragraphTabs.d.ts +18 -0
  86. package/dist/layout-engine/measure/paragraphTabs.js +78 -0
  87. package/dist/layout-engine/paragraphLayout.d.ts +23 -0
  88. package/dist/layout-engine/paragraphLayout.js +204 -0
  89. package/dist/layout-engine/sectionLayout.d.ts +45 -0
  90. package/dist/layout-engine/sectionLayout.js +216 -0
  91. package/dist/layout-engine/tableLayout.d.ts +17 -0
  92. package/dist/layout-engine/tableLayout.js +369 -0
  93. package/dist/layout-engine/textBoxLayout.d.ts +15 -0
  94. package/dist/layout-engine/textBoxLayout.js +137 -0
  95. package/dist/layout-painter/renderPage.js +1 -0
  96. package/dist/managers/editorShortcuts.d.ts +16 -3
  97. package/dist/managers/editorShortcuts.js +6 -4
  98. package/dist/paged-layout/sectionBlockWidths.d.ts +2 -1
  99. package/dist/paged-layout/sectionBlockWidths.js +1 -1
  100. package/dist/prosemirror/extensions/StarterKit.d.ts +3 -0
  101. package/dist/prosemirror/extensions/StarterKit.js +2 -1
  102. package/dist/prosemirror/extensions/core/HistoryExtension.d.ts +9 -1
  103. package/dist/prosemirror/extensions/core/HistoryExtension.js +4 -3
  104. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +12 -5
  105. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +91 -33
  106. package/dist/server.d.ts +3 -2
  107. package/package.json +2 -2
@@ -1,5 +1,5 @@
1
1
  import { DocxInput } from "./utils/docxInput.js";
2
- import { DisplayMetadata, DisplayUnsupported } from "./display-list/types.js";
2
+ import { DisplayList, DisplayMetadata, DisplayUnsupported } from "./display-list/types.js";
3
3
  import { HeadlessFontSource, HeadlessFontSubstitution } from "./fonts/headlessMeasure.js";
4
4
  import { HeadlessLayoutGap } from "./headless-layout.js";
5
5
  import { PdfSubstitution } from "./pdf/fonts.js";
@@ -23,6 +23,11 @@ type ExportDocxToPdfOptions = {
23
23
  readonly timestamp: string;
24
24
  readonly metadata?: DisplayMetadata;
25
25
  readonly producer?: string;
26
+ /**
27
+ * 0-based indices of the pages to write, in order. Links and outline
28
+ * entries to other pages are dropped. Omit for every page.
29
+ */
30
+ readonly pages?: readonly number[];
26
31
  };
27
32
  type ExportDocxToPdfResult = {
28
33
  readonly bytes: Uint8Array;
@@ -38,6 +43,34 @@ type ExportDocxToPdfResult = {
38
43
  /** Code points painted as `.notdef` because no supplied face covers them. */
39
44
  readonly unencodable: readonly PdfUnencodable[];
40
45
  };
46
+ type BuildDocxDisplayListOptions = {
47
+ /** Supplies face binaries for measurement. */
48
+ readonly fonts: HeadlessFontSource;
49
+ readonly metadata?: DisplayMetadata;
50
+ };
51
+ type BuildDocxDisplayListResult = {
52
+ /** The pages every backend paints: the PDF writer, the DOM and HTML backends. */
53
+ readonly list: DisplayList;
54
+ /** Stories the headless pipeline does not paginate. */
55
+ readonly layoutGaps: readonly HeadlessLayoutGap[];
56
+ /** Faces measured with a stand-in. */
57
+ readonly measurementSubstitutions: readonly HeadlessFontSubstitution[];
58
+ };
59
+ /**
60
+ * Lay a package out headlessly and build its display list: the one structure
61
+ * the PDF writer and the DOM backend both paint. Runs one at a time with
62
+ * every other headless build and export, for the reason
63
+ * {@link runExclusively} gives.
64
+ */
65
+ declare const buildDocxDisplayList: (input: DocxInput, options: BuildDocxDisplayListOptions) => Promise<Result<BuildDocxDisplayListResult, ExportPdfError>>;
41
66
  declare const exportDocxToPdf: (input: DocxInput, options: ExportDocxToPdfOptions) => Promise<Result<ExportDocxToPdfResult, ExportPdfError>>;
67
+ type WriteDisplayListPdfOptions = Pick<ExportDocxToPdfOptions, "fonts" | "timestamp" | "producer">;
68
+ type WriteDisplayListPdfResult = Pick<ExportDocxToPdfResult, "bytes" | "pageCount" | "embeddingSubstitutions" | "unencodable">;
69
+ /**
70
+ * Write a display list built by {@link buildDocxDisplayList} (or narrowed by
71
+ * `selectDisplayPages`) as a PDF, embedding faces from the same source the
72
+ * layout measured with.
73
+ */
74
+ declare const writeDisplayListPdf: (list: DisplayList, options: WriteDisplayListPdfOptions) => Promise<Result<WriteDisplayListPdfResult, ExportPdfError>>;
42
75
  //#endregion
43
- export { ExportDocxToPdfOptions, ExportDocxToPdfResult, ExportPdfError, exportDocxToPdf };
76
+ export { BuildDocxDisplayListOptions, BuildDocxDisplayListResult, ExportDocxToPdfOptions, ExportDocxToPdfResult, ExportPdfError, WriteDisplayListPdfOptions, WriteDisplayListPdfResult, buildDocxDisplayList, exportDocxToPdf, writeDisplayListPdf };
@@ -1,5 +1,6 @@
1
1
  import { buildDisplayList } from "./display-list/build/buildDisplayList.js";
2
2
  import { displayCommentsFrom } from "./display-list/build/commentAnnotations.js";
3
+ import { selectDisplayPages } from "./display-list/selectDisplayPages.js";
3
4
  import { installHeadlessMeasureProvider } from "./fonts/headlessMeasure.js";
4
5
  import { layoutDocxHeadless } from "./headless-layout.js";
5
6
  import { getMeasureProvider, setMeasureProvider } from "./layout-engine/measure/measureProvider.js";
@@ -52,16 +53,59 @@ const runExclusively = (run) => {
52
53
  exportQueue = current.then(() => void 0, () => void 0);
53
54
  return current;
54
55
  };
55
- const exportDocxToPdf = (input, options) => runExclusively(() => exportOnce(input, options));
56
- const exportOnce = async (input, options) => {
56
+ /**
57
+ * Lay a package out headlessly and build its display list: the one structure
58
+ * the PDF writer and the DOM backend both paint. Runs one at a time with
59
+ * every other headless build and export, for the reason
60
+ * {@link runExclusively} gives.
61
+ */
62
+ const buildDocxDisplayList = (input, options) => runExclusively(async () => {
57
63
  const callerProvider = getMeasureProvider();
58
64
  try {
59
- return await exportWithHeadlessProvider(input, options);
65
+ return await buildWithHeadlessProvider(input, options);
60
66
  } finally {
61
67
  setMeasureProvider(callerProvider);
62
68
  }
69
+ });
70
+ const exportDocxToPdf = async (input, options) => {
71
+ const built = await buildDocxDisplayList(input, {
72
+ fonts: options.fonts,
73
+ ...options.metadata === void 0 ? {} : { metadata: options.metadata }
74
+ });
75
+ if (built.isErr()) return Result.err(built.error);
76
+ const list = options.pages === void 0 ? built.value.list : selectDisplayPages(built.value.list, options.pages);
77
+ const written = await writeDisplayListPdf(list, options);
78
+ if (written.isErr()) return Result.err(written.error);
79
+ return Result.ok({
80
+ ...written.value,
81
+ unsupported: list.unsupported,
82
+ layoutGaps: built.value.layoutGaps,
83
+ measurementSubstitutions: built.value.measurementSubstitutions
84
+ });
85
+ };
86
+ /**
87
+ * Write a display list built by {@link buildDocxDisplayList} (or narrowed by
88
+ * `selectDisplayPages`) as a PDF, embedding faces from the same source the
89
+ * layout measured with.
90
+ */
91
+ const writeDisplayListPdf = async (list, options) => {
92
+ const written = await writePdf(list, {
93
+ fonts: { load: (face) => options.fonts.load(toFontRequest(face)) },
94
+ timestamp: options.timestamp,
95
+ ...options.producer === void 0 ? {} : { producer: options.producer }
96
+ });
97
+ if (written.isErr()) return Result.err(new ExportPdfError({
98
+ message: written.error.message,
99
+ cause: written.error
100
+ }));
101
+ return Result.ok({
102
+ bytes: written.value.bytes,
103
+ pageCount: list.pages.length,
104
+ embeddingSubstitutions: written.value.substitutions,
105
+ unencodable: written.value.unencodable
106
+ });
63
107
  };
64
- const exportWithHeadlessProvider = async (input, options) => {
108
+ const buildWithHeadlessProvider = async (input, options) => {
65
109
  const headless = installHeadlessMeasureProvider(options.fonts);
66
110
  const laidOut = await layoutDocxHeadless(input, { pageGap: 0 });
67
111
  if (laidOut.isErr()) return Result.err(new ExportPdfError({
@@ -77,24 +121,11 @@ const exportWithHeadlessProvider = async (input, options) => {
77
121
  ...laidOut.value.document.package.document.comments === void 0 ? {} : { comments: displayCommentsFrom(laidOut.value.document.package.document.comments) },
78
122
  ...options.metadata === void 0 ? {} : { metadata: options.metadata }
79
123
  });
80
- const written = await writePdf(list, {
81
- fonts: { load: (face) => options.fonts.load(toFontRequest(face)) },
82
- timestamp: options.timestamp,
83
- ...options.producer === void 0 ? {} : { producer: options.producer }
84
- });
85
- if (written.isErr()) return Result.err(new ExportPdfError({
86
- message: written.error.message,
87
- cause: written.error
88
- }));
89
124
  return Result.ok({
90
- bytes: written.value.bytes,
91
- pageCount: list.pages.length,
92
- unsupported: list.unsupported,
125
+ list,
93
126
  layoutGaps: laidOut.value.unsupported,
94
- measurementSubstitutions: headless.substitutions(),
95
- embeddingSubstitutions: written.value.substitutions,
96
- unencodable: written.value.unencodable
127
+ measurementSubstitutions: headless.substitutions()
97
128
  });
98
129
  };
99
130
  //#endregion
100
- export { ExportPdfError, exportDocxToPdf };
131
+ export { ExportPdfError, buildDocxDisplayList, exportDocxToPdf, writeDisplayListPdf };
@@ -0,0 +1,47 @@
1
+ import { HeadlessFontSource } from "./headlessMeasure.js";
2
+ //#region src/fonts/fontsourceFaces.d.ts
3
+ /** Where `@fontsource` files come from: `null` when the package or file is absent. */
4
+ type FontsourceFiles = {
5
+ /** Bytes of `@fontsource/<packageName>/<relativePath>`. */
6
+ readonly read: (packageName: string, relativePath: string) => Uint8Array | null;
7
+ };
8
+ /** Each bundled family and the `@fontsource` package that ships it. */
9
+ declare const FONTSOURCE_PACKAGES: {
10
+ readonly Arimo: "arimo";
11
+ readonly Caladea: "caladea";
12
+ readonly Carlito: "carlito";
13
+ readonly Cousine: "cousine";
14
+ readonly Lato: "lato";
15
+ readonly "Noto Sans Arabic": "noto-sans-arabic";
16
+ readonly "Source Sans 3": "source-sans-3";
17
+ readonly Tinos: "tinos";
18
+ };
19
+ /**
20
+ * A CSS `<string>` token, quoted and escaped. A family name comes from an
21
+ * authored document and is emitted into a `<style>` element, so the
22
+ * backslash is escaped first, then the quote, and line terminators become hex
23
+ * escapes, which CSS strings require.
24
+ */
25
+ declare const cssString: (value: string) => string;
26
+ type FontsourceFontFaceCssOptions = {
27
+ /**
28
+ * Families to declare beyond the names the faces answer to directly. Pass a
29
+ * display list's own `fonts` families: a face reached through the fallback
30
+ * chain has no rule of its own, and the page names it by its authored name.
31
+ */
32
+ readonly families?: readonly string[];
33
+ };
34
+ type FontsourceFaces = {
35
+ /** Binaries for the headless measurer and the PDF writer. */
36
+ readonly source: HeadlessFontSource;
37
+ /**
38
+ * `@font-face` rules with the `.woff` bytes inlined as `data:` URLs, one
39
+ * rule per served subset of each face, so a browser paints each code point
40
+ * from the binary that was measured.
41
+ */
42
+ readonly fontFaceCss: (options?: FontsourceFontFaceCssOptions) => string;
43
+ };
44
+ /** The `@fontsource` faces, read through `files` and cached per face. */
45
+ declare const createFontsourceFaces: (files: FontsourceFiles) => FontsourceFaces;
46
+ //#endregion
47
+ export { FONTSOURCE_PACKAGES, FontsourceFaces, FontsourceFiles, FontsourceFontFaceCssOptions, createFontsourceFaces, cssString };
@@ -0,0 +1,230 @@
1
+ import { bytesToDataUrl } from "../utils/base64.js";
2
+ import { FONT_MAPPING } from "../utils/fontLoader.js";
3
+ import { parseFontFamilyList, resolveFontFamily } from "../utils/fontResolver.js";
4
+ //#region src/fonts/fontsourceFaces.ts
5
+ /**
6
+ * The subsets served, in resolution priority: a code point two subsets cover
7
+ * resolves to the earlier one on both sides.
8
+ */
9
+ const SUBSET_PRIORITY = [
10
+ "latin",
11
+ "latin-ext",
12
+ "arabic"
13
+ ];
14
+ /** Folio paints 400 and 700 only, as `DisplayFontFace` states. */
15
+ const REGULAR_WEIGHT = 400;
16
+ const BOLD_WEIGHT = 700;
17
+ /** Each bundled family and the `@fontsource` package that ships it. */
18
+ const FONTSOURCE_PACKAGES = {
19
+ Arimo: "arimo",
20
+ Caladea: "caladea",
21
+ Carlito: "carlito",
22
+ Cousine: "cousine",
23
+ Lato: "lato",
24
+ "Noto Sans Arabic": "noto-sans-arabic",
25
+ "Source Sans 3": "source-sans-3",
26
+ Tinos: "tinos"
27
+ };
28
+ /** A CSS stack ends in a generic, so a generic must resolve to a bundled family too. */
29
+ const GENERIC_FAMILIES = {
30
+ serif: "Tinos",
31
+ "sans-serif": "Arimo",
32
+ monospace: "Cousine"
33
+ };
34
+ const isBundledFamily = (name) => Object.hasOwn(FONTSOURCE_PACKAGES, name);
35
+ const isGenericFamily = (name) => Object.hasOwn(GENERIC_FAMILIES, name);
36
+ /** Every name a bundled family answers to: its own, and the authored names mapped to it. */
37
+ const familyIndexEntries = () => {
38
+ const entries = [];
39
+ for (const family of Object.keys(FONTSOURCE_PACKAGES)) if (isBundledFamily(family)) entries.push([family.toLowerCase(), family]);
40
+ for (const [authored, mapped] of Object.entries(FONT_MAPPING)) {
41
+ if (isBundledFamily(mapped)) {
42
+ entries.push([authored.toLowerCase(), mapped]);
43
+ continue;
44
+ }
45
+ if (isGenericFamily(mapped)) entries.push([authored.toLowerCase(), GENERIC_FAMILIES[mapped]]);
46
+ }
47
+ for (const [generic, bundled] of Object.entries(GENERIC_FAMILIES)) entries.push([generic, bundled]);
48
+ return entries;
49
+ };
50
+ const FAMILY_INDEX = new Map(familyIndexEntries());
51
+ /** Every family name the faces answer to, in a stable order for the CSS. */
52
+ const bundledFamilyAliases = () => [...Object.keys(FONTSOURCE_PACKAGES), ...Object.keys(FONT_MAPPING).filter((authored) => FAMILY_INDEX.has(authored.toLowerCase()))].sort((a, b) => a.localeCompare(b, "en"));
53
+ /**
54
+ * The bundled family a requested name resolves to: directly, or through the
55
+ * same fallback chain the editor's font resolver walks.
56
+ */
57
+ const resolveBundledFamily = (family) => {
58
+ const direct = FAMILY_INDEX.get(family.trim().toLowerCase());
59
+ if (direct !== void 0) return direct;
60
+ for (const candidate of parseFontFamilyList(resolveFontFamily(family).cssFallback)) {
61
+ const resolved = FAMILY_INDEX.get(candidate.trim().toLowerCase());
62
+ if (resolved !== void 0) return resolved;
63
+ }
64
+ return null;
65
+ };
66
+ const weightOf = (bold) => bold ? BOLD_WEIGHT : REGULAR_WEIGHT;
67
+ /** `@fontsource` ships the ranges it cut each subset with, beside the files. */
68
+ const UNICODE_RANGE_FILE = "unicode.json";
69
+ const RANGE_TOKEN_RE = /^U\+([0-9A-Fa-f]+)(?:-([0-9A-Fa-f]+))?$/u;
70
+ const parseRangeToken = (token) => {
71
+ const match = RANGE_TOKEN_RE.exec(token.trim());
72
+ if (match === null) return [];
73
+ const first = Number.parseInt(match[1] ?? "", 16);
74
+ const last = match[2] === void 0 ? first : Number.parseInt(match[2], 16);
75
+ if (Number.isNaN(first) || Number.isNaN(last)) return [];
76
+ return [[first, last]];
77
+ };
78
+ const parseRangeList = (declared) => declared.split(",").flatMap(parseRangeToken);
79
+ const parseDeclaredRanges = (bytes) => {
80
+ const ranges = /* @__PURE__ */ new Map();
81
+ const parsed = JSON.parse(new TextDecoder().decode(bytes));
82
+ if (typeof parsed !== "object" || parsed === null) return ranges;
83
+ for (const [subset, declared] of Object.entries(parsed)) if (typeof declared === "string") ranges.set(subset, parseRangeList(declared));
84
+ return ranges;
85
+ };
86
+ const withoutRange = (range, [start, end]) => {
87
+ const [first, last] = range;
88
+ if (end < first || start > last) return [range];
89
+ const remainder = [];
90
+ if (start > first) remainder.push([first, start - 1]);
91
+ if (end < last) remainder.push([end + 1, last]);
92
+ return remainder;
93
+ };
94
+ /**
95
+ * `subject` minus everything a higher-priority subset already serves. The
96
+ * declared ranges overlap (`latin` and `latin-ext` both claim some combining
97
+ * marks); cutting the overlap out of the lower-priority rule makes a browser,
98
+ * which resolves an overlap by declaration order, agree with this table.
99
+ */
100
+ const subtractRanges = (subject, taken) => {
101
+ let remaining = subject;
102
+ for (const range of taken) remaining = remaining.flatMap((candidate) => withoutRange(candidate, range));
103
+ return remaining;
104
+ };
105
+ const hex = (codePoint) => codePoint.toString(16).toUpperCase().padStart(4, "0");
106
+ const formatRangeList = (ranges) => ranges.map(([first, last]) => first === last ? `U+${hex(first)}` : `U+${hex(first)}-${hex(last)}`).join(",");
107
+ /**
108
+ * A CSS `<string>` token, quoted and escaped. A family name comes from an
109
+ * authored document and is emitted into a `<style>` element, so the
110
+ * backslash is escaped first, then the quote, and line terminators become hex
111
+ * escapes, which CSS strings require.
112
+ */
113
+ const cssString = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"").replaceAll("\n", "\\A ").replaceAll("\r", "\\D ").replaceAll("\f", "\\C ")}"`;
114
+ const FACE_VARIANTS = [
115
+ {
116
+ weight: REGULAR_WEIGHT,
117
+ italic: false
118
+ },
119
+ {
120
+ weight: REGULAR_WEIGHT,
121
+ italic: true
122
+ },
123
+ {
124
+ weight: BOLD_WEIGHT,
125
+ italic: false
126
+ },
127
+ {
128
+ weight: BOLD_WEIGHT,
129
+ italic: true
130
+ }
131
+ ];
132
+ /** The media type a `.woff` face is inlined under. */
133
+ const WOFF_MEDIA_TYPE = "font/woff";
134
+ const fontFaceRule = ({ family, dataUrl, weight, italic, unicodeRange }) => [
135
+ "@font-face {",
136
+ ` font-family: ${cssString(family)};`,
137
+ ` src: url(${dataUrl}) format("woff");`,
138
+ ` font-weight: ${String(weight)};`,
139
+ ` font-style: ${italic ? "italic" : "normal"};`,
140
+ ` unicode-range: ${formatRangeList(unicodeRange)};`,
141
+ " font-display: block;",
142
+ "}"
143
+ ].join("\n");
144
+ /** The `@fontsource` faces, read through `files` and cached per face. */
145
+ const createFontsourceFaces = (files) => {
146
+ const faceCache = /* @__PURE__ */ new Map();
147
+ const rangeCache = /* @__PURE__ */ new Map();
148
+ const dataUrlCache = /* @__PURE__ */ new Map();
149
+ const faceBinaries = ({ family, weight, italic }) => {
150
+ const bundled = resolveBundledFamily(family);
151
+ if (bundled === null) return [];
152
+ const packageName = FONTSOURCE_PACKAGES[bundled];
153
+ const style = italic ? "italic" : "normal";
154
+ const key = `${packageName}|${String(weight)}|${style}`;
155
+ const cached = faceCache.get(key);
156
+ if (cached !== void 0) return cached;
157
+ const binaries = SUBSET_PRIORITY.flatMap((subset) => {
158
+ const bytes = files.read(packageName, `files/${packageName}-${subset}-${String(weight)}-${style}.woff`);
159
+ return bytes === null ? [] : [{
160
+ subset,
161
+ bytes
162
+ }];
163
+ });
164
+ faceCache.set(key, binaries);
165
+ return binaries;
166
+ };
167
+ const declaredRanges = (packageName) => {
168
+ const cached = rangeCache.get(packageName);
169
+ if (cached !== void 0) return cached;
170
+ const bytes = files.read(packageName, UNICODE_RANGE_FILE);
171
+ const ranges = bytes === null ? /* @__PURE__ */ new Map() : parseDeclaredRanges(bytes);
172
+ rangeCache.set(packageName, ranges);
173
+ return ranges;
174
+ };
175
+ const dataUrlOf = (bytes) => {
176
+ const cached = dataUrlCache.get(bytes);
177
+ if (cached !== void 0) return cached;
178
+ const encoded = bytesToDataUrl(bytes, WOFF_MEDIA_TYPE);
179
+ dataUrlCache.set(bytes, encoded);
180
+ return encoded;
181
+ };
182
+ /**
183
+ * The rules for one variant of one family name. A subset whose range a
184
+ * higher-priority subset already serves, or that `unicode.json` does not
185
+ * describe, is not declared: a rule with no `unicode-range` would cover
186
+ * every code point.
187
+ */
188
+ const variantRules = ({ family, weight, italic }) => {
189
+ const bundled = resolveBundledFamily(family);
190
+ if (bundled === null) return [];
191
+ const ranges = declaredRanges(FONTSOURCE_PACKAGES[bundled]);
192
+ const rules = [];
193
+ const taken = [];
194
+ for (const binary of faceBinaries({
195
+ family,
196
+ weight,
197
+ italic
198
+ })) {
199
+ const unicodeRange = subtractRanges(ranges.get(binary.subset) ?? [], taken);
200
+ if (unicodeRange.length === 0) continue;
201
+ rules.push(fontFaceRule({
202
+ family,
203
+ dataUrl: dataUrlOf(binary.bytes),
204
+ weight,
205
+ italic,
206
+ unicodeRange
207
+ }));
208
+ taken.push(...unicodeRange);
209
+ }
210
+ return rules;
211
+ };
212
+ return {
213
+ source: { load: ({ family, bold, italic }) => faceBinaries({
214
+ family,
215
+ weight: weightOf(bold),
216
+ italic
217
+ }).map(({ bytes }) => bytes) },
218
+ fontFaceCss: (options = {}) => {
219
+ const declared = /* @__PURE__ */ new Map();
220
+ for (const family of [...bundledFamilyAliases(), ...options.families ?? []]) declared.set(family.trim().toLowerCase(), family.trim());
221
+ return [...declared.values()].flatMap((family) => FACE_VARIANTS.flatMap(({ weight, italic }) => variantRules({
222
+ family,
223
+ weight,
224
+ italic
225
+ }))).join("\n");
226
+ }
227
+ };
228
+ };
229
+ //#endregion
230
+ export { FONTSOURCE_PACKAGES, createFontsourceFaces, cssString };
@@ -0,0 +1,8 @@
1
+ import { FlowBlock } from "../../layout-engine/types.js";
2
+ //#region src/layout-bridge/convert/flowBlockPostprocessing.d.ts
3
+ declare function suppressFinalEmptyParagraphAfterTable(blocks: FlowBlock[]): void;
4
+ declare function suppressFinalParagraphInRepeatedEmptySuffix(blocks: FlowBlock[]): void;
5
+ declare function reserveLeadingEmptyOutlineHeight(blocks: FlowBlock[]): void;
6
+ declare function mergeRunInParagraphs(blocks: FlowBlock[]): FlowBlock[];
7
+ //#endregion
8
+ export { mergeRunInParagraphs, reserveLeadingEmptyOutlineHeight, suppressFinalEmptyParagraphAfterTable, suppressFinalParagraphInRepeatedEmptySuffix };
@@ -0,0 +1,106 @@
1
+ import { headingLevelOf } from "@stll/docx-core/model";
2
+ //#region src/layout-bridge/convert/flowBlockPostprocessing.ts
3
+ /**
4
+ * Post-conversion passes over the flow block list: terminal empty-paragraph
5
+ * suppression, leading empty outline reservation, and run-in paragraph
6
+ * merging.
7
+ */
8
+ /**
9
+ * Word keeps a final empty body paragraph after a table as an editable anchor,
10
+ * but that final anchor does not create a page of its own. Earlier authored
11
+ * empty paragraphs retain their height and may carry the document onto a blank
12
+ * page. Preserve every block and PM range while collapsing only the final one.
13
+ */
14
+ function isPaintlessTerminalParagraph(block) {
15
+ if (block?.kind !== "paragraph" || block.runs.length !== 0) return false;
16
+ const attrs = block.attrs;
17
+ if (attrs?.suppressEmptyParagraphHeight === false) return false;
18
+ return !(attrs?.listMarker !== void 0 && !attrs.listMarkerHidden || attrs?.borders?.top || attrs?.borders?.bottom || attrs?.borders?.left || attrs?.borders?.right || attrs?.borders?.between || attrs?.borders?.bar || attrs?.shading || attrs?.spacingExplicit?.before || attrs?.spacingExplicit?.after || attrs?.pageBreakBefore || attrs?.renderedPageBreakBefore);
19
+ }
20
+ function suppressFinalEmptyParagraphAfterTable(blocks) {
21
+ let suffixStart = blocks.length;
22
+ while (suffixStart > 0 && isPaintlessTerminalParagraph(blocks[suffixStart - 1])) suffixStart -= 1;
23
+ if (suffixStart === blocks.length || suffixStart === 0 || blocks[suffixStart - 1]?.kind !== "table") return;
24
+ const finalBlock = blocks.at(-1);
25
+ if (isPaintlessTerminalParagraph(finalBlock)) finalBlock.attrs = {
26
+ ...finalBlock.attrs,
27
+ suppressEmptyParagraphHeight: true
28
+ };
29
+ }
30
+ function suppressFinalParagraphInRepeatedEmptySuffix(blocks) {
31
+ let suffixStart = blocks.length;
32
+ while (suffixStart > 0 && isPaintlessTerminalParagraph(blocks[suffixStart - 1])) suffixStart -= 1;
33
+ if (suffixStart === 0 || blocks.length - suffixStart < 2 || blocks[suffixStart - 1]?.kind === "table") return;
34
+ const finalBlock = blocks.at(-1);
35
+ if (isPaintlessTerminalParagraph(finalBlock)) finalBlock.attrs = {
36
+ ...finalBlock.attrs,
37
+ suppressEmptyParagraphHeight: true
38
+ };
39
+ }
40
+ function reserveLeadingEmptyOutlineHeight(blocks) {
41
+ const firstBlock = blocks.at(0);
42
+ if (firstBlock?.kind !== "paragraph" || firstBlock.runs.length !== 0 || headingLevelOf(firstBlock.attrs?.outlineLevel) !== 0) return;
43
+ firstBlock.attrs = {
44
+ ...firstBlock.attrs,
45
+ reserveEmptyOutlineHeight: true
46
+ };
47
+ }
48
+ /**
49
+ * Merge consecutive paragraph blocks where the first carries
50
+ * `runInWithNext` (`<w:specVanish/>` on the paragraph mark).
51
+ *
52
+ * Word's run-in heading feature renders the next paragraph inline on
53
+ * the same line, so for layout we collapse the pair into one
54
+ * ParagraphBlock with combined runs. The merged block keeps the first
55
+ * paragraph's attrs (heading formatting, list marker, indent) and
56
+ * extends pmEnd to the second paragraph's range so click-to-position
57
+ * resolution still maps both ranges back to body content.
58
+ *
59
+ * Chains: runInWithNext on the merged block is dropped because the
60
+ * second paragraph's mark wasn't `specVanish`. If a chain of
61
+ * specVanish paragraphs needs collapsing (rare in practice), the loop
62
+ * naturally handles it by re-inspecting the merged block's flag (we
63
+ * preserve runInWithNext only when the second paragraph itself has
64
+ * specVanish).
65
+ */
66
+ function sdtGroupStacksEqual(a, b) {
67
+ const lenA = a?.length ?? 0;
68
+ if (lenA !== (b?.length ?? 0)) return false;
69
+ if (lenA === 0) return true;
70
+ for (let i = 0; i < lenA; i++) if (a?.[i]?.pmPos !== b?.[i]?.pmPos) return false;
71
+ return true;
72
+ }
73
+ function mergeRunInParagraphs(blocks) {
74
+ const out = [];
75
+ for (let i = 0; i < blocks.length; i++) {
76
+ let current = blocks[i];
77
+ if (!current) continue;
78
+ while (current.kind === "paragraph" && current.attrs?.runInWithNext && i + 1 < blocks.length) {
79
+ const next = blocks[i + 1];
80
+ if (!next || next.kind !== "paragraph") break;
81
+ const a = current;
82
+ const b = next;
83
+ if (!sdtGroupStacksEqual(a.sdtGroups, b.sdtGroups)) break;
84
+ const mergedAttrs = { ...a.attrs };
85
+ if (b.attrs?.spacing?.after !== void 0) mergedAttrs.spacing = {
86
+ ...mergedAttrs.spacing,
87
+ after: b.attrs.spacing.after
88
+ };
89
+ if (b.attrs?.runInWithNext) mergedAttrs.runInWithNext = true;
90
+ else delete mergedAttrs.runInWithNext;
91
+ const merged = {
92
+ ...a,
93
+ runs: [...a.runs, ...b.runs],
94
+ attrs: mergedAttrs
95
+ };
96
+ const mergedPmEnd = b.pmEnd ?? a.pmEnd;
97
+ if (mergedPmEnd !== void 0) merged.pmEnd = mergedPmEnd;
98
+ current = merged;
99
+ i += 1;
100
+ }
101
+ out.push(current);
102
+ }
103
+ return out;
104
+ }
105
+ //#endregion
106
+ export { mergeRunInParagraphs, reserveLeadingEmptyOutlineHeight, suppressFinalEmptyParagraphAfterTable, suppressFinalParagraphInRepeatedEmptySuffix };
@@ -0,0 +1,17 @@
1
+ import { document_d_exports } from "../../types/document.js";
2
+ import { BorderStyle } from "../../layout-engine/types.js";
3
+ //#region src/layout-bridge/convert/flowBorders.d.ts
4
+ /** A parsed border, or the same shape read off ProseMirror attributes. */
5
+ type ConvertibleBorder = {
6
+ style?: document_d_exports.BorderStyleValue;
7
+ size?: number;
8
+ space?: number;
9
+ color?: document_d_exports.ColorValue;
10
+ };
11
+ /**
12
+ * Convert an OOXML BorderSpec to a layout-engine BorderStyle.
13
+ * Shared by paragraph borders, cell borders, and header/footer borders.
14
+ */
15
+ declare function convertBorderSpecToLayout(border: ConvertibleBorder, theme?: document_d_exports.Theme | null): BorderStyle | undefined;
16
+ //#endregion
17
+ export { ConvertibleBorder, convertBorderSpecToLayout };
@@ -0,0 +1,31 @@
1
+ import { cssBorderStyle } from "../../utils/borderCss.js";
2
+ import { resolveColor } from "../../utils/colorResolver.js";
3
+ import { pointsToPixels } from "../../utils/units.js";
4
+ import { statesNoBorder } from "@stll/docx-core/model";
5
+ //#region src/layout-bridge/convert/flowBorders.ts
6
+ /**
7
+ * Converts document-model border specifications to layout BorderStyle.
8
+ */
9
+ /**
10
+ * Convert border width from eighths of a point to pixels.
11
+ * OOXML stores border widths in eighths of a point.
12
+ */
13
+ function borderWidthToPixels(eighthsOfPoint) {
14
+ return pointsToPixels(eighthsOfPoint / 8);
15
+ }
16
+ /**
17
+ * Convert an OOXML BorderSpec to a layout-engine BorderStyle.
18
+ * Shared by paragraph borders, cell borders, and header/footer borders.
19
+ */
20
+ function convertBorderSpecToLayout(border, theme) {
21
+ if (border.style === void 0 || statesNoBorder(border.style)) return;
22
+ const result = {
23
+ style: cssBorderStyle(border.style),
24
+ width: border.size === void 0 ? 1 : borderWidthToPixels(border.size),
25
+ color: border.color ? resolveColor(border.color, theme) : "#000000"
26
+ };
27
+ if (border.space !== void 0) result.space = pointsToPixels(border.space);
28
+ return result;
29
+ }
30
+ //#endregion
31
+ export { convertBorderSpecToLayout };