@sametalmdr/word-editor 0.2.0-alpha → 0.3.0-alpha

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 (82) hide show
  1. package/CHANGELOG.md +108 -70
  2. package/LICENSE +21 -21
  3. package/README.md +286 -247
  4. package/dist/canvas/box-clipboard.js +238 -238
  5. package/dist/canvas/box-selection.js +80 -80
  6. package/dist/canvas/canvas-editor.js +502 -502
  7. package/dist/canvas/controller.js +717 -717
  8. package/dist/canvas/geometry.js +131 -131
  9. package/dist/canvas/gesture.d.ts +0 -1
  10. package/dist/canvas/gesture.js +289 -289
  11. package/dist/canvas/handles.js +212 -212
  12. package/dist/canvas/menu.js +82 -82
  13. package/dist/canvas/mode-controller.js +81 -81
  14. package/dist/canvas/nudge.js +108 -108
  15. package/dist/canvas/overlay.js +149 -149
  16. package/dist/canvas/snap.js +132 -132
  17. package/dist/canvas/viewport.js +112 -112
  18. package/dist/canvas/z-order.js +171 -171
  19. package/dist/commands/alignment.js +150 -150
  20. package/dist/commands/box.js +272 -272
  21. package/dist/commands/clipboard.js +93 -93
  22. package/dist/commands/graphemes.js +112 -112
  23. package/dist/commands/marks.js +153 -153
  24. package/dist/commands/paragraph-props.js +176 -176
  25. package/dist/commands/size.js +368 -368
  26. package/dist/commands/text.js +291 -291
  27. package/dist/docx.d.ts +1 -0
  28. package/dist/docx.js +15 -13
  29. package/dist/editor/editing-host.js +434 -434
  30. package/dist/element.js +41 -41
  31. package/dist/fonts/OFL-Carlito.txt +94 -0
  32. package/dist/fonts/calibri-metrics.d.ts +27 -0
  33. package/dist/fonts/calibri-metrics.js +88 -0
  34. package/dist/history/history.js +433 -433
  35. package/dist/index.js +63 -63
  36. package/dist/input/beforeinput.js +224 -224
  37. package/dist/input/clipboard.js +107 -107
  38. package/dist/input/composition.js +341 -341
  39. package/dist/input/keymap.js +54 -54
  40. package/dist/io/ooxml/custgeom.js +72 -72
  41. package/dist/io/ooxml/drawingml.js +232 -232
  42. package/dist/io/ooxml/edits.js +67 -67
  43. package/dist/io/ooxml/export-splice.js +880 -880
  44. package/dist/io/ooxml/export.js +365 -365
  45. package/dist/io/ooxml/fill.d.ts +93 -8
  46. package/dist/io/ooxml/fill.js +618 -562
  47. package/dist/io/ooxml/fit.d.ts +252 -0
  48. package/dist/io/ooxml/fit.js +598 -0
  49. package/dist/io/ooxml/import-gate.js +200 -200
  50. package/dist/io/ooxml/import.js +691 -691
  51. package/dist/io/ooxml/inflate.js +126 -126
  52. package/dist/io/ooxml/media.js +87 -87
  53. package/dist/io/ooxml/ns.d.ts +2 -0
  54. package/dist/io/ooxml/ns.js +26 -21
  55. package/dist/io/ooxml/numbering.js +290 -290
  56. package/dist/io/ooxml/opc-write.js +171 -171
  57. package/dist/io/ooxml/provenance.js +93 -93
  58. package/dist/io/ooxml/recipe.js +708 -708
  59. package/dist/io/ooxml/relationships.js +59 -59
  60. package/dist/io/ooxml/run-fonts.d.ts +89 -0
  61. package/dist/io/ooxml/run-fonts.js +378 -0
  62. package/dist/io/ooxml/slots.d.ts +4 -0
  63. package/dist/io/ooxml/slots.js +582 -581
  64. package/dist/io/ooxml/styles-resolve.js +257 -257
  65. package/dist/io/ooxml/units.js +43 -43
  66. package/dist/io/ooxml/value.js +120 -120
  67. package/dist/io/ooxml/xml-scan.js +616 -616
  68. package/dist/io/ooxml/zip-container.js +228 -228
  69. package/dist/io/ooxml/zip-reject.js +106 -106
  70. package/dist/measure/placement.js +83 -83
  71. package/dist/measure/report.js +75 -75
  72. package/dist/model/ids.js +70 -70
  73. package/dist/model/normalize.js +146 -146
  74. package/dist/model/position.js +283 -283
  75. package/dist/model/transaction.js +209 -209
  76. package/dist/platform/capabilities.js +25 -25
  77. package/dist/react/index.js +21 -21
  78. package/dist/selection/controller.js +315 -315
  79. package/dist/selection/dom-map.js +334 -334
  80. package/dist/state/editor-state.js +109 -109
  81. package/dist/styles.css +66 -66
  82. package/package.json +94 -93
@@ -1,59 +1,59 @@
1
- // OPC relationship resolution — the piece ADR-0020 deferred (zip-container.js:
2
- // "real _rels/.rels relationship resolution is not implemented yet ... belongs
3
- // in import.js"). A `<w:drawing>` picture references its bytes indirectly:
4
- // `a:blip r:embed="rId5"` names a relationship id, and `word/_rels/
5
- // document.xml.rels` maps that id to a part (`Target="media/image1.png"`).
6
- // This resolves the id -> part-name link so the image render branch (Doc.images)
7
- // can point at the retained `word/media/*` bytes.
8
- //
9
- // Deliberately narrow: it parses ONE relationships part into an id -> resolved
10
- // part-name map. It reads the whole `<Relationship>` set (not just images) — the
11
- // caller filters by what it looks up. Targets are resolved relative to the part
12
- // the .rels belongs to (its base directory), the OPC rule; a "/"-prefixed Target
13
- // is package-absolute. External targets (TargetMode="External", e.g. a hyperlink)
14
- // are skipped — they are URLs, not package parts.
15
-
16
- import { findChildren, getAttr } from "./xml-scan.js";
17
- import { PKG_RELS_NS } from "./ns.js";
18
-
19
- /**
20
- * Collapse `..`/`.` segments in a package part path — Targets like
21
- * `../media/image1.png` resolve against the base directory. Package paths use
22
- * forward slashes and never a drive/host, so this stays a pure segment walk.
23
- * @param {string} path
24
- * @returns {string}
25
- */
26
- function normalizePath(path) {
27
- /** @type {string[]} */
28
- const out = [];
29
- for (const segment of path.split("/")) {
30
- if (segment === "" || segment === ".") continue;
31
- if (segment === "..") out.pop();
32
- else out.push(segment);
33
- }
34
- return out.join("/");
35
- }
36
-
37
- /**
38
- * `Id -> resolved package part name` for one relationships part.
39
- *
40
- * @param {import("./xml-scan.js").XmlElement} root scanned `<Relationships>` root
41
- * @param {string} baseDir the directory the owning part lives in, e.g. `"word"`
42
- * for `word/document.xml` (whose rels is `word/_rels/document.xml.rels`).
43
- * Relative Targets resolve against it.
44
- * @returns {Map<string, string>}
45
- */
46
- export function parseRelationships(root, baseDir) {
47
- /** @type {Map<string, string>} */
48
- const map = new Map();
49
- const base = baseDir === "" ? "" : `${baseDir}/`;
50
- for (const rel of findChildren(root, PKG_RELS_NS, "Relationship")) {
51
- const id = getAttr(rel, "", "Id");
52
- const target = getAttr(rel, "", "Target");
53
- if (id === undefined || target === undefined) continue;
54
- if (getAttr(rel, "", "TargetMode") === "External") continue; // a URL, not a part
55
- const resolved = target.startsWith("/") ? normalizePath(target.slice(1)) : normalizePath(`${base}${target}`);
56
- map.set(id, resolved);
57
- }
58
- return map;
59
- }
1
+ // OPC relationship resolution — the piece ADR-0020 deferred (zip-container.js:
2
+ // "real _rels/.rels relationship resolution is not implemented yet ... belongs
3
+ // in import.js"). A `<w:drawing>` picture references its bytes indirectly:
4
+ // `a:blip r:embed="rId5"` names a relationship id, and `word/_rels/
5
+ // document.xml.rels` maps that id to a part (`Target="media/image1.png"`).
6
+ // This resolves the id -> part-name link so the image render branch (Doc.images)
7
+ // can point at the retained `word/media/*` bytes.
8
+ //
9
+ // Deliberately narrow: it parses ONE relationships part into an id -> resolved
10
+ // part-name map. It reads the whole `<Relationship>` set (not just images) — the
11
+ // caller filters by what it looks up. Targets are resolved relative to the part
12
+ // the .rels belongs to (its base directory), the OPC rule; a "/"-prefixed Target
13
+ // is package-absolute. External targets (TargetMode="External", e.g. a hyperlink)
14
+ // are skipped — they are URLs, not package parts.
15
+
16
+ import { findChildren, getAttr } from "./xml-scan.js";
17
+ import { PKG_RELS_NS } from "./ns.js";
18
+
19
+ /**
20
+ * Collapse `..`/`.` segments in a package part path — Targets like
21
+ * `../media/image1.png` resolve against the base directory. Package paths use
22
+ * forward slashes and never a drive/host, so this stays a pure segment walk.
23
+ * @param {string} path
24
+ * @returns {string}
25
+ */
26
+ function normalizePath(path) {
27
+ /** @type {string[]} */
28
+ const out = [];
29
+ for (const segment of path.split("/")) {
30
+ if (segment === "" || segment === ".") continue;
31
+ if (segment === "..") out.pop();
32
+ else out.push(segment);
33
+ }
34
+ return out.join("/");
35
+ }
36
+
37
+ /**
38
+ * `Id -> resolved package part name` for one relationships part.
39
+ *
40
+ * @param {import("./xml-scan.js").XmlElement} root scanned `<Relationships>` root
41
+ * @param {string} baseDir the directory the owning part lives in, e.g. `"word"`
42
+ * for `word/document.xml` (whose rels is `word/_rels/document.xml.rels`).
43
+ * Relative Targets resolve against it.
44
+ * @returns {Map<string, string>}
45
+ */
46
+ export function parseRelationships(root, baseDir) {
47
+ /** @type {Map<string, string>} */
48
+ const map = new Map();
49
+ const base = baseDir === "" ? "" : `${baseDir}/`;
50
+ for (const rel of findChildren(root, PKG_RELS_NS, "Relationship")) {
51
+ const id = getAttr(rel, "", "Id");
52
+ const target = getAttr(rel, "", "Target");
53
+ if (id === undefined || target === undefined) continue;
54
+ if (getAttr(rel, "", "TargetMode") === "External") continue; // a URL, not a part
55
+ const resolved = target.startsWith("/") ? normalizePath(target.slice(1)) : normalizePath(`${base}${target}`);
56
+ map.set(id, resolved);
57
+ }
58
+ return map;
59
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @param {Uint8Array} stylesXml word/styles.xml
3
+ * @param {Uint8Array | null} themeXml word/theme/theme1.xml, if the package has one
4
+ * @returns {(styleId: string | null, rPr: XmlElement | undefined) => ResolvedRunFont}
5
+ * `styleId`: the paragraph's pStyle, null for the default paragraph style
6
+ */
7
+ export function runFontResolver(stylesXml: Uint8Array, themeXml: Uint8Array | null): (styleId: string | null, rPr: XmlElement | undefined) => ResolvedRunFont;
8
+ /**
9
+ * What checkFit needs of a run and of its paragraph (ADR-0042 Karar 4).
10
+ * @param {Uint8Array} stylesXml word/styles.xml
11
+ * @param {Uint8Array | null} themeXml
12
+ * @returns {{run: (styleId: string | null, rPr: XmlElement | undefined) => RunLayout, paragraph: (styleId: string | null, pPr: XmlElement | undefined) => ParagraphLayout}}
13
+ */
14
+ export function layoutResolver(stylesXml: Uint8Array, themeXml: Uint8Array | null): {
15
+ run: (styleId: string | null, rPr: XmlElement | undefined) => RunLayout;
16
+ paragraph: (styleId: string | null, pPr: XmlElement | undefined) => ParagraphLayout;
17
+ };
18
+ /**
19
+ * The style a paragraph names (w:pPr/w:pStyle), or null.
20
+ * @param {XmlElement} paragraph
21
+ * @returns {string | null}
22
+ */
23
+ export function paragraphStyleOf(paragraph: XmlElement): string | null;
24
+ export type XmlElement = import("./xml-scan.js").XmlElement;
25
+ export type RunFont = {
26
+ ascii: string | null;
27
+ hAnsi: string | null;
28
+ sizePt: number | null;
29
+ };
30
+ export type Unsupported = {
31
+ code: "CHARACTER_STYLE";
32
+ } | {
33
+ code: "THEME_FONT_WITHOUT_THEME";
34
+ name: string;
35
+ };
36
+ export type ResolvedRunFont = {
37
+ font: RunFont;
38
+ unsupported: Unsupported[];
39
+ };
40
+ /**
41
+ * why a width cannot be known; `code` from a closed list (ADR-0042)
42
+ */
43
+ export type Unknown = {
44
+ code: string;
45
+ property?: string;
46
+ };
47
+ export type RunLayout = {
48
+ font: RunFont;
49
+ /**
50
+ * w:sz; null when nothing sets it
51
+ */
52
+ sizeHalfPoints: number | null;
53
+ bold: boolean;
54
+ italic: boolean;
55
+ /**
56
+ * w:spacing, added after every character (ADR-0042 H5)
57
+ */
58
+ spacingTwips: number;
59
+ /**
60
+ * w:rFonts@w:hint
61
+ */
62
+ hint: string | null;
63
+ /**
64
+ * w:lang@w:eastAsia
65
+ */
66
+ eastAsiaLanguage: string | null;
67
+ unknown: Unknown[];
68
+ };
69
+ /**
70
+ * measured from the box's inner left edge (ADR-0042 H6)
71
+ */
72
+ export type TabStop = {
73
+ posTwips: number;
74
+ left: boolean;
75
+ };
76
+ export type ParagraphLayout = {
77
+ leftTwips: number;
78
+ rightTwips: number;
79
+ /**
80
+ * signed: a hanging indent is negative
81
+ */
82
+ firstLineTwips: number;
83
+ jc: string;
84
+ /**
85
+ * sorted by position
86
+ */
87
+ tabs: TabStop[];
88
+ unknown: Unknown[];
89
+ };
@@ -0,0 +1,378 @@
1
+ // What a run draws with and how its paragraph is laid out, from the document
2
+ // defaults, then the paragraph's style chain (basedOn, root first, the derived
3
+ // style wins), then the element's own properties.
4
+ //
5
+ // Fonts: per slot (ascii, hAnsi), a theme attribute (asciiTheme/hAnsiTheme)
6
+ // beats the plain one at the same level and is looked up in the theme:
7
+ // minor* -> a:minorFont/a:latin, major* -> a:majorFont/a:latin. The slot logic
8
+ // is FR2-WORD's (docs/adr/evidence/fr2-word/bodytext_fonts.mjs, left as
9
+ // sealed). Moved here from templates/lib/fonts.mjs (FR3) for checkFit (FR4a,
10
+ // ADR-0042 Karar 4), which runs in the package.
11
+ //
12
+ // It refuses nothing: what it cannot resolve comes back in `unsupported` (the
13
+ // font) or `unknown` (the layout), and each caller decides — the manifest
14
+ // builder throws, checkFit says "unknown". The layout side is an ALLOWLIST: a
15
+ // property is either computed (fonts, size, bold, italic, letter spacing,
16
+ // indents, alignment, tab stops) or known to leave a line's width alone; any
17
+ // other one is a reason, never ignored.
18
+ //
19
+ // Not styles-resolve.js: that one throws on w:rStyle, does not read "off" and
20
+ // keeps firstLine and hanging apart; the fill engine never reaches it
21
+ // (test/docx-surface.test.js).
22
+
23
+ import { scanXml, findChild, findChildren, getAttr } from "./xml-scan.js";
24
+ import { W_NS, A_NS } from "./ns.js";
25
+
26
+ /**
27
+ * @typedef {import("./xml-scan.js").XmlElement} XmlElement
28
+ * @typedef {{ascii: string | null, hAnsi: string | null, sizePt: number | null}} RunFont
29
+ * @typedef {{code: "CHARACTER_STYLE"} | {code: "THEME_FONT_WITHOUT_THEME", name: string}} Unsupported
30
+ * @typedef {{font: RunFont, unsupported: Unsupported[]}} ResolvedRunFont
31
+ * @typedef {{code: string, property?: string}} Unknown why a width cannot be known; `code` from a closed list (ADR-0042)
32
+ */
33
+
34
+ /**
35
+ * @typedef {object} RunLayout
36
+ * @property {RunFont} font
37
+ * @property {number | null} sizeHalfPoints w:sz; null when nothing sets it
38
+ * @property {boolean} bold
39
+ * @property {boolean} italic
40
+ * @property {number} spacingTwips w:spacing, added after every character (ADR-0042 H5)
41
+ * @property {string | null} hint w:rFonts@w:hint
42
+ * @property {string | null} eastAsiaLanguage w:lang@w:eastAsia
43
+ * @property {Unknown[]} unknown
44
+ */
45
+
46
+ /**
47
+ * @typedef {{posTwips: number, left: boolean}} TabStop measured from the box's inner left edge (ADR-0042 H6)
48
+ * @typedef {object} ParagraphLayout
49
+ * @property {number} leftTwips
50
+ * @property {number} rightTwips
51
+ * @property {number} firstLineTwips signed: a hanging indent is negative
52
+ * @property {string} jc
53
+ * @property {TabStop[]} tabs sorted by position
54
+ * @property {Unknown[]} unknown
55
+ */
56
+
57
+ // Run properties checkFit computes, or knows to leave a line's width alone: colour, underline,
58
+ // language, highlight, shading, proofing, the complex-script twins and a vertical shift.
59
+ const RUN_PROPERTIES = new Set(["rFonts", "sz", "szCs", "b", "bCs", "i", "iCs", "spacing", "color", "u", "lang", "highlight", "shd", "noProof", "position", "vertAlign"]);
60
+ // Paragraph properties likewise: vertical spacing, pagination, East Asian line rules a Latin line
61
+ // does not reach, the paragraph mark's own run properties (it never breaks a line, ADR-0042 H4).
62
+ // numPr, textDirection, bidi and mirrorIndents pass only with the value that turns them off.
63
+ const PARAGRAPH_PROPERTIES = new Set([
64
+ "pStyle", "keepNext", "keepLines", "pageBreakBefore", "widowControl", "suppressLineNumbers", "suppressAutoHyphens",
65
+ "spacing", "ind", "contextualSpacing", "jc", "tabs", "rPr", "outlineLvl", "snapToGrid", "autoSpaceDE", "autoSpaceDN",
66
+ "adjustRightInd", "wordWrap", "kinsoku", "overflowPunct", "topLinePunct", "textAlignment", "shd", "numPr",
67
+ "textDirection", "bidi", "mirrorIndents",
68
+ ]);
69
+ const JUSTIFICATION = new Set(["left", "start", "center", "right", "end", "both"]);
70
+ const ON = new Set(["true", "1", "on"]);
71
+ const OFF = new Set(["false", "0", "off"]);
72
+ const INTEGER = /^-?[0-9]+$/;
73
+
74
+ /**
75
+ * ST_OnOff (ECMA-376 §22.9.2.7): no w:val is on.
76
+ * @param {XmlElement} el
77
+ * @returns {boolean | undefined} undefined when unreadable
78
+ */
79
+ function onOff(el) {
80
+ const val = getAttr(el, W_NS, "val");
81
+ if (val === undefined || ON.has(val)) return true;
82
+ return OFF.has(val) ? false : undefined;
83
+ }
84
+
85
+ /**
86
+ * @param {XmlElement} el
87
+ * @returns {XmlElement[]}
88
+ */
89
+ const elementChildren = (el) => /** @type {XmlElement[]} */ (el.children.filter((c) => c.kind === "element"));
90
+
91
+ /**
92
+ * The parts of word/styles.xml (and the theme) both resolvers read.
93
+ * @param {Uint8Array} stylesXml
94
+ * @param {Uint8Array | null} themeXml
95
+ */
96
+ function readStyles(stylesXml, themeXml) {
97
+ const root = scanXml(stylesXml).root;
98
+ const theme = themeXml ? scanXml(themeXml).root : null;
99
+ /** @param {string} name @returns {string | null} */
100
+ const themeFont = (name) => {
101
+ if (theme === null) return null;
102
+ const elements = findChild(theme, A_NS, "themeElements");
103
+ const scheme = elements && findChild(elements, A_NS, "fontScheme");
104
+ const set = scheme && findChild(scheme, A_NS, name.startsWith("major") ? "majorFont" : "minorFont");
105
+ const latin = set && findChild(set, A_NS, "latin");
106
+ return latin ? (getAttr(latin, "", "typeface") ?? null) : null;
107
+ };
108
+ const paragraphStyles = findChildren(root, W_NS, "style").filter((s) => getAttr(s, W_NS, "type") === "paragraph");
109
+ const byId = new Map(paragraphStyles.map((s) => [getAttr(s, W_NS, "styleId"), s]));
110
+ const defaultStyle = paragraphStyles.find((s) => getAttr(s, W_NS, "default") === "1");
111
+ const defaultId = defaultStyle ? (getAttr(defaultStyle, W_NS, "styleId") ?? null) : null;
112
+ const docDefaults = findChild(root, W_NS, "docDefaults");
113
+ const rPrDefault = docDefaults && findChild(docDefaults, W_NS, "rPrDefault");
114
+ const pPrDefault = docDefaults && findChild(docDefaults, W_NS, "pPrDefault");
115
+ /**
116
+ * The paragraph style chain, root first.
117
+ * @param {string | null} styleId null for the default paragraph style
118
+ * @returns {XmlElement[]}
119
+ */
120
+ const chainOf = (styleId) => {
121
+ /** @type {XmlElement[]} */
122
+ const chain = [];
123
+ for (let id = styleId ?? defaultId; id && byId.has(id); ) {
124
+ const style = /** @type {XmlElement} */ (byId.get(id));
125
+ if (chain.includes(style)) break;
126
+ chain.push(style);
127
+ const basedOn = findChild(style, W_NS, "basedOn");
128
+ id = basedOn ? (getAttr(basedOn, W_NS, "val") ?? null) : null;
129
+ }
130
+ return chain.reverse();
131
+ };
132
+ return {
133
+ theme,
134
+ themeFont,
135
+ chainOf,
136
+ /** @param {string} id */
137
+ hasStyle: (id) => byId.has(id),
138
+ defaultsRPr: rPrDefault && findChild(rPrDefault, W_NS, "rPr"),
139
+ defaultsPPr: pPrDefault && findChild(pPrDefault, W_NS, "pPr"),
140
+ };
141
+ }
142
+
143
+ /**
144
+ * @param {Uint8Array} stylesXml word/styles.xml
145
+ * @param {Uint8Array | null} themeXml word/theme/theme1.xml, if the package has one
146
+ * @returns {(styleId: string | null, rPr: XmlElement | undefined) => ResolvedRunFont}
147
+ * `styleId`: the paragraph's pStyle, null for the default paragraph style
148
+ */
149
+ export function runFontResolver(stylesXml, themeXml) {
150
+ const { theme, themeFont, chainOf, defaultsRPr } = readStyles(stylesXml, themeXml);
151
+ return (styleId, rPr) => {
152
+ /** @type {RunFont} */
153
+ const font = { ascii: null, hAnsi: null, sizePt: null };
154
+ /** @type {Unsupported[]} */
155
+ const unsupported = [];
156
+ /** @param {XmlElement | undefined} el */
157
+ const apply = (el) => {
158
+ if (!el) return;
159
+ if (findChild(el, W_NS, "rStyle") && !unsupported.some((u) => u.code === "CHARACTER_STYLE")) unsupported.push({ code: "CHARACTER_STYLE" });
160
+ const rFonts = findChild(el, W_NS, "rFonts");
161
+ if (rFonts) {
162
+ for (const slot of /** @type {const} */ (["ascii", "hAnsi"])) {
163
+ const themed = getAttr(rFonts, W_NS, `${slot}Theme`);
164
+ const plain = getAttr(rFonts, W_NS, slot);
165
+ if (themed !== undefined) {
166
+ if (theme === null) unsupported.push({ code: "THEME_FONT_WITHOUT_THEME", name: themed });
167
+ font[slot] = themeFont(themed);
168
+ } else if (plain !== undefined) font[slot] = plain;
169
+ }
170
+ }
171
+ const sz = findChild(el, W_NS, "sz");
172
+ const val = sz && getAttr(sz, W_NS, "val");
173
+ if (val !== undefined && /^[0-9]+$/.test(val)) font.sizePt = Number(val) / 2;
174
+ };
175
+ apply(defaultsRPr);
176
+ for (const style of chainOf(styleId)) apply(findChild(style, W_NS, "rPr"));
177
+ apply(rPr);
178
+ return { font, unsupported };
179
+ };
180
+ }
181
+
182
+ /**
183
+ * What checkFit needs of a run and of its paragraph (ADR-0042 Karar 4).
184
+ * @param {Uint8Array} stylesXml word/styles.xml
185
+ * @param {Uint8Array | null} themeXml
186
+ * @returns {{run: (styleId: string | null, rPr: XmlElement | undefined) => RunLayout, paragraph: (styleId: string | null, pPr: XmlElement | undefined) => ParagraphLayout}}
187
+ */
188
+ export function layoutResolver(stylesXml, themeXml) {
189
+ const { chainOf, hasStyle, defaultsRPr, defaultsPPr } = readStyles(stylesXml, themeXml);
190
+ const fontOf = runFontResolver(stylesXml, themeXml);
191
+ // A pStyle naming no paragraph style: which style Word falls back to is not known here.
192
+ /** @param {string | null} styleId @returns {Unknown[]} */
193
+ const missing = (styleId) => (styleId !== null && !hasStyle(styleId) ? [{ code: "STYLE_MISSING", property: "pStyle" }] : []);
194
+
195
+ /**
196
+ * @param {string | null} styleId
197
+ * @param {XmlElement | undefined} rPr
198
+ * @returns {RunLayout}
199
+ */
200
+ const run = (styleId, rPr) => {
201
+ const { font, unsupported } = fontOf(styleId, rPr);
202
+ /** @type {Unknown[]} */
203
+ const unknown = [];
204
+ /** @param {Unknown} u */
205
+ const add = (u) => {
206
+ if (!unknown.some((x) => x.code === u.code && x.property === u.property)) unknown.push(u);
207
+ };
208
+ for (const u of unsupported) add({ code: u.code });
209
+ /** @type {string | undefined} */ let sz;
210
+ /** @type {string | undefined} */ let spacing;
211
+ /** @type {string | undefined} */ let hint;
212
+ /** @type {string | undefined} */ let eastAsia;
213
+ /** @type {Record<"b" | "i", {styles: (boolean | undefined)[], direct: boolean | undefined | null}>} */
214
+ const toggles = { b: { styles: [], direct: null }, i: { styles: [], direct: null } };
215
+ /**
216
+ * @param {XmlElement | undefined} el
217
+ * @param {boolean} direct
218
+ */
219
+ const apply = (el, direct) => {
220
+ if (!el) return;
221
+ for (const c of elementChildren(el)) {
222
+ if (c.uri !== W_NS || !RUN_PROPERTIES.has(c.localName)) {
223
+ if (!(c.uri === W_NS && c.localName === "rStyle")) add({ code: "RUN_PROPERTY", property: c.localName });
224
+ continue;
225
+ }
226
+ const val = getAttr(c, W_NS, "val");
227
+ if (c.localName === "sz" && val !== undefined) sz = val;
228
+ else if (c.localName === "spacing" && val !== undefined) spacing = val;
229
+ else if (c.localName === "rFonts") hint = getAttr(c, W_NS, "hint") ?? hint;
230
+ else if (c.localName === "lang") eastAsia = getAttr(c, W_NS, "eastAsia") ?? eastAsia;
231
+ else if (c.localName === "vertAlign" && val !== "baseline") add({ code: "RUN_PROPERTY", property: "vertAlign" });
232
+ else if (c.localName === "b" || c.localName === "i") {
233
+ const t = toggles[c.localName];
234
+ if (direct) t.direct = onOff(c);
235
+ else t.styles.push(onOff(c));
236
+ }
237
+ }
238
+ };
239
+ for (const u of missing(styleId)) add(u);
240
+ apply(defaultsRPr, false);
241
+ for (const style of chainOf(styleId)) apply(findChild(style, W_NS, "rPr"), false);
242
+ apply(rPr, true);
243
+
244
+ /** @param {"b" | "i"} name @returns {boolean} */
245
+ const toggle = (name) => {
246
+ const t = toggles[name];
247
+ // Direct formatting sets a toggle property's state; in the style hierarchy ECMA-376 §17.7.3
248
+ // toggles it instead. One style-side setting reads the same either way; two do not.
249
+ const value = t.direct !== null ? t.direct : t.styles.length === 0 ? false : t.styles.length === 1 ? t.styles[0] : undefined;
250
+ if (value === undefined) {
251
+ add({ code: t.direct === null && t.styles.length > 1 ? "TOGGLE_IN_STYLES" : "RUN_PROPERTY_UNREADABLE", property: name });
252
+ return false;
253
+ }
254
+ return value;
255
+ };
256
+ const bold = toggle("b");
257
+ const italic = toggle("i");
258
+ if (sz !== undefined && !/^[0-9]+$/.test(sz)) add({ code: "RUN_PROPERTY_UNREADABLE", property: "sz" });
259
+ if (spacing !== undefined && !INTEGER.test(spacing)) add({ code: "RUN_PROPERTY_UNREADABLE", property: "spacing" });
260
+ return {
261
+ font,
262
+ sizeHalfPoints: sz !== undefined && /^[0-9]+$/.test(sz) ? Number(sz) : null,
263
+ bold,
264
+ italic,
265
+ spacingTwips: spacing !== undefined && INTEGER.test(spacing) ? Number(spacing) : 0,
266
+ hint: hint ?? null,
267
+ eastAsiaLanguage: eastAsia ?? null,
268
+ unknown,
269
+ };
270
+ };
271
+
272
+ /**
273
+ * @param {string | null} styleId
274
+ * @param {XmlElement | undefined} pPr
275
+ * @returns {ParagraphLayout}
276
+ */
277
+ const paragraph = (styleId, pPr) => {
278
+ /** @type {Unknown[]} */
279
+ const unknown = [];
280
+ /** @param {Unknown} u */
281
+ const add = (u) => {
282
+ if (!unknown.some((x) => x.code === u.code && x.property === u.property)) unknown.push(u);
283
+ };
284
+ /** @type {Record<"left" | "right" | "firstLine", string | undefined>} */
285
+ const ind = { left: undefined, right: undefined, firstLine: undefined };
286
+ let jc = "left";
287
+ /** @type {Map<number, boolean>} */
288
+ const tabs = new Map();
289
+ /** @type {string | undefined} */ let numId;
290
+ /** @param {XmlElement | undefined} el */
291
+ const apply = (el) => {
292
+ if (!el) return;
293
+ for (const c of elementChildren(el)) {
294
+ if (c.uri !== W_NS || !PARAGRAPH_PROPERTIES.has(c.localName)) {
295
+ add({ code: "PARAGRAPH_PROPERTY", property: c.localName });
296
+ continue;
297
+ }
298
+ const val = getAttr(c, W_NS, "val");
299
+ if (c.localName === "ind") {
300
+ for (const a of c.attributes) {
301
+ if (a.uri === W_NS && a.localName.endsWith("Chars")) add({ code: "INDENT_CHARS", property: a.localName });
302
+ }
303
+ /** @param {"left" | "right"} side @param {string} synonym */
304
+ const edge = (side, synonym) => {
305
+ const plain = getAttr(c, W_NS, side);
306
+ const other = getAttr(c, W_NS, synonym);
307
+ if (plain !== undefined && other !== undefined && plain !== other) add({ code: "INDENT_CONFLICT", property: side });
308
+ const v = other ?? plain;
309
+ if (v !== undefined) ind[side] = v;
310
+ };
311
+ edge("left", "start");
312
+ edge("right", "end");
313
+ // ECMA-376 §17.3.1.12: with both on one level, hanging wins.
314
+ const hanging = getAttr(c, W_NS, "hanging");
315
+ const firstLine = getAttr(c, W_NS, "firstLine");
316
+ if (hanging !== undefined) ind.firstLine = INTEGER.test(hanging) ? String(-Number(hanging)) : hanging;
317
+ else if (firstLine !== undefined) ind.firstLine = firstLine;
318
+ } else if (c.localName === "jc" && val !== undefined) {
319
+ jc = val;
320
+ } else if (c.localName === "tabs") {
321
+ for (const tab of findChildren(c, W_NS, "tab")) {
322
+ const pos = getAttr(tab, W_NS, "pos");
323
+ const kind = getAttr(tab, W_NS, "val");
324
+ if (pos === undefined || !INTEGER.test(pos) || kind === undefined) {
325
+ add({ code: "PARAGRAPH_PROPERTY_UNREADABLE", property: "tabs" });
326
+ continue;
327
+ }
328
+ if (kind === "clear") tabs.delete(Number(pos));
329
+ else tabs.set(Number(pos), kind === "left" || kind === "start");
330
+ }
331
+ } else if (c.localName === "numPr") {
332
+ const id = findChild(c, W_NS, "numId");
333
+ numId = (id && getAttr(id, W_NS, "val")) ?? numId;
334
+ } else if (c.localName === "textDirection") {
335
+ if (val !== "lrTb" && val !== "tb") add({ code: "PARAGRAPH_PROPERTY", property: "textDirection" });
336
+ } else if (c.localName === "bidi" || c.localName === "mirrorIndents") {
337
+ if (onOff(c) !== false) add({ code: "PARAGRAPH_PROPERTY", property: c.localName });
338
+ }
339
+ }
340
+ };
341
+ for (const u of missing(styleId)) add(u);
342
+ apply(defaultsPPr);
343
+ for (const style of chainOf(styleId)) apply(findChild(style, W_NS, "pPr"));
344
+ apply(pPr);
345
+
346
+ if (numId !== undefined && numId !== "0") add({ code: "PARAGRAPH_PROPERTY", property: "numPr" });
347
+ if (!JUSTIFICATION.has(jc)) add({ code: "PARAGRAPH_PROPERTY", property: "jc" });
348
+ /** @param {"left" | "right" | "firstLine"} name @returns {number} */
349
+ const twips = (name) => {
350
+ const v = ind[name];
351
+ if (v === undefined) return 0;
352
+ if (INTEGER.test(v)) return Number(v);
353
+ add({ code: "PARAGRAPH_PROPERTY_UNREADABLE", property: "ind" });
354
+ return 0;
355
+ };
356
+ return {
357
+ leftTwips: twips("left"),
358
+ rightTwips: twips("right"),
359
+ firstLineTwips: twips("firstLine"),
360
+ jc,
361
+ tabs: [...tabs].sort((x, y) => x[0] - y[0]).map(([posTwips, left]) => ({ posTwips, left })),
362
+ unknown,
363
+ };
364
+ };
365
+
366
+ return { run, paragraph };
367
+ }
368
+
369
+ /**
370
+ * The style a paragraph names (w:pPr/w:pStyle), or null.
371
+ * @param {XmlElement} paragraph
372
+ * @returns {string | null}
373
+ */
374
+ export function paragraphStyleOf(paragraph) {
375
+ const pPr = findChild(paragraph, W_NS, "pPr");
376
+ const pStyle = pPr && findChild(pPr, W_NS, "pStyle");
377
+ return pStyle ? (getAttr(pStyle, W_NS, "val") ?? null) : null;
378
+ }
@@ -78,6 +78,10 @@ export type TagSite = {
78
78
  name: string;
79
79
  t: import("./xml-scan.js").XmlElement;
80
80
  run: import("./xml-scan.js").XmlElement;
81
+ /**
82
+ * the w:p holding the run (contextProblem checked it)
83
+ */
84
+ paragraph: import("./xml-scan.js").XmlElement;
81
85
  };
82
86
  /**
83
87
  * one tag of a box's Choice copy, with its Fallback twin