@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,150 +1,150 @@
|
|
|
1
|
-
import { DOC_DEFAULT_STYLE_REF, locateParagraph } from "../model/node.js";
|
|
2
|
-
import { paragraphIdOf, selectionRange } from "../model/position.js";
|
|
3
|
-
import { setAlignmentStep } from "../model/step.js";
|
|
4
|
-
import { createTransaction } from "../model/transaction.js";
|
|
5
|
-
|
|
6
|
-
// ADR-0029: paragraph alignment is editable through a DIRECT `Paragraph.jc`
|
|
7
|
-
// override — never by mutating `Doc.styles`, which 70.5% of real corpus
|
|
8
|
-
// paragraphs share with at least one other paragraph (worst case 471 on one
|
|
9
|
-
// ref). This module is `commands/size.js`'s paragraph-level counterpart.
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* The OOXML `w:jc` tokens this editor writes. Deliberately NOT the full
|
|
13
|
-
* `ST_Jc` enum — `distribute`, `thaiDistribute`, `mediumKashida` and the rest
|
|
14
|
-
* exist in the schema and are READ correctly on import (they reach
|
|
15
|
-
* `applyParagraphStyle` and render), but nothing in this UI produces them, so
|
|
16
|
-
* nothing here claims to. `start`/`end` are likewise read-only: they are the
|
|
17
|
-
* bidi-aware spellings of left/right, and writing them would silently change
|
|
18
|
-
* meaning in an RTL document.
|
|
19
|
-
* @type {readonly string[]}
|
|
20
|
-
*/
|
|
21
|
-
export const ALIGNMENT_VALUES = /** @type {const} */ (["left", "center", "right", "both"]);
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* OOXML's own default when nothing in the chain declares one. ECMA-376's
|
|
25
|
-
* `ST_Jc` has no "default" member — the default is the ABSENCE of `w:jc`, and
|
|
26
|
-
* Word renders that as left-aligned for an LTR paragraph. Measured 2026-08-31:
|
|
27
|
-
* all 46 real corpus documents declare an explicit `docDefaults` jc anyway
|
|
28
|
-
* (every one of them "left"), so this last resort is only reachable for a
|
|
29
|
-
* document that never went through `import.js` at all.
|
|
30
|
-
*/
|
|
31
|
-
const FALLBACK_ALIGNMENT = "left";
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* The effective alignment at a paragraph: its own direct override, else the
|
|
35
|
-
* import-resolved value for its `pPrRef`, else the document's own
|
|
36
|
-
* `docDefaults` (ADR-0029 EKLEME A), else OOXML's absent-means-left.
|
|
37
|
-
* @param {import("../model/node.js").Doc} doc
|
|
38
|
-
* @param {import("../model/node.js").Paragraph} para
|
|
39
|
-
* @returns {string}
|
|
40
|
-
*/
|
|
41
|
-
export function paragraphAlignment(doc, para) {
|
|
42
|
-
if (typeof para.jc === "string") {
|
|
43
|
-
return para.jc;
|
|
44
|
-
}
|
|
45
|
-
if (para.pPrRef !== undefined) {
|
|
46
|
-
const resolved = doc.styles[para.pPrRef]?.jc;
|
|
47
|
-
if (typeof resolved === "string") {
|
|
48
|
-
return resolved;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
const fromDefaults = doc.styles[DOC_DEFAULT_STYLE_REF]?.jc;
|
|
52
|
-
return typeof fromDefaults === "string" ? fromDefaults : FALLBACK_ALIGNMENT;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* The effective alignment at a position — what a toolbar button should light
|
|
57
|
-
* up for, `marksAtSelection`'s paragraph-level analogue.
|
|
58
|
-
* @param {import("../model/node.js").Doc} doc
|
|
59
|
-
* @param {import("../model/position.js").Position} pos
|
|
60
|
-
* @returns {string | null} null when the position names no real paragraph
|
|
61
|
-
*/
|
|
62
|
-
export function effectiveAlignment(doc, pos) {
|
|
63
|
-
const at = locateParagraph(doc, paragraphIdOf(pos));
|
|
64
|
-
return at === null ? null : paragraphAlignment(doc, at.para);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* The alignment shared by EVERY paragraph the selection touches, or null when
|
|
69
|
-
* they disagree — the same all-or-nothing shape `marksAtSelection` uses, so a
|
|
70
|
-
* lit button always means "every selected paragraph is already this".
|
|
71
|
-
* @param {import("../state/editor-state.js").EditorState} state
|
|
72
|
-
* @returns {string | null}
|
|
73
|
-
*/
|
|
74
|
-
export function alignmentAtSelection(state) {
|
|
75
|
-
const paragraphs = selectedParagraphs(state);
|
|
76
|
-
if (paragraphs === null || paragraphs.length === 0) {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
const first = paragraphAlignment(state.doc, paragraphs[0].para);
|
|
80
|
-
for (const entry of paragraphs) {
|
|
81
|
-
if (paragraphAlignment(state.doc, entry.para) !== first) {
|
|
82
|
-
return null; // mixed — no button lights up
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return first;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Every paragraph the selection touches, in document order. A collapsed
|
|
90
|
-
* caret selects the ONE paragraph it sits in — unlike a run-level mark, an
|
|
91
|
-
* alignment change with no selection still has an unambiguous target, so
|
|
92
|
-
* there is no `storedMarks`-style pending state to model here.
|
|
93
|
-
* @param {import("../state/editor-state.js").EditorState} state
|
|
94
|
-
* @returns {{container: import("../model/node.js").Container, para: import("../model/node.js").Paragraph, paraIndex: number}[] | null}
|
|
95
|
-
*/
|
|
96
|
-
function selectedParagraphs(state) {
|
|
97
|
-
const { doc, selection } = state;
|
|
98
|
-
const range = selectionRange(doc, selection) ?? { from: selection.head, to: selection.head };
|
|
99
|
-
|
|
100
|
-
const fromAt = locateParagraph(doc, paragraphIdOf(range.from));
|
|
101
|
-
const toAt = locateParagraph(doc, paragraphIdOf(range.to));
|
|
102
|
-
// A selection never spans containers (ADR-0007), same rule marks follow.
|
|
103
|
-
if (fromAt === null || toAt === null || fromAt.containerIndex !== toAt.containerIndex) {
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const out = [];
|
|
108
|
-
for (let i = fromAt.paraIndex; i <= toAt.paraIndex; i += 1) {
|
|
109
|
-
out.push({ container: fromAt.container, para: fromAt.container.content[i], paraIndex: i });
|
|
110
|
-
}
|
|
111
|
-
return out;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Sets the alignment of every paragraph the selection touches.
|
|
116
|
-
*
|
|
117
|
-
* Unlike `toggleMark`, this is NOT a toggle: the four alignments are mutually
|
|
118
|
-
* exclusive, so pressing "center" on already-centered text is a no-op rather
|
|
119
|
-
* than a revert-to-nothing. Returns null when nothing would change, so a
|
|
120
|
-
* redundant press never reaches the history stack.
|
|
121
|
-
*
|
|
122
|
-
* @param {import("../state/editor-state.js").EditorState} state
|
|
123
|
-
* @param {string} value one of `ALIGNMENT_VALUES`
|
|
124
|
-
* @returns {import("../model/transaction.js").Transaction | null}
|
|
125
|
-
*/
|
|
126
|
-
export function setAlignment(state, value) {
|
|
127
|
-
const { doc, selection } = state;
|
|
128
|
-
const paragraphs = selectedParagraphs(state);
|
|
129
|
-
if (paragraphs === null || paragraphs.length === 0) {
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const entries = paragraphs
|
|
134
|
-
.filter((entry) => entry.para.jc !== value)
|
|
135
|
-
.map((entry) => ({ containerId: entry.container.id, paragraphId: entry.para.id, value }));
|
|
136
|
-
if (entries.length === 0) {
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const tr = createTransaction(doc, selection, "format");
|
|
141
|
-
tr.step(setAlignmentStep(entries));
|
|
142
|
-
if (!tr.changesDoc) {
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Alignment never moves characters, so the selection is preserved as-is.
|
|
147
|
-
tr.setSelection(selection);
|
|
148
|
-
tr.setStoredMarks(null);
|
|
149
|
-
return tr.build();
|
|
150
|
-
}
|
|
1
|
+
import { DOC_DEFAULT_STYLE_REF, locateParagraph } from "../model/node.js";
|
|
2
|
+
import { paragraphIdOf, selectionRange } from "../model/position.js";
|
|
3
|
+
import { setAlignmentStep } from "../model/step.js";
|
|
4
|
+
import { createTransaction } from "../model/transaction.js";
|
|
5
|
+
|
|
6
|
+
// ADR-0029: paragraph alignment is editable through a DIRECT `Paragraph.jc`
|
|
7
|
+
// override — never by mutating `Doc.styles`, which 70.5% of real corpus
|
|
8
|
+
// paragraphs share with at least one other paragraph (worst case 471 on one
|
|
9
|
+
// ref). This module is `commands/size.js`'s paragraph-level counterpart.
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The OOXML `w:jc` tokens this editor writes. Deliberately NOT the full
|
|
13
|
+
* `ST_Jc` enum — `distribute`, `thaiDistribute`, `mediumKashida` and the rest
|
|
14
|
+
* exist in the schema and are READ correctly on import (they reach
|
|
15
|
+
* `applyParagraphStyle` and render), but nothing in this UI produces them, so
|
|
16
|
+
* nothing here claims to. `start`/`end` are likewise read-only: they are the
|
|
17
|
+
* bidi-aware spellings of left/right, and writing them would silently change
|
|
18
|
+
* meaning in an RTL document.
|
|
19
|
+
* @type {readonly string[]}
|
|
20
|
+
*/
|
|
21
|
+
export const ALIGNMENT_VALUES = /** @type {const} */ (["left", "center", "right", "both"]);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* OOXML's own default when nothing in the chain declares one. ECMA-376's
|
|
25
|
+
* `ST_Jc` has no "default" member — the default is the ABSENCE of `w:jc`, and
|
|
26
|
+
* Word renders that as left-aligned for an LTR paragraph. Measured 2026-08-31:
|
|
27
|
+
* all 46 real corpus documents declare an explicit `docDefaults` jc anyway
|
|
28
|
+
* (every one of them "left"), so this last resort is only reachable for a
|
|
29
|
+
* document that never went through `import.js` at all.
|
|
30
|
+
*/
|
|
31
|
+
const FALLBACK_ALIGNMENT = "left";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The effective alignment at a paragraph: its own direct override, else the
|
|
35
|
+
* import-resolved value for its `pPrRef`, else the document's own
|
|
36
|
+
* `docDefaults` (ADR-0029 EKLEME A), else OOXML's absent-means-left.
|
|
37
|
+
* @param {import("../model/node.js").Doc} doc
|
|
38
|
+
* @param {import("../model/node.js").Paragraph} para
|
|
39
|
+
* @returns {string}
|
|
40
|
+
*/
|
|
41
|
+
export function paragraphAlignment(doc, para) {
|
|
42
|
+
if (typeof para.jc === "string") {
|
|
43
|
+
return para.jc;
|
|
44
|
+
}
|
|
45
|
+
if (para.pPrRef !== undefined) {
|
|
46
|
+
const resolved = doc.styles[para.pPrRef]?.jc;
|
|
47
|
+
if (typeof resolved === "string") {
|
|
48
|
+
return resolved;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const fromDefaults = doc.styles[DOC_DEFAULT_STYLE_REF]?.jc;
|
|
52
|
+
return typeof fromDefaults === "string" ? fromDefaults : FALLBACK_ALIGNMENT;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The effective alignment at a position — what a toolbar button should light
|
|
57
|
+
* up for, `marksAtSelection`'s paragraph-level analogue.
|
|
58
|
+
* @param {import("../model/node.js").Doc} doc
|
|
59
|
+
* @param {import("../model/position.js").Position} pos
|
|
60
|
+
* @returns {string | null} null when the position names no real paragraph
|
|
61
|
+
*/
|
|
62
|
+
export function effectiveAlignment(doc, pos) {
|
|
63
|
+
const at = locateParagraph(doc, paragraphIdOf(pos));
|
|
64
|
+
return at === null ? null : paragraphAlignment(doc, at.para);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The alignment shared by EVERY paragraph the selection touches, or null when
|
|
69
|
+
* they disagree — the same all-or-nothing shape `marksAtSelection` uses, so a
|
|
70
|
+
* lit button always means "every selected paragraph is already this".
|
|
71
|
+
* @param {import("../state/editor-state.js").EditorState} state
|
|
72
|
+
* @returns {string | null}
|
|
73
|
+
*/
|
|
74
|
+
export function alignmentAtSelection(state) {
|
|
75
|
+
const paragraphs = selectedParagraphs(state);
|
|
76
|
+
if (paragraphs === null || paragraphs.length === 0) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const first = paragraphAlignment(state.doc, paragraphs[0].para);
|
|
80
|
+
for (const entry of paragraphs) {
|
|
81
|
+
if (paragraphAlignment(state.doc, entry.para) !== first) {
|
|
82
|
+
return null; // mixed — no button lights up
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return first;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Every paragraph the selection touches, in document order. A collapsed
|
|
90
|
+
* caret selects the ONE paragraph it sits in — unlike a run-level mark, an
|
|
91
|
+
* alignment change with no selection still has an unambiguous target, so
|
|
92
|
+
* there is no `storedMarks`-style pending state to model here.
|
|
93
|
+
* @param {import("../state/editor-state.js").EditorState} state
|
|
94
|
+
* @returns {{container: import("../model/node.js").Container, para: import("../model/node.js").Paragraph, paraIndex: number}[] | null}
|
|
95
|
+
*/
|
|
96
|
+
function selectedParagraphs(state) {
|
|
97
|
+
const { doc, selection } = state;
|
|
98
|
+
const range = selectionRange(doc, selection) ?? { from: selection.head, to: selection.head };
|
|
99
|
+
|
|
100
|
+
const fromAt = locateParagraph(doc, paragraphIdOf(range.from));
|
|
101
|
+
const toAt = locateParagraph(doc, paragraphIdOf(range.to));
|
|
102
|
+
// A selection never spans containers (ADR-0007), same rule marks follow.
|
|
103
|
+
if (fromAt === null || toAt === null || fromAt.containerIndex !== toAt.containerIndex) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const out = [];
|
|
108
|
+
for (let i = fromAt.paraIndex; i <= toAt.paraIndex; i += 1) {
|
|
109
|
+
out.push({ container: fromAt.container, para: fromAt.container.content[i], paraIndex: i });
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Sets the alignment of every paragraph the selection touches.
|
|
116
|
+
*
|
|
117
|
+
* Unlike `toggleMark`, this is NOT a toggle: the four alignments are mutually
|
|
118
|
+
* exclusive, so pressing "center" on already-centered text is a no-op rather
|
|
119
|
+
* than a revert-to-nothing. Returns null when nothing would change, so a
|
|
120
|
+
* redundant press never reaches the history stack.
|
|
121
|
+
*
|
|
122
|
+
* @param {import("../state/editor-state.js").EditorState} state
|
|
123
|
+
* @param {string} value one of `ALIGNMENT_VALUES`
|
|
124
|
+
* @returns {import("../model/transaction.js").Transaction | null}
|
|
125
|
+
*/
|
|
126
|
+
export function setAlignment(state, value) {
|
|
127
|
+
const { doc, selection } = state;
|
|
128
|
+
const paragraphs = selectedParagraphs(state);
|
|
129
|
+
if (paragraphs === null || paragraphs.length === 0) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const entries = paragraphs
|
|
134
|
+
.filter((entry) => entry.para.jc !== value)
|
|
135
|
+
.map((entry) => ({ containerId: entry.container.id, paragraphId: entry.para.id, value }));
|
|
136
|
+
if (entries.length === 0) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const tr = createTransaction(doc, selection, "format");
|
|
141
|
+
tr.step(setAlignmentStep(entries));
|
|
142
|
+
if (!tr.changesDoc) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Alignment never moves characters, so the selection is preserved as-is.
|
|
147
|
+
tr.setSelection(selection);
|
|
148
|
+
tr.setStoredMarks(null);
|
|
149
|
+
return tr.build();
|
|
150
|
+
}
|