@portone/docx-editor 0.2.0 → 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 (101) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/CONTRIBUTING.md +4 -0
  3. package/dist/docx/commentOnlyChange.d.ts +3 -1
  4. package/dist/docx/commentOnlyChange.js +38 -21
  5. package/dist/docx/comments/constants.d.ts +3 -3
  6. package/dist/docx/comments/constants.js +4 -3
  7. package/dist/docx/comments/grammar.d.ts +80 -0
  8. package/dist/docx/comments/grammar.js +213 -0
  9. package/dist/docx/comments/people.js +12 -14
  10. package/dist/docx/comments/reading.d.ts +5 -3
  11. package/dist/docx/comments/reading.js +23 -20
  12. package/dist/docx/comments/verifying.d.ts +44 -0
  13. package/dist/docx/comments/verifying.js +206 -0
  14. package/dist/docx/comments/writing.js +73 -46
  15. package/dist/docx/exportDocx.js +2 -1
  16. package/dist/docx/formatting/direct.js +3 -6
  17. package/dist/docx/headersFooters.js +8 -10
  18. package/dist/docx/notes.js +3 -5
  19. package/dist/docx/paraProps.js +51 -46
  20. package/dist/docx/propsXml.d.ts +20 -2
  21. package/dist/docx/propsXml.js +72 -32
  22. package/dist/docx/relationships.js +8 -3
  23. package/dist/docx/runProps.js +34 -34
  24. package/dist/docx/sdt.js +16 -14
  25. package/dist/docx/serializeBlock.js +1 -2
  26. package/dist/docx/serializeParagraph.d.ts +2 -1
  27. package/dist/docx/serializeParagraph.js +25 -14
  28. package/dist/docx/serializeTable.js +38 -24
  29. package/dist/docx/storyProjection.d.ts +36 -0
  30. package/dist/docx/storyProjection.js +23 -0
  31. package/dist/docx/tableFormatting/editing.js +49 -73
  32. package/dist/docx/tableTemplate.js +24 -3
  33. package/dist/docx/theme.d.ts +0 -6
  34. package/dist/docx/theme.js +0 -8
  35. package/dist/editor/clipboard/images.js +17 -3
  36. package/dist/editor/clipboard/inlineFormatting.d.ts +7 -0
  37. package/dist/editor/clipboard/inlineFormatting.js +1 -0
  38. package/dist/editor/commands/breakCommands.js +4 -3
  39. package/dist/editor/commands/canRunCommand.d.ts +2 -2
  40. package/dist/editor/commands/canRunCommand.js +1 -1
  41. package/dist/editor/commands/comments/editing.js +3 -6
  42. package/dist/editor/commands/historyCommands.js +2 -1
  43. package/dist/editor/commands/lockCommands.js +10 -1
  44. package/dist/editor/commands/tabCommands.js +4 -3
  45. package/dist/editor/createEditor.js +3 -9
  46. package/dist/editor/externalClipboard.js +166 -4
  47. package/dist/editor/insertImage.js +4 -3
  48. package/dist/editor/plugins/documentProtection.d.ts +3 -3
  49. package/dist/editor/plugins/lockedContent.d.ts +4 -4
  50. package/dist/editor/plugins/lockedContent.js +1 -1
  51. package/dist/numbering/listTemplate.js +32 -10
  52. package/dist/ooxml/element.d.ts +52 -0
  53. package/dist/ooxml/element.js +49 -0
  54. package/dist/ooxml/fragment.d.ts +53 -0
  55. package/dist/ooxml/fragment.js +76 -0
  56. package/dist/ooxml/image.js +4 -3
  57. package/dist/ooxml/names.d.ts +27 -0
  58. package/dist/ooxml/names.js +29 -0
  59. package/dist/ooxml/precedence.d.ts +31 -0
  60. package/dist/ooxml/precedence.js +51 -0
  61. package/dist/ooxml/xml.d.ts +31 -0
  62. package/dist/ooxml/xml.js +27 -0
  63. package/dist/page/blockKinds.d.ts +42 -0
  64. package/dist/page/blockKinds.js +0 -0
  65. package/dist/page/measureBlocks.d.ts +14 -1
  66. package/dist/page/measureBlocks.js +31 -11
  67. package/dist/page/pageDecorations.d.ts +16 -0
  68. package/dist/page/pageDecorations.js +1 -0
  69. package/dist/page/pageLayout.d.ts +8 -43
  70. package/dist/page/pageLayout.js +28 -51
  71. package/dist/page/tableMeasurements.d.ts +10 -2
  72. package/dist/page/tableMeasurements.js +20 -16
  73. package/dist/page/usePageLayout.d.ts +15 -0
  74. package/dist/page/usePageLayout.js +30 -2
  75. package/dist/schema/attrRoles.d.ts +35 -0
  76. package/dist/schema/attrRoles.js +106 -0
  77. package/dist/schema/docxSchema.d.ts +6 -0
  78. package/dist/schema/docxSchema.js +212 -101
  79. package/dist/schema/editGuard.d.ts +99 -0
  80. package/dist/schema/editGuard.js +43 -0
  81. package/dist/schema/guards.d.ts +68 -0
  82. package/dist/schema/guards.js +82 -0
  83. package/dist/schema/locks.d.ts +8 -35
  84. package/dist/schema/locks.js +29 -64
  85. package/dist/schema/preservedGuards.d.ts +30 -0
  86. package/dist/schema/preservedGuards.js +50 -0
  87. package/dist/schema/protection.d.ts +10 -3
  88. package/dist/schema/protection.js +2 -1
  89. package/dist/schema/sourceEquality.d.ts +27 -0
  90. package/dist/schema/sourceEquality.js +65 -0
  91. package/dist/table/cellFormatting.js +2 -1
  92. package/dist/table/commands.d.ts +2 -2
  93. package/dist/table/commands.js +1 -1
  94. package/dist/table/index.d.ts +2 -2
  95. package/dist/table/merge.d.ts +2 -2
  96. package/dist/table/merge.js +1 -1
  97. package/package.json +4 -1
  98. package/dist/editor/plugins/bookmarkProtection.d.ts +0 -3
  99. package/dist/editor/plugins/bookmarkProtection.js +0 -28
  100. package/dist/editor/plugins/noteProtection.d.ts +0 -3
  101. package/dist/editor/plugins/noteProtection.js +0 -25
package/dist/docx/sdt.js CHANGED
@@ -1,4 +1,6 @@
1
1
  // src/docx/sdt.ts
2
+ import { elementXml, emptyTagXml, openTagXml } from "../ooxml/element.js";
3
+ import { wName } from "../ooxml/names.js";
2
4
  import { wAttr } from "../ooxml/units.js";
3
5
  import { attrString, elementChildren, serializeXml } from "../ooxml/xml.js";
4
6
  import {
@@ -31,7 +33,7 @@ function readSdtWrapper(el) {
31
33
  const attrs = attrString(el);
32
34
  const val = lockValue(sdtPr);
33
35
  return {
34
- prefix: (attrs ? `<w:sdt ${attrs}>` : "<w:sdt>") + head.map(serializeXml).join(""),
36
+ prefix: openTagXml(wName("sdt"), attrs) + head.map(serializeXml).join(""),
35
37
  content,
36
38
  contentsLocked: val !== null && CONTENTS_LOCKED.includes(val),
37
39
  deletionLocked: val !== null && DELETION_LOCKED.includes(val)
@@ -53,8 +55,8 @@ function newControlId() {
53
55
  return Math.floor(Math.random() * 2147483647);
54
56
  }
55
57
  function renderPrefix(props) {
56
- const open = props.attrs ? `<${props.tag} ${props.attrs}>` : `<${props.tag}>`;
57
- return open + props.children.map((child) => child.xml).join("");
58
+ const inner = props.children.map((child) => (child.before ?? "") + child.xml).join("");
59
+ return openTagXml(props.tag, props.attrs) + inner + (props.tail ?? "");
58
60
  }
59
61
  var NAMES_NOTHING = ["id", "lock"];
60
62
  function namesNothing(prefix) {
@@ -66,31 +68,31 @@ function namesNothing(prefix) {
66
68
  return props.children.every((child) => NAMES_NOTHING.includes(child.name));
67
69
  }
68
70
  function emptyProps(props) {
69
- return props.attrs ? `<${props.tag} ${props.attrs}/>` : `<${props.tag}/>`;
71
+ return emptyTagXml(props.tag, props.attrs);
70
72
  }
71
73
  function editSdtPrefix(prefix, edits) {
72
74
  const sdt = parseProps(`${prefix}</w:sdt>`);
73
75
  const sdtPr = sdt && propsChild(sdt.children, "sdtPr");
74
76
  const props = sdtPr ? parseProps(sdtPr.xml) : null;
75
77
  if (!sdt || !props) return null;
76
- const children = edits.reduce(
77
- (kept, [name, xml]) => setPropsChild(kept, name, xml, SDT_PR_ORDER),
78
- props.children
78
+ const rendered = renderProps(
79
+ edits.reduce(
80
+ (kept, [name, xml]) => setPropsChild(kept, name, xml, SDT_PR_ORDER),
81
+ props
82
+ )
79
83
  );
80
- const rendered = renderProps({ ...props, children });
81
- return renderPrefix({
82
- ...sdt,
83
- children: setPropsChild(
84
- sdt.children,
84
+ return renderPrefix(
85
+ setPropsChild(
86
+ sdt,
85
87
  "sdtPr",
86
88
  rendered === "" ? emptyProps(props) : rendered,
87
89
  SDT_ORDER
88
90
  )
89
- });
91
+ );
90
92
  }
91
93
  function copiedControlPrefix(prefix, id) {
92
94
  const copied = editSdtPrefix(prefix, [
93
- ["id", `<w:id w:val="${id}"/>`],
95
+ ["id", elementXml(wName("id"), [[wName("val"), `${id}`]])],
94
96
  ["dataBinding", null]
95
97
  ]);
96
98
  return copied ?? prefix;
@@ -18,8 +18,7 @@ function serializeBlock(node, session, refs = NO_EXPORT_REFS) {
18
18
  if (node.type.name === "paragraph") return serializeParagraph(node, refs);
19
19
  if (node.type.name === "table") return serializeTable(node, refs);
20
20
  if (node.type.name === "rawBlock") return preservedXml(node);
21
- if (node.type.name === "bookmarkBlock") return serializeRaw(node, session);
22
- if (node.type.name === "docxRaw") return serializeRaw(node, session);
21
+ if (node.type.isInGroup("preserved")) return serializeRaw(node, session);
23
22
  throw new DocxExportError(
24
23
  "unsupported-content",
25
24
  `block we cannot serialize: ${node.type.name}`
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import type { Node as PMNode } from "prosemirror-model";
12
12
  import { type ExportRefs } from "./exportRefs";
13
- export declare function openTag(name: string, attrs: unknown): string;
13
+ /** The attribute text a node attr holds, which is either what the original wrote or nothing */
14
+ export declare function rawAttrsOf(value: unknown): string | null;
14
15
  export declare function preservedXml(node: PMNode): string;
15
16
  export declare function serializeParagraph(node: PMNode, refs?: ExportRefs): string;
@@ -1,4 +1,5 @@
1
1
  // src/docx/serializeParagraph.ts
2
+ import { elementXml, emptyTagXml, openTagXml } from "../ooxml/element.js";
2
3
  import { DocxExportError } from "../ooxml/errors.js";
3
4
  import {
4
5
  imageDrawingXml,
@@ -6,6 +7,7 @@ import {
6
7
  toImageSrc,
7
8
  withExtent
8
9
  } from "../ooxml/image.js";
10
+ import { wName } from "../ooxml/names.js";
9
11
  import { escapeXml } from "../ooxml/xml.js";
10
12
  import { NO_EXPORT_REFS } from "./exportRefs.js";
11
13
  import { relIdIn, withRelId } from "./hyperlink.js";
@@ -16,11 +18,8 @@ function sameMark(a, b) {
16
18
  function markOf(node, name) {
17
19
  return node.marks.find((mark) => mark.type.name === name) ?? null;
18
20
  }
19
- function openTag(name, attrs) {
20
- return typeof attrs === "string" ? `<${name} ${attrs}>` : `<${name}>`;
21
- }
22
- function emptyTag(name, attrs) {
23
- return typeof attrs === "string" ? `<${name} ${attrs}/>` : `<${name}/>`;
21
+ function rawAttrsOf(value) {
22
+ return typeof value === "string" ? value : null;
24
23
  }
25
24
  function renderImage(node, images) {
26
25
  const extent = toImageExtent(node.attrs.extent);
@@ -51,19 +50,28 @@ function renderInline(node, images) {
51
50
  if (node.isText) {
52
51
  const tab = markOf(node, "tab");
53
52
  return (node.text ?? "").split(" ").flatMap((text, index, pieces) => {
54
- const rendered = text ? [`<w:t xml:space="preserve">${escapeXml(text)}</w:t>`] : [];
55
- return index < pieces.length - 1 ? [...rendered, emptyTag("w:tab", tab?.attrs.tabAttrs)] : rendered;
53
+ const rendered = text ? [
54
+ elementXml(
55
+ wName("t"),
56
+ [["xml:space", "preserve"]],
57
+ [escapeXml(text)]
58
+ )
59
+ ] : [];
60
+ return index < pieces.length - 1 ? [
61
+ ...rendered,
62
+ emptyTagXml(wName("tab"), rawAttrsOf(tab?.attrs.tabAttrs))
63
+ ] : rendered;
56
64
  }).join("");
57
65
  }
58
66
  if (node.type.name === "hardBreak")
59
- return emptyTag("w:br", node.attrs.brAttrs);
67
+ return emptyTagXml(wName("br"), rawAttrsOf(node.attrs.brAttrs));
60
68
  if (node.type.name === "image") return renderImage(node, images);
61
69
  if (node.type.name === "commentReference") {
62
70
  const original = node.attrs.referenceXml;
63
71
  if (typeof original === "string") return original;
64
72
  const id = node.attrs.id;
65
73
  if (typeof id === "string") {
66
- return `<w:commentReference w:id="${escapeXml(id)}"/>`;
74
+ return elementXml(wName("commentReference"), [[wName("id"), id]]);
67
75
  }
68
76
  throw new DocxExportError(
69
77
  "lost-original",
@@ -76,7 +84,7 @@ function renderInline(node, images) {
76
84
  const id = node.attrs.id;
77
85
  const name = node.attrs.kind === "endnote" ? "endnoteReference" : "footnoteReference";
78
86
  if (typeof id === "string") {
79
- return `<w:${name} w:id="${escapeXml(id)}"/>`;
87
+ return elementXml(wName(name), [[wName("id"), id]]);
80
88
  }
81
89
  throw new DocxExportError(
82
90
  "lost-original",
@@ -117,7 +125,10 @@ function addInline(parts, child, images) {
117
125
  );
118
126
  }
119
127
  const name = child.type.name === "commentStart" ? "commentRangeStart" : "commentRangeEnd";
120
- parts.push({ kind: "raw", xml: `<w:${name} w:id="${escapeXml(id)}"/>` });
128
+ parts.push({
129
+ kind: "raw",
130
+ xml: elementXml(wName(name), [[wName("id"), id]])
131
+ });
121
132
  return;
122
133
  }
123
134
  const piece = renderInline(child, images);
@@ -150,7 +161,7 @@ function splitParagraphGroups(paragraph, refs) {
150
161
  }
151
162
  function renderParagraphPart(part) {
152
163
  if (part.kind === "raw") return part.xml;
153
- const open = openTag("w:r", part.mark?.attrs.rAttrs);
164
+ const open = openTagXml(wName("r"), rawAttrsOf(part.mark?.attrs.rAttrs));
154
165
  const rPr = part.mark?.attrs.rPr;
155
166
  return open + (typeof rPr === "string" ? rPr : "") + part.pieces.join("") + "</w:r>";
156
167
  }
@@ -196,13 +207,13 @@ function renderParagraphGroup(group, refs) {
196
207
  return `${prefix}<w:sdtContent>${body}</w:sdtContent></w:sdt>`;
197
208
  }
198
209
  function serializeParagraph(node, refs = NO_EXPORT_REFS) {
199
- const open = openTag("w:p", node.attrs.pAttrs);
210
+ const open = openTagXml(wName("p"), rawAttrsOf(node.attrs.pAttrs));
200
211
  const pPr = node.attrs.pPr;
201
212
  const body = splitParagraphGroups(node, refs).map((group) => renderParagraphGroup(group, refs)).join("");
202
213
  return open + (typeof pPr === "string" ? pPr : "") + body + "</w:p>";
203
214
  }
204
215
  export {
205
- openTag,
206
216
  preservedXml,
217
+ rawAttrsOf,
207
218
  serializeParagraph
208
219
  };
@@ -5,18 +5,22 @@ import {
5
5
  toTableWidth,
6
6
  widthNumber
7
7
  } from "../model/format.js";
8
+ import { elementXml, openTagXml } from "../ooxml/element.js";
8
9
  import { DocxExportError } from "../ooxml/errors.js";
10
+ import { wName } from "../ooxml/names.js";
9
11
  import { NO_EXPORT_REFS } from "./exportRefs.js";
10
12
  import {
13
+ innerXml,
11
14
  parseProps,
15
+ propsChild,
12
16
  renderProps,
13
17
  setPropsChild,
14
18
  TBL_PR_ORDER,
15
19
  TC_PR_ORDER
16
20
  } from "./propsXml.js";
17
21
  import {
18
- openTag,
19
22
  preservedXml,
23
+ rawAttrsOf,
20
24
  serializeParagraph
21
25
  } from "./serializeParagraph.js";
22
26
  function propsOf(xml, tag) {
@@ -27,15 +31,24 @@ function propsOf(xml, tag) {
27
31
  }
28
32
  return parsed;
29
33
  }
30
- function widthXml(name, width) {
31
- return `<w:${name} w:w="${widthNumber(width) ?? 0}" w:type="${width.type}"/>`;
34
+ function modelled(name, attrs, replacing) {
35
+ const inner = replacing === void 0 ? "" : innerXml(replacing);
36
+ return elementXml(wName(name), attrs, inner === "" ? [] : [inner]);
37
+ }
38
+ function widthXml(name, width, replacing) {
39
+ return modelled(
40
+ name,
41
+ [
42
+ [wName("w"), `${widthNumber(width) ?? 0}`],
43
+ [wName("type"), width.type]
44
+ ],
45
+ replacing
46
+ );
32
47
  }
33
48
  function withWidth(props, name, width, order) {
34
49
  if (!width) return props;
35
- return {
36
- ...props,
37
- children: setPropsChild(props.children, name, widthXml(name, width), order)
38
- };
50
+ const replacing = propsChild(props.children, name)?.xml;
51
+ return setPropsChild(props, name, widthXml(name, width, replacing), order);
39
52
  }
40
53
  function tablePropsXml(table) {
41
54
  const props = propsOf(table.attrs.tblPr, "w:tblPr");
@@ -45,26 +58,27 @@ function tablePropsXml(table) {
45
58
  function tableGridXml(table) {
46
59
  const gridCols = toGridCols(table.attrs.gridCols);
47
60
  if (gridCols.length === 0) return "";
48
- const cols = gridCols.map((w) => `<w:gridCol w:w="${w}"/>`).join("");
49
- return `<w:tblGrid>${cols}</w:tblGrid>`;
61
+ const cols = gridCols.map((w) => elementXml(wName("gridCol"), [[wName("w"), `${w}`]])).join("");
62
+ return elementXml(wName("tblGrid"), [], [cols]);
50
63
  }
51
64
  function cellPropsXml(cell, role) {
52
65
  const props = propsOf(cell.attrs.tcPr, "w:tcPr");
53
66
  const colspan = spanCount(cell.attrs.colspan);
54
67
  const rowspan = spanCount(cell.attrs.rowspan);
55
- const gridSpan = colspan > 1 ? `<w:gridSpan w:val="${colspan}"/>` : null;
56
- const vMerge = role === "continue" ? "<w:vMerge/>" : rowspan > 1 ? '<w:vMerge w:val="restart"/>' : null;
57
- let children = setPropsChild(
58
- props.children,
68
+ const gridSpan = colspan > 1 ? modelled(
59
69
  "gridSpan",
60
- gridSpan,
61
- TC_PR_ORDER
62
- );
63
- children = setPropsChild(children, "vMerge", vMerge, TC_PR_ORDER);
70
+ [[wName("val"), `${colspan}`]],
71
+ propsChild(props.children, "gridSpan")?.xml
72
+ ) : null;
73
+ const vMerge = role === "continue" ? elementXml(wName("vMerge"), []) : rowspan > 1 ? modelled(
74
+ "vMerge",
75
+ [[wName("val"), "restart"]],
76
+ propsChild(props.children, "vMerge")?.xml
77
+ ) : null;
78
+ const spanned = setPropsChild(props, "gridSpan", gridSpan, TC_PR_ORDER);
79
+ const merged = setPropsChild(spanned, "vMerge", vMerge, TC_PR_ORDER);
64
80
  const width = toTableWidth(cell.attrs.tcW);
65
- return renderProps(
66
- withWidth({ ...props, children }, "tcW", width, TC_PR_ORDER)
67
- );
81
+ return renderProps(withWidth(merged, "tcW", width, TC_PR_ORDER));
68
82
  }
69
83
  function cellBlockXml(block, refs) {
70
84
  if (block.type.name === "paragraph") return serializeParagraph(block, refs);
@@ -81,8 +95,8 @@ function wrapInSdt(xml, cell, role) {
81
95
  return `${prefix}<w:sdtContent>${xml}</w:sdtContent></w:sdt>`;
82
96
  }
83
97
  function cellXml(cell, role, refs) {
84
- const body = role === "continue" ? "<w:p/>" : cell.children.map((block) => cellBlockXml(block, refs)).join("");
85
- const xml = openTag("w:tc", cell.attrs.tcAttrs) + cellPropsXml(cell, role) + body + "</w:tc>";
98
+ const body = role === "continue" ? elementXml(wName("p"), []) : cell.children.map((block) => cellBlockXml(block, refs)).join("");
99
+ const xml = openTagXml(wName("tc"), rawAttrsOf(cell.attrs.tcAttrs)) + cellPropsXml(cell, role) + body + "</w:tc>";
86
100
  return wrapInSdt(xml, cell, role);
87
101
  }
88
102
  function placeRow(row, covering) {
@@ -116,7 +130,7 @@ function rowXml(row, covering, refs) {
116
130
  const cells = placeRow(row, covering).map((placed) => cellXml(placed.cell, placed.role, refs)).join("");
117
131
  const tblPrEx = row.attrs.tblPrEx;
118
132
  const trPr = row.attrs.trPr;
119
- return openTag("w:tr", row.attrs.trAttrs) + (typeof tblPrEx === "string" ? tblPrEx : "") + (typeof trPr === "string" ? trPr : "") + cells + "</w:tr>";
133
+ return openTagXml(wName("tr"), rawAttrsOf(row.attrs.trAttrs)) + (typeof tblPrEx === "string" ? tblPrEx : "") + (typeof trPr === "string" ? trPr : "") + cells + "</w:tr>";
120
134
  }
121
135
  function serializeTable(table, refs = NO_EXPORT_REFS) {
122
136
  const covering = [];
@@ -127,7 +141,7 @@ function serializeTable(table, refs = NO_EXPORT_REFS) {
127
141
  "a vertical merge in the table reaches past the last row"
128
142
  );
129
143
  }
130
- return openTag("w:tbl", table.attrs.tblAttrs) + tablePropsXml(table) + tableGridXml(table) + rows + "</w:tbl>";
144
+ return openTagXml(wName("tbl"), rawAttrsOf(table.attrs.tblAttrs)) + tablePropsXml(table) + tableGridXml(table) + rows + "</w:tbl>";
131
145
  }
132
146
  export {
133
147
  serializeTable
@@ -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);