@portone/docx-editor 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/CONTRIBUTING.md +4 -0
  3. package/README.md +17 -5
  4. package/dist/DocxEditor.d.ts +34 -10
  5. package/dist/DocxEditor.js +69 -24
  6. package/dist/core.d.ts +2 -0
  7. package/dist/core.js +2 -0
  8. package/dist/docx/commentOnlyChange.d.ts +47 -0
  9. package/dist/docx/commentOnlyChange.js +162 -0
  10. package/dist/docx/comments/constants.d.ts +11 -3
  11. package/dist/docx/comments/constants.js +10 -3
  12. package/dist/docx/comments/contentTypes.d.ts +7 -0
  13. package/dist/docx/comments/contentTypes.js +38 -0
  14. package/dist/docx/comments/grammar.d.ts +80 -0
  15. package/dist/docx/comments/grammar.js +213 -0
  16. package/dist/docx/comments/model.d.ts +2 -0
  17. package/dist/docx/comments/model.js +3 -0
  18. package/dist/docx/comments/people.d.ts +39 -0
  19. package/dist/docx/comments/people.js +196 -0
  20. package/dist/docx/comments/reading.d.ts +18 -1
  21. package/dist/docx/comments/reading.js +33 -20
  22. package/dist/docx/comments/verifying.d.ts +44 -0
  23. package/dist/docx/comments/verifying.js +206 -0
  24. package/dist/docx/comments/writing.d.ts +4 -1
  25. package/dist/docx/comments/writing.js +84 -77
  26. package/dist/docx/exportDocx.js +2 -1
  27. package/dist/docx/formatting/direct.js +3 -6
  28. package/dist/docx/headersFooters.js +8 -10
  29. package/dist/docx/importParagraph.js +1 -0
  30. package/dist/docx/notes.js +3 -5
  31. package/dist/docx/paraProps.js +51 -46
  32. package/dist/docx/propsXml.d.ts +20 -2
  33. package/dist/docx/propsXml.js +72 -32
  34. package/dist/docx/relationships.js +8 -3
  35. package/dist/docx/runProps.js +34 -34
  36. package/dist/docx/sdt.js +16 -14
  37. package/dist/docx/serializeBlock.js +1 -2
  38. package/dist/docx/serializeParagraph.d.ts +2 -1
  39. package/dist/docx/serializeParagraph.js +25 -14
  40. package/dist/docx/serializeTable.js +38 -24
  41. package/dist/docx/storyProjection.d.ts +36 -0
  42. package/dist/docx/storyProjection.js +23 -0
  43. package/dist/docx/tableFormatting/editing.js +49 -73
  44. package/dist/docx/tableTemplate.js +24 -3
  45. package/dist/docx/theme.d.ts +0 -6
  46. package/dist/docx/theme.js +0 -8
  47. package/dist/editor/clipboard/images.js +17 -3
  48. package/dist/editor/clipboard/inlineFormatting.d.ts +7 -0
  49. package/dist/editor/clipboard/inlineFormatting.js +1 -0
  50. package/dist/editor/commands/breakCommands.js +6 -2
  51. package/dist/editor/commands/canRunCommand.d.ts +3 -2
  52. package/dist/editor/commands/canRunCommand.js +2 -2
  53. package/dist/editor/commands/comments/editing.js +9 -10
  54. package/dist/editor/commands/comments/model.d.ts +9 -0
  55. package/dist/editor/commands/comments/reading.d.ts +7 -0
  56. package/dist/editor/commands/comments/reading.js +14 -0
  57. package/dist/editor/commands/formatting/editing.js +6 -1
  58. package/dist/editor/commands/historyCommands.js +14 -5
  59. package/dist/editor/commands/index.d.ts +7 -1
  60. package/dist/editor/commands/index.js +4 -0
  61. package/dist/editor/commands/linkCommands.js +2 -0
  62. package/dist/editor/commands/lockCommands.js +12 -1
  63. package/dist/editor/commands/tabCommands.js +4 -2
  64. package/dist/editor/createEditor.d.ts +13 -3
  65. package/dist/editor/createEditor.js +23 -15
  66. package/dist/editor/externalClipboard.js +166 -4
  67. package/dist/editor/insertImage.js +6 -2
  68. package/dist/editor/insertTable.js +2 -1
  69. package/dist/editor/paragraphEdits.d.ts +2 -0
  70. package/dist/editor/paragraphEdits.js +2 -0
  71. package/dist/editor/plugins/documentProtection.d.ts +21 -0
  72. package/dist/editor/plugins/documentProtection.js +44 -0
  73. package/dist/editor/plugins/imagePaste.js +4 -1
  74. package/dist/editor/plugins/keymap.d.ts +11 -1
  75. package/dist/editor/plugins/keymap.js +24 -4
  76. package/dist/editor/plugins/lockedContent.d.ts +4 -2
  77. package/dist/editor/plugins/lockedContent.js +2 -2
  78. package/dist/editor/plugins/tableContextMenu.js +2 -1
  79. package/dist/editor/plugins/textContextMenu.js +6 -1
  80. package/dist/index.d.ts +2 -0
  81. package/dist/numbering/listTemplate.js +32 -10
  82. package/dist/ooxml/element.d.ts +52 -0
  83. package/dist/ooxml/element.js +49 -0
  84. package/dist/ooxml/fragment.d.ts +53 -0
  85. package/dist/ooxml/fragment.js +76 -0
  86. package/dist/ooxml/image.js +4 -3
  87. package/dist/ooxml/names.d.ts +27 -0
  88. package/dist/ooxml/names.js +29 -0
  89. package/dist/ooxml/precedence.d.ts +31 -0
  90. package/dist/ooxml/precedence.js +51 -0
  91. package/dist/ooxml/xml.d.ts +31 -0
  92. package/dist/ooxml/xml.js +27 -0
  93. package/dist/page/blockKinds.d.ts +42 -0
  94. package/dist/page/blockKinds.js +0 -0
  95. package/dist/page/measureBlocks.d.ts +14 -1
  96. package/dist/page/measureBlocks.js +31 -11
  97. package/dist/page/pageDecorations.d.ts +16 -0
  98. package/dist/page/pageDecorations.js +1 -0
  99. package/dist/page/pageLayout.d.ts +8 -43
  100. package/dist/page/pageLayout.js +28 -51
  101. package/dist/page/tableMeasurements.d.ts +10 -2
  102. package/dist/page/tableMeasurements.js +20 -16
  103. package/dist/page/usePageLayout.d.ts +15 -0
  104. package/dist/page/usePageLayout.js +30 -2
  105. package/dist/schema/attrRoles.d.ts +35 -0
  106. package/dist/schema/attrRoles.js +106 -0
  107. package/dist/schema/docxSchema.d.ts +6 -0
  108. package/dist/schema/docxSchema.js +216 -100
  109. package/dist/schema/editGuard.d.ts +99 -0
  110. package/dist/schema/editGuard.js +43 -0
  111. package/dist/schema/guards.d.ts +68 -0
  112. package/dist/schema/guards.js +82 -0
  113. package/dist/schema/locks.d.ts +9 -8
  114. package/dist/schema/locks.js +32 -16
  115. package/dist/schema/preservedGuards.d.ts +30 -0
  116. package/dist/schema/preservedGuards.js +50 -0
  117. package/dist/schema/protection.d.ts +84 -0
  118. package/dist/schema/protection.js +171 -0
  119. package/dist/schema/protectionState.d.ts +20 -0
  120. package/dist/schema/protectionState.js +37 -0
  121. package/dist/schema/sourceEquality.d.ts +27 -0
  122. package/dist/schema/sourceEquality.js +65 -0
  123. package/dist/table/cellFormatting.js +5 -2
  124. package/dist/table/commands.d.ts +2 -2
  125. package/dist/table/commands.js +2 -2
  126. package/dist/table/index.d.ts +2 -2
  127. package/dist/table/merge.d.ts +2 -2
  128. package/dist/table/merge.js +3 -3
  129. package/dist/ui/CommentsPanel.d.ts +3 -3
  130. package/dist/ui/CommentsPanel.js +26 -18
  131. package/dist/ui/LinkCard.d.ts +1 -2
  132. package/dist/ui/LinkCard.js +3 -4
  133. package/dist/ui/TextMenu.js +15 -10
  134. package/package.json +5 -2
  135. package/dist/editor/plugins/bookmarkProtection.d.ts +0 -3
  136. package/dist/editor/plugins/bookmarkProtection.js +0 -28
  137. package/dist/editor/plugins/noteProtection.d.ts +0 -3
  138. package/dist/editor/plugins/noteProtection.js +0 -25
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The document story as the parts of the package see it: which blocks the editor models and
3
+ * writes back itself, and the text those blocks go back out as.
4
+ *
5
+ * Two files saying the same thing can spell it differently. A block carries the XML it arrived
6
+ * as on its attrs, so a table Word wrote (`w:w` before `w:type` inside a `w:tcW`) and the same
7
+ * table this editor wrote (the other way round) compare unequal attr for attr, and a comparison
8
+ * over the model would call that a body edit. Writing both stories out through the one writer
9
+ * takes the wording out of the comparison and leaves what the story says, because the writer
10
+ * reaches a fixed point on its own output: opening what it wrote and writing it again gives the
11
+ * same text (the test beside this file holds that over every fixture).
12
+ *
13
+ * What falls out of the comparison is of two kinds, and the test beside this file keeps them
14
+ * apart. One is two spellings of a single thing, which the writer settles on one of; forgiving
15
+ * those is the point, and the set of them is open. The other is content the writer builds afresh
16
+ * instead of reading, a table's grid and a drawing's extent among it, which the comparison cannot
17
+ * report because it can no longer see it. That second set is enumerated and meant to shrink: a
18
+ * case joining it is a preservation defect in the writer, not a detail of the comparison.
19
+ */
20
+ import type { Node as PMNode } from "prosemirror-model";
21
+ import type { SessionStore } from "./session";
22
+ /** A document and the package it was opened from, which is what writing it back out takes */
23
+ export interface Story {
24
+ readonly doc: PMNode;
25
+ readonly session: SessionStore;
26
+ }
27
+ /** Whether this is a block the editor takes apart and writes back itself, rather than one it keeps as it came */
28
+ export declare function isModelledBlock(node: PMNode): boolean;
29
+ /**
30
+ * Every top-level block as the writer puts it out, with the markup that is allowed to have
31
+ * changed taken away first.
32
+ *
33
+ * null where a block cannot be written back at all, which is a story no comparison can be held
34
+ * over: a caller judging two files answers such a story the way it answers a changed one.
35
+ */
36
+ export declare function comparableStory(story: Story, strip: (node: PMNode) => PMNode): readonly string[] | null;
@@ -0,0 +1,23 @@
1
+ // src/docx/storyProjection.ts
2
+ import { DocxExportError } from "../ooxml/errors.js";
3
+ import { NO_EXPORT_REFS } from "./exportRefs.js";
4
+ import { serializeBlock } from "./serializeBlock.js";
5
+ function isModelledBlock(node) {
6
+ return node.type.isInGroup("modelled");
7
+ }
8
+ function comparableStory(story, strip) {
9
+ const blocks = [];
10
+ try {
11
+ story.doc.forEach((block) => {
12
+ blocks.push(serializeBlock(strip(block), story.session, NO_EXPORT_REFS));
13
+ });
14
+ } catch (error) {
15
+ if (error instanceof DocxExportError) return null;
16
+ throw error;
17
+ }
18
+ return blocks;
19
+ }
20
+ export {
21
+ comparableStory,
22
+ isModelledBlock
23
+ };
@@ -1,6 +1,9 @@
1
1
  // src/docx/tableFormatting/editing.ts
2
+ import { attrPairs, elementXml } from "../../ooxml/element.js";
3
+ import { wName } from "../../ooxml/names.js";
4
+ import { setAttr } from "../../ooxml/precedence.js";
2
5
  import { normalizeHex, wAttr } from "../../ooxml/units.js";
3
- import { childByLocalName, escapeXml, localPart } from "../../ooxml/xml.js";
6
+ import { childByLocalName } from "../../ooxml/xml.js";
4
7
  import {
5
8
  parseProps,
6
9
  parsePropsXml,
@@ -41,55 +44,34 @@ var SIDE_NAMES = {
41
44
  right: ["right", "end"]
42
45
  };
43
46
  var PRESET_BORDER_EIGHTHS = 4;
44
- var THEME_COLOR_ATTRS = ["themeColor", "themeTint", "themeShade"];
45
- var THEME_FILL_ATTRS = ["themeFill", "themeFillTint", "themeFillShade"];
46
47
  var EMPTY_TC_PR = { tag: "w:tcPr", attrs: null, children: [] };
47
- function attrsOf(el) {
48
- return el ? Array.from(el.attributes).map((attr) => [attr.name, attr.value]) : [];
49
- }
50
- function withAttr(attrs, name, value, overridden = []) {
51
- const kept = attrs.filter(([attr]) => !overridden.includes(localPart(attr)));
52
- const at = kept.findIndex(([attr]) => localPart(attr) === name);
53
- if (at === -1) return [...kept, [`w:${name}`, value]];
54
- return kept.map(
55
- (attr, index) => index === at ? [attr[0], value] : attr
56
- );
57
- }
58
- function elementXml(tag, attrs) {
59
- const text = attrs.map(([name, value]) => `${name}="${escapeXml(value)}"`).join(" ");
60
- return `<${tag} ${text}/>`;
61
- }
62
48
  function drawsLine(side) {
63
49
  if (!side) return false;
64
50
  const val = wAttr(side, "val");
65
51
  return val !== null && val !== "nil" && val !== "none";
66
52
  }
67
- function sideAttrs(current, edit, fallback = null) {
53
+ function sideAttrs(name, current, edit, fallback = null) {
68
54
  if (edit.kind === "color") {
69
55
  if (!drawsLine(current)) {
70
56
  if (current || !fallback || fallback === "none") return null;
71
57
  return inheritedBorderAttrs(fallback, edit.hex);
72
58
  }
73
- return withAttr(
74
- attrsOf(current),
75
- "color",
76
- edit.hex ?? "auto",
77
- THEME_COLOR_ATTRS
78
- );
59
+ return setAttr(attrPairs(current), name, "color", edit.hex ?? "auto");
79
60
  }
80
61
  if (edit.line === "none") {
81
- return current ? withAttr(attrsOf(current), "val", "none") : [["w:val", "none"]];
62
+ return current ? setAttr(attrPairs(current), name, "val", "none") : [[wName("val"), "none"]];
82
63
  }
83
64
  if (!current) {
84
65
  return [
85
- ["w:val", "single"],
86
- ["w:sz", `${PRESET_BORDER_EIGHTHS}`],
87
- ["w:space", "0"],
88
- ["w:color", "auto"]
66
+ [wName("val"), "single"],
67
+ [wName("sz"), `${PRESET_BORDER_EIGHTHS}`],
68
+ [wName("space"), "0"],
69
+ [wName("color"), "auto"]
89
70
  ];
90
71
  }
91
- return withAttr(
92
- withAttr(attrsOf(current), "val", "single"),
72
+ return setAttr(
73
+ setAttr(attrPairs(current), name, "val", "single"),
74
+ name,
93
75
  "sz",
94
76
  `${PRESET_BORDER_EIGHTHS}`
95
77
  );
@@ -110,10 +92,10 @@ function inheritedBorderAttrs(border, hex) {
110
92
  const eighths = Math.round(Number.parseFloat(widthText) * 8);
111
93
  if (!Number.isSafeInteger(eighths) || eighths <= 0) return null;
112
94
  return [
113
- ["w:val", BORDER_VAL_BY_CSS_STYLE[style.toLowerCase()]],
114
- ["w:sz", `${eighths}`],
115
- ["w:space", "0"],
116
- ["w:color", hex ?? "auto"]
95
+ [wName("val"), BORDER_VAL_BY_CSS_STYLE[style.toLowerCase()]],
96
+ [wName("sz"), `${eighths}`],
97
+ [wName("space"), "0"],
98
+ [wName("color"), hex ?? "auto"]
117
99
  ];
118
100
  }
119
101
  function sideNames(borders, side) {
@@ -127,12 +109,12 @@ function editedBorders(current, edit, defaults = NO_BORDER_DEFAULTS) {
127
109
  const borders = current === null ? null : parsePropsXml(current);
128
110
  if (!props || current !== null && !borders) return null;
129
111
  const sides = edit.kind === "line" ? edit.sides : ALL_CELL_SIDES;
130
- const children = sides.reduce((kept, side) => {
112
+ const edited = sides.reduce((kept, side) => {
131
113
  const { write, drop } = sideNames(borders, side);
132
114
  const currentSide = borders ? childByLocalName(borders, write) : null;
133
- const attrs = sideAttrs(currentSide, edit, defaults[side]);
115
+ const attrs = sideAttrs(write, currentSide, edit, defaults[side]);
134
116
  if (!attrs) return kept;
135
- const tag = currentSide?.nodeName ?? `w:${write}`;
117
+ const tag = currentSide?.nodeName ?? wName(write);
136
118
  const written = setPropsChild(
137
119
  kept,
138
120
  write,
@@ -140,8 +122,8 @@ function editedBorders(current, edit, defaults = NO_BORDER_DEFAULTS) {
140
122
  TC_BORDERS_ORDER
141
123
  );
142
124
  return drop === null ? written : setPropsChild(written, drop, null, TC_BORDERS_ORDER);
143
- }, props.children);
144
- const xml = renderProps({ ...props, children });
125
+ }, props);
126
+ const xml = renderProps(edited);
145
127
  return { xml: xml === "" ? null : xml };
146
128
  }
147
129
  function hasPattern(shd) {
@@ -149,23 +131,20 @@ function hasPattern(shd) {
149
131
  return val !== null && val !== "clear" && val !== "nil";
150
132
  }
151
133
  function editedShading(current, fill) {
152
- const tag = current?.nodeName ?? "w:shd";
134
+ const tag = current?.nodeName ?? wName("shd");
153
135
  if (fill === null) {
154
136
  if (!hasPattern(current)) return null;
155
- return elementXml(
156
- tag,
157
- withAttr(attrsOf(current), "fill", "auto", THEME_FILL_ATTRS)
158
- );
137
+ return elementXml(tag, setAttr(attrPairs(current), "shd", "fill", "auto"));
159
138
  }
160
139
  if (!current) {
161
140
  return elementXml(tag, [
162
- ["w:val", "clear"],
163
- ["w:color", "auto"],
164
- ["w:fill", fill]
141
+ [wName("val"), "clear"],
142
+ [wName("color"), "auto"],
143
+ [wName("fill"), fill]
165
144
  ]);
166
145
  }
167
- const painting = hasPattern(current) ? attrsOf(current) : withAttr(attrsOf(current), "val", "clear");
168
- return elementXml(tag, withAttr(painting, "fill", fill, THEME_FILL_ATTRS));
146
+ const painting = hasPattern(current) ? attrPairs(current) : setAttr(attrPairs(current), "shd", "val", "clear");
147
+ return elementXml(tag, setAttr(painting, "shd", "fill", fill));
169
148
  }
170
149
  function bordersChange(props, edit, defaults) {
171
150
  const current = propsChild(props.children, "tcBorders")?.xml ?? null;
@@ -191,7 +170,7 @@ function paddingChange(props, values) {
191
170
  const margins = current === null ? null : parsePropsXml(current);
192
171
  const parsed = current === null ? { tag: "w:tcMar", attrs: null, children: [] } : parseProps(current);
193
172
  if (!parsed || current !== null && !margins) return null;
194
- let children = parsed.children;
173
+ let edited = parsed;
195
174
  let wrote = false;
196
175
  for (const side of ALL_CELL_SIDES) {
197
176
  const points = values[side];
@@ -202,21 +181,22 @@ function paddingChange(props, values) {
202
181
  }
203
182
  const existing = margins ? MARGIN_SIDE_NAMES[side].map((name2) => childByLocalName(margins, name2)).find((element) => element !== null) ?? null : null;
204
183
  const name = existing?.localName ?? side;
205
- const attrs = withAttr(
206
- withAttr(attrsOf(existing), "w", `${twips}`),
184
+ const attrs = setAttr(
185
+ setAttr(attrPairs(existing), name, "w", `${twips}`),
186
+ name,
207
187
  "type",
208
188
  "dxa"
209
189
  );
210
- children = setPropsChild(
211
- children,
190
+ edited = setPropsChild(
191
+ edited,
212
192
  name,
213
- elementXml(existing?.nodeName ?? `w:${side}`, attrs),
193
+ elementXml(existing?.nodeName ?? wName(side), attrs),
214
194
  TC_MAR_ORDER
215
195
  );
216
196
  wrote = true;
217
197
  }
218
198
  if (!wrote) return null;
219
- return { name: "tcMar", xml: renderProps({ ...parsed, children }) };
199
+ return { name: "tcMar", xml: renderProps(edited) };
220
200
  }
221
201
  function childChange(edit, props, defaults) {
222
202
  switch (edit.kind) {
@@ -250,8 +230,8 @@ function childChange(edit, props, defaults) {
250
230
  return {
251
231
  name: "vAlign",
252
232
  xml: elementXml(
253
- element?.nodeName ?? "w:vAlign",
254
- withAttr(attrsOf(element), "val", edit.align)
233
+ element?.nodeName ?? wName("vAlign"),
234
+ setAttr(attrPairs(element), "vAlign", "val", edit.align)
255
235
  )
256
236
  };
257
237
  }
@@ -264,13 +244,9 @@ function editCellProps(tcPr, edit, defaults = NO_BORDER_DEFAULTS, margins = NO_C
264
244
  if (!props) return null;
265
245
  const change = childChange(edit, props, defaults);
266
246
  if (!change) return null;
267
- const children = setPropsChild(
268
- props.children,
269
- change.name,
270
- change.xml,
271
- TC_PR_ORDER
247
+ const rendered = renderProps(
248
+ setPropsChild(props, change.name, change.xml, TC_PR_ORDER)
272
249
  );
273
- const rendered = renderProps({ ...props, children });
274
250
  const next = rendered === "" ? null : rendered;
275
251
  if (next === tcPr) return null;
276
252
  return { tcPr: next, format: readCellProps(next, defaults, margins) };
@@ -294,17 +270,17 @@ function editRowHeight(trPr, heightPt) {
294
270
  const element = current === null ? null : parsePropsXml(current);
295
271
  if (current !== null && !element) return null;
296
272
  const writtenRule = element ? wAttr(element, "hRule") : null;
297
- const attrs = withAttr(attrsOf(element), "val", `${twips}`);
298
- const nextAttrs = withAttr(
273
+ const attrs = setAttr(attrPairs(element), "trHeight", "val", `${twips}`);
274
+ const nextAttrs = setAttr(
299
275
  attrs,
276
+ "trHeight",
300
277
  "hRule",
301
278
  writtenRule === "exact" ? "exact" : "atLeast"
302
279
  );
303
- const child = elementXml(element?.nodeName ?? "w:trHeight", nextAttrs);
304
- const rendered = renderProps({
305
- ...props,
306
- children: setPropsChild(props.children, "trHeight", child, TR_PR_ORDER)
307
- });
280
+ const child = elementXml(element?.nodeName ?? wName("trHeight"), nextAttrs);
281
+ const rendered = renderProps(
282
+ setPropsChild(props, "trHeight", child, TR_PR_ORDER)
283
+ );
308
284
  if (rendered === trPr) return null;
309
285
  const parsed = parsePropsXml(rendered);
310
286
  const format = parsed ? readRowFormat(parsed) : null;
@@ -1,4 +1,6 @@
1
1
  // src/docx/tableTemplate.ts
2
+ import { elementXml } from "../ooxml/element.js";
3
+ import { wName } from "../ooxml/names.js";
2
4
  import { docxSchema } from "../schema/index.js";
3
5
  import { A4_PORTRAIT, bodyWidth } from "./pageGeometry.js";
4
6
  import { parsePropsXml } from "./propsXml.js";
@@ -25,14 +27,33 @@ var BORDER_SIDES = [
25
27
  "insideV"
26
28
  ];
27
29
  function borderXml(side) {
28
- return `<w:${side} w:val="single" w:sz="${BORDER_EIGHTHS}" w:space="0" w:color="000000"/>`;
30
+ return elementXml(wName(side), [
31
+ [wName("val"), "single"],
32
+ [wName("sz"), `${BORDER_EIGHTHS}`],
33
+ [wName("space"), "0"],
34
+ [wName("color"), "000000"]
35
+ ]);
29
36
  }
30
37
  function tablePropsXml(width) {
31
38
  const borders = BORDER_SIDES.map(borderXml).join("");
32
- return `<w:tblPr><w:tblW w:w="${width}" w:type="dxa"/><w:tblBorders>${borders}</w:tblBorders><w:tblLayout w:type="fixed"/></w:tblPr>`;
39
+ return elementXml(
40
+ wName("tblPr"),
41
+ [],
42
+ [
43
+ widthXml("tblW", width),
44
+ elementXml(wName("tblBorders"), [], [borders]),
45
+ elementXml(wName("tblLayout"), [[wName("type"), "fixed"]])
46
+ ]
47
+ );
48
+ }
49
+ function widthXml(name, width) {
50
+ return elementXml(wName(name), [
51
+ [wName("w"), `${width}`],
52
+ [wName("type"), "dxa"]
53
+ ]);
33
54
  }
34
55
  function cellPropsXml(width) {
35
- return `<w:tcPr><w:tcW w:w="${width}" w:type="dxa"/></w:tcPr>`;
56
+ return elementXml(wName("tcPr"), [], [widthXml("tcW", width)]);
36
57
  }
37
58
  function evenGridCols(cols, total) {
38
59
  const base = Math.floor(total / cols);
@@ -21,12 +21,6 @@ export interface ThemeFonts {
21
21
  }
22
22
  /** What applies to a document with no theme part, or one whose theme names no font */
23
23
  export declare const NO_THEME_FONTS: ThemeFonts;
24
- /**
25
- * The theme attribute paired with each font slot.
26
- * A theme reference and a name in the same slot cannot both apply, so the name wins,
27
- * the way Word reads them.
28
- */
29
- export declare const THEME_ATTRS: Record<string, readonly string[]>;
30
24
  /** The font one theme reference stands for. null for a reference we cannot resolve */
31
25
  export declare function themeFontName(fonts: ThemeFonts, value: string | null): string | null;
32
26
  /**
@@ -9,13 +9,6 @@ var NO_THEME_FONTS = {
9
9
  major: NO_FONT_SCHEME,
10
10
  minor: NO_FONT_SCHEME
11
11
  };
12
- var THEME_ATTRS = {
13
- ascii: ["asciiTheme"],
14
- hAnsi: ["hAnsiTheme"],
15
- eastAsia: ["eastAsiaTheme"],
16
- // This is the slot whose capitalization varies from document to document
17
- cs: ["cstheme", "csTheme"]
18
- };
19
12
  var THEME_SLOTS = {
20
13
  majorascii: { scheme: "major", script: "latin" },
21
14
  majorhansi: { scheme: "major", script: "latin" },
@@ -55,7 +48,6 @@ function readThemeFonts(theme) {
55
48
  }
56
49
  export {
57
50
  NO_THEME_FONTS,
58
- THEME_ATTRS,
59
51
  readThemeFonts,
60
52
  themeFontName
61
53
  };
@@ -1,6 +1,11 @@
1
1
  // src/editor/clipboard/images.ts
2
2
  import { MAX_IMAGE_BYTES } from "../../docx/media.js";
3
- import { isImageMime, toImageExtent, toImageSrc } from "../../ooxml/image.js";
3
+ import {
4
+ isImageMime,
5
+ pxToEmu,
6
+ toImageExtent,
7
+ toImageSrc
8
+ } from "../../ooxml/image.js";
4
9
  import { editorClassNames } from "../../styles/classNames.js";
5
10
  import {
6
11
  boundedImageSrc,
@@ -9,8 +14,7 @@ import {
9
14
  readImageSource
10
15
  } from "../imageFiles.js";
11
16
  var PASTED_IMAGE_ATTRIBUTE = "data-docx-pasted-image";
12
- function extentAttribute(element) {
13
- if (!element.classList.contains(editorClassNames.image)) return null;
17
+ function declaredExtent(element) {
14
18
  try {
15
19
  return toImageExtent(
16
20
  JSON.parse(element.getAttribute("data-extent") ?? "null")
@@ -19,6 +23,16 @@ function extentAttribute(element) {
19
23
  return null;
20
24
  }
21
25
  }
26
+ function renderedExtent(element) {
27
+ const width = Number.parseInt(element.getAttribute("width") ?? "", 10);
28
+ const height = Number.parseInt(element.getAttribute("height") ?? "", 10);
29
+ if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
30
+ return toImageExtent({ cx: pxToEmu(width), cy: pxToEmu(height) });
31
+ }
32
+ function extentAttribute(element) {
33
+ if (!element.classList.contains(editorClassNames.image)) return null;
34
+ return declaredExtent(element) ?? renderedExtent(element);
35
+ }
22
36
  function fetchableUrl(source) {
23
37
  try {
24
38
  const url = new URL(source);
@@ -15,6 +15,13 @@ export interface InlineContext {
15
15
  preserveWhitespace: boolean;
16
16
  }
17
17
  export type InlineElementReader = (target: PMNode[], element: HTMLElement, context: InlineContext) => boolean;
18
+ /**
19
+ * The address a link may carry, whichever way it travels.
20
+ *
21
+ * A copy writes one out and a paste reads one in, and an address this rule turns down is one
22
+ * neither end should act on, so both ask here.
23
+ */
24
+ export declare function safeHref(value: string | null): string | null;
18
25
  export declare function contextFor(parent: InlineContext, element: HTMLElement, readElementStyle?: boolean): InlineContext;
19
26
  export declare function withInlineStyle(context: InlineContext, style: InlineStyle): InlineContext;
20
27
  export declare function marksFor(context: InlineContext): Mark[];
@@ -228,5 +228,6 @@ export {
228
228
  appendInline,
229
229
  contextFor,
230
230
  marksFor,
231
+ safeHref,
231
232
  withInlineStyle
232
233
  };
@@ -1,9 +1,13 @@
1
1
  // src/editor/commands/breakCommands.ts
2
2
  import { docxSchema } from "../../schema/index.js";
3
- import { replacementShut } from "../../schema/locks.js";
3
+ import { editShut, selectionIntents } from "../../schema/guards.js";
4
4
  function insertBreak(brAttrs) {
5
5
  return (state, dispatch) => {
6
- if (replacementShut(state.selection, state.doc)) return false;
6
+ if (selectionIntents(state.selection, "replace").some(
7
+ (intent) => editShut(state, intent)
8
+ )) {
9
+ return false;
10
+ }
7
11
  if (dispatch) {
8
12
  const br = docxSchema.nodes.hardBreak.create({ brAttrs });
9
13
  dispatch(state.tr.replaceSelectionWith(br).scrollIntoView());
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Dry-runs external commands and checks every produced transaction against document locks without
3
- * dispatching them or triggering the locked-content plugin's side effects.
2
+ * Dry-runs external commands and checks every produced transaction against every guard
3
+ * (`schema/guards`) without dispatching them or triggering the locked-content plugin's side
4
+ * effects.
4
5
  */
5
6
  import type { Command, EditorState } from "prosemirror-state";
6
7
  export declare function canRunCommand(command: Command, state: EditorState): boolean;
@@ -1,9 +1,9 @@
1
1
  // src/editor/commands/canRunCommand.ts
2
- import { transactionAllowed } from "../../schema/locks.js";
2
+ import { transactionAllowed } from "../../schema/guards.js";
3
3
  function canRunCommand(command, state) {
4
4
  const built = [];
5
5
  if (!command(state, (tr) => built.push(tr))) return false;
6
- return built.every((tr) => transactionAllowed(tr, state.doc));
6
+ return built.every((tr) => transactionAllowed(tr, state));
7
7
  }
8
8
  export {
9
9
  canRunCommand
@@ -4,7 +4,7 @@ import {
4
4
  } from "prosemirror-state";
5
5
  import { commentParaId } from "../../../docx/comments.js";
6
6
  import { docxSchema } from "../../../schema/index.js";
7
- import { transactionAllowed } from "../../../schema/locks.js";
7
+ import { transactionAllowed } from "../../../schema/guards.js";
8
8
  import {
9
9
  reservedCommentIds,
10
10
  reservedCommentParaIds
@@ -93,6 +93,7 @@ function addCommentTransaction(state, comment) {
93
93
  id,
94
94
  referenceXml: null,
95
95
  author: comment.author,
96
+ authorId: comment.authorId ?? null,
96
97
  initials: comment.initials ?? null,
97
98
  date,
98
99
  text: comment.text,
@@ -113,7 +114,7 @@ function addCommentTransaction(state, comment) {
113
114
  startMarks
114
115
  );
115
116
  const transaction = state.tr.insert(to, end).insert(to + 1, reference).insert(from, start);
116
- return transactionAllowed(transaction, state.doc) ? transaction : null;
117
+ return transactionAllowed(transaction, state) ? transaction : null;
117
118
  }
118
119
  function canAddComment(state) {
119
120
  return addCommentTransaction(state, {
@@ -147,7 +148,7 @@ function updateComment(id, text) {
147
148
  }
148
149
  return true;
149
150
  });
150
- if (!changed || !transactionAllowed(transaction, state.doc)) return false;
151
+ if (!changed || !transactionAllowed(transaction, state)) return false;
151
152
  dispatch?.(transaction);
152
153
  return true;
153
154
  };
@@ -167,7 +168,7 @@ function updateReference(id, change) {
167
168
  }
168
169
  return false;
169
170
  });
170
- if (!changed || !transactionAllowed(transaction, state.doc)) return false;
171
+ if (!changed || !transactionAllowed(transaction, state)) return false;
171
172
  dispatch?.(transaction);
172
173
  return true;
173
174
  };
@@ -177,11 +178,9 @@ function setCommentResolved(id, resolved) {
177
178
  if (node.attrs.resolved === resolved) return null;
178
179
  return {
179
180
  ...node.attrs,
180
- paraId: node.attrs.threadImported === true && node.attrs.extensionXml === null ? nextCommentParaId(state, `comment-${id}`) : node.attrs.paraId,
181
181
  resolved,
182
182
  extensionXml: null,
183
- threadImported: false,
184
- imported: node.attrs.commentXml !== null && node.attrs.extensionXml === null ? false : node.attrs.imported
183
+ threadImported: false
185
184
  };
186
185
  })(state, dispatch);
187
186
  }
@@ -191,7 +190,7 @@ function addCommentReply(id, reply) {
191
190
  const replyId = nextCommentId(state);
192
191
  const date = reply.date ?? (/* @__PURE__ */ new Date()).toISOString();
193
192
  return updateReference(id, (node) => {
194
- const parentParaId = node.attrs.threadImported === true && node.attrs.extensionXml === null ? nextCommentParaId(state, `comment-${id}`) : stringAttr(node.attrs.paraId) ?? nextCommentParaId(state, `comment-${id}`);
193
+ const parentParaId = stringAttr(node.attrs.paraId) ?? nextCommentParaId(state, `comment-${id}`);
195
194
  const paraId = nextCommentParaId(state, `comment-${replyId}-${date}`, [
196
195
  parentParaId
197
196
  ]);
@@ -201,12 +200,12 @@ function addCommentReply(id, reply) {
201
200
  resolved: false,
202
201
  extensionXml: null,
203
202
  threadImported: false,
204
- imported: node.attrs.commentXml !== null && node.attrs.extensionXml === null ? false : node.attrs.imported,
205
203
  replies: [
206
204
  ...repliesAttr(node.attrs.replies),
207
205
  {
208
206
  id: replyId,
209
207
  author: reply.author,
208
+ authorId: reply.authorId ?? null,
210
209
  initials: reply.initials ?? null,
211
210
  date,
212
211
  text: reply.text,
@@ -275,7 +274,7 @@ function removeComment(id) {
275
274
  for (const marker of positions.sort((a, b) => b.pos - a.pos)) {
276
275
  transaction.delete(marker.pos, marker.pos + marker.size);
277
276
  }
278
- if (!transactionAllowed(transaction, state.doc)) return false;
277
+ if (!transactionAllowed(transaction, state)) return false;
279
278
  dispatch?.(transaction);
280
279
  return true;
281
280
  };
@@ -1,11 +1,18 @@
1
1
  /** Public comment values and shared attribute readers. */
2
2
  export interface CommentAuthor {
3
+ /**
4
+ * The identity behind the name, an opaque string the host application chooses. Recorded in the
5
+ * document's people part, and what decides whose comment a comment is (`schema/protection`).
6
+ */
7
+ id: string;
3
8
  name: string;
4
9
  initials?: string;
5
10
  }
6
11
  export interface NewComment {
7
12
  text: string;
8
13
  author: string;
14
+ /** The identity behind `author`. A comment written without one belongs to nobody in particular */
15
+ authorId?: string;
9
16
  initials?: string;
10
17
  /** ISO 8601 timestamp. The current time is used when omitted. */
11
18
  date?: string;
@@ -13,6 +20,7 @@ export interface NewComment {
13
20
  export interface DocumentComment {
14
21
  id: string;
15
22
  author: string | null;
23
+ authorId: string | null;
16
24
  initials: string | null;
17
25
  date: string | null;
18
26
  text: string;
@@ -25,6 +33,7 @@ export interface DocumentComment {
25
33
  export interface DocumentCommentReply {
26
34
  id: string;
27
35
  author: string | null;
36
+ authorId: string | null;
28
37
  initials: string | null;
29
38
  date: string | null;
30
39
  text: string;
@@ -6,3 +6,10 @@ export declare function stringAttr(value: unknown): string | null;
6
6
  export declare function repliesAttr(value: unknown): readonly CommentReplyData[];
7
7
  /** Comments in document order, including point comments that have no explicit range. */
8
8
  export declare function documentComments(state: EditorState): readonly DocumentComment[];
9
+ /**
10
+ * Whether the body of this comment, or of one of its replies, may be edited or deleted here: the
11
+ * protection lets comments be edited at all, and the body is one of one's own to edit
12
+ * (`schema/protection`). Replying and settling a thread are not governed by this; a comment command
13
+ * asked without `dispatch` answers for those.
14
+ */
15
+ export declare function canEditComment(state: EditorState, commentId: string, replyId?: string | null): boolean;
@@ -1,4 +1,6 @@
1
1
  // src/editor/commands/comments/reading.ts
2
+ import { commentOwned } from "../../../schema/protection.js";
3
+ import { protectionOf } from "../../../schema/protectionState.js";
2
4
  function stringAttr(value) {
3
5
  return typeof value === "string" ? value : null;
4
6
  }
@@ -34,6 +36,7 @@ function documentComments(state) {
34
36
  return {
35
37
  id,
36
38
  author: stringAttr(node.attrs.author),
39
+ authorId: stringAttr(node.attrs.authorId),
37
40
  initials: stringAttr(node.attrs.initials),
38
41
  date: stringAttr(node.attrs.date),
39
42
  text: stringAttr(node.attrs.text) ?? "",
@@ -44,6 +47,7 @@ function documentComments(state) {
44
47
  replies: repliesAttr(node.attrs.replies).map((reply) => ({
45
48
  id: reply.id,
46
49
  author: reply.author,
50
+ authorId: reply.authorId,
47
51
  initials: reply.initials,
48
52
  date: reply.date,
49
53
  text: reply.text
@@ -51,7 +55,17 @@ function documentComments(state) {
51
55
  };
52
56
  });
53
57
  }
58
+ function canEditComment(state, commentId, replyId = null) {
59
+ const rules = protectionOf(state);
60
+ if (rules.protection === "readOnly") return false;
61
+ const comment = documentComments(state).find(
62
+ (entry) => entry.id === commentId
63
+ );
64
+ const body = replyId === null ? comment : comment?.replies.find((reply) => reply.id === replyId);
65
+ return body !== void 0 && commentOwned(rules, body.authorId);
66
+ }
54
67
  export {
68
+ canEditComment,
55
69
  documentComments,
56
70
  repliesAttr,
57
71
  stringAttr