@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
@@ -3,6 +3,7 @@ import { isHistoryTransaction } from "prosemirror-history";
3
3
  import { Plugin, PluginKey } from "prosemirror-state";
4
4
  import { Decoration, DecorationSet } from "prosemirror-view";
5
5
  import { replacementShut } from "../../schema/locks.js";
6
+ import { editsShut } from "../../schema/protectionState.js";
6
7
  import {
7
8
  hasResolvableClipboardImages,
8
9
  resolveClipboardImages
@@ -222,7 +223,9 @@ function imagePaste() {
222
223
  props: {
223
224
  decorations: (state) => imagePasteKey.getState(state)?.decorations,
224
225
  handlePaste(view, event) {
225
- if (replacementShut(view.state.selection, view.state.doc)) return false;
226
+ if (editsShut(view.state) || replacementShut(view.state.selection, view.state.doc)) {
227
+ return false;
228
+ }
226
229
  if (view.state.selection.ranges.length > 1) return false;
227
230
  const html = event.clipboardData?.getData("text/html") ?? "";
228
231
  if (!hasResolvableClipboardImages(view.dom.ownerDocument, html)) {
@@ -1,5 +1,15 @@
1
1
  /**
2
2
  * Keys not handled here are taken by ProseMirror's base keymap.
3
3
  */
4
- import type { Command } from "prosemirror-state";
4
+ import { type Command, Plugin } from "prosemirror-state";
5
+ /**
6
+ * Keeps undo and redo on their keys where the keymap above cannot reach them.
7
+ *
8
+ * A view that took editing away drops every keydown before the keymap plugin sees it
9
+ * (`view.editable || !(event.type in editHandlers)` in prosemirror-view), so a commenter, who is
10
+ * given no caret, would have no way to take a comment back. A DOM handler is asked ahead of that
11
+ * check. An editable view leaves the keys to the keymap plugin, which would otherwise run them a
12
+ * second time.
13
+ */
14
+ export declare function historyKeys(): Plugin;
5
15
  export declare const docxKeymap: Record<string, Command>;
@@ -1,6 +1,8 @@
1
1
  // src/editor/plugins/keymap.ts
2
2
  import { chainCommands } from "prosemirror-commands";
3
3
  import { undoInputRule } from "prosemirror-inputrules";
4
+ import { keydownHandler } from "prosemirror-keymap";
5
+ import { Plugin } from "prosemirror-state";
4
6
  import { goToNextCell } from "prosemirror-tables";
5
7
  import { canSplit } from "prosemirror-transform";
6
8
  import { toParagraphFormat } from "../../model/format.js";
@@ -55,6 +57,25 @@ var preserveTableFollowingParagraph = (state) => {
55
57
  const paragraphIndex = $from.index(0);
56
58
  return paragraphIndex > 0 && state.doc.child(paragraphIndex - 1).type.spec.tableRole === "table";
57
59
  };
60
+ var historyKeymap = {
61
+ "Mod-z": undo,
62
+ "Mod-y": redo,
63
+ "Shift-Mod-z": redo
64
+ };
65
+ function historyKeys() {
66
+ const runHistoryKeys = keydownHandler(historyKeymap);
67
+ return new Plugin({
68
+ props: {
69
+ handleDOMEvents: {
70
+ keydown: (view, event) => {
71
+ if (view.editable || !runHistoryKeys(view, event)) return false;
72
+ event.preventDefault();
73
+ return true;
74
+ }
75
+ }
76
+ }
77
+ });
78
+ }
58
79
  var docxKeymap = {
59
80
  Enter: chainCommands(leaveEmptyListItem, splitParagraph),
60
81
  "Shift-Enter": insertLineBreak,
@@ -62,9 +83,7 @@ var docxKeymap = {
62
83
  "Mod-Enter": insertPageBreak,
63
84
  // The paragraph keeps adjacent tables separate without changing imported documents.
64
85
  Backspace: chainCommands(preserveTableFollowingParagraph, undoInputRule),
65
- "Mod-z": undo,
66
- "Mod-y": redo,
67
- "Shift-Mod-z": redo,
86
+ ...historyKeymap,
68
87
  "Mod-b": toggleBold,
69
88
  "Mod-i": toggleItalic,
70
89
  "Mod-u": toggleUnderline,
@@ -90,5 +109,6 @@ var docxKeymap = {
90
109
  "Shift-ArrowRight": moveAcrossTab("right", true)
91
110
  };
92
111
  export {
93
- docxKeymap
112
+ docxKeymap,
113
+ historyKeys
94
114
  };
@@ -1,6 +1,8 @@
1
1
  /**
2
- * Rejects transactions disallowed by `schema/locks`. A rejected IME edit also ends the browser
3
- * composition on the next frame so ProseMirror cannot remain stuck in composing state.
2
+ * Rejects transactions no guard `schema/guards` registers would let through, the locks the document
3
+ * carries and the protection the editor runs under (`schema/protection`) among them. A rejected IME
4
+ * edit also ends the browser composition on the next frame so ProseMirror cannot remain stuck in
5
+ * composing state.
4
6
  */
5
7
  import { Plugin } from "prosemirror-state";
6
8
  export declare function lockedContent(): Plugin;
@@ -1,6 +1,6 @@
1
1
  // src/editor/plugins/lockedContent.ts
2
2
  import { Plugin } from "prosemirror-state";
3
- import { transactionAllowed } from "../../schema/locks.js";
3
+ import { transactionAllowed } from "../../schema/guards.js";
4
4
  function endComposition(view) {
5
5
  if (!view.composing) return;
6
6
  const marks = view.state.storedMarks ?? view.state.selection.$from.marks();
@@ -30,7 +30,7 @@ function lockedContent() {
30
30
  };
31
31
  },
32
32
  filterTransaction(tr, state) {
33
- const allowed = transactionAllowed(tr, state.doc);
33
+ const allowed = transactionAllowed(tr, state);
34
34
  if (!allowed) afterRefusal();
35
35
  return allowed;
36
36
  }
@@ -4,6 +4,7 @@ import {
4
4
  PluginKey,
5
5
  TextSelection
6
6
  } from "prosemirror-state";
7
+ import { editsShut } from "../../schema/protectionState.js";
7
8
  var CLOSE = "close";
8
9
  var menuKey = new PluginKey("docxEditorTableMenu");
9
10
  function isRecord(value) {
@@ -67,7 +68,7 @@ function tableContextMenu() {
67
68
  props: {
68
69
  handleDOMEvents: {
69
70
  contextmenu(view, event) {
70
- if (!view.editable) return false;
71
+ if (editsShut(view.state)) return false;
71
72
  const cellPos = cellAtEvent(view, event.target);
72
73
  if (cellPos === null) return false;
73
74
  event.preventDefault();
@@ -5,6 +5,7 @@ import {
5
5
  TextSelection
6
6
  } from "prosemirror-state";
7
7
  import { CellSelection } from "prosemirror-tables";
8
+ import { editingProtection, editsShut } from "../../schema/protectionState.js";
8
9
  var CLOSE = "close";
9
10
  var menuKey = new PluginKey("docxEditorTextMenu");
10
11
  function isRecord(value) {
@@ -44,6 +45,7 @@ function isInSelection(state, spot) {
44
45
  );
45
46
  }
46
47
  function forTableMenu(view, target, spot) {
48
+ if (editsShut(view.state)) return false;
47
49
  const aboutCells = view.state.selection instanceof CellSelection || !isInSelection(view.state, spot);
48
50
  return aboutCells && isInTableCell(view, target);
49
51
  }
@@ -71,9 +73,12 @@ function textContextMenu() {
71
73
  props: {
72
74
  handleDOMEvents: {
73
75
  contextmenu(view, event) {
74
- if (!view.editable) return false;
76
+ if (editingProtection(view.state) === "readOnly") return false;
75
77
  if (!isInEditor(view, event.target)) return false;
76
78
  const spot = clickedSpot(view, event);
79
+ if (editsShut(view.state) && !isInSelection(view.state, spot)) {
80
+ return false;
81
+ }
77
82
  if (forTableMenu(view, event.target, spot)) return false;
78
83
  event.preventDefault();
79
84
  view.dispatch(openMenu(view.state, event, spot));
package/dist/index.d.ts CHANGED
@@ -22,6 +22,8 @@ export type { CellFormat, CellVerticalAlign, DocumentDefaults, HighlightName, Li
22
22
  export type { DocxExportErrorCode, DocxImportErrorCode, } from "./ooxml/errors";
23
23
  export { DocxExportError, DocxImportError } from "./ooxml/errors";
24
24
  export { docxSchema } from "./schema";
25
+ /** Whose comments the panel offers to edit, which `DocxEditorMode` takes */
26
+ export type { EditableComments } from "./schema/protection";
25
27
  /**
26
28
  * The lists the built-in pickers offer. They are exported so that a toolbar of your own can
27
29
  * offer the same ones without writing them out again; the pickers themselves read these very
@@ -1,4 +1,6 @@
1
1
  // src/numbering/listTemplate.ts
2
+ import { elementXml } from "../ooxml/element.js";
3
+ import { wName } from "../ooxml/names.js";
2
4
  var LEVEL_COUNT = 9;
3
5
  var MAX_ILVL = LEVEL_COUNT - 1;
4
6
  var NUMBER_CYCLE = [
@@ -51,25 +53,45 @@ function nextNumId(used, kind) {
51
53
  return listKindOf(from) === kind ? from : from + 1;
52
54
  }
53
55
  function indXml(indent) {
54
- const attrs = [
55
- ["w:left", indent?.startTwips],
56
- ["w:right", indent?.endTwips],
57
- ["w:hanging", indent?.hangingTwips],
58
- ["w:firstLine", indent?.firstLineTwips]
59
- ].filter((entry) => typeof entry[1] === "number").map(([name, value]) => `${name}="${value}"`).join(" ");
60
- return attrs === "" ? "" : `<w:pPr><w:ind ${attrs}/></w:pPr>`;
56
+ const slots = [
57
+ ["left", indent?.startTwips ?? null],
58
+ ["right", indent?.endTwips ?? null],
59
+ ["hanging", indent?.hangingTwips ?? null],
60
+ ["firstLine", indent?.firstLineTwips ?? null]
61
+ ];
62
+ const attrs = slots.filter((slot) => slot[1] !== null).map(([name, twips]) => [wName(name), `${twips}`]);
63
+ if (attrs.length === 0) return "";
64
+ return elementXml(wName("pPr"), [], [elementXml(wName("ind"), attrs)]);
61
65
  }
62
66
  function levelXml(ilvl, level) {
63
67
  const ind = indXml(level.indent);
64
- return `<w:lvl w:ilvl="${ilvl}"><w:start w:val="${level.start}"/><w:numFmt w:val="${level.format}"/><w:lvlText w:val="${level.text}"/><w:lvlJc w:val="left"/>${ind}</w:lvl>`;
68
+ return elementXml(
69
+ wName("lvl"),
70
+ [[wName("ilvl"), `${ilvl}`]],
71
+ [
72
+ elementXml(wName("start"), [[wName("val"), `${level.start}`]]),
73
+ elementXml(wName("numFmt"), [[wName("val"), level.format]]),
74
+ elementXml(wName("lvlText"), [[wName("val"), level.text]]),
75
+ elementXml(wName("lvlJc"), [[wName("val"), "left"]]),
76
+ ind
77
+ ]
78
+ );
65
79
  }
66
80
  function abstractNumXml(abstractNumId, kind) {
67
81
  const levels = TEMPLATES[kind].levels;
68
82
  const body = Array.from(levels.entries()).map(([ilvl, level]) => levelXml(ilvl, level)).join("");
69
- return `<w:abstractNum w:abstractNumId="${abstractNumId}">${body}</w:abstractNum>`;
83
+ return elementXml(
84
+ wName("abstractNum"),
85
+ [[wName("abstractNumId"), `${abstractNumId}`]],
86
+ [body]
87
+ );
70
88
  }
71
89
  function numXml(numId, abstractNumId) {
72
- return `<w:num w:numId="${numId}"><w:abstractNumId w:val="${abstractNumId}"/></w:num>`;
90
+ return elementXml(
91
+ wName("num"),
92
+ [[wName("numId"), `${numId}`]],
93
+ [elementXml(wName("abstractNumId"), [[wName("val"), `${abstractNumId}`]])]
94
+ );
73
95
  }
74
96
  export {
75
97
  LEVEL_COUNT,
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Writing one element out as text, and merging the attributes of one.
3
+ *
4
+ * Everything here works on strings, because that is what the writers hold: a fragment the file
5
+ * arrived with is carried around as its original text, and an edit swaps out one element or one
6
+ * attribute of it. Rebuilding a parsed element instead would rewrite markup nobody touched.
7
+ *
8
+ * An attribute is matched by its local part rather than by its written name, so a document that
9
+ * bound the WordprocessingML namespace to a prefix of its own is edited in the spelling it chose.
10
+ */
11
+ import { type KnownPrefix } from "./names";
12
+ /** One attribute as it is written: the whole name, prefix included, and the value before escaping */
13
+ export type XmlAttr = readonly [name: string, value: string];
14
+ /** `a="b" c="d"`. Empty for no attributes, so an opening tag needs no separator of its own */
15
+ export declare function attrsText(attrs: readonly XmlAttr[]): string;
16
+ /**
17
+ * `<name a="b"/>`, or `<name a="b">children</name>` for an element that holds something.
18
+ *
19
+ * Attribute values are escaped here; children are spliced in as they are, being markup already, so
20
+ * a caller writing text as a child escapes it itself. An element handed no child at all closes on
21
+ * its own, which means a caller that wants an empty pair of tags passes the empty text as a child.
22
+ */
23
+ export declare function elementXml(name: string, attrs: readonly XmlAttr[], children?: readonly string[]): string;
24
+ /**
25
+ * The namespace declaration a fragment carries when the part it is spliced into does not declare
26
+ * the prefix itself.
27
+ */
28
+ export declare function xmlnsAttr(prefix: KnownPrefix): XmlAttr;
29
+ /**
30
+ * The attributes with the one of this local name written as `value`, or removed when it is null.
31
+ *
32
+ * One already there keeps the slot it sat in and the name it was written under, so an edited
33
+ * element reads as the original did. One that is not there yet goes on the end under the `w`
34
+ * prefix.
35
+ */
36
+ export declare function withAttr(attrs: readonly XmlAttr[], name: string, value: string | null): XmlAttr[];
37
+ export declare function withoutAttrs(attrs: readonly XmlAttr[], localNames: readonly string[]): XmlAttr[];
38
+ export declare function attrValue(attrs: readonly XmlAttr[], localName: string): string | null;
39
+ /**
40
+ * An opening tag whose attributes are the ones the original wrote.
41
+ *
42
+ * The text is spliced in exactly as it arrived rather than parsed and written again, so the
43
+ * spelling, the order, and the escaping a producer chose all survive being edited.
44
+ */
45
+ export declare function openTagXml(name: string, rawAttrs: string | null): string;
46
+ /** The same for an element that closes on its own */
47
+ export declare function emptyTagXml(name: string, rawAttrs: string | null): string;
48
+ /**
49
+ * The attributes of a parsed element as pairs, for a caller that still holds the element rather
50
+ * than its text.
51
+ */
52
+ export declare function attrPairs(el: Element | null): XmlAttr[];
@@ -0,0 +1,49 @@
1
+ // src/ooxml/element.ts
2
+ import { NAMESPACES, wName } from "./names.js";
3
+ import { escapeXml, localPart } from "./xml.js";
4
+ function attrsText(attrs) {
5
+ return attrs.map(([name, value]) => `${name}="${escapeXml(value)}"`).join(" ");
6
+ }
7
+ function elementXml(name, attrs, children = []) {
8
+ const text = attrsText(attrs);
9
+ const open = text === "" ? `<${name}` : `<${name} ${text}`;
10
+ if (children.length === 0) return `${open}/>`;
11
+ return `${open}>${children.join("")}</${name}>`;
12
+ }
13
+ function xmlnsAttr(prefix) {
14
+ return [`xmlns:${prefix}`, NAMESPACES[prefix]];
15
+ }
16
+ function withAttr(attrs, name, value) {
17
+ if (value === null) return withoutAttrs(attrs, [name]);
18
+ const at = attrs.findIndex(([attr]) => localPart(attr) === name);
19
+ if (at === -1) return [...attrs, [wName(name), value]];
20
+ return attrs.map(
21
+ (attr, index) => index === at ? [attr[0], value] : attr
22
+ );
23
+ }
24
+ function withoutAttrs(attrs, localNames) {
25
+ return attrs.filter(([name]) => !localNames.includes(localPart(name)));
26
+ }
27
+ function attrValue(attrs, localName) {
28
+ return attrs.find(([name]) => localPart(name) === localName)?.[1] ?? null;
29
+ }
30
+ function openTagXml(name, rawAttrs) {
31
+ return rawAttrs ? `<${name} ${rawAttrs}>` : `<${name}>`;
32
+ }
33
+ function emptyTagXml(name, rawAttrs) {
34
+ return rawAttrs ? `<${name} ${rawAttrs}/>` : `<${name}/>`;
35
+ }
36
+ function attrPairs(el) {
37
+ return el ? Array.from(el.attributes, (attr) => [attr.name, attr.value]) : [];
38
+ }
39
+ export {
40
+ attrPairs,
41
+ attrValue,
42
+ attrsText,
43
+ elementXml,
44
+ emptyTagXml,
45
+ openTagXml,
46
+ withAttr,
47
+ withoutAttrs,
48
+ xmlnsAttr
49
+ };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The gate a raw OOXML string passes through on its way from the DOM into a node or mark attr.
3
+ *
4
+ * A preserved fragment goes back out the way it came, spliced into a slot the writer opens and
5
+ * closes around it (`docx/serializeParagraph`), so a fragment that closes that slot itself, opens
6
+ * a sibling beside it, or lays text down beside what it holds writes something the editor never
7
+ * modelled into the exported file. What stands inside the element it carries is that element's own
8
+ * and travels with it. Reading one back is the only place such a string can arrive from outside,
9
+ * so every attr that carries one is read through here and a fragment that does not hold its shape
10
+ * is turned down rather than corrected.
11
+ *
12
+ * A refusal is not an error: `getAttrs` answers `false` with it, which drops the parse rule and
13
+ * leaves ProseMirror to settle the content one level plainer. Demotion, not contamination.
14
+ */
15
+ /**
16
+ * What a raw XML string has to look like to be let into the attr that carries it.
17
+ *
18
+ * `element` is a whole element: a properties fragment, a drawing, an annotation reference.
19
+ * `attributes` is what stood inside an opening tag, as `attrString` writes it.
20
+ * `openTag` is an opening tag with everything it wrapped cut away, which the writer puts back by
21
+ * appending the closing text `closedBy` names. `head` is what may still stand between the two:
22
+ * the property elements the tag carries ahead of its content, by local name.
23
+ */
24
+ export type RawXmlShape = {
25
+ kind: "element";
26
+ names: readonly string[] | "any";
27
+ } | {
28
+ kind: "attributes";
29
+ } | {
30
+ kind: "openTag";
31
+ name: string;
32
+ closedBy: string;
33
+ head: readonly string[];
34
+ };
35
+ /** A single element by local name, rejecting an explicitly foreign namespace */
36
+ export declare function ELEMENT(...names: string[]): RawXmlShape;
37
+ /**
38
+ * A single element of any name, in any namespace.
39
+ *
40
+ * What import could not model keeps its own XML (`rawInline`, `rawBlock`), and that is a
41
+ * `m:oMathPara`, a `mc:AlternateContent`, a `w:ins`, a bookmark: naming the ones allowed would
42
+ * turn a document the editor reads today into a demoted one.
43
+ */
44
+ export declare const ANY_ELEMENT: RawXmlShape;
45
+ /** The attributes of an opening tag, with neither the tag nor anything it held */
46
+ export declare const ATTRIBUTES: RawXmlShape;
47
+ /**
48
+ * The value if it holds the shape, null if there was none, `false` if it does not.
49
+ *
50
+ * The three answers are what `getAttrs` needs: a value to carry, an absent attr, and a rule to
51
+ * give up on.
52
+ */
53
+ export declare function acceptRawXml(shape: RawXmlShape, value: string | null): string | null | false;
@@ -0,0 +1,76 @@
1
+ // src/ooxml/fragment.ts
2
+ import {
3
+ elementChildren,
4
+ namespaceDecls,
5
+ parseXml,
6
+ RESERVED_PREFIXES,
7
+ W_NS
8
+ } from "./xml.js";
9
+ function ELEMENT(...names) {
10
+ return { kind: "element", names };
11
+ }
12
+ var ANY_ELEMENT = { kind: "element", names: "any" };
13
+ var ATTRIBUTES = { kind: "attributes" };
14
+ function loneElement(xml) {
15
+ let root;
16
+ try {
17
+ root = parseXml(`<x ${namespaceDecls(xml)}>${xml}</x>`).documentElement;
18
+ } catch {
19
+ return null;
20
+ }
21
+ if (root.childNodes.length !== 1) return null;
22
+ const el = elementChildren(root)[0];
23
+ return el === void 0 || rebindsReservedPrefix(el) ? null : el;
24
+ }
25
+ function isNamed(el, names) {
26
+ if (!names.includes(el.localName)) return false;
27
+ const declaration = el.prefix === null ? "xmlns" : `xmlns:${el.prefix}`;
28
+ for (let scope = el; scope.parentElement !== null; scope = scope.parentElement) {
29
+ if (scope.hasAttribute(declaration)) return el.namespaceURI === W_NS;
30
+ }
31
+ return true;
32
+ }
33
+ function holdsHeadAlone(el, shape) {
34
+ const closing = loneElement(`<w:${shape.name}>${shape.closedBy}`);
35
+ if (closing === null) return false;
36
+ const children = elementChildren(el);
37
+ const own = children.slice(
38
+ 0,
39
+ children.length - elementChildren(closing).length
40
+ );
41
+ return el.childNodes.length === children.length && own.every((child) => isNamed(child, shape.head));
42
+ }
43
+ function rebindsReservedPrefix(el) {
44
+ return Array.from(el.attributes).some((attr) => {
45
+ if (attr.prefix !== "xmlns") return false;
46
+ const reserved = RESERVED_PREFIXES.get(attr.localName);
47
+ return reserved !== void 0 && reserved !== attr.value;
48
+ }) || elementChildren(el).some(rebindsReservedPrefix);
49
+ }
50
+ function holdsShape(shape, value) {
51
+ switch (shape.kind) {
52
+ case "element": {
53
+ const el = loneElement(value);
54
+ if (el === null) return false;
55
+ return shape.names === "any" || isNamed(el, shape.names);
56
+ }
57
+ case "attributes": {
58
+ const el = loneElement(`<y ${value}></y>`);
59
+ return el !== null && el.childNodes.length === 0;
60
+ }
61
+ case "openTag": {
62
+ const el = loneElement(value + shape.closedBy);
63
+ return el !== null && holdsHeadAlone(el, shape);
64
+ }
65
+ }
66
+ }
67
+ function acceptRawXml(shape, value) {
68
+ if (value === null) return null;
69
+ return holdsShape(shape, value) ? value : false;
70
+ }
71
+ export {
72
+ ANY_ELEMENT,
73
+ ATTRIBUTES,
74
+ ELEMENT,
75
+ acceptRawXml
76
+ };
@@ -1,4 +1,5 @@
1
1
  // src/ooxml/image.ts
2
+ import { NAMESPACES } from "./names.js";
2
3
  import { childByLocalName, escapeXml, R_NS } from "./xml.js";
3
4
  var EMU_PER_PX = 9525;
4
5
  function emuToPx(emu) {
@@ -42,7 +43,7 @@ function toImageExtent(value) {
42
43
  const cy = positiveInt(value.cy);
43
44
  return cx === null || cy === null ? null : { cx, cy };
44
45
  }
45
- var PICTURE_URI = "http://schemas.openxmlformats.org/drawingml/2006/picture";
46
+ var PICTURE_URI = NAMESPACES.pic;
46
47
  function childOf(el, name) {
47
48
  return el ? childByLocalName(el, name) : null;
48
49
  }
@@ -82,8 +83,8 @@ function withExtent(xml, extent) {
82
83
  const size = `cx="${extent.cx}" cy="${extent.cy}"`;
83
84
  return xml.replace(EXTENT_TAG, (_match, prefix) => `<${prefix ?? ""}extent ${size}/>`).replace(EXT_TAG, (_match, prefix) => `<${prefix ?? ""}ext ${size}/>`);
84
85
  }
85
- var WP_NS = "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing";
86
- var A_NS = "http://schemas.openxmlformats.org/drawingml/2006/main";
86
+ var WP_NS = NAMESPACES.wp;
87
+ var A_NS = NAMESPACES.a;
87
88
  function imageDrawingXml(image) {
88
89
  const size = `cx="${image.extent.cx}" cy="${image.extent.cy}"`;
89
90
  const name = `Picture ${image.docPrId}`;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The namespace prefixes the writer spells out, and the namespace each one stands for.
3
+ *
4
+ * Reading is another matter: a document may bind these prefixes to names of its own, so an element
5
+ * that arrived in the file is looked up by its local part. What this package writes is settled
6
+ * here, and the part a fragment is spliced into declares the same binding, so the prefix is
7
+ * decided once rather than at every call site.
8
+ */
9
+ /** The prefix every WordprocessingML element and attribute this package writes carries */
10
+ export declare const W_PREFIX = "w";
11
+ export declare const NAMESPACES: {
12
+ readonly w: "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
13
+ readonly r: "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
14
+ readonly w14: "http://schemas.microsoft.com/office/word/2010/wordml";
15
+ readonly w15: "http://schemas.microsoft.com/office/word/2012/wordml";
16
+ readonly mc: "http://schemas.openxmlformats.org/markup-compatibility/2006";
17
+ readonly wp: "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing";
18
+ readonly a: "http://schemas.openxmlformats.org/drawingml/2006/main";
19
+ readonly pic: "http://schemas.openxmlformats.org/drawingml/2006/picture";
20
+ };
21
+ export type KnownPrefix = keyof typeof NAMESPACES;
22
+ /** The name `local` goes out under (`qualify("w15", "commentEx")` -> `w15:commentEx`) */
23
+ export declare function qualify(prefix: KnownPrefix, local: string): string;
24
+ /** `wName("val")` -> `w:val`. The one place the written WordprocessingML prefix is decided */
25
+ export declare function wName(local: string): string;
26
+ /** The declaration a part root carries for one prefix, ready to write inside an opening tag */
27
+ export declare function xmlnsDecl(prefix: KnownPrefix): string;
@@ -0,0 +1,29 @@
1
+ // src/ooxml/names.ts
2
+ import { R_NS, W_NS } from "./xml.js";
3
+ var W_PREFIX = "w";
4
+ var NAMESPACES = {
5
+ w: W_NS,
6
+ r: R_NS,
7
+ w14: "http://schemas.microsoft.com/office/word/2010/wordml",
8
+ w15: "http://schemas.microsoft.com/office/word/2012/wordml",
9
+ mc: "http://schemas.openxmlformats.org/markup-compatibility/2006",
10
+ wp: "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
11
+ a: "http://schemas.openxmlformats.org/drawingml/2006/main",
12
+ pic: "http://schemas.openxmlformats.org/drawingml/2006/picture"
13
+ };
14
+ function qualify(prefix, local) {
15
+ return `${prefix}:${local}`;
16
+ }
17
+ function wName(local) {
18
+ return qualify(W_PREFIX, local);
19
+ }
20
+ function xmlnsDecl(prefix) {
21
+ return `xmlns:${prefix}="${NAMESPACES[prefix]}"`;
22
+ }
23
+ export {
24
+ NAMESPACES,
25
+ W_PREFIX,
26
+ qualify,
27
+ wName,
28
+ xmlnsDecl
29
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Which attribute beats which when an element writes down both.
3
+ *
4
+ * OOXML records the same setting twice over in several places: a font slot beside the theme
5
+ * reference that overrules it, a color beside the theme color, an indent in twips beside the same
6
+ * indent counted in characters. Writing one of a pair while leaving the other standing means
7
+ * writing a value Word will not show, so the one that would win is dropped as the other is written.
8
+ *
9
+ * The table is keyed by the local names of the owning element and the attribute together, because
10
+ * neither is enough on its own: `color` is an attribute of `w:shd` but an element of its own whose
11
+ * value sits in `w:val`, and `left` is an attribute of `w:ind` but an element of `w:tcMar`.
12
+ */
13
+ import { type XmlAttr } from "./element";
14
+ /**
15
+ * The attributes that override the named one when both are there, so writing the named one drops
16
+ * them (§17.3.1.12 ind, §17.3.2.26 rFonts, §17.3.2.6 color, §17.3.5 shd, §17.3.1.33 spacing).
17
+ *
18
+ * The border sides are the children of `w:tcBorders`, each of them a CT_Border that records a
19
+ * color of its own.
20
+ */
21
+ export declare const OVERRIDING_ATTRS: Readonly<Record<`${string}/${string}`, readonly string[]>>;
22
+ /** What writing `element`'s `name` drops along with it. Empty for a pair nothing overrides */
23
+ export declare function overridingAttrs(element: string, name: string): readonly string[];
24
+ /**
25
+ * `withAttr` with the attributes the new value would be fighting with dropped as it is written.
26
+ *
27
+ * This is how an attribute is meant to be written, so that a writer added later cannot forget the
28
+ * drop. A caller that means to leave an overriding attribute standing says so by reaching for
29
+ * `withAttr` instead.
30
+ */
31
+ export declare function setAttr(attrs: readonly XmlAttr[], element: string, name: string, value: string | null): XmlAttr[];
@@ -0,0 +1,51 @@
1
+ // src/ooxml/precedence.ts
2
+ import { withAttr, withoutAttrs } from "./element.js";
3
+ var THEME_COLOR = [
4
+ "themeColor",
5
+ "themeTint",
6
+ "themeShade"
7
+ ];
8
+ var NO_OVERRIDES = [];
9
+ var OVERRIDING_ATTRS = {
10
+ "rFonts/ascii": ["asciiTheme"],
11
+ "rFonts/hAnsi": ["hAnsiTheme"],
12
+ "rFonts/eastAsia": ["eastAsiaTheme"],
13
+ // This is the slot whose capitalization varies from document to document
14
+ "rFonts/cs": ["cstheme", "csTheme"],
15
+ "color/val": THEME_COLOR,
16
+ "shd/color": THEME_COLOR,
17
+ "shd/fill": ["themeFill", "themeFillTint", "themeFillShade"],
18
+ "ind/left": ["leftChars"],
19
+ "ind/start": ["startChars"],
20
+ "ind/right": ["rightChars"],
21
+ "ind/end": ["endChars"],
22
+ "ind/hanging": ["hangingChars"],
23
+ "ind/firstLine": ["firstLineChars"],
24
+ "spacing/before": ["beforeLines"],
25
+ "spacing/after": ["afterLines"],
26
+ "top/color": THEME_COLOR,
27
+ "start/color": THEME_COLOR,
28
+ "left/color": THEME_COLOR,
29
+ "bottom/color": THEME_COLOR,
30
+ "end/color": THEME_COLOR,
31
+ "right/color": THEME_COLOR,
32
+ "insideH/color": THEME_COLOR,
33
+ "insideV/color": THEME_COLOR,
34
+ "tl2br/color": THEME_COLOR,
35
+ "tr2bl/color": THEME_COLOR
36
+ };
37
+ function overridingAttrs(element, name) {
38
+ return OVERRIDING_ATTRS[`${element}/${name}`] ?? NO_OVERRIDES;
39
+ }
40
+ function setAttr(attrs, element, name, value) {
41
+ return withAttr(
42
+ withoutAttrs(attrs, overridingAttrs(element, name)),
43
+ name,
44
+ value
45
+ );
46
+ }
47
+ export {
48
+ OVERRIDING_ATTRS,
49
+ overridingAttrs,
50
+ setAttr
51
+ };