@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.
- package/CHANGELOG.md +108 -70
- package/LICENSE +21 -21
- package/README.md +286 -247
- package/dist/canvas/box-clipboard.js +238 -238
- package/dist/canvas/box-selection.js +80 -80
- package/dist/canvas/canvas-editor.js +502 -502
- package/dist/canvas/controller.js +717 -717
- package/dist/canvas/geometry.js +131 -131
- package/dist/canvas/gesture.d.ts +0 -1
- package/dist/canvas/gesture.js +289 -289
- package/dist/canvas/handles.js +212 -212
- package/dist/canvas/menu.js +82 -82
- package/dist/canvas/mode-controller.js +81 -81
- package/dist/canvas/nudge.js +108 -108
- package/dist/canvas/overlay.js +149 -149
- package/dist/canvas/snap.js +132 -132
- package/dist/canvas/viewport.js +112 -112
- package/dist/canvas/z-order.js +171 -171
- package/dist/commands/alignment.js +150 -150
- package/dist/commands/box.js +272 -272
- package/dist/commands/clipboard.js +93 -93
- package/dist/commands/graphemes.js +112 -112
- package/dist/commands/marks.js +153 -153
- package/dist/commands/paragraph-props.js +176 -176
- package/dist/commands/size.js +368 -368
- package/dist/commands/text.js +291 -291
- package/dist/docx.d.ts +1 -0
- package/dist/docx.js +15 -13
- package/dist/editor/editing-host.js +434 -434
- package/dist/element.js +41 -41
- package/dist/fonts/OFL-Carlito.txt +94 -0
- package/dist/fonts/calibri-metrics.d.ts +27 -0
- package/dist/fonts/calibri-metrics.js +88 -0
- package/dist/history/history.js +433 -433
- package/dist/index.js +63 -63
- package/dist/input/beforeinput.js +224 -224
- package/dist/input/clipboard.js +107 -107
- package/dist/input/composition.js +341 -341
- package/dist/input/keymap.js +54 -54
- package/dist/io/ooxml/custgeom.js +72 -72
- package/dist/io/ooxml/drawingml.js +232 -232
- package/dist/io/ooxml/edits.js +67 -67
- package/dist/io/ooxml/export-splice.js +880 -880
- package/dist/io/ooxml/export.js +365 -365
- package/dist/io/ooxml/fill.d.ts +93 -8
- package/dist/io/ooxml/fill.js +618 -562
- package/dist/io/ooxml/fit.d.ts +252 -0
- package/dist/io/ooxml/fit.js +598 -0
- package/dist/io/ooxml/import-gate.js +200 -200
- package/dist/io/ooxml/import.js +691 -691
- package/dist/io/ooxml/inflate.js +126 -126
- package/dist/io/ooxml/media.js +87 -87
- package/dist/io/ooxml/ns.d.ts +2 -0
- package/dist/io/ooxml/ns.js +26 -21
- package/dist/io/ooxml/numbering.js +290 -290
- package/dist/io/ooxml/opc-write.js +171 -171
- package/dist/io/ooxml/provenance.js +93 -93
- package/dist/io/ooxml/recipe.js +708 -708
- package/dist/io/ooxml/relationships.js +59 -59
- package/dist/io/ooxml/run-fonts.d.ts +89 -0
- package/dist/io/ooxml/run-fonts.js +378 -0
- package/dist/io/ooxml/slots.d.ts +4 -0
- package/dist/io/ooxml/slots.js +582 -581
- package/dist/io/ooxml/styles-resolve.js +257 -257
- package/dist/io/ooxml/units.js +43 -43
- package/dist/io/ooxml/value.js +120 -120
- package/dist/io/ooxml/xml-scan.js +616 -616
- package/dist/io/ooxml/zip-container.js +228 -228
- package/dist/io/ooxml/zip-reject.js +106 -106
- package/dist/measure/placement.js +83 -83
- package/dist/measure/report.js +75 -75
- package/dist/model/ids.js +70 -70
- package/dist/model/normalize.js +146 -146
- package/dist/model/position.js +283 -283
- package/dist/model/transaction.js +209 -209
- package/dist/platform/capabilities.js +25 -25
- package/dist/react/index.js +21 -21
- package/dist/selection/controller.js +315 -315
- package/dist/selection/dom-map.js +334 -334
- package/dist/state/editor-state.js +109 -109
- package/dist/styles.css +66 -66
- package/package.json +94 -93
|
@@ -1,257 +1,257 @@
|
|
|
1
|
-
// styles.xml -> docDefaults / style chain / effective-value resolver. Ported
|
|
2
|
-
// from scripts/measure-style-inheritance.py's load_styles()/style_chain()/
|
|
3
|
-
// resolve()/same_state() — the algorithm, not the aggregate-measurement
|
|
4
|
-
// machinery (new_acc/measure_document), which belongs only to that
|
|
5
|
-
// standalone script. ADR-0017: "Çözümleyici sıfırdan yazılmayacak."
|
|
6
|
-
//
|
|
7
|
-
// Resolution order (docDefaults -> style chain root-first, derived wins ->
|
|
8
|
-
// direct), ported unchanged.
|
|
9
|
-
|
|
10
|
-
import { getAttr, findChild, findChildren } from "./xml-scan.js";
|
|
11
|
-
import { W_NS } from "./ns.js";
|
|
12
|
-
import { rejectCharacterStyle } from "./import-gate.js";
|
|
13
|
-
|
|
14
|
-
const OFF_VALUES = new Set(["0", "false", "none"]);
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* A `w:pPr` element -> flat {field: value}. Absent field = not set here.
|
|
18
|
-
* @param {import("./xml-scan.js").XmlElement | undefined} pPrEl
|
|
19
|
-
* @returns {Record<string, string>}
|
|
20
|
-
*/
|
|
21
|
-
export function readPPr(pPrEl) {
|
|
22
|
-
/** @type {Record<string, string>} */
|
|
23
|
-
const out = {};
|
|
24
|
-
if (!pPrEl) return out;
|
|
25
|
-
const jc = findChild(pPrEl, W_NS, "jc");
|
|
26
|
-
if (jc) {
|
|
27
|
-
const val = getAttr(jc, W_NS, "val");
|
|
28
|
-
if (val !== undefined) out.jc = val;
|
|
29
|
-
}
|
|
30
|
-
const ind = findChild(pPrEl, W_NS, "ind");
|
|
31
|
-
if (ind) {
|
|
32
|
-
for (const a of ["left", "right", "firstLine", "hanging"]) {
|
|
33
|
-
const v = getAttr(ind, W_NS, a);
|
|
34
|
-
if (v !== undefined) out[`ind.${a}`] = v;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
const sp = findChild(pPrEl, W_NS, "spacing");
|
|
38
|
-
if (sp) {
|
|
39
|
-
for (const a of ["before", "after", "line", "lineRule"]) {
|
|
40
|
-
const v = getAttr(sp, W_NS, a);
|
|
41
|
-
if (v !== undefined) out[`spacing.${a}`] = v;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
// Raw numId/ilvl only -- resolving them to a display marker needs
|
|
45
|
-
// numbering.xml, which this function has no access to (it is called for
|
|
46
|
-
// BOTH a style's own pPr and a paragraph's direct pPr, same as jc/ind/
|
|
47
|
-
// spacing above, so direct numPr correctly overrides a style's numPr
|
|
48
|
-
// through the existing resolve() cascade). import.js's internPPrRef adds
|
|
49
|
-
// the resolved `numPr.marker` field after this cascade runs.
|
|
50
|
-
const numPr = findChild(pPrEl, W_NS, "numPr");
|
|
51
|
-
if (numPr) {
|
|
52
|
-
const ilvl = findChild(numPr, W_NS, "ilvl");
|
|
53
|
-
if (ilvl) {
|
|
54
|
-
const v = getAttr(ilvl, W_NS, "val");
|
|
55
|
-
if (v !== undefined) out["numPr.ilvl"] = v;
|
|
56
|
-
}
|
|
57
|
-
const numId = findChild(numPr, W_NS, "numId");
|
|
58
|
-
if (numId) {
|
|
59
|
-
const v = getAttr(numId, W_NS, "val");
|
|
60
|
-
if (v !== undefined) out["numPr.numId"] = v;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return out;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* A `w:rPr` element -> flat {field: value}. `font` collapses w:rFonts'
|
|
68
|
-
* ascii/hAnsi/cs in that order; a theme-only rFonts yields the sentinel
|
|
69
|
-
* "(tema)" — theme1.xml is deliberately not opened (ADR-0017 DOĞRULANMALI
|
|
70
|
-
* #6), and pretending the field is unset would hide that.
|
|
71
|
-
* A `w:rStyle` child (a reference to a character style) is rejected rather
|
|
72
|
-
* than silently ignored — ADR-0020 Karar 4: 0/81.973 real corpus runs carry
|
|
73
|
-
* one, so a run that does is corpus-external and this resolver has no
|
|
74
|
-
* mechanism to fold the referenced style's properties in.
|
|
75
|
-
* `color` is DISPLAY-only (render, not the `Marks`/toggle path — 2026-08-31
|
|
76
|
-
* decision, ADR-0026): a 6-hex-digit `w:val` is read straight through; a
|
|
77
|
-
* `w:themeColor` reference or `w:val="auto"` is left unset rather than
|
|
78
|
-
* guessed — measured 211 real corpus `w:color` elements, 0 carry either
|
|
79
|
-
* (theme1.xml is deliberately not opened, same reason as the `rFonts` theme
|
|
80
|
-
* sentinel above), so this is not a real-corpus gap today, only a documented
|
|
81
|
-
* refusal to invent a value.
|
|
82
|
-
* `vertAlign` is DISPLAY-only, same class as `color` (2026-09-01): only the
|
|
83
|
-
* three ST_VerticalAlignRun enum values (`superscript`/`subscript`/
|
|
84
|
-
* `baseline`) are read straight through; anything else is left unset rather
|
|
85
|
-
* than guessed — measured 0/576 real corpus `w:vertAlign` elements carry a
|
|
86
|
-
* fourth value.
|
|
87
|
-
* @param {import("./xml-scan.js").XmlElement | undefined} rPrEl
|
|
88
|
-
* @returns {Record<string, string>}
|
|
89
|
-
*/
|
|
90
|
-
export function readRPr(rPrEl) {
|
|
91
|
-
/** @type {Record<string, string>} */
|
|
92
|
-
const out = {};
|
|
93
|
-
if (!rPrEl) return out;
|
|
94
|
-
if (findChild(rPrEl, W_NS, "rStyle")) {
|
|
95
|
-
rejectCharacterStyle(`w:rPr at byte ${rPrEl.start}`);
|
|
96
|
-
}
|
|
97
|
-
const sz = findChild(rPrEl, W_NS, "sz");
|
|
98
|
-
if (sz) {
|
|
99
|
-
const val = getAttr(sz, W_NS, "val");
|
|
100
|
-
if (val) out.sz = val;
|
|
101
|
-
}
|
|
102
|
-
const rf = findChild(rPrEl, W_NS, "rFonts");
|
|
103
|
-
if (rf) {
|
|
104
|
-
let name;
|
|
105
|
-
for (const a of ["ascii", "hAnsi", "cs"]) {
|
|
106
|
-
const v = getAttr(rf, W_NS, a);
|
|
107
|
-
if (v) {
|
|
108
|
-
name = v;
|
|
109
|
-
break;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
if (name === undefined) {
|
|
113
|
-
for (const a of ["asciiTheme", "hAnsiTheme", "cstheme"]) {
|
|
114
|
-
if (getAttr(rf, W_NS, a)) {
|
|
115
|
-
name = "(tema)";
|
|
116
|
-
break;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
if (name !== undefined) out.font = name;
|
|
121
|
-
}
|
|
122
|
-
const color = findChild(rPrEl, W_NS, "color");
|
|
123
|
-
if (color) {
|
|
124
|
-
const val = getAttr(color, W_NS, "val");
|
|
125
|
-
// Only a genuine 6-hex-digit value — "auto" and a themeColor reference
|
|
126
|
-
// (both absent from this val, or this val itself non-hex) are left
|
|
127
|
-
// unset rather than guessed, see this function's own doc comment above.
|
|
128
|
-
if (val !== undefined && /^[0-9A-Fa-f]{6}$/.test(val)) {
|
|
129
|
-
out.color = val;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
const vertAlign = findChild(rPrEl, W_NS, "vertAlign");
|
|
133
|
-
if (vertAlign) {
|
|
134
|
-
const val = getAttr(vertAlign, W_NS, "val");
|
|
135
|
-
if (val === "superscript" || val === "subscript" || val === "baseline") {
|
|
136
|
-
out.vertAlign = val;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
for (const tag of ["b", "i", "u"]) {
|
|
140
|
-
const el = findChild(rPrEl, W_NS, tag);
|
|
141
|
-
if (el) {
|
|
142
|
-
const val = getAttr(el, W_NS, "val");
|
|
143
|
-
out[tag] = val !== undefined && OFF_VALUES.has(val) ? "off" : "on";
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return out;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Compares two resolved values, treating an absent toggle and an explicit
|
|
151
|
-
* "off" as the SAME state — `w:u w:val="none"` is not a deviation from "no
|
|
152
|
-
* underline", it IS no underline spelled out (measured: 542/622 corpus
|
|
153
|
-
* w:u are val="none"). Ported from same_state(), caught by that script's own
|
|
154
|
-
* self-test after a first version that missed this.
|
|
155
|
-
* @param {string | undefined} a
|
|
156
|
-
* @param {string | undefined} b
|
|
157
|
-
* @returns {boolean}
|
|
158
|
-
*/
|
|
159
|
-
export function sameState(a, b) {
|
|
160
|
-
/** @param {string | undefined} v */
|
|
161
|
-
const isOff = (v) => v === undefined || v === "off";
|
|
162
|
-
if (isOff(a) && isOff(b)) return true;
|
|
163
|
-
return a === b;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* @typedef {object} StyleEntry
|
|
168
|
-
* @property {string | null} basedOn
|
|
169
|
-
* @property {Record<string, string>} pPr
|
|
170
|
-
* @property {Record<string, string>} rPr
|
|
171
|
-
*/
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* @typedef {object} LoadedStyles
|
|
175
|
-
* @property {{pPr: Record<string,string>, rPr: Record<string,string>}} docDefaults
|
|
176
|
-
* @property {Record<string, StyleEntry>} styles paragraph styles only, keyed by styleId
|
|
177
|
-
* @property {string | null} defaultParagraphStyle
|
|
178
|
-
*/
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* docDefaults + every PARAGRAPH style, keyed by styleId. Character/table/
|
|
182
|
-
* numbering styles are read (for `total`/`custom` bookkeeping parity with
|
|
183
|
-
* the measurement script) but not stored — `rStyle` (character styles) is
|
|
184
|
-
* 0/81,973 in the corpus (ADR-0017), so this scope matches what is actually
|
|
185
|
-
* used.
|
|
186
|
-
* @param {import("./xml-scan.js").XmlElement} stylesRoot
|
|
187
|
-
* @returns {LoadedStyles}
|
|
188
|
-
*/
|
|
189
|
-
export function loadStyles(stylesRoot) {
|
|
190
|
-
const dd = findChild(stylesRoot, W_NS, "docDefaults");
|
|
191
|
-
const pPrDefault = dd && findChild(dd, W_NS, "pPrDefault");
|
|
192
|
-
const rPrDefault = dd && findChild(dd, W_NS, "rPrDefault");
|
|
193
|
-
const docPPr = readPPr(pPrDefault && findChild(pPrDefault, W_NS, "pPr"));
|
|
194
|
-
const docRPr = readRPr(rPrDefault && findChild(rPrDefault, W_NS, "rPr"));
|
|
195
|
-
|
|
196
|
-
/** @type {Record<string, StyleEntry>} */
|
|
197
|
-
const styles = {};
|
|
198
|
-
let defaultPara = null;
|
|
199
|
-
for (const st of findChildren(stylesRoot, W_NS, "style")) {
|
|
200
|
-
if (getAttr(st, W_NS, "type") !== "paragraph") continue;
|
|
201
|
-
const sid = getAttr(st, W_NS, "styleId");
|
|
202
|
-
if (sid === undefined) continue;
|
|
203
|
-
const bo = findChild(st, W_NS, "basedOn");
|
|
204
|
-
styles[sid] = {
|
|
205
|
-
basedOn: bo ? (getAttr(bo, W_NS, "val") ?? null) : null,
|
|
206
|
-
pPr: readPPr(findChild(st, W_NS, "pPr")),
|
|
207
|
-
rPr: readRPr(findChild(st, W_NS, "rPr")),
|
|
208
|
-
};
|
|
209
|
-
if (getAttr(st, W_NS, "default") === "1") defaultPara = sid;
|
|
210
|
-
}
|
|
211
|
-
return { docDefaults: { pPr: docPPr, rPr: docRPr }, styles, defaultParagraphStyle: defaultPara };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Root-first chain: [Normal, BodyText]. Cycle-safe.
|
|
216
|
-
* @param {Record<string, StyleEntry>} styles
|
|
217
|
-
* @param {string | null | undefined} styleId
|
|
218
|
-
* @returns {string[]}
|
|
219
|
-
*/
|
|
220
|
-
export function styleChain(styles, styleId) {
|
|
221
|
-
/** @type {string[]} */
|
|
222
|
-
const chain = [];
|
|
223
|
-
const seen = new Set();
|
|
224
|
-
let cur = styleId;
|
|
225
|
-
while (cur && styles[cur] && !seen.has(cur)) {
|
|
226
|
-
seen.add(cur);
|
|
227
|
-
chain.push(cur);
|
|
228
|
-
cur = styles[cur].basedOn;
|
|
229
|
-
}
|
|
230
|
-
chain.reverse();
|
|
231
|
-
return chain;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* @typedef {[string, "docDefaults" | "style" | "direct"]} ResolvedValue
|
|
236
|
-
*/
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Effective value + WHERE it came from, per field. Order: docDefaults ->
|
|
240
|
-
* style chain root-first (derived wins) -> direct.
|
|
241
|
-
* @param {Record<string,string>} defaults
|
|
242
|
-
* @param {Record<string, StyleEntry>} styles
|
|
243
|
-
* @param {string | null | undefined} styleId
|
|
244
|
-
* @param {Record<string,string>} direct
|
|
245
|
-
* @param {"pPr" | "rPr"} which
|
|
246
|
-
* @returns {Record<string, ResolvedValue>}
|
|
247
|
-
*/
|
|
248
|
-
export function resolve(defaults, styles, styleId, direct, which) {
|
|
249
|
-
/** @type {Record<string, ResolvedValue>} */
|
|
250
|
-
const eff = {};
|
|
251
|
-
for (const [f, v] of Object.entries(defaults)) eff[f] = [v, "docDefaults"];
|
|
252
|
-
for (const sid of styleChain(styles, styleId)) {
|
|
253
|
-
for (const [f, v] of Object.entries(styles[sid][which])) eff[f] = [v, "style"];
|
|
254
|
-
}
|
|
255
|
-
for (const [f, v] of Object.entries(direct)) eff[f] = [v, "direct"];
|
|
256
|
-
return eff;
|
|
257
|
-
}
|
|
1
|
+
// styles.xml -> docDefaults / style chain / effective-value resolver. Ported
|
|
2
|
+
// from scripts/measure-style-inheritance.py's load_styles()/style_chain()/
|
|
3
|
+
// resolve()/same_state() — the algorithm, not the aggregate-measurement
|
|
4
|
+
// machinery (new_acc/measure_document), which belongs only to that
|
|
5
|
+
// standalone script. ADR-0017: "Çözümleyici sıfırdan yazılmayacak."
|
|
6
|
+
//
|
|
7
|
+
// Resolution order (docDefaults -> style chain root-first, derived wins ->
|
|
8
|
+
// direct), ported unchanged.
|
|
9
|
+
|
|
10
|
+
import { getAttr, findChild, findChildren } from "./xml-scan.js";
|
|
11
|
+
import { W_NS } from "./ns.js";
|
|
12
|
+
import { rejectCharacterStyle } from "./import-gate.js";
|
|
13
|
+
|
|
14
|
+
const OFF_VALUES = new Set(["0", "false", "none"]);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A `w:pPr` element -> flat {field: value}. Absent field = not set here.
|
|
18
|
+
* @param {import("./xml-scan.js").XmlElement | undefined} pPrEl
|
|
19
|
+
* @returns {Record<string, string>}
|
|
20
|
+
*/
|
|
21
|
+
export function readPPr(pPrEl) {
|
|
22
|
+
/** @type {Record<string, string>} */
|
|
23
|
+
const out = {};
|
|
24
|
+
if (!pPrEl) return out;
|
|
25
|
+
const jc = findChild(pPrEl, W_NS, "jc");
|
|
26
|
+
if (jc) {
|
|
27
|
+
const val = getAttr(jc, W_NS, "val");
|
|
28
|
+
if (val !== undefined) out.jc = val;
|
|
29
|
+
}
|
|
30
|
+
const ind = findChild(pPrEl, W_NS, "ind");
|
|
31
|
+
if (ind) {
|
|
32
|
+
for (const a of ["left", "right", "firstLine", "hanging"]) {
|
|
33
|
+
const v = getAttr(ind, W_NS, a);
|
|
34
|
+
if (v !== undefined) out[`ind.${a}`] = v;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const sp = findChild(pPrEl, W_NS, "spacing");
|
|
38
|
+
if (sp) {
|
|
39
|
+
for (const a of ["before", "after", "line", "lineRule"]) {
|
|
40
|
+
const v = getAttr(sp, W_NS, a);
|
|
41
|
+
if (v !== undefined) out[`spacing.${a}`] = v;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Raw numId/ilvl only -- resolving them to a display marker needs
|
|
45
|
+
// numbering.xml, which this function has no access to (it is called for
|
|
46
|
+
// BOTH a style's own pPr and a paragraph's direct pPr, same as jc/ind/
|
|
47
|
+
// spacing above, so direct numPr correctly overrides a style's numPr
|
|
48
|
+
// through the existing resolve() cascade). import.js's internPPrRef adds
|
|
49
|
+
// the resolved `numPr.marker` field after this cascade runs.
|
|
50
|
+
const numPr = findChild(pPrEl, W_NS, "numPr");
|
|
51
|
+
if (numPr) {
|
|
52
|
+
const ilvl = findChild(numPr, W_NS, "ilvl");
|
|
53
|
+
if (ilvl) {
|
|
54
|
+
const v = getAttr(ilvl, W_NS, "val");
|
|
55
|
+
if (v !== undefined) out["numPr.ilvl"] = v;
|
|
56
|
+
}
|
|
57
|
+
const numId = findChild(numPr, W_NS, "numId");
|
|
58
|
+
if (numId) {
|
|
59
|
+
const v = getAttr(numId, W_NS, "val");
|
|
60
|
+
if (v !== undefined) out["numPr.numId"] = v;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A `w:rPr` element -> flat {field: value}. `font` collapses w:rFonts'
|
|
68
|
+
* ascii/hAnsi/cs in that order; a theme-only rFonts yields the sentinel
|
|
69
|
+
* "(tema)" — theme1.xml is deliberately not opened (ADR-0017 DOĞRULANMALI
|
|
70
|
+
* #6), and pretending the field is unset would hide that.
|
|
71
|
+
* A `w:rStyle` child (a reference to a character style) is rejected rather
|
|
72
|
+
* than silently ignored — ADR-0020 Karar 4: 0/81.973 real corpus runs carry
|
|
73
|
+
* one, so a run that does is corpus-external and this resolver has no
|
|
74
|
+
* mechanism to fold the referenced style's properties in.
|
|
75
|
+
* `color` is DISPLAY-only (render, not the `Marks`/toggle path — 2026-08-31
|
|
76
|
+
* decision, ADR-0026): a 6-hex-digit `w:val` is read straight through; a
|
|
77
|
+
* `w:themeColor` reference or `w:val="auto"` is left unset rather than
|
|
78
|
+
* guessed — measured 211 real corpus `w:color` elements, 0 carry either
|
|
79
|
+
* (theme1.xml is deliberately not opened, same reason as the `rFonts` theme
|
|
80
|
+
* sentinel above), so this is not a real-corpus gap today, only a documented
|
|
81
|
+
* refusal to invent a value.
|
|
82
|
+
* `vertAlign` is DISPLAY-only, same class as `color` (2026-09-01): only the
|
|
83
|
+
* three ST_VerticalAlignRun enum values (`superscript`/`subscript`/
|
|
84
|
+
* `baseline`) are read straight through; anything else is left unset rather
|
|
85
|
+
* than guessed — measured 0/576 real corpus `w:vertAlign` elements carry a
|
|
86
|
+
* fourth value.
|
|
87
|
+
* @param {import("./xml-scan.js").XmlElement | undefined} rPrEl
|
|
88
|
+
* @returns {Record<string, string>}
|
|
89
|
+
*/
|
|
90
|
+
export function readRPr(rPrEl) {
|
|
91
|
+
/** @type {Record<string, string>} */
|
|
92
|
+
const out = {};
|
|
93
|
+
if (!rPrEl) return out;
|
|
94
|
+
if (findChild(rPrEl, W_NS, "rStyle")) {
|
|
95
|
+
rejectCharacterStyle(`w:rPr at byte ${rPrEl.start}`);
|
|
96
|
+
}
|
|
97
|
+
const sz = findChild(rPrEl, W_NS, "sz");
|
|
98
|
+
if (sz) {
|
|
99
|
+
const val = getAttr(sz, W_NS, "val");
|
|
100
|
+
if (val) out.sz = val;
|
|
101
|
+
}
|
|
102
|
+
const rf = findChild(rPrEl, W_NS, "rFonts");
|
|
103
|
+
if (rf) {
|
|
104
|
+
let name;
|
|
105
|
+
for (const a of ["ascii", "hAnsi", "cs"]) {
|
|
106
|
+
const v = getAttr(rf, W_NS, a);
|
|
107
|
+
if (v) {
|
|
108
|
+
name = v;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (name === undefined) {
|
|
113
|
+
for (const a of ["asciiTheme", "hAnsiTheme", "cstheme"]) {
|
|
114
|
+
if (getAttr(rf, W_NS, a)) {
|
|
115
|
+
name = "(tema)";
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (name !== undefined) out.font = name;
|
|
121
|
+
}
|
|
122
|
+
const color = findChild(rPrEl, W_NS, "color");
|
|
123
|
+
if (color) {
|
|
124
|
+
const val = getAttr(color, W_NS, "val");
|
|
125
|
+
// Only a genuine 6-hex-digit value — "auto" and a themeColor reference
|
|
126
|
+
// (both absent from this val, or this val itself non-hex) are left
|
|
127
|
+
// unset rather than guessed, see this function's own doc comment above.
|
|
128
|
+
if (val !== undefined && /^[0-9A-Fa-f]{6}$/.test(val)) {
|
|
129
|
+
out.color = val;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const vertAlign = findChild(rPrEl, W_NS, "vertAlign");
|
|
133
|
+
if (vertAlign) {
|
|
134
|
+
const val = getAttr(vertAlign, W_NS, "val");
|
|
135
|
+
if (val === "superscript" || val === "subscript" || val === "baseline") {
|
|
136
|
+
out.vertAlign = val;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const tag of ["b", "i", "u"]) {
|
|
140
|
+
const el = findChild(rPrEl, W_NS, tag);
|
|
141
|
+
if (el) {
|
|
142
|
+
const val = getAttr(el, W_NS, "val");
|
|
143
|
+
out[tag] = val !== undefined && OFF_VALUES.has(val) ? "off" : "on";
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Compares two resolved values, treating an absent toggle and an explicit
|
|
151
|
+
* "off" as the SAME state — `w:u w:val="none"` is not a deviation from "no
|
|
152
|
+
* underline", it IS no underline spelled out (measured: 542/622 corpus
|
|
153
|
+
* w:u are val="none"). Ported from same_state(), caught by that script's own
|
|
154
|
+
* self-test after a first version that missed this.
|
|
155
|
+
* @param {string | undefined} a
|
|
156
|
+
* @param {string | undefined} b
|
|
157
|
+
* @returns {boolean}
|
|
158
|
+
*/
|
|
159
|
+
export function sameState(a, b) {
|
|
160
|
+
/** @param {string | undefined} v */
|
|
161
|
+
const isOff = (v) => v === undefined || v === "off";
|
|
162
|
+
if (isOff(a) && isOff(b)) return true;
|
|
163
|
+
return a === b;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* @typedef {object} StyleEntry
|
|
168
|
+
* @property {string | null} basedOn
|
|
169
|
+
* @property {Record<string, string>} pPr
|
|
170
|
+
* @property {Record<string, string>} rPr
|
|
171
|
+
*/
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @typedef {object} LoadedStyles
|
|
175
|
+
* @property {{pPr: Record<string,string>, rPr: Record<string,string>}} docDefaults
|
|
176
|
+
* @property {Record<string, StyleEntry>} styles paragraph styles only, keyed by styleId
|
|
177
|
+
* @property {string | null} defaultParagraphStyle
|
|
178
|
+
*/
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* docDefaults + every PARAGRAPH style, keyed by styleId. Character/table/
|
|
182
|
+
* numbering styles are read (for `total`/`custom` bookkeeping parity with
|
|
183
|
+
* the measurement script) but not stored — `rStyle` (character styles) is
|
|
184
|
+
* 0/81,973 in the corpus (ADR-0017), so this scope matches what is actually
|
|
185
|
+
* used.
|
|
186
|
+
* @param {import("./xml-scan.js").XmlElement} stylesRoot
|
|
187
|
+
* @returns {LoadedStyles}
|
|
188
|
+
*/
|
|
189
|
+
export function loadStyles(stylesRoot) {
|
|
190
|
+
const dd = findChild(stylesRoot, W_NS, "docDefaults");
|
|
191
|
+
const pPrDefault = dd && findChild(dd, W_NS, "pPrDefault");
|
|
192
|
+
const rPrDefault = dd && findChild(dd, W_NS, "rPrDefault");
|
|
193
|
+
const docPPr = readPPr(pPrDefault && findChild(pPrDefault, W_NS, "pPr"));
|
|
194
|
+
const docRPr = readRPr(rPrDefault && findChild(rPrDefault, W_NS, "rPr"));
|
|
195
|
+
|
|
196
|
+
/** @type {Record<string, StyleEntry>} */
|
|
197
|
+
const styles = {};
|
|
198
|
+
let defaultPara = null;
|
|
199
|
+
for (const st of findChildren(stylesRoot, W_NS, "style")) {
|
|
200
|
+
if (getAttr(st, W_NS, "type") !== "paragraph") continue;
|
|
201
|
+
const sid = getAttr(st, W_NS, "styleId");
|
|
202
|
+
if (sid === undefined) continue;
|
|
203
|
+
const bo = findChild(st, W_NS, "basedOn");
|
|
204
|
+
styles[sid] = {
|
|
205
|
+
basedOn: bo ? (getAttr(bo, W_NS, "val") ?? null) : null,
|
|
206
|
+
pPr: readPPr(findChild(st, W_NS, "pPr")),
|
|
207
|
+
rPr: readRPr(findChild(st, W_NS, "rPr")),
|
|
208
|
+
};
|
|
209
|
+
if (getAttr(st, W_NS, "default") === "1") defaultPara = sid;
|
|
210
|
+
}
|
|
211
|
+
return { docDefaults: { pPr: docPPr, rPr: docRPr }, styles, defaultParagraphStyle: defaultPara };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Root-first chain: [Normal, BodyText]. Cycle-safe.
|
|
216
|
+
* @param {Record<string, StyleEntry>} styles
|
|
217
|
+
* @param {string | null | undefined} styleId
|
|
218
|
+
* @returns {string[]}
|
|
219
|
+
*/
|
|
220
|
+
export function styleChain(styles, styleId) {
|
|
221
|
+
/** @type {string[]} */
|
|
222
|
+
const chain = [];
|
|
223
|
+
const seen = new Set();
|
|
224
|
+
let cur = styleId;
|
|
225
|
+
while (cur && styles[cur] && !seen.has(cur)) {
|
|
226
|
+
seen.add(cur);
|
|
227
|
+
chain.push(cur);
|
|
228
|
+
cur = styles[cur].basedOn;
|
|
229
|
+
}
|
|
230
|
+
chain.reverse();
|
|
231
|
+
return chain;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* @typedef {[string, "docDefaults" | "style" | "direct"]} ResolvedValue
|
|
236
|
+
*/
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Effective value + WHERE it came from, per field. Order: docDefaults ->
|
|
240
|
+
* style chain root-first (derived wins) -> direct.
|
|
241
|
+
* @param {Record<string,string>} defaults
|
|
242
|
+
* @param {Record<string, StyleEntry>} styles
|
|
243
|
+
* @param {string | null | undefined} styleId
|
|
244
|
+
* @param {Record<string,string>} direct
|
|
245
|
+
* @param {"pPr" | "rPr"} which
|
|
246
|
+
* @returns {Record<string, ResolvedValue>}
|
|
247
|
+
*/
|
|
248
|
+
export function resolve(defaults, styles, styleId, direct, which) {
|
|
249
|
+
/** @type {Record<string, ResolvedValue>} */
|
|
250
|
+
const eff = {};
|
|
251
|
+
for (const [f, v] of Object.entries(defaults)) eff[f] = [v, "docDefaults"];
|
|
252
|
+
for (const sid of styleChain(styles, styleId)) {
|
|
253
|
+
for (const [f, v] of Object.entries(styles[sid][which])) eff[f] = [v, "style"];
|
|
254
|
+
}
|
|
255
|
+
for (const [f, v] of Object.entries(direct)) eff[f] = [v, "direct"];
|
|
256
|
+
return eff;
|
|
257
|
+
}
|
package/dist/io/ooxml/units.js
CHANGED
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
// EMU <-> pt, in ONE place. OOXML measures geometry in EMU (English Metric
|
|
2
|
-
// Units); the model measures it in points. Import needs one direction, the
|
|
3
|
-
// geometry export (P6) needs the other, and a second copy of the constant in a
|
|
4
|
-
// second file is exactly how the two directions drift apart.
|
|
5
|
-
|
|
6
|
-
export const EMU_PER_PT = 12700;
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* `round(value / 12700, 2)` — measured 2026-08-27: across all 7,297 unique EMU
|
|
10
|
-
* values actually present in the 46-document corpus (posOffset, extent, off,
|
|
11
|
-
* ext), Python's round() (round-half-to-even) and this expression never
|
|
12
|
-
* disagree. A bit-exact port of Python's round() was NOT written — there was
|
|
13
|
-
* nothing, measured, for it to fix.
|
|
14
|
-
* @param {number} emu
|
|
15
|
-
* @returns {number}
|
|
16
|
-
*/
|
|
17
|
-
export function emuToPt(emu) {
|
|
18
|
-
return Math.round((emu / EMU_PER_PT) * 100) / 100;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* pt -> EMU, the geometry export's write direction.
|
|
23
|
-
*
|
|
24
|
-
* 🔴 NOT a lossless inverse of `emuToPt`, and callers must not treat it as one:
|
|
25
|
-
* `emuToPt` rounds to 2 decimals (~0.005 pt ~ 64 EMU), so a value that made a
|
|
26
|
-
* round trip can differ from the original integer. Measured 2026-08-29 on the
|
|
27
|
-
* corpus: 791 of 7,990 `cx` values and 482 `cy` values do NOT survive
|
|
28
|
-
* `emuToPt -> ptToEmu` (AA01's first box: 6674484 -> 525.55 pt -> 6674485).
|
|
29
|
-
* That is why the splice writes ONLY the coordinates the user actually changed
|
|
30
|
-
* and carries every untouched one through as its original bytes.
|
|
31
|
-
*
|
|
32
|
-
* Throws on a non-finite input rather than emitting it: `Math.round(NaN)` is
|
|
33
|
-
* `NaN` and `String(NaN)` is `"NaN"`, which would reach document.xml and make
|
|
34
|
-
* Word refuse the file.
|
|
35
|
-
* @param {number} pt
|
|
36
|
-
* @returns {number} integer EMU
|
|
37
|
-
*/
|
|
38
|
-
export function ptToEmu(pt) {
|
|
39
|
-
if (!Number.isFinite(pt)) {
|
|
40
|
-
throw new TypeError(`ptToEmu: non-finite pt value (${pt}) — refusing to write it to document.xml`);
|
|
41
|
-
}
|
|
42
|
-
return Math.round(pt * EMU_PER_PT);
|
|
43
|
-
}
|
|
1
|
+
// EMU <-> pt, in ONE place. OOXML measures geometry in EMU (English Metric
|
|
2
|
+
// Units); the model measures it in points. Import needs one direction, the
|
|
3
|
+
// geometry export (P6) needs the other, and a second copy of the constant in a
|
|
4
|
+
// second file is exactly how the two directions drift apart.
|
|
5
|
+
|
|
6
|
+
export const EMU_PER_PT = 12700;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `round(value / 12700, 2)` — measured 2026-08-27: across all 7,297 unique EMU
|
|
10
|
+
* values actually present in the 46-document corpus (posOffset, extent, off,
|
|
11
|
+
* ext), Python's round() (round-half-to-even) and this expression never
|
|
12
|
+
* disagree. A bit-exact port of Python's round() was NOT written — there was
|
|
13
|
+
* nothing, measured, for it to fix.
|
|
14
|
+
* @param {number} emu
|
|
15
|
+
* @returns {number}
|
|
16
|
+
*/
|
|
17
|
+
export function emuToPt(emu) {
|
|
18
|
+
return Math.round((emu / EMU_PER_PT) * 100) / 100;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* pt -> EMU, the geometry export's write direction.
|
|
23
|
+
*
|
|
24
|
+
* 🔴 NOT a lossless inverse of `emuToPt`, and callers must not treat it as one:
|
|
25
|
+
* `emuToPt` rounds to 2 decimals (~0.005 pt ~ 64 EMU), so a value that made a
|
|
26
|
+
* round trip can differ from the original integer. Measured 2026-08-29 on the
|
|
27
|
+
* corpus: 791 of 7,990 `cx` values and 482 `cy` values do NOT survive
|
|
28
|
+
* `emuToPt -> ptToEmu` (AA01's first box: 6674484 -> 525.55 pt -> 6674485).
|
|
29
|
+
* That is why the splice writes ONLY the coordinates the user actually changed
|
|
30
|
+
* and carries every untouched one through as its original bytes.
|
|
31
|
+
*
|
|
32
|
+
* Throws on a non-finite input rather than emitting it: `Math.round(NaN)` is
|
|
33
|
+
* `NaN` and `String(NaN)` is `"NaN"`, which would reach document.xml and make
|
|
34
|
+
* Word refuse the file.
|
|
35
|
+
* @param {number} pt
|
|
36
|
+
* @returns {number} integer EMU
|
|
37
|
+
*/
|
|
38
|
+
export function ptToEmu(pt) {
|
|
39
|
+
if (!Number.isFinite(pt)) {
|
|
40
|
+
throw new TypeError(`ptToEmu: non-finite pt value (${pt}) — refusing to write it to document.xml`);
|
|
41
|
+
}
|
|
42
|
+
return Math.round(pt * EMU_PER_PT);
|
|
43
|
+
}
|