@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,200 +1,200 @@
1
- // ADR-0010's two-granularity import gate, ported from check_group_identity()
2
- // and convert()'s own `is_locked` computation — never re-decided here.
3
- //
4
- // 1. A class with ZERO corpus examples (non-identity group transform, a
5
- // nested group) -> REJECT THE WHOLE DOCUMENT rather than silently
6
- // mis-place one shape.
7
- // 2. A class that DOES occur (a shape's own rotation/flip, independent of
8
- // any group) -> never rejected, the shape is just marked `locked`.
9
-
10
- import { WPG_NS, A_NS } from "./ns.js";
11
-
12
- /**
13
- * Plain Error, not a custom class — matches every other src/ file's
14
- * convention (zip-reject.js, xml-scan.js): a distinguishing message, not a
15
- * type hierarchy.
16
- * @param {string} message
17
- * @returns {never}
18
- */
19
- function rejectImport(message) {
20
- throw new Error(`import-gate: REJECTED — ${message}`);
21
- }
22
-
23
- /**
24
- * @param {import("./xml-scan.js").XmlNode} el
25
- * @returns {boolean}
26
- */
27
- function isNestedGroup(el) {
28
- if (el.kind === "element" && ((el.uri === WPG_NS && el.localName === "grpSp") || (el.uri === A_NS && el.localName === "grpSp"))) {
29
- return true;
30
- }
31
- if (el.kind !== "element") return false;
32
- for (const child of el.children) {
33
- if (isNestedGroup(child)) return true;
34
- }
35
- return false;
36
- }
37
-
38
- /**
39
- * Raises if the group's OWN transform is not identity (scale != 1.0 on
40
- * either axis, chOff != (0,0), rot != 0, or any flip), or if the group
41
- * contains a nested group at any depth (`wpg:grpSp` or `a:grpSp` — corpus-
42
- * measured as never occurring, but checked both ways since which spelling a
43
- * document might use was exactly the kind of thing a prior regex pass in
44
- * this project missed three times).
45
- *
46
- * A shape's OWN rotation (independent of its group) is NOT checked here —
47
- * see `isShapeLocked`. This function is about the GROUP's transform only.
48
- *
49
- * @param {import("./drawingml.js").Xfrm} gXfrm
50
- * @param {import("./xml-scan.js").XmlElement} groupEl
51
- * @param {string} context
52
- * @returns {{scaleX: number, scaleY: number}}
53
- */
54
- export function checkGroupIdentity(gXfrm, groupEl, context) {
55
- const scaleX = gXfrm.chExtCx ? gXfrm.extCx / gXfrm.chExtCx : 1.0;
56
- const scaleY = gXfrm.chExtCy ? gXfrm.extCy / gXfrm.chExtCy : 1.0;
57
-
58
- /** @type {string[]} */
59
- const problems = [];
60
- if (scaleX !== 1.0 || scaleY !== 1.0) {
61
- problems.push(`non-identity scale (scale_x=${scaleX}, scale_y=${scaleY})`);
62
- }
63
- if (gXfrm.chOffX !== 0 || gXfrm.chOffY !== 0) {
64
- problems.push(`chOff != (0,0): (${gXfrm.chOffX},${gXfrm.chOffY})`);
65
- }
66
- if (gXfrm.rot !== 0) {
67
- problems.push(`group rot != 0: ${gXfrm.rot}`);
68
- }
69
- if (gXfrm.flipH || gXfrm.flipV) {
70
- problems.push(`group flip: flipH=${gXfrm.flipH} flipV=${gXfrm.flipV}`);
71
- }
72
- if (isNestedGroup(groupEl)) {
73
- problems.push("nested group found (wpg:grpSp or a:grpSp)");
74
- }
75
-
76
- if (problems.length > 0) {
77
- rejectImport(`${context}: ${problems.join("; ")}`);
78
- }
79
- return { scaleX, scaleY };
80
- }
81
-
82
- /**
83
- * @param {{rot: number, flipH: boolean, flipV: boolean}} ownXfrm
84
- * @returns {boolean}
85
- */
86
- export function isShapeLocked(ownXfrm) {
87
- return ownXfrm.rot !== 0 || ownXfrm.flipH || ownXfrm.flipV;
88
- }
89
-
90
- /**
91
- * An anchor without `wp:posOffset` under positionH/positionV (an
92
- * alignment-based anchor using `wp:align` instead uses a different
93
- * addressing scheme this importer does not interpret), or without
94
- * `wp:extent`, has no safe on-page placement. Measured 2026-08-27: 0/8,737
95
- * real corpus anchors trigger this — every anchor in all 46 documents uses
96
- * absolute posOffset + extent. Silently dropping such an anchor (the
97
- * pre-2026-08-27 behavior, a bare `continue`) risked discarding real,
98
- * positioned content from a corpus-external document with no signal
99
- * anywhere that anything was lost. Call only when at least one of the three
100
- * booleans is true — matches the caller's own `if` guard, like `continue`
101
- * did before it.
102
- * @param {boolean} missingPosOffsetH
103
- * @param {boolean} missingPosOffsetV
104
- * @param {boolean} missingExtent
105
- * @param {string} context
106
- * @returns {never}
107
- */
108
- export function rejectAnchorPlacement(missingPosOffsetH, missingPosOffsetV, missingExtent, context) {
109
- /** @type {string[]} */
110
- const problems = [];
111
- if (missingPosOffsetH || missingPosOffsetV) {
112
- problems.push("missing wp:posOffset under positionH/positionV (an alignment-based anchor, e.g. wp:align, is not supported)");
113
- }
114
- if (missingExtent) {
115
- problems.push("missing wp:extent");
116
- }
117
- rejectImport(`${context}: ${problems.join("; ")}`);
118
- }
119
-
120
- /**
121
- * An anchor with no `a:graphic/a:graphicData` at all wraps nothing this
122
- * importer can render. Measured 2026-08-27: 0/8,737 real corpus anchors
123
- * trigger this.
124
- * @param {string} context
125
- * @returns {never}
126
- */
127
- export function rejectMissingGraphicData(context) {
128
- rejectImport(`${context}: anchor has no a:graphic/a:graphicData`);
129
- }
130
-
131
- /**
132
- * A group (`wpg:wgp`) with no parseable `a:xfrm` under its own
133
- * `wpg:grpSpPr` has no chOff/chExt to place or scale its children by —
134
- * `checkGroupIdentity` (which checks whether that transform IS identity)
135
- * can only run once this passes. Measured 2026-08-27: 0/459 real corpus
136
- * groups trigger this.
137
- * @param {string} context
138
- * @returns {never}
139
- */
140
- export function rejectMissingGroupTransform(context) {
141
- rejectImport(`${context}: group has no parseable a:xfrm under wpg:grpSpPr`);
142
- }
143
-
144
- /**
145
- * A shape (`wps:wsp`) inside a group with no parseable `a:xfrm` under its
146
- * own `wps:spPr` has no off/ext to compute a position from — unlike a
147
- * non-grouped wsp (whose absolute size comes from the anchor's own
148
- * wp:extent instead, independent of spPr), a group child's position is
149
- * defined ONLY relative to the group's chOff/chExt via its own xfrm.
150
- * Measured 2026-08-27: 0/2,598 real corpus group-child shapes trigger this.
151
- * @param {string} context
152
- * @returns {never}
153
- */
154
- export function rejectMissingShapeTransform(context) {
155
- rejectImport(`${context}: group child shape has no parseable a:xfrm under wps:spPr`);
156
- }
157
-
158
- /**
159
- * A non-grouped anchor's `a:graphicData` holding none of `wpg:wgp`,
160
- * `wps:wsp`, or `pic:pic` is some other graphic type this importer has no
161
- * model for (a chart, SmartArt diagram, OLE object, or ink annotation —
162
- * ADR-0020 §7's own reserved REJECTED class). Measured 2026-08-27: 0/8,278
163
- * real corpus non-group anchors trigger this.
164
- * @param {string} context
165
- * @returns {never}
166
- */
167
- export function rejectUnsupportedGraphicDataKind(context) {
168
- rejectImport(`${context}: a:graphicData holds none of wpg:wgp, wps:wsp, pic:pic (unsupported graphic type — chart, diagram, OLE object, or ink)`);
169
- }
170
-
171
- /**
172
- * A `w:drawing` wrapping `wp:inline` (in-text-flow placement) instead of
173
- * `wp:anchor` (floating placement) uses an addressing scheme this importer
174
- * never reads any coordinate from — `collectAnchors` only walks for
175
- * `wp:anchor` children, so an inline drawing was previously absent from the
176
- * anchors array with no signal anywhere that anything had been dropped.
177
- * ADR-0020 Karar 4's own table (`wp:inline` -> REJECTED, "corpus'ta 0")
178
- * predates this function; measured again 2026-08-27 against the real
179
- * corpus: 0/46 documents contain one.
180
- * @param {string} context
181
- * @returns {never}
182
- */
183
- export function rejectInlineDrawing(context) {
184
- rejectImport(`${context}: w:drawing wraps wp:inline (in-text-flow placement is not supported, only wp:anchor)`);
185
- }
186
-
187
- /**
188
- * A `w:rPr` carrying `w:rStyle` (a reference to a character style defined in
189
- * styles.xml) is never resolved by this importer — `readRPr` reads direct
190
- * toggles (b/i/u), size, and font only, and a character-style reference
191
- * silently produced whatever the run's OWN direct properties happened to be,
192
- * with no signal that the style's contribution was missing. ADR-0020 Karar
193
- * 4's own table (`w:rStyle` -> REJECTED, "0/81.973" runs); measured again
194
- * 2026-08-27 against the real corpus: still 0/46 documents contain one.
195
- * @param {string} context
196
- * @returns {never}
197
- */
198
- export function rejectCharacterStyle(context) {
199
- rejectImport(`${context}: w:rPr carries w:rStyle (character-style reference is not resolved)`);
200
- }
1
+ // ADR-0010's two-granularity import gate, ported from check_group_identity()
2
+ // and convert()'s own `is_locked` computation — never re-decided here.
3
+ //
4
+ // 1. A class with ZERO corpus examples (non-identity group transform, a
5
+ // nested group) -> REJECT THE WHOLE DOCUMENT rather than silently
6
+ // mis-place one shape.
7
+ // 2. A class that DOES occur (a shape's own rotation/flip, independent of
8
+ // any group) -> never rejected, the shape is just marked `locked`.
9
+
10
+ import { WPG_NS, A_NS } from "./ns.js";
11
+
12
+ /**
13
+ * Plain Error, not a custom class — matches every other src/ file's
14
+ * convention (zip-reject.js, xml-scan.js): a distinguishing message, not a
15
+ * type hierarchy.
16
+ * @param {string} message
17
+ * @returns {never}
18
+ */
19
+ function rejectImport(message) {
20
+ throw new Error(`import-gate: REJECTED — ${message}`);
21
+ }
22
+
23
+ /**
24
+ * @param {import("./xml-scan.js").XmlNode} el
25
+ * @returns {boolean}
26
+ */
27
+ function isNestedGroup(el) {
28
+ if (el.kind === "element" && ((el.uri === WPG_NS && el.localName === "grpSp") || (el.uri === A_NS && el.localName === "grpSp"))) {
29
+ return true;
30
+ }
31
+ if (el.kind !== "element") return false;
32
+ for (const child of el.children) {
33
+ if (isNestedGroup(child)) return true;
34
+ }
35
+ return false;
36
+ }
37
+
38
+ /**
39
+ * Raises if the group's OWN transform is not identity (scale != 1.0 on
40
+ * either axis, chOff != (0,0), rot != 0, or any flip), or if the group
41
+ * contains a nested group at any depth (`wpg:grpSp` or `a:grpSp` — corpus-
42
+ * measured as never occurring, but checked both ways since which spelling a
43
+ * document might use was exactly the kind of thing a prior regex pass in
44
+ * this project missed three times).
45
+ *
46
+ * A shape's OWN rotation (independent of its group) is NOT checked here —
47
+ * see `isShapeLocked`. This function is about the GROUP's transform only.
48
+ *
49
+ * @param {import("./drawingml.js").Xfrm} gXfrm
50
+ * @param {import("./xml-scan.js").XmlElement} groupEl
51
+ * @param {string} context
52
+ * @returns {{scaleX: number, scaleY: number}}
53
+ */
54
+ export function checkGroupIdentity(gXfrm, groupEl, context) {
55
+ const scaleX = gXfrm.chExtCx ? gXfrm.extCx / gXfrm.chExtCx : 1.0;
56
+ const scaleY = gXfrm.chExtCy ? gXfrm.extCy / gXfrm.chExtCy : 1.0;
57
+
58
+ /** @type {string[]} */
59
+ const problems = [];
60
+ if (scaleX !== 1.0 || scaleY !== 1.0) {
61
+ problems.push(`non-identity scale (scale_x=${scaleX}, scale_y=${scaleY})`);
62
+ }
63
+ if (gXfrm.chOffX !== 0 || gXfrm.chOffY !== 0) {
64
+ problems.push(`chOff != (0,0): (${gXfrm.chOffX},${gXfrm.chOffY})`);
65
+ }
66
+ if (gXfrm.rot !== 0) {
67
+ problems.push(`group rot != 0: ${gXfrm.rot}`);
68
+ }
69
+ if (gXfrm.flipH || gXfrm.flipV) {
70
+ problems.push(`group flip: flipH=${gXfrm.flipH} flipV=${gXfrm.flipV}`);
71
+ }
72
+ if (isNestedGroup(groupEl)) {
73
+ problems.push("nested group found (wpg:grpSp or a:grpSp)");
74
+ }
75
+
76
+ if (problems.length > 0) {
77
+ rejectImport(`${context}: ${problems.join("; ")}`);
78
+ }
79
+ return { scaleX, scaleY };
80
+ }
81
+
82
+ /**
83
+ * @param {{rot: number, flipH: boolean, flipV: boolean}} ownXfrm
84
+ * @returns {boolean}
85
+ */
86
+ export function isShapeLocked(ownXfrm) {
87
+ return ownXfrm.rot !== 0 || ownXfrm.flipH || ownXfrm.flipV;
88
+ }
89
+
90
+ /**
91
+ * An anchor without `wp:posOffset` under positionH/positionV (an
92
+ * alignment-based anchor using `wp:align` instead uses a different
93
+ * addressing scheme this importer does not interpret), or without
94
+ * `wp:extent`, has no safe on-page placement. Measured 2026-08-27: 0/8,737
95
+ * real corpus anchors trigger this — every anchor in all 46 documents uses
96
+ * absolute posOffset + extent. Silently dropping such an anchor (the
97
+ * pre-2026-08-27 behavior, a bare `continue`) risked discarding real,
98
+ * positioned content from a corpus-external document with no signal
99
+ * anywhere that anything was lost. Call only when at least one of the three
100
+ * booleans is true — matches the caller's own `if` guard, like `continue`
101
+ * did before it.
102
+ * @param {boolean} missingPosOffsetH
103
+ * @param {boolean} missingPosOffsetV
104
+ * @param {boolean} missingExtent
105
+ * @param {string} context
106
+ * @returns {never}
107
+ */
108
+ export function rejectAnchorPlacement(missingPosOffsetH, missingPosOffsetV, missingExtent, context) {
109
+ /** @type {string[]} */
110
+ const problems = [];
111
+ if (missingPosOffsetH || missingPosOffsetV) {
112
+ problems.push("missing wp:posOffset under positionH/positionV (an alignment-based anchor, e.g. wp:align, is not supported)");
113
+ }
114
+ if (missingExtent) {
115
+ problems.push("missing wp:extent");
116
+ }
117
+ rejectImport(`${context}: ${problems.join("; ")}`);
118
+ }
119
+
120
+ /**
121
+ * An anchor with no `a:graphic/a:graphicData` at all wraps nothing this
122
+ * importer can render. Measured 2026-08-27: 0/8,737 real corpus anchors
123
+ * trigger this.
124
+ * @param {string} context
125
+ * @returns {never}
126
+ */
127
+ export function rejectMissingGraphicData(context) {
128
+ rejectImport(`${context}: anchor has no a:graphic/a:graphicData`);
129
+ }
130
+
131
+ /**
132
+ * A group (`wpg:wgp`) with no parseable `a:xfrm` under its own
133
+ * `wpg:grpSpPr` has no chOff/chExt to place or scale its children by —
134
+ * `checkGroupIdentity` (which checks whether that transform IS identity)
135
+ * can only run once this passes. Measured 2026-08-27: 0/459 real corpus
136
+ * groups trigger this.
137
+ * @param {string} context
138
+ * @returns {never}
139
+ */
140
+ export function rejectMissingGroupTransform(context) {
141
+ rejectImport(`${context}: group has no parseable a:xfrm under wpg:grpSpPr`);
142
+ }
143
+
144
+ /**
145
+ * A shape (`wps:wsp`) inside a group with no parseable `a:xfrm` under its
146
+ * own `wps:spPr` has no off/ext to compute a position from — unlike a
147
+ * non-grouped wsp (whose absolute size comes from the anchor's own
148
+ * wp:extent instead, independent of spPr), a group child's position is
149
+ * defined ONLY relative to the group's chOff/chExt via its own xfrm.
150
+ * Measured 2026-08-27: 0/2,598 real corpus group-child shapes trigger this.
151
+ * @param {string} context
152
+ * @returns {never}
153
+ */
154
+ export function rejectMissingShapeTransform(context) {
155
+ rejectImport(`${context}: group child shape has no parseable a:xfrm under wps:spPr`);
156
+ }
157
+
158
+ /**
159
+ * A non-grouped anchor's `a:graphicData` holding none of `wpg:wgp`,
160
+ * `wps:wsp`, or `pic:pic` is some other graphic type this importer has no
161
+ * model for (a chart, SmartArt diagram, OLE object, or ink annotation —
162
+ * ADR-0020 §7's own reserved REJECTED class). Measured 2026-08-27: 0/8,278
163
+ * real corpus non-group anchors trigger this.
164
+ * @param {string} context
165
+ * @returns {never}
166
+ */
167
+ export function rejectUnsupportedGraphicDataKind(context) {
168
+ rejectImport(`${context}: a:graphicData holds none of wpg:wgp, wps:wsp, pic:pic (unsupported graphic type — chart, diagram, OLE object, or ink)`);
169
+ }
170
+
171
+ /**
172
+ * A `w:drawing` wrapping `wp:inline` (in-text-flow placement) instead of
173
+ * `wp:anchor` (floating placement) uses an addressing scheme this importer
174
+ * never reads any coordinate from — `collectAnchors` only walks for
175
+ * `wp:anchor` children, so an inline drawing was previously absent from the
176
+ * anchors array with no signal anywhere that anything had been dropped.
177
+ * ADR-0020 Karar 4's own table (`wp:inline` -> REJECTED, "corpus'ta 0")
178
+ * predates this function; measured again 2026-08-27 against the real
179
+ * corpus: 0/46 documents contain one.
180
+ * @param {string} context
181
+ * @returns {never}
182
+ */
183
+ export function rejectInlineDrawing(context) {
184
+ rejectImport(`${context}: w:drawing wraps wp:inline (in-text-flow placement is not supported, only wp:anchor)`);
185
+ }
186
+
187
+ /**
188
+ * A `w:rPr` carrying `w:rStyle` (a reference to a character style defined in
189
+ * styles.xml) is never resolved by this importer — `readRPr` reads direct
190
+ * toggles (b/i/u), size, and font only, and a character-style reference
191
+ * silently produced whatever the run's OWN direct properties happened to be,
192
+ * with no signal that the style's contribution was missing. ADR-0020 Karar
193
+ * 4's own table (`w:rStyle` -> REJECTED, "0/81.973" runs); measured again
194
+ * 2026-08-27 against the real corpus: still 0/46 documents contain one.
195
+ * @param {string} context
196
+ * @returns {never}
197
+ */
198
+ export function rejectCharacterStyle(context) {
199
+ rejectImport(`${context}: w:rPr carries w:rStyle (character-style reference is not resolved)`);
200
+ }