@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
@@ -1,20 +1,30 @@
1
1
  // src/docx/comments/writing.ts
2
+ import { elementXml, xmlnsAttr } from "../../ooxml/element.js";
2
3
  import { DocxExportError } from "../../ooxml/errors.js";
3
- import { decodeUtf8, encodeUtf8, escapeXml, W_NS } from "../../ooxml/xml.js";
4
+ import { wName, xmlnsDecl } from "../../ooxml/names.js";
5
+ import { encodeUtf8 } from "../../ooxml/xml.js";
4
6
  import { directoryOf } from "../relationships.js";
5
7
  import {
6
8
  COMMENTS_CONTENT_TYPE,
7
9
  COMMENTS_EXTENDED_CONTENT_TYPE,
8
10
  COMMENTS_EXTENDED_REL_TYPE,
9
11
  COMMENTS_REL_TYPE,
10
- CONTENT_TYPES_PATH,
11
- MC_NS,
12
- W14_NS,
13
- W15_NS
12
+ CONTENT_TYPES_PATH
14
13
  } from "./constants.js";
14
+ import { withContentType } from "./contentTypes.js";
15
+ import {
16
+ arrivedEntries,
17
+ renderCommentBody,
18
+ renderCommentExtension,
19
+ withThreadKey
20
+ } from "./grammar.js";
15
21
  import {
16
22
  commentReferencesIn
17
23
  } from "./model.js";
24
+ import { planPeoplePart } from "./people.js";
25
+ function needsThreadKey(id, comment, session) {
26
+ return comment.imported && carriesThreadMetadata(comment) && (session.comments.byId.get(id)?.paraId ?? null) === null;
27
+ }
18
28
  function commentsChanged(doc, session) {
19
29
  const current = commentReferencesIn(doc);
20
30
  if (current.size !== session.commentReferenceIds.size) return true;
@@ -34,14 +44,11 @@ function commentsChanged(doc, session) {
34
44
  for (const comment of current.values()) {
35
45
  if (comment.replies.some((reply) => !reply.imported)) return true;
36
46
  }
47
+ for (const [id, comment] of current) {
48
+ if (needsThreadKey(id, comment, session)) return true;
49
+ }
37
50
  return false;
38
51
  }
39
- function renderedExtension(comment) {
40
- if (comment.extensionXml !== null) return comment.extensionXml;
41
- const parent = "parentParaId" in comment ? ` w15:paraIdParent="${escapeXml(comment.parentParaId)}"` : "";
42
- const done = "resolved" in comment ? ` w15:done="${comment.resolved ? "1" : "0"}"` : "";
43
- return `<w15:commentEx w15:paraId="${escapeXml(comment.paraId)}"${parent}${done}/>`;
44
- }
45
52
  function extensionsXml(references, comments, originallyReferenced) {
46
53
  const current = currentCommentBodies(references);
47
54
  const originalThreads = originalThreadIds(comments, originallyReferenced);
@@ -56,19 +63,23 @@ function extensionsXml(references, comments, originallyReferenced) {
56
63
  const id = idByParaId.get(original.paraId);
57
64
  const item = id === void 0 ? void 0 : current.get(id);
58
65
  if (id !== void 0 && item && !written.has(id)) {
59
- pieces.push(renderedExtension(item));
66
+ pieces.push(renderCommentExtension(item));
60
67
  written.add(id);
61
68
  } else if (id === void 0 || !originalThreads.has(id)) {
62
69
  pieces.push(original.xml);
63
70
  }
64
71
  }
65
72
  for (const [id, comment] of current) {
66
- if (!written.has(id)) pieces.push(renderedExtension(comment));
73
+ if (!written.has(id) && carriesThreadMetadata(comment)) {
74
+ pieces.push(renderCommentExtension(comment));
75
+ }
67
76
  }
68
77
  if (comments.extendedXml === null) {
69
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w15:commentsEx xmlns:w15="${W15_NS}">${pieces.join("")}</w15:commentsEx>`;
78
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w15:commentsEx ${xmlnsDecl("w15")}>${pieces.join("")}</w15:commentsEx>`;
70
79
  }
71
- const open = /<(?:[\w.-]+:)?commentsEx\b[^>]*>/.exec(comments.extendedXml);
80
+ const open = /<(?:[^\s<>/:="']+:)?commentsEx\b[^>]*>/.exec(
81
+ comments.extendedXml
82
+ );
72
83
  if (!open) {
73
84
  throw new DocxExportError(
74
85
  "malformed-xml",
@@ -76,14 +87,23 @@ function extensionsXml(references, comments, originallyReferenced) {
76
87
  );
77
88
  }
78
89
  const name = /^<([^\s>]+)/.exec(open[0])?.[1];
79
- const close = name ? comments.extendedXml.lastIndexOf(`</${name}>`) : -1;
90
+ if (name === void 0) {
91
+ throw new DocxExportError(
92
+ "malformed-xml",
93
+ "the Comments Extended part has no commentsEx root element"
94
+ );
95
+ }
96
+ const bodyAt = open.index + open[0].length;
97
+ if (open[0].endsWith("/>")) {
98
+ return comments.extendedXml.slice(0, open.index) + `${open[0].slice(0, -2)}>${pieces.join("")}</${name}>` + comments.extendedXml.slice(bodyAt);
99
+ }
100
+ const close = comments.extendedXml.lastIndexOf(`</${name}>`);
80
101
  if (close === -1) {
81
102
  throw new DocxExportError(
82
103
  "malformed-xml",
83
104
  "the Comments Extended part has no closing commentsEx tag"
84
105
  );
85
106
  }
86
- const bodyAt = open.index + open[0].length;
87
107
  return comments.extendedXml.slice(0, bodyAt) + pieces.join("") + comments.extendedXml.slice(close);
88
108
  }
89
109
  function extensionsChanged(doc, session) {
@@ -101,41 +121,42 @@ function extensionsChanged(doc, session) {
101
121
  }
102
122
  return false;
103
123
  }
104
- function commentTextXml(text) {
105
- const lines = text.split("\n");
106
- const pieces = [];
107
- lines.forEach((line, index) => {
108
- if (index > 0) pieces.push("<w:br/>");
109
- if (line.length > 0 || lines.length === 1) {
110
- pieces.push(`<w:t xml:space="preserve">${escapeXml(line)}</w:t>`);
111
- }
112
- });
113
- return `<w:r>${pieces.join("")}</w:r>`;
114
- }
115
124
  function carriesThreadMetadata(comment) {
116
125
  return comment.extensionXml !== null || "threadImported" in comment && !comment.threadImported || "parentParaId" in comment;
117
126
  }
118
- function renderedComment(comment) {
127
+ function keyedEntry(comment, arrivedKeyed) {
128
+ return carriesThreadMetadata(comment) || arrivedKeyed.has(comment.id);
129
+ }
130
+ function renderedComment(comment, arrivedKeyed, arrived) {
119
131
  if (comment.imported && comment.commentXml !== null) {
120
- return comment.commentXml;
132
+ return carriesThreadMetadata(comment) ? withThreadKey(
133
+ comment.commentXml,
134
+ comment.paraId,
135
+ arrived.get(comment.id) ?? null
136
+ ) : comment.commentXml;
121
137
  }
122
138
  const attrs = [
123
- `w:id="${escapeXml(comment.id)}"`,
124
- comment.author === null ? null : `w:author="${escapeXml(comment.author)}"`,
125
- comment.date === null ? null : `w:date="${escapeXml(comment.date)}"`,
126
- comment.initials === null ? null : `w:initials="${escapeXml(comment.initials)}"`
127
- ].filter((entry) => entry !== null).join(" ");
128
- const threaded = carriesThreadMetadata(comment);
129
- const paragraphAttrs = threaded ? ` xmlns:w14="${W14_NS}" w14:paraId="${escapeXml(comment.paraId)}"` : "";
130
- return `<w:comment xmlns:w="${W_NS}" ${attrs}><w:p${paragraphAttrs}>${commentTextXml(comment.text)}</w:p></w:comment>`;
139
+ xmlnsAttr("w"),
140
+ [wName("id"), comment.id],
141
+ comment.author === null ? null : [wName("author"), comment.author],
142
+ comment.date === null ? null : [wName("date"), comment.date],
143
+ comment.initials === null ? null : [wName("initials"), comment.initials]
144
+ ];
145
+ const paraId = keyedEntry(comment, arrivedKeyed) ? comment.paraId : null;
146
+ const body = renderCommentBody(comment.text, paraId);
147
+ return elementXml(
148
+ wName("comment"),
149
+ attrs.filter((attr) => attr !== null),
150
+ [body]
151
+ );
131
152
  }
132
153
  function withThreadMarkupCompatibility(openTag) {
133
154
  let updated = openTag;
134
155
  if (!/\sxmlns:w14\s*=/.test(updated)) {
135
- updated = updated.replace(/>$/, ` xmlns:w14="${W14_NS}">`);
156
+ updated = updated.replace(/>$/, ` ${xmlnsDecl("w14")}>`);
136
157
  }
137
158
  if (!/\sxmlns:mc\s*=/.test(updated)) {
138
- updated = updated.replace(/>$/, ` xmlns:mc="${MC_NS}">`);
159
+ updated = updated.replace(/>$/, ` ${xmlnsDecl("mc")}>`);
139
160
  }
140
161
  const ignorable = /\smc:Ignorable\s*=\s*(["'])([^"']*)\1/.exec(updated);
141
162
  if (!ignorable) {
@@ -170,16 +191,22 @@ function originalThreadIds(comments, rootIds) {
170
191
  }
171
192
  function commentsXml(references, comments, originallyReferenced) {
172
193
  const currentBodies = currentCommentBodies(references);
194
+ const arrivedKeyed = new Set(
195
+ Array.from(comments.byId.values()).flatMap(
196
+ (entry) => entry.paraId === null ? [] : [entry.id]
197
+ )
198
+ );
173
199
  const hasThreadMetadata = Array.from(currentBodies.values()).some(
174
- carriesThreadMetadata
200
+ (comment) => keyedEntry(comment, arrivedKeyed)
175
201
  );
202
+ const arrived = arrivedEntries(comments.xml);
176
203
  const originalThreads = originalThreadIds(comments, originallyReferenced);
177
204
  const pieces = [];
178
205
  const written = /* @__PURE__ */ new Set();
179
206
  for (const original of comments.ordered) {
180
207
  const current = currentBodies.get(original.id);
181
208
  if (current) {
182
- pieces.push(renderedComment(current));
209
+ pieces.push(renderedComment(current, arrivedKeyed, arrived));
183
210
  written.add(original.id);
184
211
  continue;
185
212
  }
@@ -189,13 +216,15 @@ function commentsXml(references, comments, originallyReferenced) {
189
216
  }
190
217
  }
191
218
  for (const [id, comment] of currentBodies) {
192
- if (!written.has(id)) pieces.push(renderedComment(comment));
219
+ if (!written.has(id)) {
220
+ pieces.push(renderedComment(comment, arrivedKeyed, arrived));
221
+ }
193
222
  }
194
223
  if (comments.xml === null) {
195
- const compatibility = hasThreadMetadata ? ` xmlns:w14="${W14_NS}" xmlns:mc="${MC_NS}" mc:Ignorable="w14"` : "";
196
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:comments xmlns:w="${W_NS}"${compatibility}>${pieces.join("")}</w:comments>`;
224
+ const compatibility = hasThreadMetadata ? ` ${xmlnsDecl("w14")} ${xmlnsDecl("mc")} mc:Ignorable="w14"` : "";
225
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:comments ${xmlnsDecl("w")}${compatibility}>${pieces.join("")}</w:comments>`;
197
226
  }
198
- const open = /<(?:[\w.-]+:)?comments\b[^>]*>/.exec(comments.xml);
227
+ const open = /<(?:[^\s<>/:="']+:)?comments\b[^>]*>/.exec(comments.xml);
199
228
  if (!open) {
200
229
  throw new DocxExportError(
201
230
  "malformed-xml",
@@ -213,37 +242,6 @@ function commentsXml(references, comments, originallyReferenced) {
213
242
  const openTag = hasThreadMetadata ? withThreadMarkupCompatibility(open[0]) : open[0];
214
243
  return comments.xml.slice(0, open.index) + openTag + pieces.join("") + comments.xml.slice(close);
215
244
  }
216
- var TYPES_OPEN_TAG = /<(?:[\w.-]+:)?Types\b[^>]*>/;
217
- function withContentType(parts, partPath, contentType, current) {
218
- const original = current ?? parts.get(CONTENT_TYPES_PATH);
219
- if (!original) {
220
- throw new DocxExportError(
221
- "missing-content-types",
222
- `cannot add a Comments part to a package that has no ${CONTENT_TYPES_PATH}`
223
- );
224
- }
225
- const { text, hadBom } = decodeUtf8(original);
226
- const partName = `/${partPath}`;
227
- if (new RegExp(
228
- `<(?:[\\w.-]+:)?Override[^>]+PartName=["']${partName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']`,
229
- "i"
230
- ).test(text)) {
231
- return null;
232
- }
233
- const open = TYPES_OPEN_TAG.exec(text);
234
- if (!open) {
235
- throw new DocxExportError(
236
- "malformed-xml",
237
- `${CONTENT_TYPES_PATH} has no Types element`
238
- );
239
- }
240
- const rootName = /^<([^\s>]+)/.exec(open[0])?.[1] ?? "Types";
241
- const separator = rootName.indexOf(":");
242
- const prefix = separator < 0 ? "" : `${rootName.slice(0, separator)}:`;
243
- const declaration = `<${prefix}Override PartName="${partName}" ContentType="${contentType}"/>`;
244
- const at = open.index + open[0].length;
245
- return encodeUtf8(text.slice(0, at) + declaration + text.slice(at), hadBom);
246
- }
247
245
  function availableCommentsPath(session) {
248
246
  const directory = directoryOf(session.mainPartPath);
249
247
  for (let suffix = 0; ; suffix += 1) {
@@ -320,6 +318,15 @@ function planCommentParts(doc, session, relationships, currentContentTypes) {
320
318
  if (contentTypes) parts.set(CONTENT_TYPES_PATH, contentTypes);
321
319
  }
322
320
  }
321
+ if (bodyChanged) {
322
+ const people = planPeoplePart(
323
+ currentCommentBodies(references).values(),
324
+ session,
325
+ relationships,
326
+ parts.get(CONTENT_TYPES_PATH) ?? currentContentTypes
327
+ );
328
+ for (const [path, bytes] of people ?? []) parts.set(path, bytes);
329
+ }
323
330
  return { parts };
324
331
  }
325
332
  export {
@@ -4,6 +4,7 @@ import { parseNumbering } from "../numbering/parseNumbering.js";
4
4
  import { addListDefinitions } from "../numbering/writeNumbering.js";
5
5
  import { DocxExportError } from "../ooxml/errors.js";
6
6
  import { decodeUtf8, encodeUtf8, parseXml, W_NS } from "../ooxml/xml.js";
7
+ import { sameSource } from "../schema/sourceEquality.js";
7
8
  import { planCommentParts } from "./comments.js";
8
9
  import { repackParts } from "./container.js";
9
10
  import { hyperlinkRefs } from "./hyperlink.js";
@@ -21,7 +22,7 @@ import {
21
22
  import { withUniqueControls } from "./uniqueControls.js";
22
23
  function blockXml(node, session, refs) {
23
24
  const imported = originalBlock(node, session);
24
- if (imported && node.eq(imported.node)) return imported.xml;
25
+ if (imported && sameSource(node, imported.node)) return imported.xml;
25
26
  return serializeBlock(node, session, refs);
26
27
  }
27
28
  function buildDocumentXml(doc, session, refs) {
@@ -3,6 +3,7 @@ import {
3
3
  isHighlightName,
4
4
  isUnderlineKind
5
5
  } from "../../model/format.js";
6
+ import { overridingAttrs } from "../../ooxml/precedence.js";
6
7
  import { readTabStopDirectives } from "../../ooxml/tabStops.js";
7
8
  import {
8
9
  ALIGN_BY_JC,
@@ -18,11 +19,7 @@ import {
18
19
  wAttr
19
20
  } from "../../ooxml/units.js";
20
21
  import { childByLocalName } from "../../ooxml/xml.js";
21
- import {
22
- NO_THEME_FONTS,
23
- THEME_ATTRS,
24
- themeFontName
25
- } from "../theme.js";
22
+ import { NO_THEME_FONTS, themeFontName } from "../theme.js";
26
23
  var VERTICAL_ALIGN_BY_VAL = {
27
24
  superscript: "superscript",
28
25
  subscript: "subscript"
@@ -35,7 +32,7 @@ var FONT_SLOTS = ["ascii", "eastAsia", "hAnsi", "cs"];
35
32
  function slotFontName(rFonts, slot, themeFonts) {
36
33
  const written = wAttr(rFonts, slot);
37
34
  if (written !== null) return written;
38
- for (const attr of THEME_ATTRS[slot] ?? []) {
35
+ for (const attr of overridingAttrs("rFonts", slot)) {
39
36
  const resolved = themeFontName(themeFonts, wAttr(rFonts, attr));
40
37
  if (resolved !== null) return resolved;
41
38
  }
@@ -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;
@@ -81,6 +81,7 @@ function buildRunChild(el, marks, images, comments, notes, noteLabel) {
81
81
  id,
82
82
  referenceXml: serializeXml(el),
83
83
  author: comment?.author ?? null,
84
+ authorId: comment?.authorId ?? null,
84
85
  initials: comment?.initials ?? null,
85
86
  date: comment?.date ?? null,
86
87
  text: comment?.text ?? "",
@@ -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.