@portone/docx-editor 0.3.0 → 0.5.0

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 (217) hide show
  1. package/CHANGELOG.md +205 -0
  2. package/assets/editor.png +0 -0
  3. package/dist/DocxEditor.d.ts +1 -1
  4. package/dist/DocxEditor.js +45 -25
  5. package/dist/core.d.ts +1 -1
  6. package/dist/docx/cloning.js +4 -9
  7. package/dist/docx/commentOnlyChange.d.ts +5 -3
  8. package/dist/docx/comments/grammar.d.ts +27 -10
  9. package/dist/docx/comments/grammar.js +18 -67
  10. package/dist/docx/comments/model.d.ts +5 -6
  11. package/dist/docx/comments/model.js +0 -9
  12. package/dist/docx/comments/parts.d.ts +16 -10
  13. package/dist/docx/comments/parts.js +65 -13
  14. package/dist/docx/comments/people.d.ts +11 -3
  15. package/dist/docx/comments/people.js +20 -6
  16. package/dist/docx/comments/policy.js +7 -1
  17. package/dist/docx/comments/reading.d.ts +4 -2
  18. package/dist/docx/comments/reading.js +1 -14
  19. package/dist/docx/comments/writing.js +78 -35
  20. package/dist/docx/exportDocx.js +30 -18
  21. package/dist/docx/exportRefs.d.ts +8 -3
  22. package/dist/docx/exportRefs.js +2 -1
  23. package/dist/docx/fidelity.js +24 -10
  24. package/dist/docx/fields.d.ts +42 -0
  25. package/dist/docx/fields.js +76 -0
  26. package/dist/docx/formatting/attrs.d.ts +20 -2
  27. package/dist/docx/formatting/attrs.js +57 -2
  28. package/dist/docx/formatting/context.d.ts +10 -2
  29. package/dist/docx/formatting/context.js +23 -5
  30. package/dist/docx/formatting/resolve.d.ts +10 -4
  31. package/dist/docx/formatting/resolve.js +31 -11
  32. package/dist/docx/formatting/styles.d.ts +25 -2
  33. package/dist/docx/formatting/styles.js +84 -3
  34. package/dist/docx/headersFooters.d.ts +89 -20
  35. package/dist/docx/headersFooters.js +163 -172
  36. package/dist/docx/hyperlink.d.ts +14 -4
  37. package/dist/docx/hyperlink.js +4 -1
  38. package/dist/docx/importDocx.d.ts +1 -1
  39. package/dist/docx/importDocx.js +135 -74
  40. package/dist/docx/importParagraph.d.ts +16 -4
  41. package/dist/docx/importParagraph.js +87 -93
  42. package/dist/docx/importPolicy.d.ts +70 -0
  43. package/dist/docx/importPolicy.js +190 -0
  44. package/dist/docx/importPreserved.d.ts +30 -0
  45. package/dist/docx/importPreserved.js +54 -0
  46. package/dist/docx/importTable.d.ts +2 -2
  47. package/dist/docx/importTable.js +107 -66
  48. package/dist/docx/invariants.js +42 -31
  49. package/dist/docx/newLists.d.ts +30 -0
  50. package/dist/docx/newLists.js +24 -1
  51. package/dist/docx/notes.d.ts +6 -1
  52. package/dist/docx/notes.js +6 -16
  53. package/dist/docx/numberingPlanner.d.ts +6 -3
  54. package/dist/docx/numberingPlanner.js +38 -7
  55. package/dist/docx/pageGeometry.d.ts +5 -14
  56. package/dist/docx/pageGeometry.js +0 -12
  57. package/dist/docx/protectionPolicy.d.ts +13 -5
  58. package/dist/docx/protectionPolicy.js +32 -22
  59. package/dist/docx/scan.d.ts +18 -8
  60. package/dist/docx/scan.js +17 -11
  61. package/dist/docx/sections.d.ts +126 -0
  62. package/dist/docx/sections.js +207 -0
  63. package/dist/docx/serializeBlock.d.ts +13 -4
  64. package/dist/docx/serializeBlock.js +16 -18
  65. package/dist/docx/serializeParagraph.d.ts +4 -0
  66. package/dist/docx/serializeParagraph.js +1 -0
  67. package/dist/docx/serializePreserved.d.ts +14 -0
  68. package/dist/docx/serializePreserved.js +24 -0
  69. package/dist/docx/serializeStory.d.ts +17 -0
  70. package/dist/docx/serializeStory.js +17 -0
  71. package/dist/docx/serializeTable.js +10 -13
  72. package/dist/docx/session.d.ts +24 -7
  73. package/dist/docx/session.js +37 -6
  74. package/dist/docx/story.d.ts +140 -0
  75. package/dist/docx/story.js +237 -0
  76. package/dist/docx/storyProjection.js +6 -1
  77. package/dist/docx/tableFormatting/conditions.d.ts +76 -0
  78. package/dist/docx/tableFormatting/conditions.js +196 -0
  79. package/dist/docx/tableFormatting/editing.d.ts +7 -3
  80. package/dist/docx/tableFormatting/editing.js +4 -5
  81. package/dist/docx/tableFormatting/reading.d.ts +49 -19
  82. package/dist/docx/tableFormatting/reading.js +109 -35
  83. package/dist/docx/tableFormatting.d.ts +1 -0
  84. package/dist/docx/tableFormatting.js +1 -0
  85. package/dist/docx/tableTemplate.js +12 -12
  86. package/dist/editor/clipboard/htmlReader.d.ts +30 -0
  87. package/dist/editor/clipboard/htmlReader.js +302 -0
  88. package/dist/editor/clipboard/internalChannel.d.ts +57 -0
  89. package/dist/editor/clipboard/internalChannel.js +58 -0
  90. package/dist/editor/clipboard/normalizers.d.ts +99 -0
  91. package/dist/editor/clipboard/normalizers.js +199 -0
  92. package/dist/editor/clipboard/parser.d.ts +34 -0
  93. package/dist/editor/clipboard/parser.js +58 -0
  94. package/dist/editor/clipboard/plugin.d.ts +21 -0
  95. package/dist/editor/clipboard/plugin.js +320 -0
  96. package/dist/editor/clipboard/readContext.d.ts +24 -0
  97. package/dist/editor/clipboard/readContext.js +19 -0
  98. package/dist/editor/clipboard/readers.d.ts +33 -0
  99. package/dist/editor/clipboard/readers.js +37 -0
  100. package/dist/editor/commands/comments/editing.d.ts +26 -6
  101. package/dist/editor/commands/comments/editing.js +175 -105
  102. package/dist/editor/commands/comments/model.d.ts +34 -17
  103. package/dist/editor/commands/comments/model.js +11 -0
  104. package/dist/editor/commands/comments/reading.d.ts +5 -3
  105. package/dist/editor/commands/comments/reading.js +8 -58
  106. package/dist/editor/commands/formatting/propertyCommands.js +6 -1
  107. package/dist/editor/commands/index.d.ts +2 -2
  108. package/dist/editor/commands/index.js +2 -0
  109. package/dist/editor/commands/listCommands.d.ts +12 -3
  110. package/dist/editor/commands/listCommands.js +76 -30
  111. package/dist/editor/commands/noteQueries.d.ts +22 -5
  112. package/dist/editor/commands/noteQueries.js +34 -6
  113. package/dist/editor/commands/paragraphCommands.js +6 -1
  114. package/dist/editor/createEditor.js +14 -4
  115. package/dist/editor/documentStyles.d.ts +26 -4
  116. package/dist/editor/documentStyles.js +9 -4
  117. package/dist/editor/editorDocument.d.ts +7 -6
  118. package/dist/editor/editorDocument.js +18 -6
  119. package/dist/editor/imageFiles.d.ts +2 -2
  120. package/dist/editor/imageFiles.js +2 -0
  121. package/dist/editor/insertTable.d.ts +1 -1
  122. package/dist/editor/insertTable.js +2 -2
  123. package/dist/editor/paragraphEdits.d.ts +16 -1
  124. package/dist/editor/paragraphEdits.js +13 -5
  125. package/dist/editor/paragraphPlacement.d.ts +11 -0
  126. package/dist/editor/paragraphPlacement.js +18 -0
  127. package/dist/editor/plainText.d.ts +12 -0
  128. package/dist/editor/plainText.js +2 -1
  129. package/dist/editor/plugins/columnResize.js +6 -3
  130. package/dist/editor/plugins/commentComposer.d.ts +28 -0
  131. package/dist/editor/plugins/commentComposer.js +46 -0
  132. package/dist/editor/plugins/commentDecorations.d.ts +29 -2
  133. package/dist/editor/plugins/commentDecorations.js +89 -15
  134. package/dist/editor/plugins/documentProjection.d.ts +31 -0
  135. package/dist/editor/plugins/documentProjection.js +22 -0
  136. package/dist/editor/plugins/imagePaste.js +20 -10
  137. package/dist/editor/plugins/linkPanel.d.ts +7 -3
  138. package/dist/editor/plugins/linkPanel.js +13 -27
  139. package/dist/editor/plugins/numberingDecorations.d.ts +10 -4
  140. package/dist/editor/plugins/numberingDecorations.js +43 -2
  141. package/dist/editor/plugins/panelState.d.ts +67 -0
  142. package/dist/editor/plugins/panelState.js +54 -0
  143. package/dist/editor/plugins/paragraphDisplay.js +9 -3
  144. package/dist/editor/plugins/tableContextMenu.d.ts +1 -1
  145. package/dist/editor/plugins/tableContextMenu.js +30 -47
  146. package/dist/editor/plugins/tableDisplay.js +19 -5
  147. package/dist/editor/plugins/textContextMenu.d.ts +1 -1
  148. package/dist/editor/plugins/textContextMenu.js +30 -47
  149. package/dist/model/format.d.ts +50 -0
  150. package/dist/model/format.js +65 -0
  151. package/dist/numbering/listRegistry.d.ts +37 -0
  152. package/dist/numbering/listRegistry.js +112 -0
  153. package/dist/numbering/listTemplate.d.ts +26 -17
  154. package/dist/numbering/listTemplate.js +66 -20
  155. package/dist/numbering/markers.d.ts +15 -6
  156. package/dist/numbering/markers.js +26 -59
  157. package/dist/numbering/parseNumbering.d.ts +70 -5
  158. package/dist/numbering/parseNumbering.js +100 -26
  159. package/dist/numbering/spellers.d.ts +15 -0
  160. package/dist/numbering/spellers.js +96 -0
  161. package/dist/numbering/writeNumbering.d.ts +5 -3
  162. package/dist/numbering/writeNumbering.js +15 -15
  163. package/dist/ooxml/conformance.d.ts +28 -0
  164. package/dist/ooxml/conformance.js +20 -0
  165. package/dist/ooxml/errors.d.ts +5 -5
  166. package/dist/ooxml/fragment.d.ts +11 -0
  167. package/dist/ooxml/fragment.js +14 -4
  168. package/dist/ooxml/image.d.ts +3 -4
  169. package/dist/ooxml/image.js +50 -5
  170. package/dist/ooxml/names.d.ts +15 -0
  171. package/dist/ooxml/names.js +10 -1
  172. package/dist/ooxml/partSplice.d.ts +1 -1
  173. package/dist/ooxml/partSplice.js +10 -3
  174. package/dist/ooxml/props.d.ts +8 -0
  175. package/dist/ooxml/props.js +14 -4
  176. package/dist/ooxml/rangeMarkers.d.ts +5 -0
  177. package/dist/ooxml/rangeMarkers.js +24 -0
  178. package/dist/ooxml/xml.d.ts +8 -9
  179. package/dist/ooxml/xml.js +6 -6
  180. package/dist/page/PageGuides.d.ts +8 -4
  181. package/dist/page/PageGuides.js +13 -37
  182. package/dist/page/pageLayout.d.ts +62 -13
  183. package/dist/page/pageLayout.js +91 -37
  184. package/dist/page/usePageLayout.d.ts +14 -11
  185. package/dist/page/usePageLayout.js +21 -29
  186. package/dist/schema/attrRoles.js +44 -19
  187. package/dist/schema/docxSchema.d.ts +9 -1
  188. package/dist/schema/docxSchema.js +220 -139
  189. package/dist/schema/editGuard.d.ts +1 -1
  190. package/dist/schema/guards.js +2 -2
  191. package/dist/schema/index.d.ts +1 -0
  192. package/dist/schema/preservedFragments.d.ts +12 -0
  193. package/dist/schema/preservedFragments.js +38 -0
  194. package/dist/schema/preservedGuards.d.ts +9 -7
  195. package/dist/schema/preservedGuards.js +112 -11
  196. package/dist/schema/protection.d.ts +34 -5
  197. package/dist/schema/protection.js +42 -14
  198. package/dist/schema/rendering.js +3 -1
  199. package/dist/schema/stories.d.ts +49 -0
  200. package/dist/schema/stories.js +78 -0
  201. package/dist/styles/classNames.d.ts +16 -7
  202. package/dist/styles/classNames.js +16 -7
  203. package/dist/styles.css +90 -30
  204. package/dist/table/cellFormatting.js +1 -2
  205. package/dist/table/gridBorders.d.ts +11 -15
  206. package/dist/table/gridBorders.js +32 -18
  207. package/dist/table/resize.d.ts +5 -3
  208. package/dist/ui/CommentsPanel.d.ts +1 -3
  209. package/dist/ui/CommentsPanel.js +24 -15
  210. package/dist/ui/NotesPanel.js +2 -2
  211. package/dist/ui/TextMenu.d.ts +1 -3
  212. package/dist/ui/TextMenu.js +10 -6
  213. package/dist/ui/comments/CommentComposer.d.ts +6 -1
  214. package/dist/ui/comments/CommentComposer.js +26 -3
  215. package/package.json +1 -1
  216. package/dist/editor/externalClipboard.d.ts +0 -11
  217. package/dist/editor/externalClipboard.js +0 -456
@@ -1,6 +1,9 @@
1
1
  // src/docx/importDocx.ts
2
- import { Fragment } from "prosemirror-model";
3
- import { parseNumbering } from "../numbering/parseNumbering.js";
2
+ import {
3
+ bindsWritingPrefix,
4
+ conformanceOf,
5
+ STRICT_OFFICE_DOCUMENT_REL
6
+ } from "../ooxml/conformance.js";
4
7
  import { DocxImportError } from "../ooxml/errors.js";
5
8
  import {
6
9
  childByLocalName,
@@ -12,7 +15,10 @@ import {
12
15
  withXmlParser
13
16
  } from "../ooxml/xml.js";
14
17
  import { docxSchema } from "../schema/index.js";
15
- import { commentReferencesIn, readComments } from "./comments.js";
18
+ import {
19
+ commentReferencesIn,
20
+ readComments
21
+ } from "./comments.js";
16
22
  import { openParts } from "./container.js";
17
23
  import {
18
24
  DEFAULT_TAB_STOP_PT,
@@ -23,39 +29,53 @@ import { fidelityNotesOf } from "./fidelity.js";
23
29
  import {
24
30
  formattingContextOf,
25
31
  NO_DOCUMENT_DEFAULTS,
26
- paragraphAttrsOf,
27
32
  readDocumentDefaults,
28
- readParagraphStyles,
29
- resolveParagraph,
30
- runMarkUnder
33
+ readParagraphStyles
31
34
  } from "./formatting.js";
32
- import { readHeadersFooters } from "./headersFooters.js";
35
+ import { readHeaderFooterStories } from "./headersFooters.js";
33
36
  import { readLinkTargets } from "./hyperlink.js";
34
- import { buildParagraph } from "./importParagraph.js";
35
- import { buildTable } from "./importTable.js";
37
+ import { NO_IMPORT_SOURCES } from "./importParagraph.js";
36
38
  import { readImageSources } from "./media.js";
39
+ import { NUMBERING_REL_TYPE } from "./newLists.js";
37
40
  import { readNotes } from "./notes.js";
38
41
  import { readPart, relatedPartPath } from "./packageParts.js";
39
- import { readBodyGeometry } from "./pageGeometry.js";
42
+ import { A4_PORTRAIT } from "./pageGeometry.js";
40
43
  import { readRelationships } from "./relationships.js";
41
44
  import { scanBody } from "./scan.js";
45
+ import {
46
+ firstSectPrElement,
47
+ readSectionProperties,
48
+ storyReferenceIds
49
+ } from "./sections.js";
42
50
  import {
43
51
  BODY_STORY_KEY,
44
52
  blockKey,
45
53
  newSessionId,
46
54
  SessionStore
47
55
  } from "./session.js";
56
+ import {
57
+ buildBlock,
58
+ readStories,
59
+ storiesByKey,
60
+ withStyleFormats
61
+ } from "./story.js";
48
62
  import { NO_THEME_FONTS, readThemeFonts } from "./theme.js";
49
63
  var OFFICE_DOCUMENT_REL = `${R_NS}/officeDocument`;
50
64
  var STYLES_REL = `${R_NS}/styles`;
51
- var NUMBERING_REL = `${R_NS}/numbering`;
52
65
  var THEME_REL = `${R_NS}/theme`;
53
66
  var SETTINGS_REL = `${R_NS}/settings`;
54
67
  function findMainPartPath(parts) {
55
- const target = readRelationships(parts, "_rels/.rels").find(
68
+ const relationships = readRelationships(parts, "_rels/.rels");
69
+ const target = relationships.find(
56
70
  (rel) => rel.type === OFFICE_DOCUMENT_REL
57
71
  )?.target;
58
72
  if (!target) {
73
+ if (relationships.some((rel) => rel.type === STRICT_OFFICE_DOCUMENT_REL)) {
74
+ throw new DocxImportError(
75
+ "unsupported-conformance",
76
+ "the package relates its main document part as an ECMA-376 Strict one"
77
+ );
78
+ }
59
79
  throw new DocxImportError(
60
80
  "missing-part",
61
81
  "no relationship pointing at the main document part"
@@ -63,60 +83,25 @@ function findMainPartPath(parts) {
63
83
  }
64
84
  return target.replace(/^\//, "");
65
85
  }
66
- function buildBlock(el, srcId, sources, context) {
67
- if (el.localName === "bookmarkStart" || el.localName === "bookmarkEnd") {
68
- return docxSchema.nodes.bookmarkBlock.create({
69
- srcId,
70
- name: el.nodeName
71
- });
72
- }
73
- if (el.localName === "p") {
74
- const paragraph = buildParagraph(el, srcId, sources);
75
- if (paragraph) return paragraph;
86
+ function assertWritableMainPart(root) {
87
+ if (conformanceOf(root) === "strict") {
88
+ throw new DocxImportError(
89
+ "unsupported-conformance",
90
+ "the main document part is written in the ECMA-376 Strict vocabulary"
91
+ );
76
92
  }
77
- if (el.localName === "tbl") {
78
- const table = buildTable(
79
- el,
80
- srcId,
81
- sources,
82
- context.styles,
83
- context.defaultTableStyleId
93
+ if (conformanceOf(root) !== "transitional" || root.localName !== "document") {
94
+ throw new DocxImportError(
95
+ "unsupported-content",
96
+ "the main part is not a Transitional WordprocessingML document"
84
97
  );
85
- if (table) return table;
86
98
  }
87
- return docxSchema.nodes.docxRaw.create({ srcId, name: el.nodeName });
88
- }
89
- function styledParagraph(node, context) {
90
- const pPr = node.attrs.pPr;
91
- const paragraph = resolveParagraph(
92
- typeof pPr === "string" ? pPr : null,
93
- context
94
- );
95
- const inline = node.children.map((child) => {
96
- const mark = runMarkUnder(
97
- child.marks.find((entry) => entry.type === docxSchema.marks.run) ?? null,
98
- child.isText,
99
- paragraph,
100
- context
99
+ if (!bindsWritingPrefix(root)) {
100
+ throw new DocxImportError(
101
+ "unsupported-content",
102
+ "the main document part binds WordprocessingML to a prefix other than w"
101
103
  );
102
- return mark ? child.mark(mark.addToSet(child.marks)) : child;
103
- });
104
- return node.type.create(
105
- { ...node.attrs, ...paragraphAttrsOf(paragraph) },
106
- Fragment.fromArray(inline),
107
- node.marks
108
- );
109
- }
110
- function withStyleFormats(node, context) {
111
- if (node.type === docxSchema.nodes.paragraph) {
112
- return styledParagraph(node, context);
113
104
  }
114
- if (node.childCount === 0) return node;
115
- const children = node.children.map(
116
- (child) => withStyleFormats(child, context)
117
- );
118
- if (children.every((child, i) => child === node.child(i))) return node;
119
- return node.copy(Fragment.fromArray(children));
120
105
  }
121
106
  function assertScanMatchesDom(children, scan) {
122
107
  if (children.length !== scan.blocks.length) {
@@ -134,16 +119,46 @@ function assertScanMatchesDom(children, scan) {
134
119
  }
135
120
  });
136
121
  }
137
- function foldTrailingSectPr(scan) {
122
+ function splitTrailingSectPr(scan) {
138
123
  const last = scan.blocks.at(-1);
139
- if (scan.blocks.length < 2 || !last) return scan;
140
- if (localPart(last.name) !== "sectPr") return scan;
124
+ if (!last || localPart(last.name) !== "sectPr") {
125
+ return { ...scan, sectPr: null };
126
+ }
141
127
  return {
142
128
  prefix: scan.prefix,
143
129
  blocks: scan.blocks.slice(0, -1),
144
- suffix: last.xml + scan.suffix
130
+ suffix: scan.suffix,
131
+ sectPr: last.xml
145
132
  };
146
133
  }
134
+ function storyDeps(parts, partPath, sessionId, formatting) {
135
+ return {
136
+ session: { sessionId },
137
+ sources: {
138
+ ...NO_IMPORT_SOURCES,
139
+ images: readImageSources(parts, partPath),
140
+ links: readLinkTargets(parts, partPath),
141
+ themeFonts: formatting.themeFonts
142
+ },
143
+ formatting
144
+ };
145
+ }
146
+ function readCommentStories(comments, parts, sessionId, formatting) {
147
+ const { partPath, xml } = comments;
148
+ if (partPath === null || xml === null) return [];
149
+ return readStories(
150
+ { kind: "comment", partPath, xml, entryName: "comment", idAttr: "id" },
151
+ storyDeps(parts, partPath, sessionId, formatting)
152
+ );
153
+ }
154
+ function readNoteStories(part, kind, parts, sessionId, formatting) {
155
+ const { partPath, xml } = part;
156
+ if (partPath === null || xml === null) return [];
157
+ return readStories(
158
+ { kind, partPath, xml, entryName: kind, idAttr: "id" },
159
+ storyDeps(parts, partPath, sessionId, formatting)
160
+ );
161
+ }
147
162
  function importDocx(input, options) {
148
163
  return withXmlParser(options?.xmlParser, () => readDocx(input));
149
164
  }
@@ -158,14 +173,17 @@ function readDocx(input) {
158
173
  const { text: source, hadBom } = decodeUtf8(mainPart);
159
174
  const scanned = scanBody(source);
160
175
  const dom = parseXml(source);
176
+ assertWritableMainPart(dom.documentElement);
161
177
  const body = childByLocalName(dom.documentElement, "body");
162
178
  if (!body) {
163
179
  throw new DocxImportError("missing-body", "document has no w:body");
164
180
  }
165
181
  const children = elementChildren(body);
166
182
  assertScanMatchesDom(children, scanned);
167
- const geometry = readBodyGeometry(body);
168
- const scan = foldTrailingSectPr(scanned);
183
+ const firstSectPr = firstSectPrElement(body);
184
+ const firstSection = firstSectPr ? readSectionProperties(firstSectPr) : null;
185
+ const geometry = firstSection?.geometry ?? A4_PORTRAIT;
186
+ const scan = splitTrailingSectPr(scanned);
169
187
  const blockElements = children.slice(0, scan.blocks.length);
170
188
  const stylesXml = readPart(
171
189
  parts,
@@ -178,7 +196,11 @@ function readDocx(input) {
178
196
  );
179
197
  const settingsDom = settingsXml === null ? null : parseXml(settingsXml);
180
198
  const defaultTabStopPt = readDefaultTabStop(settingsDom) ?? DEFAULT_TAB_STOP_PT;
181
- const numberingPartPath = relatedPartPath(parts, mainPartPath, NUMBERING_REL);
199
+ const numberingPartPath = relatedPartPath(
200
+ parts,
201
+ mainPartPath,
202
+ NUMBERING_REL_TYPE
203
+ );
182
204
  const themeXml = readPart(
183
205
  parts,
184
206
  relatedPartPath(parts, mainPartPath, THEME_REL)
@@ -187,7 +209,7 @@ function readDocx(input) {
187
209
  const numberingXml = readPart(parts, numberingPartPath);
188
210
  const formatting = formattingContextOf(
189
211
  stylesDom,
190
- parseNumbering(numberingXml),
212
+ numberingXml,
191
213
  themeFonts,
192
214
  readCompatSettings(settingsDom)
193
215
  );
@@ -205,7 +227,6 @@ function readDocx(input) {
205
227
  labels.set(id, label);
206
228
  return label;
207
229
  };
208
- const headersFooters = readHeadersFooters(parts, mainPartPath, body);
209
230
  const sources = {
210
231
  images: readImageSources(parts, mainPartPath),
211
232
  themeFonts,
@@ -215,6 +236,24 @@ function readDocx(input) {
215
236
  noteLabel
216
237
  };
217
238
  const sessionId = newSessionId();
239
+ const headerFooters = readHeaderFooterStories(
240
+ parts,
241
+ mainPartPath,
242
+ storyReferenceIds(body),
243
+ (partPath) => storyDeps(parts, partPath, sessionId, formatting)
244
+ );
245
+ const stories = storiesByKey([
246
+ ...readCommentStories(comments, parts, sessionId, formatting),
247
+ ...readNoteStories(
248
+ notes.footnotes,
249
+ "footnote",
250
+ parts,
251
+ sessionId,
252
+ formatting
253
+ ),
254
+ ...readNoteStories(notes.endnotes, "endnote", parts, sessionId, formatting),
255
+ ...headerFooters.stories
256
+ ]);
218
257
  const blockNodes = blockElements.map(
219
258
  (el, i) => withStyleFormats(
220
259
  buildBlock(
@@ -226,7 +265,25 @@ function readDocx(input) {
226
265
  formatting
227
266
  )
228
267
  );
229
- const doc = docxSchema.nodes.doc.create(null, blockNodes);
268
+ if (blockNodes.length === 0 && scan.sectPr !== null) {
269
+ blockNodes.push(
270
+ withStyleFormats(
271
+ docxSchema.nodes.paragraph.create({
272
+ srcId: blockKey({ sessionId }, BODY_STORY_KEY, 0)
273
+ }),
274
+ formatting
275
+ )
276
+ );
277
+ }
278
+ const doc = docxSchema.nodes.doc.create(
279
+ {
280
+ sectPr: scan.sectPr,
281
+ stories: Object.fromEntries(
282
+ Array.from(stories, ([key, story]) => [key, story.doc.toJSON()])
283
+ )
284
+ },
285
+ blockNodes
286
+ );
230
287
  return {
231
288
  doc,
232
289
  notes: fidelityNotesOf(doc, mainPartPath),
@@ -237,7 +294,10 @@ function readDocx(input) {
237
294
  documentPrefix: scan.prefix,
238
295
  documentSuffix: scan.suffix,
239
296
  documentHadBom: hadBom,
240
- blocks: blockNodes.map((node, i) => ({ xml: scan.blocks[i].xml, node })),
297
+ blocks: blockNodes.map((node, i) => ({
298
+ xml: scan.blocks[i]?.xml ?? "",
299
+ node
300
+ })),
241
301
  defaults: stylesDom ? readDocumentDefaults(stylesDom, themeFonts) : NO_DOCUMENT_DEFAULTS,
242
302
  defaultTabStopPt,
243
303
  geometry,
@@ -247,7 +307,8 @@ function readDocx(input) {
247
307
  numberingPartPath,
248
308
  comments,
249
309
  commentReferenceIds: new Set(commentReferencesIn(doc).keys()),
250
- headersFooters
310
+ headerFooterStories: headerFooters.refs,
311
+ stories
251
312
  })
252
313
  };
253
314
  }
@@ -6,10 +6,16 @@
6
6
  * A `w:sdt` content control and a `w:hyperlink` standing inside the paragraph are both unwrapped, so
7
7
  * the text they hold stays editable, and the wrapper each came in rides along on a mark to go back
8
8
  * out around the same text. A control may hold a link; a link holding a control is a nesting the
9
- * marks cannot record in that order, so the paragraph stays preserved.
9
+ * marks cannot record in that order, and that control stays whole as one preserved fragment.
10
+ *
11
+ * Nothing here demotes a paragraph. What the editor has no model for - a field character, a
12
+ * tracked insertion, a symbol, a drawing nobody could read - is kept where it stood, inside its
13
+ * run or beside it, by the rule `docx/importPolicy` gives it for the level it stands at. The
14
+ * paragraph around it stays editable, which is what a document full of `w:lastRenderedPageBreak`
15
+ * needs.
10
16
  */
11
17
  import type { Node as PMNode } from "prosemirror-model";
12
- import { type ImportedComments } from "./comments";
18
+ import { type ImportedComments } from "./comments/reading";
13
19
  import { type LinkTargets } from "./hyperlink";
14
20
  import type { ImageSources } from "./media";
15
21
  import { type ImportedNotes, type NoteKind } from "./notes";
@@ -28,5 +34,11 @@ export interface ImportSources {
28
34
  noteLabel: (kind: NoteKind, id: string) => string;
29
35
  }
30
36
  export declare const NO_IMPORT_SOURCES: ImportSources;
31
- /** Moves a paragraph into an editable node. null if it holds content we do not model */
32
- export declare function buildParagraph(el: Element, srcId: string | null, sources?: ImportSources): PMNode | null;
37
+ /**
38
+ * Moves a paragraph into an editable node.
39
+ *
40
+ * Every paragraph opens editable. A child the reader has no model for, and a control or a link
41
+ * whose shape it could not put back together, is kept whole where it stood instead of standing
42
+ * the whole paragraph down.
43
+ */
44
+ export declare function buildParagraph(el: Element, srcId: string | null, sources?: ImportSources): PMNode;
@@ -9,16 +9,19 @@ import {
9
9
  serializeXml
10
10
  } from "../ooxml/xml.js";
11
11
  import { docxSchema } from "../schema/index.js";
12
- import {
13
- commentParaId,
14
- importedCommentReplies,
15
- NO_COMMENTS
16
- } from "./comments.js";
12
+ import { commentParaId, importedCommentReplies } from "./comments/model.js";
13
+ import { NO_COMMENTS } from "./comments/reading.js";
17
14
  import { readParagraphFormat, readRunFormat } from "./formatting.js";
18
15
  import {
19
16
  NO_LINK_TARGETS,
20
17
  readHyperlinkWrapper
21
18
  } from "./hyperlink.js";
19
+ import { policyFor } from "./importPolicy.js";
20
+ import {
21
+ buildPreservedInline,
22
+ buildPreservedRunContent,
23
+ preservationOf
24
+ } from "./importPreserved.js";
22
25
  import { NO_IMAGES } from "./media.js";
23
26
  import { NO_NOTES, noteById } from "./notes.js";
24
27
  import { readSdtWrapper } from "./sdt.js";
@@ -47,7 +50,7 @@ function buildImage(el, marks, images) {
47
50
  marks
48
51
  );
49
52
  }
50
- function buildRunChild(el, marks, images, comments, notes, noteLabel) {
53
+ function buildModelledRunChild(el, marks, images, comments, notes, noteLabel) {
51
54
  switch (el.localName) {
52
55
  case "t": {
53
56
  const text = el.textContent ?? "";
@@ -86,9 +89,6 @@ function buildRunChild(el, marks, images, comments, notes, noteLabel) {
86
89
  authorId: comment?.authorId ?? null,
87
90
  initials: comment?.initials ?? null,
88
91
  date: comment?.date ?? null,
89
- text: comment?.text ?? "",
90
- commentXml: comment?.xml ?? null,
91
- imported: true,
92
92
  paraId,
93
93
  resolved: comment?.resolved ?? false,
94
94
  extensionXml: comment?.extensionXml ?? null,
@@ -114,7 +114,6 @@ function buildRunChild(el, marks, images, comments, notes, noteLabel) {
114
114
  kind,
115
115
  id,
116
116
  label: note ? noteLabel(kind, id) : "?",
117
- text: note?.text ?? "",
118
117
  customMarkFollows,
119
118
  referenceXml: serializeXml(el)
120
119
  },
@@ -135,22 +134,30 @@ function buildRunNodes(run, images, themeFonts, comments, notes, noteLabel, wrap
135
134
  );
136
135
  const nodes = [];
137
136
  for (const child of elementChildren(run)) {
138
- if (child.localName === "rPr") continue;
139
- const built = buildRunChild(
140
- child,
141
- marks,
142
- images,
143
- comments,
144
- notes,
145
- noteLabel
137
+ const policy = policyFor(child, "r");
138
+ if (policy.tier === "model") {
139
+ if (child.localName === "rPr") continue;
140
+ const built = buildModelledRunChild(
141
+ child,
142
+ marks,
143
+ images,
144
+ comments,
145
+ notes,
146
+ noteLabel
147
+ );
148
+ if (built !== null) {
149
+ nodes.push(...built);
150
+ continue;
151
+ }
152
+ }
153
+ nodes.push(
154
+ buildPreservedRunContent(child, marks, preservationOf(policy, "r"))
146
155
  );
147
- if (built === null) return null;
148
- nodes.push(...built);
149
156
  }
150
157
  if (nodes.length === 0) {
151
158
  return [
152
159
  docxSchema.nodes.rawInline.create(
153
- { xml: serializeXml(run) },
160
+ { xml: serializeXml(run), element: run.localName },
154
161
  null,
155
162
  // The wrappers have to close again around this XML on export, so their marks ride along here too
156
163
  wrappers
@@ -166,13 +173,6 @@ function nextKey(counts, el) {
166
173
  counts.set(el.ownerDocument, key + 1);
167
174
  return key;
168
175
  }
169
- var NOTHING_ON_SCREEN = [
170
- "bookmarkStart",
171
- "bookmarkEnd",
172
- "proofErr",
173
- "permStart",
174
- "permEnd"
175
- ];
176
176
  function annotationId(el) {
177
177
  return Array.from(el.attributes).find((attribute) => attribute.localName === "id")?.value ?? null;
178
178
  }
@@ -190,41 +190,42 @@ var NO_IMPORT_SOURCES = {
190
190
  notes: NO_NOTES,
191
191
  noteLabel: () => "?"
192
192
  };
193
+ function buildModelledWrapperChild(child, sources, wrappers, linkAllowed) {
194
+ if (child.localName === "r") {
195
+ return buildRunNodes(
196
+ child,
197
+ sources.images,
198
+ sources.themeFonts,
199
+ sources.comments,
200
+ sources.notes,
201
+ sources.noteLabel,
202
+ wrappers
203
+ );
204
+ }
205
+ if (child.localName === "hyperlink") {
206
+ return linkAllowed ? buildHyperlinkNodes(child, sources, wrappers) : null;
207
+ }
208
+ const commentMarker = commentRangeNode(child, wrappers);
209
+ return commentMarker === null ? null : [commentMarker];
210
+ }
193
211
  function buildWrappedNodes(content, sources, wrappers, linkAllowed) {
194
212
  const nodes = [];
195
213
  for (const child of elementChildren(content)) {
196
- if (child.localName === "r") {
197
- const built = buildRunNodes(
214
+ const policy = policyFor(child, "wrapper");
215
+ if (policy.tier === "model") {
216
+ const built = buildModelledWrapperChild(
198
217
  child,
199
- sources.images,
200
- sources.themeFonts,
201
- sources.comments,
202
- sources.notes,
203
- sources.noteLabel,
204
- wrappers
218
+ sources,
219
+ wrappers,
220
+ linkAllowed
205
221
  );
206
- if (built === null) return null;
207
- nodes.push(...built);
208
- continue;
209
- }
210
- if (child.localName === "hyperlink" && linkAllowed) {
211
- const built = buildHyperlinkNodes(child, sources, wrappers);
212
- if (built === null) return null;
213
- nodes.push(...built);
214
- continue;
222
+ if (built !== null) {
223
+ nodes.push(...built);
224
+ continue;
225
+ }
215
226
  }
216
- const commentMarker = commentRangeNode(child, wrappers);
217
- if (commentMarker) {
218
- nodes.push(commentMarker);
219
- continue;
220
- }
221
- if (!NOTHING_ON_SCREEN.includes(child.localName)) return null;
222
227
  nodes.push(
223
- docxSchema.nodes.rawInline.create(
224
- { xml: serializeXml(child) },
225
- null,
226
- wrappers
227
- )
228
+ buildPreservedInline(child, wrappers, preservationOf(policy, "wrapper"))
228
229
  );
229
230
  }
230
231
  if (nodes.length === 0) return null;
@@ -251,48 +252,41 @@ function buildSdtNodes(el, sources) {
251
252
  });
252
253
  return buildWrappedNodes(wrapper.content, sources, [mark], true);
253
254
  }
255
+ function buildModelledParagraphChild(child, sources) {
256
+ if (child.localName === "r") {
257
+ return buildRunNodes(
258
+ child,
259
+ sources.images,
260
+ sources.themeFonts,
261
+ sources.comments,
262
+ sources.notes,
263
+ sources.noteLabel
264
+ );
265
+ }
266
+ if (child.localName === "sdt") return buildSdtNodes(child, sources);
267
+ if (child.localName === "hyperlink") {
268
+ return buildHyperlinkNodes(child, sources, []);
269
+ }
270
+ const commentMarker = commentRangeNode(child);
271
+ return commentMarker === null ? null : [commentMarker];
272
+ }
254
273
  function buildParagraph(el, srcId, sources = NO_IMPORT_SOURCES) {
255
- const { images, themeFonts } = sources;
256
274
  const inline = [];
257
275
  let pPrElement = null;
258
276
  for (const child of elementChildren(el)) {
259
- if (child.localName === "pPr") {
260
- pPrElement = child;
261
- continue;
277
+ const policy = policyFor(child, "p");
278
+ if (policy.tier === "model") {
279
+ if (child.localName === "pPr") {
280
+ pPrElement = child;
281
+ continue;
282
+ }
283
+ const built = buildModelledParagraphChild(child, sources);
284
+ if (built !== null) {
285
+ inline.push(...built);
286
+ continue;
287
+ }
262
288
  }
263
- if (child.localName === "r") {
264
- const runNodes = buildRunNodes(
265
- child,
266
- images,
267
- themeFonts,
268
- sources.comments,
269
- sources.notes,
270
- sources.noteLabel
271
- );
272
- if (runNodes === null) return null;
273
- inline.push(...runNodes);
274
- continue;
275
- }
276
- if (child.localName === "sdt") {
277
- const sdtNodes = buildSdtNodes(child, sources);
278
- if (sdtNodes === null) return null;
279
- inline.push(...sdtNodes);
280
- continue;
281
- }
282
- if (child.localName === "hyperlink") {
283
- const linkNodes = buildHyperlinkNodes(child, sources, []);
284
- if (linkNodes === null) return null;
285
- inline.push(...linkNodes);
286
- continue;
287
- }
288
- const commentMarker = commentRangeNode(child);
289
- if (commentMarker) {
290
- inline.push(commentMarker);
291
- continue;
292
- }
293
- inline.push(
294
- docxSchema.nodes.rawInline.create({ xml: serializeXml(child) })
295
- );
289
+ inline.push(buildPreservedInline(child, [], preservationOf(policy, "p")));
296
290
  }
297
291
  return docxSchema.nodes.paragraph.create(
298
292
  {
@@ -0,0 +1,70 @@
1
+ /**
2
+ * What every WordprocessingML element the readers may meet is worth keeping as, one sub-table per
3
+ * level of the document.
4
+ *
5
+ * A reader used to know a handful of elements and answer `null` for the rest, and that `null`
6
+ * travelled up through the run and the wrapper until a whole paragraph stood as a placeholder. The
7
+ * table below turns that around: every level has a narrowest preservation of its own - a run child
8
+ * stays inside its run, a paragraph child inside its paragraph, a block stays a block - so an
9
+ * element nobody modelled costs the element and nothing around it.
10
+ *
11
+ * Only `tbl` and `tr` have no node to hold a stranger (`table` holds rows, `tableRow` holds
12
+ * cells), so those two levels demote the table instead, and that is the whole of what demotion is
13
+ * now: this default and a structure a reader could not take apart.
14
+ *
15
+ * A level is a content model of `wml.xsd` rather than an element name, because the same name means
16
+ * different things in different places: `sdt` is a control the paragraph reader unwraps, a cell
17
+ * wrapper under `tr`, a block placeholder under the body, and a row wrapper nothing reads. Keys are
18
+ * Clark names (`{namespace}localName`) so that `m:oMath` and `w:sdt` sit in one map.
19
+ */
20
+ import type { PreservedDisplay } from "../schema";
21
+ export type { PreservedDisplay };
22
+ export type PreservationTier =
23
+ /** A reader turns it into an editable node */
24
+ "model"
25
+ /** A range marker: invisible, and its pair and order must survive every edit */
26
+ | "marker"
27
+ /** A trace the producer regenerates: invisible, and an edit may drop it */
28
+ | "ignorable"
29
+ /** A `CT_R` child: kept verbatim inside its run, wearing the run mark */
30
+ | "runContent"
31
+ /** A paragraph child: kept verbatim inside the paragraph */
32
+ | "inline"
33
+ /** A body or cell child: kept verbatim as a block placeholder */
34
+ | "block";
35
+ export interface PreservationRule {
36
+ readonly tier: Exclude<PreservationTier, "model">;
37
+ readonly display: PreservedDisplay;
38
+ /** The character a `text` display puts on screen */
39
+ readonly text?: string;
40
+ /**
41
+ * Whether the deletion guard answers for this element: a range marker whose pair must survive,
42
+ * and a field piece whose begin/separate/end order must survive.
43
+ */
44
+ readonly guarded: boolean;
45
+ }
46
+ export type ElementPolicy = {
47
+ readonly tier: "model";
48
+ } | PreservationRule;
49
+ /** What a level with no node to hold a stranger answers: the container around it is demoted */
50
+ export type DemotionPolicy = {
51
+ readonly tier: "demote";
52
+ };
53
+ /** The levels a reader walks. Each is a content model of its own in `wml.xsd` */
54
+ export type ContentLevel = "body" | "tbl" | "tr" | "tc" | "p" | "wrapper" | "r";
55
+ export declare function clarkName(el: Element): string;
56
+ export declare const WML_POLICY: Readonly<Record<ContentLevel, ReadonlyMap<string, ElementPolicy>>>;
57
+ /** The levels that have a node of their own to keep a stranger in */
58
+ export type PreservingLevel = Exclude<ContentLevel, "tbl" | "tr">;
59
+ export declare function defaultPreservation(level: PreservingLevel): PreservationRule;
60
+ export declare function policyFor(el: Element, level: ContentLevel): ElementPolicy | DemotionPolicy;
61
+ /**
62
+ * The text one run child puts on screen, or null for a child that puts nothing there.
63
+ *
64
+ * This is where the table's answer becomes text, and a preserved fragment carries it along as the
65
+ * `text` a reader wrote onto it. Everything reading a story as plain text afterwards - a footnote
66
+ * body, a comment body, a header - asks `docx/story`'s `storyLeafText` for that same answer rather
67
+ * than keeping a vocabulary of its own, so the three of them cannot disagree about what a `w:cr`
68
+ * or a `w:noBreakHyphen` reads as.
69
+ */
70
+ export declare function runContentText(el: Element): string | null;