@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
@@ -1,6 +1,7 @@
1
1
  // src/docx/headersFooters.ts
2
2
  import { ALIGN_BY_JC } from "../ooxml/units.js";
3
3
  import {
4
+ attributeByLocalName,
4
5
  decodeUtf8,
5
6
  elementChildren,
6
7
  parseXml,
@@ -20,12 +21,9 @@ var NO_HEADERS_FOOTERS = {
20
21
  evenAndOdd: false,
21
22
  pageNumberStart: 1
22
23
  };
23
- function attribute(el, localName) {
24
- return Array.from(el.attributes).find((entry) => entry.localName === localName)?.value ?? null;
25
- }
26
24
  function isOn(el) {
27
25
  if (!el) return false;
28
- const value = attribute(el, "val")?.toLowerCase();
26
+ const value = attributeByLocalName(el, "val")?.toLowerCase();
29
27
  return value !== "0" && value !== "false" && value !== "off";
30
28
  }
31
29
  function firstSectPrIn(root) {
@@ -58,13 +56,13 @@ function paragraphSegments(paragraph) {
58
56
  return;
59
57
  }
60
58
  if (el.namespaceURI === W_NS && el.localName === "fldSimple") {
61
- const field = pageField(attribute(el, "instr") ?? "");
59
+ const field = pageField(attributeByLocalName(el, "instr") ?? "");
62
60
  if (field) dynamic(field);
63
61
  else for (const child of elementChildren(el)) visit(child);
64
62
  return;
65
63
  }
66
64
  if (el.namespaceURI === W_NS && el.localName === "fldChar") {
67
- const kind = attribute(el, "fldCharType");
65
+ const kind = attributeByLocalName(el, "fldCharType");
68
66
  if (kind === "begin") {
69
67
  fields.push({ instruction: "", separated: false, field: null });
70
68
  } else if (kind === "separate") {
@@ -129,7 +127,7 @@ function readContent(bytes) {
129
127
  ) : void 0;
130
128
  return {
131
129
  segments,
132
- align: jc ? ALIGN_BY_JC[attribute(jc, "val") ?? ""] ?? null : null
130
+ align: jc ? ALIGN_BY_JC[attributeByLocalName(jc, "val") ?? ""] ?? null : null
133
131
  };
134
132
  }
135
133
  function settingsEvenAndOdd(parts, mainPartPath) {
@@ -153,9 +151,9 @@ function readVariants(parts, mainPartPath, sectPr, kind) {
153
151
  const variants = { ...EMPTY_VARIANTS };
154
152
  for (const reference of elementChildren(sectPr)) {
155
153
  if (reference.localName !== `${kind}Reference`) continue;
156
- const type = attribute(reference, "type") ?? "default";
154
+ const type = attributeByLocalName(reference, "type") ?? "default";
157
155
  if (type !== "default" && type !== "first" && type !== "even") continue;
158
- const id = reference.getAttributeNS(R_NS, "id") ?? attribute(reference, "id");
156
+ const id = reference.getAttributeNS(R_NS, "id") ?? attributeByLocalName(reference, "id");
159
157
  if (!id) continue;
160
158
  const relationship = relationships.get(id);
161
159
  if (!relationship || relationship.external || relationship.type !== `${R_NS}/${kind}`) {
@@ -170,7 +168,7 @@ function pageNumberStart(sectPr) {
170
168
  const pgNumType = elementChildren(sectPr).find(
171
169
  (child) => child.localName === "pgNumType"
172
170
  );
173
- const declared = pgNumType ? attribute(pgNumType, "start") : null;
171
+ const declared = pgNumType ? attributeByLocalName(pgNumType, "start") : null;
174
172
  if (declared === null) return 1;
175
173
  const start = Number(declared);
176
174
  return Number.isSafeInteger(start) && start >= 0 ? start : 1;
@@ -1,5 +1,6 @@
1
1
  // src/docx/notes.ts
2
2
  import {
3
+ attributeByLocalName,
3
4
  decodeUtf8,
4
5
  elementChildren,
5
6
  parseXml,
@@ -16,9 +17,6 @@ var NO_NOTES = {
16
17
  footnotes: EMPTY_PART,
17
18
  endnotes: EMPTY_PART
18
19
  };
19
- function attribute(el, localName) {
20
- return Array.from(el.attributes).find((entry) => entry.localName === localName)?.value ?? null;
21
- }
22
20
  function inlineText(node) {
23
21
  if (node.localName === "t") return node.textContent ?? "";
24
22
  if (node.localName === "tab") return " ";
@@ -42,9 +40,9 @@ function notePart(parts, mainPartPath, kind) {
42
40
  const root = parseXml(decodeUtf8(bytes).text).documentElement;
43
41
  const elements = elementChildren(root).filter((el) => el.localName === kind);
44
42
  const ordered = elements.flatMap((el) => {
45
- const id = attribute(el, "id");
43
+ const id = attributeByLocalName(el, "id");
46
44
  if (id === null) return [];
47
- const type = attribute(el, "type");
45
+ const type = attributeByLocalName(el, "type");
48
46
  const regular = type === null || type === "normal";
49
47
  const label = regular ? "?" : "";
50
48
  return [{ kind, id, label, text: noteText(el), type }];
@@ -1,5 +1,12 @@
1
1
  // src/docx/paraProps.ts
2
- import { childByLocalName, escapeXml, localPart } from "../ooxml/xml.js";
2
+ import {
3
+ attrPairs,
4
+ elementXml,
5
+ withoutAttrs
6
+ } from "../ooxml/element.js";
7
+ import { wName } from "../ooxml/names.js";
8
+ import { setAttr } from "../ooxml/precedence.js";
9
+ import { childByLocalName, localPart } from "../ooxml/xml.js";
3
10
  import {
4
11
  layerParagraphFormat,
5
12
  layerRunFormat,
@@ -33,65 +40,66 @@ function readParagraphStyleRun(pPr, styles = NO_STYLES, defaultStyleId = null) {
33
40
  }
34
41
  function numPrXml(ref) {
35
42
  if (!ref) return null;
36
- return `<w:numPr><w:ilvl w:val="${ref.ilvl}"/><w:numId w:val="${ref.numId}"/></w:numPr>`;
37
- }
38
- function keptIndAttrs(ind, dropped) {
39
- if (!ind) return [];
40
- return Array.from(ind.attributes).filter((attr) => !dropped.includes(localPart(attr.name))).map((attr) => [attr.name, attr.value]);
43
+ return elementXml(
44
+ wName("numPr"),
45
+ [],
46
+ [
47
+ elementXml(wName("ilvl"), [[wName("val"), `${ref.ilvl}`]]),
48
+ elementXml(wName("numId"), [[wName("val"), `${ref.numId}`]])
49
+ ]
50
+ );
41
51
  }
42
52
  function levelIndAttrs(indent) {
43
53
  const attrs = [];
44
54
  if (indent.startTwips !== null)
45
- attrs.push(["w:left", `${indent.startTwips}`]);
55
+ attrs.push([wName("left"), `${indent.startTwips}`]);
46
56
  if (indent.hangingTwips !== null) {
47
- attrs.push(["w:hanging", `${indent.hangingTwips}`]);
57
+ attrs.push([wName("hanging"), `${indent.hangingTwips}`]);
48
58
  } else if (indent.firstLineTwips !== null) {
49
- attrs.push(["w:firstLine", `${indent.firstLineTwips}`]);
59
+ attrs.push([wName("firstLine"), `${indent.firstLineTwips}`]);
50
60
  }
51
61
  return attrs;
52
62
  }
53
- function attrsXml(tag, attrs) {
54
- if (attrs.length === 0) return null;
55
- const text = attrs.map(([name, value]) => `${name}="${escapeXml(value)}"`).join(" ");
56
- return `<${tag} ${text}/>`;
57
- }
58
63
  function indXml(attrs) {
59
- return attrsXml("w:ind", attrs);
64
+ return attrs.length === 0 ? null : elementXml(wName("ind"), attrs);
60
65
  }
61
66
  function nextIndXml(ind, change) {
62
67
  if (change.kind === "keep") return void 0;
63
68
  if (change.kind === "clearHanging") {
64
- return indXml(keptIndAttrs(ind, HANGING_IND_ATTRS));
69
+ return indXml(withoutAttrs(attrPairs(ind), HANGING_IND_ATTRS));
65
70
  }
66
71
  const dropped = [...LEFT_IND_ATTRS, ...FIRST_LINE_IND_ATTRS];
67
72
  return indXml([
68
73
  ...levelIndAttrs(change.indent),
69
- ...keptIndAttrs(ind, dropped)
74
+ ...withoutAttrs(attrPairs(ind), dropped)
70
75
  ]);
71
76
  }
72
77
  function leftIndAttrs(ind, leftTwips) {
73
- const original = ind ? Array.from(ind.attributes) : [];
78
+ const original = attrPairs(ind);
74
79
  const slot = original.find(
75
- (attr) => LEFT_TWIPS_IND_ATTRS.includes(localPart(attr.name))
76
- )?.name;
77
- const moved = original.flatMap((attr) => {
78
- if (!LEFT_IND_ATTRS.includes(localPart(attr.name))) {
79
- return [[attr.name, attr.value]];
80
- }
81
- return attr.name === slot && leftTwips > 0 ? [[attr.name, `${leftTwips}`]] : [];
82
- });
83
- return slot === void 0 && leftTwips > 0 ? [["w:left", `${leftTwips}`], ...moved] : moved;
80
+ ([name]) => LEFT_TWIPS_IND_ATTRS.includes(localPart(name))
81
+ )?.[0];
82
+ const written = leftTwips > 0 ? `${leftTwips}` : null;
83
+ const kept = withoutAttrs(
84
+ original,
85
+ LEFT_IND_ATTRS.filter(
86
+ (name) => slot === void 0 || name !== localPart(slot)
87
+ )
88
+ );
89
+ if (slot !== void 0) return setAttr(kept, "ind", localPart(slot), written);
90
+ return written === null ? kept : [[wName("left"), written], ...kept];
84
91
  }
85
92
  var EMPTY_P_PR = { tag: "w:pPr", attrs: null, children: [] };
86
93
  function editParagraphProps(pPr, styles, defaultStyleId, plan) {
87
94
  const props = pPr === null ? EMPTY_P_PR : parseProps(pPr);
88
95
  const element = pPr === null ? null : parsePropsXml(pPr);
89
96
  if (!props || pPr !== null && !element) return null;
90
- const children = plan(element).reduce(
91
- (kept, [name, xml]) => setPropsChild(kept, name, xml, P_PR_ORDER),
92
- props.children
97
+ const rendered = renderProps(
98
+ plan(element).reduce(
99
+ (kept, [name, xml]) => setPropsChild(kept, name, xml, P_PR_ORDER),
100
+ props
101
+ )
93
102
  );
94
- const rendered = renderProps({ ...props, children });
95
103
  const next = rendered === "" ? null : rendered;
96
104
  return {
97
105
  pPr: next,
@@ -116,19 +124,16 @@ function withLeftIndent(pPr, leftTwips, styles = NO_STYLES, defaultStyleId = nul
116
124
  });
117
125
  }
118
126
  function spacingAttrs(spacing, edits) {
119
- const original = spacing ? Array.from(spacing.attributes) : [];
120
- const editOf = (name) => edits.find(([edited]) => edited === localPart(name));
121
- const changed = original.map((attr) => [
122
- attr.name,
123
- editOf(attr.name)?.[1] ?? attr.value
124
- ]);
125
- const added = edits.filter(
126
- ([name]) => !original.some((attr) => localPart(attr.name) === name)
127
+ const changed = attrPairs(spacing).map(
128
+ ([name, value]) => [
129
+ name,
130
+ edits.find(([edited]) => edited === localPart(name))?.[1] ?? value
131
+ ]
132
+ );
133
+ return edits.reduce(
134
+ (attrs, [name, value]) => setAttr(attrs, "spacing", name, value),
135
+ changed
127
136
  );
128
- return [
129
- ...changed,
130
- ...added.map(([name, value]) => [`w:${name}`, value])
131
- ];
132
137
  }
133
138
  function withLineSpacing(pPr, spacing, styles = NO_STYLES, defaultStyleId = null) {
134
139
  const line = spacing.rule === "auto" ? Math.round(spacing.lines * 240) : Math.round(spacing.pt * 20);
@@ -138,11 +143,11 @@ function withLineSpacing(pPr, spacing, styles = NO_STYLES, defaultStyleId = null
138
143
  ["line", `${line}`],
139
144
  ["lineRule", spacing.rule]
140
145
  ]);
141
- return [["spacing", attrsXml("w:spacing", attrs)]];
146
+ return [["spacing", elementXml(wName("spacing"), attrs)]];
142
147
  });
143
148
  }
144
149
  function withParagraphStyle(pPr, styleId, styles = NO_STYLES, defaultStyleId = null) {
145
- const pStyle = styleId === null ? null : `<w:pStyle w:val="${escapeXml(styleId)}"/>`;
150
+ const pStyle = styleId === null ? null : elementXml(wName("pStyle"), [[wName("val"), styleId]]);
146
151
  return editParagraphProps(pPr, styles, defaultStyleId, () => [
147
152
  ["pStyle", pStyle]
148
153
  ]);
@@ -154,7 +159,7 @@ var JC_BY_ALIGN = {
154
159
  justify: "both"
155
160
  };
156
161
  function withParagraphAlign(pPr, align, styles = NO_STYLES, defaultStyleId = null) {
157
- const jc = `<w:jc w:val="${JC_BY_ALIGN[align]}"/>`;
162
+ const jc = elementXml(wName("jc"), [[wName("val"), JC_BY_ALIGN[align]]]);
158
163
  return editParagraphProps(pPr, styles, defaultStyleId, () => [["jc", jc]]);
159
164
  }
160
165
  export {
@@ -10,12 +10,20 @@ export interface PropsChild {
10
10
  name: string;
11
11
  /** This child's original XML fragment exactly as it was */
12
12
  xml: string;
13
+ /**
14
+ * Whatever stood between the child before this one and this one: line breaks a producer laid
15
+ * out, comments, anything that is not an element. Carried so that rewriting one child does not
16
+ * quietly drop the rest of what the fragment said. Absent rather than empty when nothing did.
17
+ */
18
+ before?: string;
13
19
  }
14
20
  export interface Props {
15
21
  /** The opening tag name exactly as written (e.g. `w:tcPr`) */
16
22
  tag: string;
17
23
  attrs: string | null;
18
24
  children: PropsChild[];
25
+ /** The same, for what stood between the last child and the closing tag */
26
+ tail?: string;
19
27
  }
20
28
  /** The order the children are laid out in under `w:rPr` (CT_RPr) */
21
29
  export declare const RUN_PR_ORDER: readonly string[];
@@ -27,18 +35,28 @@ export declare const TC_PR_ORDER: readonly string[];
27
35
  export declare const TR_PR_ORDER: readonly string[];
28
36
  /** The order the children are laid out in under `w:tblPr` (CT_TblPr) */
29
37
  export declare const TBL_PR_ORDER: readonly string[];
38
+ /**
39
+ * What stood inside a single element, and "" for one that stood empty or cannot be made out.
40
+ *
41
+ * The opening tag is read rather than scanned for, so a `>` inside an attribute value does not
42
+ * pass for the end of it.
43
+ */
44
+ export declare function innerXml(xml: string): string;
30
45
  /**
31
46
  * Splits a formatting fragment into its opening tag and its list of children.
32
47
  * null if its shape cannot be made out (in which case leaving the original untouched is the safe move).
33
48
  */
34
49
  export declare function parseProps(xml: string): Props | null;
35
- /** With no children at all, the formatting fragment itself is not written */
50
+ /**
51
+ * With nothing to say, the formatting fragment itself is not written. A fragment holding only
52
+ * whitespace says nothing; one holding a comment does, so it is written back.
53
+ */
36
54
  export declare function renderProps(props: Props): string;
37
55
  /**
38
56
  * Replaces a single child with new XML.
39
57
  * A null `xml` removes that child. A child that was not there goes into the spot the order calls for.
40
58
  */
41
- export declare function setPropsChild(children: PropsChild[], name: string, xml: string | null, order: readonly string[]): PropsChild[];
59
+ export declare function setPropsChild(props: Props, name: string, xml: string | null, order: readonly string[]): Props;
42
60
  export declare function propsChild(children: PropsChild[], name: string): PropsChild | undefined;
43
61
  /**
44
62
  * Reads a single formatting fragment into an element.
@@ -1,5 +1,10 @@
1
1
  // src/docx/propsXml.ts
2
- import { elementChildren, localPart, parseXml, W_NS } from "../ooxml/xml.js";
2
+ import {
3
+ elementChildren,
4
+ localPart,
5
+ namespaceDecls,
6
+ parseXml
7
+ } from "../ooxml/xml.js";
3
8
  var RUN_PR_ORDER = [
4
9
  "rStyle",
5
10
  "rFonts",
@@ -179,6 +184,12 @@ function attrsOf(source, tag) {
179
184
  const attrs = source.slice(tag.nameEnd, tag.end - closeLength).trim();
180
185
  return attrs.length > 0 ? attrs : null;
181
186
  }
187
+ function innerXml(xml) {
188
+ const open = readTag(xml, 0);
189
+ if (!open || xml[0] !== "<" || open.kind !== "open") return "";
190
+ const close = xml.lastIndexOf("</");
191
+ return close > open.end ? xml.slice(open.end, close) : "";
192
+ }
182
193
  function parseProps(xml) {
183
194
  const open = readTag(xml, 0);
184
195
  if (!open || xml[0] !== "<") return null;
@@ -192,6 +203,11 @@ function parseProps(xml) {
192
203
  let childStart = -1;
193
204
  let childName = "";
194
205
  let i = open.end;
206
+ let gapStart = open.end;
207
+ const gapBefore = (start) => {
208
+ const gap = xml.slice(gapStart, start);
209
+ return gap.length > 0 ? { before: gap } : {};
210
+ };
195
211
  while (i < xml.length) {
196
212
  const lt = xml.indexOf("<", i);
197
213
  if (lt === -1) return null;
@@ -204,15 +220,18 @@ function parseProps(xml) {
204
220
  if (depth === 0) {
205
221
  if (tag.kind === "close") {
206
222
  if (tag.name !== open.name || tag.end !== xml.length) return null;
207
- return { tag: open.name, attrs, children };
223
+ const tail = xml.slice(gapStart, lt);
224
+ return tail.length > 0 ? { tag: open.name, attrs, children, tail } : { tag: open.name, attrs, children };
208
225
  }
209
226
  childStart = lt;
210
227
  childName = tag.name;
211
228
  if (tag.kind === "empty") {
212
229
  children.push({
213
230
  name: localPart(childName),
214
- xml: xml.slice(childStart, tag.end)
231
+ xml: xml.slice(childStart, tag.end),
232
+ ...gapBefore(childStart)
215
233
  });
234
+ gapStart = tag.end;
216
235
  } else {
217
236
  depth = 1;
218
237
  }
@@ -223,8 +242,10 @@ function parseProps(xml) {
223
242
  if (depth === 0) {
224
243
  children.push({
225
244
  name: localPart(childName),
226
- xml: xml.slice(childStart, tag.end)
245
+ xml: xml.slice(childStart, tag.end),
246
+ ...gapBefore(childStart)
227
247
  });
248
+ gapStart = tag.end;
228
249
  }
229
250
  }
230
251
  i = tag.end;
@@ -232,9 +253,11 @@ function parseProps(xml) {
232
253
  return null;
233
254
  }
234
255
  function renderProps(props) {
235
- if (props.children.length === 0) return "";
256
+ const tail = props.tail ?? "";
257
+ if (props.children.length === 0 && tail.trim().length === 0) return "";
236
258
  const open = props.attrs ? `<${props.tag} ${props.attrs}>` : `<${props.tag}>`;
237
- return open + props.children.map((child) => child.xml).join("") + `</${props.tag}>`;
259
+ const inner = props.children.map((child) => (child.before ?? "") + child.xml).join("");
260
+ return open + inner + tail + `</${props.tag}>`;
238
261
  }
239
262
  function insertIndex(children, name, order) {
240
263
  const target = order.indexOf(name);
@@ -248,37 +271,53 @@ function insertIndex(children, name, order) {
248
271
  }
249
272
  return children.length;
250
273
  }
251
- function setPropsChild(children, name, xml, order) {
252
- const without = children.filter((child2) => child2.name !== name);
253
- if (xml === null) return without;
254
- const child = { name, xml };
255
- const at = children.findIndex((entry) => entry.name === name);
256
- if (at === -1) {
257
- const index = insertIndex(without, name, order);
258
- return [...without.slice(0, index), child, ...without.slice(index)];
274
+ function dropChildren(children, name) {
275
+ const kept = [];
276
+ let carried = "";
277
+ for (const child of children) {
278
+ const before = carried + (child.before ?? "");
279
+ if (child.name === name) {
280
+ carried = before;
281
+ continue;
282
+ }
283
+ carried = "";
284
+ kept.push(
285
+ before === "" ? { name: child.name, xml: child.xml } : { ...child, before }
286
+ );
259
287
  }
260
- return [
261
- ...children.slice(0, at),
262
- child,
263
- ...children.slice(at + 1).filter((entry) => entry.name !== name)
264
- ];
288
+ return { kept, carried };
265
289
  }
266
- function propsChild(children, name) {
267
- return children.find((child) => child.name === name);
290
+ function tailWith(props, carried) {
291
+ const tail = carried + (props.tail ?? "");
292
+ return tail === "" ? {} : { tail };
268
293
  }
269
- function namespaceDecls(xml) {
270
- const prefixes = /* @__PURE__ */ new Set(["w"]);
271
- for (const [, prefix] of xml.matchAll(/<\/?([A-Za-z_][\w.-]*):/g)) {
272
- prefixes.add(prefix);
294
+ function setPropsChild(props, name, xml, order) {
295
+ const at = props.children.findIndex((entry) => entry.name === name);
296
+ if (at !== -1 && xml !== null) {
297
+ const rest = dropChildren(props.children.slice(at + 1), name);
298
+ return {
299
+ ...props,
300
+ children: [
301
+ ...props.children.slice(0, at),
302
+ { ...props.children[at], xml },
303
+ ...rest.kept
304
+ ],
305
+ ...tailWith(props, rest.carried)
306
+ };
273
307
  }
274
- for (const [, prefix] of xml.matchAll(/[\s"']([A-Za-z_][\w.-]*):[\w.-]+=/g)) {
275
- prefixes.add(prefix);
308
+ const { kept, carried } = dropChildren(props.children, name);
309
+ if (xml === null) {
310
+ return { ...props, children: kept, ...tailWith(props, carried) };
276
311
  }
277
- prefixes.delete("xml");
278
- prefixes.delete("xmlns");
279
- return Array.from(prefixes).map(
280
- (prefix) => `xmlns:${prefix}="${prefix === "w" ? W_NS : `urn:docx-editor:${prefix}`}"`
281
- ).join(" ");
312
+ const index = insertIndex(kept, name, order);
313
+ return {
314
+ ...props,
315
+ children: [...kept.slice(0, index), { name, xml }, ...kept.slice(index)],
316
+ ...tailWith(props, carried)
317
+ };
318
+ }
319
+ function propsChild(children, name) {
320
+ return children.find((child) => child.name === name);
282
321
  }
283
322
  function parsePropsXml(xml) {
284
323
  try {
@@ -294,6 +333,7 @@ export {
294
333
  TBL_PR_ORDER,
295
334
  TC_PR_ORDER,
296
335
  TR_PR_ORDER,
336
+ innerXml,
297
337
  parseProps,
298
338
  parsePropsXml,
299
339
  propsChild,
@@ -1,10 +1,10 @@
1
1
  // src/docx/relationships.ts
2
+ import { elementXml } from "../ooxml/element.js";
2
3
  import { DocxExportError } from "../ooxml/errors.js";
3
4
  import {
4
5
  decodeUtf8,
5
6
  elementChildren,
6
7
  encodeUtf8,
7
- escapeXml,
8
8
  parseXml
9
9
  } from "../ooxml/xml.js";
10
10
  var RELATIONSHIPS_NS = "http://schemas.openxmlformats.org/package/2006/relationships";
@@ -48,8 +48,13 @@ function takeRelId(taken) {
48
48
  }
49
49
  }
50
50
  function relationshipXml(added) {
51
- const mode = added.external ? ' TargetMode="External"' : "";
52
- return `<Relationship Id="${added.id}" Type="${added.type}" Target="${escapeXml(added.target)}"${mode}/>`;
51
+ const mode = added.external ? [["TargetMode", "External"]] : [];
52
+ return elementXml("Relationship", [
53
+ ["Id", added.id],
54
+ ["Type", added.type],
55
+ ["Target", added.target],
56
+ ...mode
57
+ ]);
53
58
  }
54
59
  function relationshipsPart(original, added) {
55
60
  const entries = added.map(relationshipXml).join("");
@@ -1,7 +1,13 @@
1
1
  // src/docx/runProps.ts
2
2
  import { toRunFormat } from "../model/format.js";
3
+ import {
4
+ attrPairs,
5
+ attrValue,
6
+ elementXml
7
+ } from "../ooxml/element.js";
8
+ import { wName } from "../ooxml/names.js";
9
+ import { setAttr } from "../ooxml/precedence.js";
3
10
  import { normalizeHex } from "../ooxml/units.js";
4
- import { escapeXml, localPart } from "../ooxml/xml.js";
5
11
  import { isEastAsianFontName } from "../styles/fontStack.js";
6
12
  import { fontNamesOf, readRunFormat } from "./formatting.js";
7
13
  import {
@@ -12,7 +18,6 @@ import {
12
18
  renderProps,
13
19
  setPropsChild
14
20
  } from "./propsXml.js";
15
- import { THEME_ATTRS } from "./theme.js";
16
21
  var TOGGLE_CHILDREN = {
17
22
  bold: ["b", "bCs"],
18
23
  italic: ["i", "iCs"],
@@ -32,11 +37,15 @@ var TOGGLE_OFF_VALUE = {
32
37
  strike: "0"
33
38
  };
34
39
  var MAX_FONT_SIZE_PT = 819;
35
- function elementXml(name, value) {
36
- return value === null ? `<w:${name}/>` : `<w:${name} w:val="${value}"/>`;
40
+ function valXml(name, value) {
41
+ return elementXml(wName(name), value === null ? [] : [[wName("val"), value]]);
37
42
  }
38
43
  function shadingXml(fill) {
39
- return `<w:shd w:val="clear" w:color="auto" w:fill="${fill}"/>`;
44
+ return elementXml(wName("shd"), [
45
+ [wName("val"), "clear"],
46
+ [wName("color"), "auto"],
47
+ [wName("fill"), fill]
48
+ ]);
40
49
  }
41
50
  function halfPoints(pt) {
42
51
  if (!Number.isFinite(pt) || pt <= 0 || pt > MAX_FONT_SIZE_PT) return null;
@@ -53,29 +62,19 @@ function fontName(value) {
53
62
  const name = value.trim();
54
63
  return name.length > 0 && !/["';<>&]/.test(name) ? name : null;
55
64
  }
56
- function rFontsAttrs(xml) {
57
- if (xml === null) return [];
58
- const el = parsePropsXml(xml);
59
- if (!el) return null;
60
- return Array.from(el.attributes, (attr) => [
61
- attr.name,
62
- attr.value
63
- ]);
64
- }
65
65
  function rFontsXml(current, name) {
66
- const attrs = rFontsAttrs(current);
67
- if (!attrs) return null;
68
- const slots = fontSlots(
69
- name,
70
- attrs.some(([attr]) => localPart(attr) === "cs")
66
+ const el = current === null ? null : parsePropsXml(current);
67
+ if (current !== null && !el) return null;
68
+ const attrs = attrPairs(el);
69
+ const slots = fontSlots(name, attrValue(attrs, "cs") !== null);
70
+ const kept = slots.reduce(
71
+ (rest, slot) => setAttr(rest, "rFonts", slot, null),
72
+ attrs
71
73
  );
72
- const dropped = slots.flatMap((slot) => [slot, ...THEME_ATTRS[slot] ?? []]);
73
- const kept = attrs.filter(([attr]) => !dropped.includes(localPart(attr)));
74
- const text = [
75
- ...slots.map((slot) => [`w:${slot}`, name]),
74
+ return elementXml(wName("rFonts"), [
75
+ ...slots.map((slot) => [wName(slot), name]),
76
76
  ...kept
77
- ].map(([attr, value]) => `${attr}="${escapeXml(value)}"`).join(" ");
78
- return `<w:rFonts ${text}/>`;
77
+ ]);
79
78
  }
80
79
  function hasChild(xml, name) {
81
80
  if (xml === null) return false;
@@ -87,7 +86,7 @@ function propsOf(rPr) {
87
86
  return parseProps(rPr);
88
87
  }
89
88
  function offEdit(name, value, inherited) {
90
- return [name, inherited ? elementXml(name, value) : null];
89
+ return [name, inherited ? valXml(name, value) : null];
91
90
  }
92
91
  function childEdits(edit, inherited, rFonts) {
93
92
  switch (edit.kind) {
@@ -95,7 +94,7 @@ function childEdits(edit, inherited, rFonts) {
95
94
  const names = TOGGLE_CHILDREN[edit.toggle];
96
95
  if (edit.on) {
97
96
  const value = TOGGLE_ON_VALUE[edit.toggle];
98
- return names.map((name) => [name, elementXml(name, value)]);
97
+ return names.map((name) => [name, valXml(name, value)]);
99
98
  }
100
99
  const off = TOGGLE_OFF_VALUE[edit.toggle];
101
100
  return names.map((name) => offEdit(name, off, inherited));
@@ -110,8 +109,8 @@ function childEdits(edit, inherited, rFonts) {
110
109
  const half = halfPoints(edit.pt);
111
110
  if (half === null) return null;
112
111
  return [
113
- ["sz", elementXml("sz", `${half}`)],
114
- ["szCs", elementXml("szCs", `${half}`)]
112
+ ["sz", valXml("sz", `${half}`)],
113
+ ["szCs", valXml("szCs", `${half}`)]
115
114
  ];
116
115
  }
117
116
  case "fontFamily": {
@@ -124,7 +123,7 @@ function childEdits(edit, inherited, rFonts) {
124
123
  case "color": {
125
124
  if (edit.hex === null) return [offEdit("color", "auto", inherited)];
126
125
  const hex = normalizeHex(edit.hex);
127
- return hex === null ? null : [["color", elementXml("color", hex)]];
126
+ return hex === null ? null : [["color", valXml("color", hex)]];
128
127
  }
129
128
  case "background": {
130
129
  const highlight = offEdit("highlight", "none", inherited);
@@ -162,11 +161,12 @@ function editRunProps(current, pPr, edit) {
162
161
  const rFonts = propsChild(props.children, "rFonts")?.xml ?? null;
163
162
  const edits = childEdits(edit, inherited, rFonts);
164
163
  if (!edits) return null;
165
- const children = edits.reduce(
166
- (kept, [name, xml]) => setPropsChild(kept, name, xml, RUN_PR_ORDER),
167
- props.children
164
+ const rPr = renderProps(
165
+ edits.reduce(
166
+ (kept, [name, xml]) => setPropsChild(kept, name, xml, RUN_PR_ORDER),
167
+ props
168
+ )
168
169
  );
169
- const rPr = renderProps({ ...props, children });
170
170
  return nextProps(rPr === "" ? null : rPr, current);
171
171
  }
172
172
  function isRunToggleOn(format, toggle) {