@stll/folio-core 0.65.0 → 0.66.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 (49) hide show
  1. package/dist/ai-edits/apply.js +31 -9
  2. package/dist/docx/attributeRemainder.js +1 -1
  3. package/dist/docx/documentSectionFacts.js +1 -1
  4. package/dist/docx/ensureParaIds.js +2 -2
  5. package/dist/docx/importedIdCollisions.js +1 -1
  6. package/dist/docx/mathToMathml.js +1 -1
  7. package/dist/docx/numericIdNormalization.js +2 -2
  8. package/dist/docx/parser.js +1 -1
  9. package/dist/docx/revisionIdNormalization.js +2 -2
  10. package/dist/docx/rezip.js +1 -1
  11. package/dist/docx/sdtProperties.js +1 -2
  12. package/dist/docx/selectedXmlIdentityScan.d.ts +1 -1
  13. package/dist/docx/selectedXmlIdentityScan.js +1 -1
  14. package/dist/docx/selectiveXmlPatch.js +2 -2
  15. package/dist/docx/server/boundedArchive.d.ts +1 -1
  16. package/dist/docx/server/boundedArchive.js +1 -1
  17. package/dist/docx/server/validateDocxConformance.js +1 -1
  18. package/dist/docx/shapeAlternateContent.js +1 -1
  19. package/dist/docx/storyBlockReplay.js +1 -1
  20. package/dist/docx/unzip.js +1 -1
  21. package/dist/docx/verbatimCapture.js +1 -1
  22. package/dist/docx/xmlParser.d.ts +5 -36
  23. package/dist/docx/xmlParser.js +14 -78
  24. package/dist/docx/xmlSafety.js +5 -5
  25. package/dist/fonts/bundledFonts.d.ts +54 -0
  26. package/dist/fonts/bundledFonts.js +45 -0
  27. package/dist/fonts/fontsourceFaces.d.ts +9 -14
  28. package/dist/fonts/fontsourceFaces.js +10 -22
  29. package/dist/layout-painter/renderPage.js +4 -2
  30. package/dist/markdown/countParagraphList.d.ts +18 -0
  31. package/dist/markdown/countParagraphList.js +90 -0
  32. package/dist/markdown/renderParagraph.js +2 -62
  33. package/dist/markdown/renderRuns.js +4 -1
  34. package/dist/prosemirror/conversion/fromProseDoc.js +5 -3
  35. package/dist/prosemirror/conversion/toProseDoc.js +4 -1
  36. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +4 -1
  37. package/dist/prosemirror/replacedAnnotations.d.ts +6 -0
  38. package/dist/prosemirror/replacedAnnotations.js +30 -8
  39. package/dist/prosemirror/schema/nodes.d.ts +7 -0
  40. package/dist/prosemirror/textBoxAnchorAttrs.js +9 -1
  41. package/dist/utils/fontLoader.d.ts +1 -1
  42. package/dist/utils/fontLoader.js +3 -11
  43. package/package.json +5 -4
  44. package/dist/docx/streamingXmlParser.d.ts +0 -60
  45. package/dist/docx/streamingXmlParser.js +0 -406
  46. package/dist/docx/xmlNamespaceContext.d.ts +0 -6
  47. package/dist/docx/xmlNamespaceContext.js +0 -53
  48. package/dist/docx/xmlResourceLimits.d.ts +0 -107
  49. package/dist/docx/xmlResourceLimits.js +0 -194
@@ -0,0 +1,45 @@
1
+ /** Each bundled family and the directory (an `@fontsource` package name) that ships it. */
2
+ const FONTSOURCE_PACKAGES = {
3
+ Arimo: "arimo",
4
+ Caladea: "caladea",
5
+ Carlito: "carlito",
6
+ Cousine: "cousine",
7
+ Lato: "lato",
8
+ "Noto Sans Arabic": "noto-sans-arabic",
9
+ "Source Sans 3": "source-sans-3",
10
+ Tinos: "tinos"
11
+ };
12
+ /**
13
+ * What each bundled family stands in for, as prose for the package's notices.
14
+ * The resolver's own mapping lives in `utils/fontLoader.ts` and
15
+ * `utils/fontResolver.ts`.
16
+ */
17
+ const BUNDLED_FAMILY_SUBSTITUTES = {
18
+ Arimo: "Arial, and the generic sans-serif",
19
+ Caladea: "Cambria",
20
+ Carlito: "Calibri",
21
+ Cousine: "Courier New, and the generic monospace",
22
+ Lato: "Aptos and Aptos Display",
23
+ "Noto Sans Arabic": "Dubai, and Arabic script in every other family",
24
+ "Source Sans 3": "Segoe UI",
25
+ Tinos: "Times New Roman, and the generic serif"
26
+ };
27
+ /**
28
+ * The subsets a headless measurer or the PDF writer reads, in resolution
29
+ * priority: a code point two subsets cover resolves to the earlier one.
30
+ * A browser is served every subset the family is cut into.
31
+ */
32
+ const MEASURED_SUBSETS = [
33
+ "latin",
34
+ "latin-ext",
35
+ "arabic"
36
+ ];
37
+ /** Folio paints 400 and 700 only, as `DisplayFontFace` states. */
38
+ const BUNDLED_WEIGHTS = [400, 700];
39
+ /** The styles a family may be cut in; a family without italics ships `normal` only. */
40
+ const BUNDLED_STYLES = ["normal", "italic"];
41
+ /** `files/<package>-<subset>-<weight>-<style>.<format>`: the path of one served binary. */
42
+ const bundledFontFilePath = ({ packageName, subset, weight, style, format }) => `files/${packageName}-${subset}-${String(weight)}-${style}.${format}`;
43
+ /** Each family's directory carries the ranges its subsets were cut with. */
44
+ const UNICODE_RANGE_FILE = "unicode.json";
45
+ export { BUNDLED_FAMILY_SUBSTITUTES, BUNDLED_STYLES, BUNDLED_WEIGHTS, FONTSOURCE_PACKAGES, MEASURED_SUBSETS, UNICODE_RANGE_FILE, bundledFontFilePath };
@@ -1,20 +1,14 @@
1
1
  import { HeadlessFontSource } from "./headlessMeasure.js";
2
- /** Where `@fontsource` files come from: `null` when the package or file is absent. */
2
+ import { FONTSOURCE_PACKAGES } from "./bundledFonts.js";
3
+ /**
4
+ * Where the face files come from: `null` when the family or file is absent.
5
+ * Each family is a directory laid out like its `@fontsource` package;
6
+ * `@stll/folio-fonts/node` reads the copies that package ships.
7
+ */
3
8
  export type FontsourceFiles = {
4
- /** Bytes of `@fontsource/<packageName>/<relativePath>`. */
9
+ /** Bytes of `<relativePath>` in the family directory `<packageName>`. */
5
10
  readonly read: (packageName: string, relativePath: string) => Uint8Array | null;
6
11
  };
7
- /** Each bundled family and the `@fontsource` package that ships it. */
8
- export declare const FONTSOURCE_PACKAGES: {
9
- readonly Arimo: "arimo";
10
- readonly Caladea: "caladea";
11
- readonly Carlito: "carlito";
12
- readonly Cousine: "cousine";
13
- readonly Lato: "lato";
14
- readonly "Noto Sans Arabic": "noto-sans-arabic";
15
- readonly "Source Sans 3": "source-sans-3";
16
- readonly Tinos: "tinos";
17
- };
18
12
  /**
19
13
  * A CSS `<string>` token, quoted and escaped. A family name comes from an
20
14
  * authored document and is emitted into a `<style>` element, so the
@@ -41,4 +35,5 @@ export type FontsourceFaces = {
41
35
  readonly fontFaceCss: (options?: FontsourceFontFaceCssOptions) => string;
42
36
  };
43
37
  /** The `@fontsource` faces, read through `files` and cached per face. */
44
- export declare const createFontsourceFaces: (files: FontsourceFiles) => FontsourceFaces;
38
+ export declare const createFontsourceFaces: (files: FontsourceFiles) => FontsourceFaces;
39
+ export { FONTSOURCE_PACKAGES };
@@ -2,6 +2,7 @@ import { bytesToDataUrl } from "../utils/base64.js";
2
2
  import { FONT_MAPPING } from "../utils/fontLoader.js";
3
3
  import { parseFontFamilyList, resolveFontFamily } from "../utils/fontResolver.js";
4
4
  import { decodeText } from "../utils/textEncoding.js";
5
+ import { BUNDLED_WEIGHTS, FONTSOURCE_PACKAGES, MEASURED_SUBSETS, UNICODE_RANGE_FILE, bundledFontFilePath } from "./bundledFonts.js";
5
6
  // src/fonts/fontsourceFaces.ts
6
7
  /**
7
8
  * Faces from the `@fontsource` packages, for a caller that measures and paints
@@ -23,25 +24,8 @@ import { decodeText } from "../utils/textEncoding.js";
23
24
  * The subsets served, in resolution priority: a code point two subsets cover
24
25
  * resolves to the earlier one on both sides.
25
26
  */
26
- const SUBSET_PRIORITY = [
27
- "latin",
28
- "latin-ext",
29
- "arabic"
30
- ];
31
- /** Folio paints 400 and 700 only, as `DisplayFontFace` states. */
32
- const REGULAR_WEIGHT = 400;
33
- const BOLD_WEIGHT = 700;
34
- /** Each bundled family and the `@fontsource` package that ships it. */
35
- const FONTSOURCE_PACKAGES = {
36
- Arimo: "arimo",
37
- Caladea: "caladea",
38
- Carlito: "carlito",
39
- Cousine: "cousine",
40
- Lato: "lato",
41
- "Noto Sans Arabic": "noto-sans-arabic",
42
- "Source Sans 3": "source-sans-3",
43
- Tinos: "tinos"
44
- };
27
+ const SUBSET_PRIORITY = MEASURED_SUBSETS;
28
+ const [REGULAR_WEIGHT, BOLD_WEIGHT] = BUNDLED_WEIGHTS;
45
29
  /** A CSS stack ends in a generic, so a generic must resolve to a bundled family too. */
46
30
  const GENERIC_FAMILIES = {
47
31
  serif: "Tinos",
@@ -81,8 +65,6 @@ const resolveBundledFamily = (family) => {
81
65
  return null;
82
66
  };
83
67
  const weightOf = (bold) => bold ? BOLD_WEIGHT : REGULAR_WEIGHT;
84
- /** `@fontsource` ships the ranges it cut each subset with, beside the files. */
85
- const UNICODE_RANGE_FILE = "unicode.json";
86
68
  const RANGE_TOKEN_RE = /^U\+([0-9A-Fa-f]+)(?:-([0-9A-Fa-f]+))?$/u;
87
69
  const parseRangeToken = (token) => {
88
70
  const match = RANGE_TOKEN_RE.exec(token.trim());
@@ -172,7 +154,13 @@ const createFontsourceFaces = (files) => {
172
154
  const cached = faceCache.get(key);
173
155
  if (cached !== void 0) return cached;
174
156
  const binaries = SUBSET_PRIORITY.flatMap((subset) => {
175
- const bytes = files.read(packageName, `files/${packageName}-${subset}-${String(weight)}-${style}.woff`);
157
+ const bytes = files.read(packageName, bundledFontFilePath({
158
+ packageName,
159
+ subset,
160
+ weight,
161
+ style,
162
+ format: "woff"
163
+ }));
176
164
  return bytes === null ? [] : [{
177
165
  subset,
178
166
  bytes
@@ -1124,7 +1124,8 @@ function renderPage(page, context, options = {}) {
1124
1124
  }
1125
1125
  if (shouldClipHeader) {
1126
1126
  headerEl.style.maxHeight = `${availableHeaderHeight}px`;
1127
- headerEl.style.overflow = "clip";
1127
+ headerEl.style.overflowX = "visible";
1128
+ headerEl.style.overflowY = "clip";
1128
1129
  headerEl.style.overflowClipMargin = `${HEADER_FOOTER_RULE_CLIP_MARGIN}px`;
1129
1130
  }
1130
1131
  pageEl.append(headerEl);
@@ -1177,7 +1178,8 @@ function renderPage(page, context, options = {}) {
1177
1178
  }
1178
1179
  if (shouldClipFooter) {
1179
1180
  footerEl.style.maxHeight = `${availableFooterHeight}px`;
1180
- footerEl.style.overflow = "clip";
1181
+ footerEl.style.overflowX = "visible";
1182
+ footerEl.style.overflowY = "clip";
1181
1183
  footerEl.style.overflowClipMargin = `${HEADER_FOOTER_RULE_CLIP_MARGIN}px`;
1182
1184
  }
1183
1185
  pageEl.append(footerEl);
@@ -0,0 +1,18 @@
1
+ import { document_d_exports } from "../types/document.js";
2
+ import { RenderContext } from "./types.js";
3
+ export type CountedParagraphList = {
4
+ /**
5
+ * The list the paragraph shows: its own numbering's, or, while a pending
6
+ * property change removed it, the one that numbering showed.
7
+ */
8
+ visibleList: document_d_exports.ListRendering | undefined;
9
+ /** The label the counter gave the paragraph; absent when it shows none. */
10
+ label: string | undefined;
11
+ };
12
+ export declare function countParagraphList(ctx: RenderContext, pkg: document_d_exports.DocxPackage | undefined, para: document_d_exports.Paragraph): CountedParagraphList;
13
+ /**
14
+ * Count the paragraphs of a text box Markdown does not write, cells
15
+ * included, in document order. A text box holds no text box, so the walk
16
+ * stops at each paragraph.
17
+ */
18
+ export declare function countTextBoxLists(ctx: RenderContext, pkg: document_d_exports.DocxPackage | undefined, shape: document_d_exports.Shape): void;
@@ -0,0 +1,90 @@
1
+ import { getCachedNumberingMap } from "../docx/numberingParser.js";
2
+ import { BLOCK_TREE_DESCENT, visitBlockTreeRecords } from "../docx/paragraphTraversal.js";
3
+ import { listLabelAttrsFromRendering } from "../prosemirror/listLabels.js";
4
+ import { effectiveParagraphNumberingReference, paragraphNumberingAttr } from "../prosemirror/numberingAttr.js";
5
+ import { resolveParagraphList } from "../prosemirror/paragraphList.js";
6
+ // src/markdown/countParagraphList.ts
7
+ /**
8
+ * Advance the Markdown walk's list counter past one paragraph, as the page
9
+ * counts it. Every paragraph the page counts, Markdown counts, whatever it
10
+ * renders as and whether or not it writes it: the items after a numbered
11
+ * heading continue from its number, and the paragraphs of a text box count
12
+ * right after the paragraph the box is drawn in (where the page and the
13
+ * content reader count them), although Markdown writes no text box.
14
+ */
15
+ /** Explicit projection carried by canonical models that are not attached to a package. */
16
+ const standaloneListRendering = (para) => para.listRendering;
17
+ function countParagraphList(ctx, pkg, para) {
18
+ const inherited = ctx.paragraphNumbering(para.formatting?.styleId);
19
+ const inheritedReference = effectiveParagraphNumberingReference({
20
+ numPr: void 0,
21
+ numPrFromStyle: inherited
22
+ });
23
+ const sources = {
24
+ numPr: para.formatting?.numPr,
25
+ numPrFromStyle: para.formatting?.numPrFromStyle
26
+ };
27
+ const numbering = pkg?.numbering ? getCachedNumberingMap(pkg.numbering) : null;
28
+ const resolvedList = resolveParagraphList({
29
+ ...sources,
30
+ inheritedReference: inheritedReference ?? null
31
+ }, numbering, standaloneListRendering(para));
32
+ const resolvedRendering = resolvedList.kind === "marker" ? resolvedList.rendering : void 0;
33
+ const counterAttrs = listLabelAttrsFromRendering(resolvedRendering, {
34
+ ...sources,
35
+ numPrFromStyle: resolvedList.kind === "none" ? void 0 : inherited ?? sources.numPrFromStyle
36
+ });
37
+ if (sources.numPr === void 0 && sources.numPrFromStyle !== void 0 && inheritedReference === void 0 && para.propertyChanges !== void 0 && para.propertyChanges.length > 0) counterAttrs.numPr = paragraphNumberingAttr({ kind: "none" });
38
+ if (para.pPrMark) counterAttrs.pPrMark = para.pPrMark;
39
+ let originalList;
40
+ if (para.propertyChanges && ctx.opts.trackedChanges !== "clean") counterAttrs._propertyChanges = para.propertyChanges.map(({ previousFormatting, ...change }) => {
41
+ if (!previousFormatting) return change;
42
+ const previousInherited = ctx.paragraphNumbering(previousFormatting.styleId);
43
+ const previousInheritedReference = effectiveParagraphNumberingReference({
44
+ numPr: void 0,
45
+ numPrFromStyle: previousInherited
46
+ });
47
+ const previousSources = {
48
+ numPr: previousFormatting.numPr,
49
+ numPrFromStyle: previousFormatting.numPrFromStyle
50
+ };
51
+ const previousList = resolveParagraphList({
52
+ ...previousSources,
53
+ inheritedReference: previousInheritedReference ?? null
54
+ }, numbering);
55
+ const previousRendering = previousList.kind === "marker" ? previousList.rendering : void 0;
56
+ originalList ??= previousRendering;
57
+ const previousAttrs = listLabelAttrsFromRendering(previousRendering, {
58
+ ...previousSources,
59
+ numPrFromStyle: previousList.kind === "none" ? void 0 : previousInherited ?? previousSources.numPrFromStyle
60
+ });
61
+ return {
62
+ ...change,
63
+ previousFormatting: {
64
+ ...previousFormatting,
65
+ ...previousAttrs,
66
+ ...previousAttrs.numPr === void 0 ? { numPr: null } : { numPr: previousAttrs.numPr },
67
+ ...previousAttrs.numPrFromStyle === void 0 ? { numPrFromStyle: null } : { numPrFromStyle: previousAttrs.numPrFromStyle }
68
+ }
69
+ };
70
+ });
71
+ const countedList = ctx.nextListLabel(counterAttrs, resolvedRendering);
72
+ return {
73
+ visibleList: resolvedRendering ?? (sources.numPr?.kind === "levelOnly" ? void 0 : originalList),
74
+ label: countedList.kind === "marker" ? countedList.label : void 0
75
+ };
76
+ }
77
+ /**
78
+ * Count the paragraphs of a text box Markdown does not write, cells
79
+ * included, in document order. A text box holds no text box, so the walk
80
+ * stops at each paragraph.
81
+ */
82
+ function countTextBoxLists(ctx, pkg, shape) {
83
+ if (shape.textBody === void 0) return;
84
+ visitBlockTreeRecords(shape.textBody.content, (record) => {
85
+ if (record.type !== "paragraph") return BLOCK_TREE_DESCENT.descend;
86
+ countParagraphList(ctx, pkg, record);
87
+ return BLOCK_TREE_DESCENT.prune;
88
+ });
89
+ }
90
+ export { countParagraphList, countTextBoxLists };
@@ -1,8 +1,5 @@
1
1
  import { isQuoteStyle, resolveHeadingLevel } from "../docx/builtInStyles.js";
2
- import { getCachedNumberingMap } from "../docx/numberingParser.js";
3
- import { listLabelAttrsFromRendering } from "../prosemirror/listLabels.js";
4
- import { effectiveParagraphNumberingReference, paragraphNumberingAttr } from "../prosemirror/numberingAttr.js";
5
- import { resolveParagraphList } from "../prosemirror/paragraphList.js";
2
+ import { countParagraphList } from "./countParagraphList.js";
6
3
  import { inlineBreaksToHtml } from "./escape.js";
7
4
  import { renderParagraphInline } from "./renderRuns.js";
8
5
  // src/markdown/renderParagraph.ts
@@ -21,8 +18,6 @@ import { renderParagraphInline } from "./renderRuns.js";
21
18
  */
22
19
  /** Markdown has six heading levels; Word has nine. */
23
20
  const MAX_MARKDOWN_HEADING_LEVEL = 6;
24
- /** Explicit projection carried by canonical models that are not attached to a package. */
25
- const standaloneListRendering = (para) => para.listRendering;
26
21
  /**
27
22
  * Render a paragraph and return the block text. No surrounding blank line: the
28
23
  * caller joins blocks.
@@ -37,61 +32,7 @@ function renderParagraph(ctx, pkg, para) {
37
32
  * does not define as prose, as every other reader reads them.
38
33
  */
39
34
  function renderParagraphBlock(ctx, pkg, para) {
40
- const inherited = ctx.paragraphNumbering(para.formatting?.styleId);
41
- const inheritedReference = effectiveParagraphNumberingReference({
42
- numPr: void 0,
43
- numPrFromStyle: inherited
44
- });
45
- const sources = {
46
- numPr: para.formatting?.numPr,
47
- numPrFromStyle: para.formatting?.numPrFromStyle
48
- };
49
- const numbering = pkg?.numbering ? getCachedNumberingMap(pkg.numbering) : null;
50
- const resolvedList = resolveParagraphList({
51
- ...sources,
52
- inheritedReference: inheritedReference ?? null
53
- }, numbering, standaloneListRendering(para));
54
- const resolvedRendering = resolvedList.kind === "marker" ? resolvedList.rendering : void 0;
55
- const counterAttrs = listLabelAttrsFromRendering(resolvedRendering, {
56
- ...sources,
57
- numPrFromStyle: resolvedList.kind === "none" ? void 0 : inherited ?? sources.numPrFromStyle
58
- });
59
- if (sources.numPr === void 0 && sources.numPrFromStyle !== void 0 && inheritedReference === void 0 && para.propertyChanges !== void 0 && para.propertyChanges.length > 0) counterAttrs.numPr = paragraphNumberingAttr({ kind: "none" });
60
- if (para.pPrMark) counterAttrs.pPrMark = para.pPrMark;
61
- let originalList;
62
- if (para.propertyChanges && ctx.opts.trackedChanges !== "clean") counterAttrs._propertyChanges = para.propertyChanges.map(({ previousFormatting, ...change }) => {
63
- if (!previousFormatting) return change;
64
- const previousInherited = ctx.paragraphNumbering(previousFormatting.styleId);
65
- const previousInheritedReference = effectiveParagraphNumberingReference({
66
- numPr: void 0,
67
- numPrFromStyle: previousInherited
68
- });
69
- const previousSources = {
70
- numPr: previousFormatting.numPr,
71
- numPrFromStyle: previousFormatting.numPrFromStyle
72
- };
73
- const previousList = resolveParagraphList({
74
- ...previousSources,
75
- inheritedReference: previousInheritedReference ?? null
76
- }, numbering);
77
- const previousRendering = previousList.kind === "marker" ? previousList.rendering : void 0;
78
- originalList ??= previousRendering;
79
- const previousAttrs = listLabelAttrsFromRendering(previousRendering, {
80
- ...previousSources,
81
- numPrFromStyle: previousList.kind === "none" ? void 0 : previousInherited ?? previousSources.numPrFromStyle
82
- });
83
- return {
84
- ...change,
85
- previousFormatting: {
86
- ...previousFormatting,
87
- ...previousAttrs,
88
- ...previousAttrs.numPr === void 0 ? { numPr: null } : { numPr: previousAttrs.numPr },
89
- ...previousAttrs.numPrFromStyle === void 0 ? { numPrFromStyle: null } : { numPrFromStyle: previousAttrs.numPrFromStyle }
90
- }
91
- };
92
- });
93
- const countedList = ctx.nextListLabel(counterAttrs, resolvedRendering);
94
- const label = countedList.kind === "marker" ? countedList.label : void 0;
35
+ const { visibleList, label } = countParagraphList(ctx, pkg, para);
95
36
  const inline = renderParagraphInline(ctx, pkg, para.content, para.paraId);
96
37
  const styleId = para.formatting?.styleId;
97
38
  const headingLevel = markdownHeadingLevel(ctx, para);
@@ -108,7 +49,6 @@ function renderParagraphBlock(ctx, pkg, para) {
108
49
  type: "paragraph"
109
50
  };
110
51
  }
111
- const visibleList = resolvedRendering ?? (sources.numPr?.kind === "levelOnly" ? void 0 : originalList);
112
52
  if (visibleList && label !== void 0) return renderListItem(ctx, visibleList, label, inline);
113
53
  ctx.listIndentWidths = [];
114
54
  if (isQuoteStyle(styleId, ctx.builtInStyles)) return {
@@ -2,6 +2,7 @@ import { getHyperlinkRuns } from "../docx/hyperlinkParser.js";
2
2
  import { RELATIONSHIP_TYPES, resolveRelationshipIdOfType, resolveRelativePath } from "../docx/relsParser.js";
3
3
  import { decodeOoxmlSymbolCharacter } from "../utils/ooxmlSymbol.js";
4
4
  import { wrapComment, wrapDeletion, wrapInsertion, wrapMoveFrom, wrapMoveTo } from "./annotations.js";
5
+ import { countTextBoxLists } from "./countParagraphList.js";
5
6
  import { escapeAltText, escapeInline, escapeLinkUrl } from "./escape.js";
6
7
  import { registerImage } from "./images.js";
7
8
  import { numberNoteReference, pushWarning } from "./internals.js";
@@ -251,7 +252,9 @@ function renderRunContent(ctx, pkg, content, paraId) {
251
252
  pushWarning(ctx, `image rId=${item.image.rId ?? "(absent)"} not resolvable`);
252
253
  break;
253
254
  }
254
- case "shape": pushWarning(ctx, "shape not representable in markdown");
255
+ case "shape":
256
+ countTextBoxLists(ctx, pkg, item.shape);
257
+ pushWarning(ctx, "shape not representable in markdown");
255
258
  }
256
259
  return out;
257
260
  }
@@ -1972,7 +1972,7 @@ function extractParagraphContent(originalParagraph, _documentCounts, emptyHyperl
1972
1972
  currentTrackedChange = void 0;
1973
1973
  return;
1974
1974
  }
1975
- const { anchorId } = expectTextBoxAnchorAttrs(node);
1975
+ const { anchorId, _docxRunFormatting: carried } = expectTextBoxAnchorAttrs(node);
1976
1976
  if (textBoxAnchorMarkers.has(anchorId)) {
1977
1977
  flushCurrentInline();
1978
1978
  currentTrackedChange = void 0;
@@ -1986,8 +1986,10 @@ function extractParagraphContent(originalParagraph, _documentCounts, emptyHyperl
1986
1986
  text: ""
1987
1987
  }]
1988
1988
  };
1989
- const formatting = getAtomRunFormattingFromMarks(node.marks, formattingContext);
1990
- if (formatting) marker.formatting = formatting;
1989
+ if (carried === void 0) {
1990
+ const formatting = getAtomRunFormattingFromMarks(node.marks, formattingContext);
1991
+ if (formatting) marker.formatting = formatting;
1992
+ } else if (Object.keys(carried).length > 0) marker.formatting = structuredClone(carried);
1991
1993
  restoreRunRecord(marker, node.marks);
1992
1994
  textBoxAnchorMarkers.set(anchorId, marker);
1993
1995
  anchorRun = marker;
@@ -2180,7 +2180,10 @@ function convertRunContent({ content, marks, formatting, textBoxAnchors, authore
2180
2180
  const shp = content.shape;
2181
2181
  if (shp.textBody) {
2182
2182
  const anchorId = textBoxAnchors?.get(shp);
2183
- return anchorId ? [schema.node("textBoxAnchor", { anchorId }).mark(marks)] : [];
2183
+ return anchorId ? [schema.node("textBoxAnchor", {
2184
+ anchorId,
2185
+ _docxRunFormatting: { ...authoredFormatting }
2186
+ }).mark(marks)] : [];
2184
2187
  }
2185
2188
  return [withRunBoundaryMarks(withAlternateContent(convertShape(shp, carriedRunFormatting(authoredFormatting)), replayableShapeAlternateContent(content), content.alternateContent), marks)];
2186
2189
  }
@@ -19,7 +19,10 @@ const TextBoxAnchorExtension = createNodeExtension({
19
19
  marks: "_",
20
20
  atom: true,
21
21
  selectable: false,
22
- attrs: { anchorId: {} },
22
+ attrs: {
23
+ anchorId: {},
24
+ _docxRunFormatting: { default: null }
25
+ },
23
26
  parseDOM: [{
24
27
  tag: "span[data-docx-textbox-anchor]",
25
28
  getAttrs(dom) {
@@ -48,6 +48,12 @@ export declare const hasReplacedAnnotations: ({ carried, leading, trailing }: Re
48
48
  * replacement either.
49
49
  */
50
50
  export declare const surveyReplacedAnnotations: (doc: Node, from: number, to: number) => ReplacedAnnotations;
51
+ /**
52
+ * {@link surveyReplacedAnnotations} over the span as it reads once accepted:
53
+ * what a replacement of it carries, which a direct edit of the same text
54
+ * carries too.
55
+ */
56
+ export declare const surveyAcceptedReplacedAnnotations: (doc: Node, from: number, to: number) => ReplacedAnnotations;
51
57
  type AnnotatedReplacementOptions = {
52
58
  annotations: ReplacedAnnotations;
53
59
  /** The replacement's own inline content, already carrying its formatting. */
@@ -197,19 +197,21 @@ const NOTHING_REPLACED = {
197
197
  trailing: []
198
198
  };
199
199
  const hasReplacedAnnotations = ({ carried, leading, trailing }) => carried.length > 0 || leading.length > 0 || trailing.length > 0;
200
- /**
201
- * The annotations carried by `[from, to)`. An anchor counts only when the
202
- * range covers it whole: a partially covered atom is not deleted by the
203
- * replacement either.
204
- */
205
- const surveyReplacedAnnotations = (doc, from, to) => {
200
+ const lendsMarks = (node, content) => {
201
+ switch (content) {
202
+ case "asWritten": return true;
203
+ case "asAccepted": return !node.marks.some(({ type }) => type.name === "deletion");
204
+ default: throw new Error(`Unhandled replaced content ${String(content)}`);
205
+ }
206
+ };
207
+ const surveyAnnotations = (doc, { from, to, content }) => {
206
208
  if (to <= from) return NOTHING_REPLACED;
207
209
  const carried = [];
208
210
  const leading = [];
209
211
  const trailing = [];
210
212
  doc.nodesBetween(from, to, (node, position) => {
211
213
  if (!node.isInline) return true;
212
- for (const mark of node.marks) if (isCarried(mark) && !alreadyCarried(mark, carried)) carried.push(mark);
214
+ for (const mark of lendsMarks(node, content) ? node.marks : []) if (isCarried(mark) && !alreadyCarried(mark, carried)) carried.push(mark);
213
215
  const placement = anchorPlacement(node);
214
216
  if (placement === void 0 || position < from || position + node.nodeSize > to) return false;
215
217
  (opensRange(node, placement) ? leading : trailing).push(node);
@@ -222,6 +224,26 @@ const surveyReplacedAnnotations = (doc, from, to) => {
222
224
  };
223
225
  };
224
226
  /**
227
+ * The annotations carried by `[from, to)`. An anchor counts only when the
228
+ * range covers it whole: a partially covered atom is not deleted by the
229
+ * replacement either.
230
+ */
231
+ const surveyReplacedAnnotations = (doc, from, to) => surveyAnnotations(doc, {
232
+ from,
233
+ to,
234
+ content: "asWritten"
235
+ });
236
+ /**
237
+ * {@link surveyReplacedAnnotations} over the span as it reads once accepted:
238
+ * what a replacement of it carries, which a direct edit of the same text
239
+ * carries too.
240
+ */
241
+ const surveyAcceptedReplacedAnnotations = (doc, from, to) => surveyAnnotations(doc, {
242
+ from,
243
+ to,
244
+ content: "asAccepted"
245
+ });
246
+ /**
225
247
  * Whether the span already contributed this mark, or one the mark would evict.
226
248
  * Comments do not exclude each other, so every comment is collected; a
227
249
  * hyperlink excludes its own type, so the first address in the span wins
@@ -476,4 +498,4 @@ const inheritedReplacementMarks = (doc, from, to) => {
476
498
  const $from = doc.resolve(from);
477
499
  return (to === from ? $from.marks() : $from.marksAcross(doc.resolve(to))) ?? Mark.none;
478
500
  };
479
- export { NON_INCLUSIVE_MARK_DISPOSITION, addCarriedMarks, annotatedReplacement, commentPoint, commentsAcrossBlockBoundary, commentsForTrackedInsertion, createResolvedCommentPointCollector, hasReplacedAnnotations, inheritedReplacementMarks, joinCommentStretches, nonInclusiveMarkNames, placeTrackedInsertion, resolvedCommentPoints, surveyReplacedAnnotations };
501
+ export { NON_INCLUSIVE_MARK_DISPOSITION, addCarriedMarks, annotatedReplacement, commentPoint, commentsAcrossBlockBoundary, commentsForTrackedInsertion, createResolvedCommentPointCollector, hasReplacedAnnotations, inheritedReplacementMarks, joinCommentStretches, nonInclusiveMarkNames, placeTrackedInsertion, resolvedCommentPoints, surveyAcceptedReplacedAnnotations, surveyReplacedAnnotations };
@@ -969,6 +969,13 @@ export type TextBoxAttrs = {
969
969
  /** Internal inline position marker for an extracted text box block. */
970
970
  export type TextBoxAnchorAttrs = {
971
971
  anchorId: string;
972
+ /**
973
+ * The `w:rPr` of the run the text box came from, possibly empty. The
974
+ * anchor's formatting marks are rendered in the paragraph's style cascade
975
+ * and are not rebased when the paragraph is restyled, so the save writes
976
+ * this instead. Absent on an anchor not read from a package.
977
+ */
978
+ _docxRunFormatting?: document_d_exports.TextFormatting;
972
979
  };
973
980
  /**
974
981
  * Table node attributes
@@ -12,13 +12,21 @@ const readTextBoxAnchorAttrs = (node) => {
12
12
  path: "textBoxAnchor.attrs.anchorId",
13
13
  message: "Expected a non-empty string."
14
14
  });
15
+ const runFormatting = node.attrs["_docxRunFormatting"];
16
+ if (runFormatting !== null && runFormatting !== void 0 && (typeof runFormatting !== "object" || Array.isArray(runFormatting))) issues.push({
17
+ path: "textBoxAnchor.attrs._docxRunFormatting",
18
+ message: "Expected run formatting or null."
19
+ });
15
20
  if (issues.length > 0 || typeof anchorId !== "string") return {
16
21
  ok: false,
17
22
  issues
18
23
  };
19
24
  return {
20
25
  ok: true,
21
- value: { anchorId }
26
+ value: runFormatting === null || runFormatting === void 0 ? { anchorId } : {
27
+ anchorId,
28
+ _docxRunFormatting: runFormatting
29
+ }
22
30
  };
23
31
  };
24
32
  const expectTextBoxAnchorAttrs = (node) => {
@@ -2,7 +2,7 @@
2
2
  * Font Loader
3
3
  *
4
4
  * Manages font availability for the DOCX editor. Fonts are bundled via
5
- * fontsource packages (WOFF2, no external requests). This module
5
+ * `@stll/folio-fonts` (WOFF2, no external requests). This module
6
6
  * tracks which fonts are loaded and provides callbacks for re-rendering
7
7
  * when fonts become available.
8
8
  */
@@ -1,9 +1,10 @@
1
+ import { FONTSOURCE_PACKAGES } from "../fonts/bundledFonts.js";
1
2
  // src/utils/fontLoader.ts
2
3
  /**
3
4
  * Font Loader
4
5
  *
5
6
  * Manages font availability for the DOCX editor. Fonts are bundled via
6
- * fontsource packages (WOFF2, no external requests). This module
7
+ * `@stll/folio-fonts` (WOFF2, no external requests). This module
7
8
  * tracks which fonts are loaded and provides callbacks for re-rendering
8
9
  * when fonts become available.
9
10
  */
@@ -34,16 +35,7 @@ const FONT_MAPPING = {
34
35
  "Lucida Console": "monospace"
35
36
  };
36
37
  /** Bundled font families (available without network requests). */
37
- const BUNDLED_FONTS = /* @__PURE__ */ new Set([
38
- "Carlito",
39
- "Caladea",
40
- "Arimo",
41
- "Tinos",
42
- "Cousine",
43
- "Lato",
44
- "Source Sans 3",
45
- "Noto Sans Arabic"
46
- ]);
38
+ const BUNDLED_FONTS = new Set(Object.keys(FONTSOURCE_PACKAGES));
47
39
  /**
48
40
  * Mark a font as loaded. For bundled fonts this is immediate;
49
41
  * for others it checks CSS Font Loading API.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/folio-core",
3
- "version": "0.65.0",
3
+ "version": "0.66.1",
4
4
  "description": "Headless, framework-neutral core of folio: the OOXML (.docx) parser, document model, ProseMirror integration, and page-layout engine. No React.",
5
5
  "keywords": [
6
6
  "document-model",
@@ -29,7 +29,9 @@
29
29
  "skills",
30
30
  "README.md",
31
31
  "LICENSE",
32
- "NOTICE.md"
32
+ "NOTICE.md",
33
+ "licenses",
34
+ "THIRD_PARTY_NOTICES.md"
33
35
  ],
34
36
  "type": "module",
35
37
  "sideEffects": false,
@@ -171,14 +173,13 @@
171
173
  "perf": "bun scripts/profile-editor.ts"
172
174
  },
173
175
  "dependencies": {
174
- "@stll/docx-core": "^0.40.0",
176
+ "@stll/docx-core": "^0.41.0",
175
177
  "@stll/docx-utils": "^0.2.0",
176
178
  "@stll/template-conditions": "^0.6.2",
177
179
  "@stll/utf8-boundaries": "0.2.0",
178
180
  "better-result": "3.0.1",
179
181
  "csstype": "^3.1.3",
180
182
  "dompurify": "^3.4.17",
181
- "fast-xml-parser": "^5.11.2",
182
183
  "hyphen": "1.14.1",
183
184
  "utif2": "^4.1.0",
184
185
  "valibot": "1.5.0"