@portone/docx-editor 0.4.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 (155) hide show
  1. package/CHANGELOG.md +159 -0
  2. package/assets/editor.png +0 -0
  3. package/dist/DocxEditor.d.ts +1 -1
  4. package/dist/DocxEditor.js +44 -25
  5. package/dist/docx/cloning.js +4 -9
  6. package/dist/docx/commentOnlyChange.d.ts +5 -3
  7. package/dist/docx/comments/grammar.d.ts +27 -10
  8. package/dist/docx/comments/grammar.js +18 -67
  9. package/dist/docx/comments/model.d.ts +5 -6
  10. package/dist/docx/comments/model.js +0 -9
  11. package/dist/docx/comments/parts.d.ts +16 -10
  12. package/dist/docx/comments/parts.js +65 -13
  13. package/dist/docx/comments/people.d.ts +11 -3
  14. package/dist/docx/comments/people.js +20 -6
  15. package/dist/docx/comments/policy.js +7 -1
  16. package/dist/docx/comments/reading.d.ts +4 -2
  17. package/dist/docx/comments/reading.js +1 -14
  18. package/dist/docx/comments/writing.js +65 -28
  19. package/dist/docx/exportDocx.js +10 -14
  20. package/dist/docx/exportRefs.d.ts +8 -3
  21. package/dist/docx/exportRefs.js +2 -1
  22. package/dist/docx/fidelity.js +24 -10
  23. package/dist/docx/fields.d.ts +42 -0
  24. package/dist/docx/fields.js +76 -0
  25. package/dist/docx/headersFooters.d.ts +89 -20
  26. package/dist/docx/headersFooters.js +163 -172
  27. package/dist/docx/importDocx.d.ts +1 -1
  28. package/dist/docx/importDocx.js +100 -48
  29. package/dist/docx/importParagraph.d.ts +16 -4
  30. package/dist/docx/importParagraph.js +87 -93
  31. package/dist/docx/importPolicy.d.ts +70 -0
  32. package/dist/docx/importPolicy.js +190 -0
  33. package/dist/docx/importPreserved.d.ts +30 -0
  34. package/dist/docx/importPreserved.js +54 -0
  35. package/dist/docx/importTable.js +56 -12
  36. package/dist/docx/invariants.js +16 -18
  37. package/dist/docx/notes.d.ts +6 -1
  38. package/dist/docx/notes.js +6 -16
  39. package/dist/docx/pageGeometry.d.ts +5 -14
  40. package/dist/docx/pageGeometry.js +0 -12
  41. package/dist/docx/protectionPolicy.d.ts +13 -5
  42. package/dist/docx/protectionPolicy.js +32 -22
  43. package/dist/docx/scan.d.ts +18 -8
  44. package/dist/docx/scan.js +17 -11
  45. package/dist/docx/sections.d.ts +126 -0
  46. package/dist/docx/sections.js +207 -0
  47. package/dist/docx/serializeBlock.d.ts +13 -4
  48. package/dist/docx/serializeBlock.js +16 -18
  49. package/dist/docx/serializeParagraph.d.ts +4 -0
  50. package/dist/docx/serializeParagraph.js +1 -0
  51. package/dist/docx/serializePreserved.d.ts +14 -0
  52. package/dist/docx/serializePreserved.js +24 -0
  53. package/dist/docx/serializeStory.d.ts +17 -0
  54. package/dist/docx/serializeStory.js +17 -0
  55. package/dist/docx/serializeTable.js +10 -13
  56. package/dist/docx/session.d.ts +23 -6
  57. package/dist/docx/session.js +26 -5
  58. package/dist/docx/story.d.ts +140 -0
  59. package/dist/docx/story.js +237 -0
  60. package/dist/docx/storyProjection.js +6 -1
  61. package/dist/editor/clipboard/htmlReader.d.ts +30 -0
  62. package/dist/editor/{externalClipboard.js → clipboard/htmlReader.js} +39 -223
  63. package/dist/editor/clipboard/internalChannel.d.ts +57 -0
  64. package/dist/editor/clipboard/internalChannel.js +58 -0
  65. package/dist/editor/clipboard/normalizers.d.ts +99 -0
  66. package/dist/editor/clipboard/normalizers.js +199 -0
  67. package/dist/editor/clipboard/parser.d.ts +34 -0
  68. package/dist/editor/clipboard/parser.js +58 -0
  69. package/dist/editor/clipboard/plugin.d.ts +21 -0
  70. package/dist/editor/clipboard/plugin.js +320 -0
  71. package/dist/editor/clipboard/readContext.d.ts +24 -0
  72. package/dist/editor/clipboard/readContext.js +19 -0
  73. package/dist/editor/clipboard/readers.d.ts +33 -0
  74. package/dist/editor/clipboard/readers.js +37 -0
  75. package/dist/editor/commands/comments/editing.d.ts +26 -6
  76. package/dist/editor/commands/comments/editing.js +175 -105
  77. package/dist/editor/commands/comments/model.d.ts +34 -17
  78. package/dist/editor/commands/comments/model.js +11 -0
  79. package/dist/editor/commands/comments/reading.d.ts +5 -3
  80. package/dist/editor/commands/comments/reading.js +8 -58
  81. package/dist/editor/commands/index.d.ts +2 -2
  82. package/dist/editor/commands/index.js +2 -0
  83. package/dist/editor/commands/listCommands.d.ts +6 -0
  84. package/dist/editor/commands/listCommands.js +3 -2
  85. package/dist/editor/commands/noteQueries.d.ts +22 -5
  86. package/dist/editor/commands/noteQueries.js +34 -6
  87. package/dist/editor/createEditor.js +13 -3
  88. package/dist/editor/documentStyles.d.ts +26 -4
  89. package/dist/editor/documentStyles.js +9 -4
  90. package/dist/editor/imageFiles.d.ts +2 -2
  91. package/dist/editor/imageFiles.js +2 -0
  92. package/dist/editor/insertTable.d.ts +1 -1
  93. package/dist/editor/insertTable.js +2 -2
  94. package/dist/editor/plainText.d.ts +12 -0
  95. package/dist/editor/plainText.js +2 -1
  96. package/dist/editor/plugins/columnResize.js +6 -3
  97. package/dist/editor/plugins/commentComposer.d.ts +28 -0
  98. package/dist/editor/plugins/commentComposer.js +46 -0
  99. package/dist/editor/plugins/commentDecorations.d.ts +29 -2
  100. package/dist/editor/plugins/commentDecorations.js +89 -15
  101. package/dist/editor/plugins/documentProjection.d.ts +31 -0
  102. package/dist/editor/plugins/documentProjection.js +22 -0
  103. package/dist/editor/plugins/imagePaste.js +15 -8
  104. package/dist/editor/plugins/linkPanel.d.ts +7 -3
  105. package/dist/editor/plugins/linkPanel.js +13 -27
  106. package/dist/editor/plugins/panelState.d.ts +67 -0
  107. package/dist/editor/plugins/panelState.js +54 -0
  108. package/dist/editor/plugins/tableContextMenu.d.ts +1 -1
  109. package/dist/editor/plugins/tableContextMenu.js +30 -47
  110. package/dist/editor/plugins/textContextMenu.d.ts +1 -1
  111. package/dist/editor/plugins/textContextMenu.js +30 -47
  112. package/dist/ooxml/fragment.d.ts +11 -0
  113. package/dist/ooxml/fragment.js +14 -4
  114. package/dist/ooxml/image.d.ts +3 -4
  115. package/dist/ooxml/image.js +50 -5
  116. package/dist/ooxml/names.d.ts +6 -0
  117. package/dist/ooxml/names.js +2 -0
  118. package/dist/ooxml/props.d.ts +8 -0
  119. package/dist/ooxml/props.js +14 -4
  120. package/dist/ooxml/rangeMarkers.d.ts +5 -0
  121. package/dist/ooxml/rangeMarkers.js +24 -0
  122. package/dist/page/PageGuides.d.ts +7 -3
  123. package/dist/page/PageGuides.js +13 -24
  124. package/dist/page/pageLayout.d.ts +64 -5
  125. package/dist/page/pageLayout.js +90 -29
  126. package/dist/page/usePageLayout.d.ts +14 -4
  127. package/dist/page/usePageLayout.js +19 -20
  128. package/dist/schema/attrRoles.js +38 -20
  129. package/dist/schema/docxSchema.d.ts +9 -1
  130. package/dist/schema/docxSchema.js +191 -139
  131. package/dist/schema/editGuard.d.ts +1 -1
  132. package/dist/schema/guards.js +2 -2
  133. package/dist/schema/index.d.ts +1 -0
  134. package/dist/schema/preservedFragments.d.ts +12 -0
  135. package/dist/schema/preservedFragments.js +38 -0
  136. package/dist/schema/preservedGuards.d.ts +9 -7
  137. package/dist/schema/preservedGuards.js +112 -11
  138. package/dist/schema/protection.d.ts +34 -5
  139. package/dist/schema/protection.js +42 -14
  140. package/dist/schema/rendering.js +3 -1
  141. package/dist/schema/stories.d.ts +49 -0
  142. package/dist/schema/stories.js +78 -0
  143. package/dist/styles/classNames.d.ts +3 -5
  144. package/dist/styles/classNames.js +3 -5
  145. package/dist/styles.css +74 -12
  146. package/dist/table/resize.d.ts +5 -3
  147. package/dist/ui/CommentsPanel.d.ts +1 -3
  148. package/dist/ui/CommentsPanel.js +24 -15
  149. package/dist/ui/NotesPanel.js +2 -2
  150. package/dist/ui/TextMenu.d.ts +1 -3
  151. package/dist/ui/TextMenu.js +10 -6
  152. package/dist/ui/comments/CommentComposer.d.ts +6 -1
  153. package/dist/ui/comments/CommentComposer.js +26 -3
  154. package/package.json +1 -1
  155. package/dist/editor/externalClipboard.d.ts +0 -22
@@ -11,7 +11,7 @@
11
11
  * undefined, and a caller holding the level alone has nothing to run.
12
12
  */
13
13
  import { type XmlParser } from "../ooxml/xml";
14
- import type { EditableComments, EditingProtection } from "../schema/protection";
14
+ import { type EditableComments, type EditingProtection } from "../schema/protection";
15
15
  import { type DocxBytes } from "./importDocx";
16
16
  import type { SessionStore } from "./session";
17
17
  import { type Story } from "./storyProjection";
@@ -59,8 +59,14 @@ export interface StoryPartKind {
59
59
  rootKept(before: SessionStore, after: SessionStore): boolean;
60
60
  /** The keys this file still stands behind, which is what an entry has to be keyed by */
61
61
  referents(story: Story): ReadonlySet<string>;
62
- /** Grammar alone: whether the entry is one this editor's writer could have put out, whoever it belongs to */
63
- wellFormed(entry: Element): boolean;
62
+ /**
63
+ * Grammar alone: whether the entry is one this editor's writer could have put out, whoever it
64
+ * belongs to. `original` is the entry the file that arrived held under the same key, and null
65
+ * for one that appeared, because what the writer can put out depends on what it had to work
66
+ * with: it writes a comment's body out of the blocks it models and passes the rest through from
67
+ * the entry it read.
68
+ */
69
+ wellFormed(entry: Element, original: Element | null): boolean;
64
70
  /**
65
71
  * Whether the entry came back differing from the one that arrived in nothing but a change this
66
72
  * protection leaves to everyone. Such an entry is nobody's rewrite, so it is held neither to
@@ -70,9 +76,11 @@ export interface StoryPartKind {
70
76
  /**
71
77
  * Permission alone: whether `authorId` may have written (`original === null`) or rewritten this
72
78
  * entry under `options`. `session` is the submission's, so a kind can look across at a sibling
73
- * part, the way a comment's author resolves through the people part.
79
+ * part, the way a comment's author resolves through the people part. `unattributed` is the
80
+ * display names the file that arrived writes comments under while recording nobody for them,
81
+ * which is what an entry claiming no identity is held against.
74
82
  */
75
- allowed(entry: Element, original: Element | null, authorId: string, options: PolicyOptions, session: SessionStore): boolean;
83
+ allowed(entry: Element, original: Element | null, authorId: string, options: PolicyOptions, session: SessionStore, unattributed: ReadonlySet<string>): boolean;
76
84
  }
77
85
  /** The two reasons the package comparison answers with, whatever the policy */
78
86
  export type PackageReason = "part-changed" | "relationship-changed";
@@ -5,6 +5,9 @@ import {
5
5
  parseXml,
6
6
  withXmlParser
7
7
  } from "../ooxml/xml.js";
8
+ import {
9
+ unattributedCommentAuthors
10
+ } from "../schema/protection.js";
8
11
  import { importDocx } from "./importDocx.js";
9
12
  import { CONTENT_TYPES_PATH } from "./packageParts.js";
10
13
  import {
@@ -31,9 +34,10 @@ function excusedPaths(policy, session) {
31
34
  (path) => path !== null
32
35
  );
33
36
  }
34
- function aroundTheStory(session) {
35
- const preserved = session.blocks.filter((block) => !isModelledBlock(block.node)).map((block) => block.xml).join("");
36
- return session.documentPrefix + preserved + session.documentSuffix;
37
+ function aroundTheStory(story) {
38
+ const preserved = story.session.blocks.filter((block) => !isModelledBlock(block.node)).map((block) => block.xml).join("");
39
+ const sectPr = story.doc.attrs.sectPr;
40
+ return story.session.documentPrefix + preserved + (typeof sectPr === "string" ? sectPr : "") + story.session.documentSuffix;
37
41
  }
38
42
  function relationshipsKept(before, after, relTypes) {
39
43
  if (new Set(before.map((entry) => entry.id)).size !== before.length || new Set(after.map((entry) => entry.id)).size !== after.length) {
@@ -77,41 +81,43 @@ function gainedPartsAreNew(policy, before, after) {
77
81
  );
78
82
  }
79
83
  function packageKept(policy, before, after) {
80
- if (before.mainPartPath !== after.mainPartPath) {
81
- return { ok: false, reason: "part-changed", part: before.mainPartPath };
84
+ const was = before.session;
85
+ const now = after.session;
86
+ if (was.mainPartPath !== now.mainPartPath) {
87
+ return { ok: false, reason: "part-changed", part: was.mainPartPath };
82
88
  }
83
- const relsPath = relsPathOf(before.mainPartPath);
89
+ const relsPath = relsPathOf(was.mainPartPath);
84
90
  const untouched = /* @__PURE__ */ new Set([
85
- before.mainPartPath,
91
+ was.mainPartPath,
86
92
  relsPath,
87
93
  CONTENT_TYPES_PATH,
88
- ...excusedPaths(policy, before),
89
- ...excusedPaths(policy, after)
94
+ ...excusedPaths(policy, was),
95
+ ...excusedPaths(policy, now)
90
96
  ]);
91
- for (const path of /* @__PURE__ */ new Set([...before.parts.keys(), ...after.parts.keys()])) {
97
+ for (const path of /* @__PURE__ */ new Set([...was.parts.keys(), ...now.parts.keys()])) {
92
98
  if (untouched.has(path)) continue;
93
- const was = before.parts.get(path);
94
- const now = after.parts.get(path);
95
- if (was === void 0 || now === void 0 || !sameBytes(was, now)) {
99
+ const arrived = was.parts.get(path);
100
+ const submitted = now.parts.get(path);
101
+ if (arrived === void 0 || submitted === void 0 || !sameBytes(arrived, submitted)) {
96
102
  return { ok: false, reason: "part-changed", part: path };
97
103
  }
98
104
  }
99
105
  if (!relationshipsKept(
100
- readRelationships(before.parts, relsPath),
101
- readRelationships(after.parts, relsPath),
106
+ readRelationships(was.parts, relsPath),
107
+ readRelationships(now.parts, relsPath),
102
108
  policy.parts.map((kind) => kind.relType)
103
- ) || !gainedPartsAreNew(policy, before, after)) {
109
+ ) || !gainedPartsAreNew(policy, was, now)) {
104
110
  return { ok: false, reason: "relationship-changed", part: relsPath };
105
111
  }
106
112
  if (!contentTypesKept(
107
- contentTypes(before.parts.get(CONTENT_TYPES_PATH)),
108
- contentTypes(after.parts.get(CONTENT_TYPES_PATH)),
113
+ contentTypes(was.parts.get(CONTENT_TYPES_PATH)),
114
+ contentTypes(now.parts.get(CONTENT_TYPES_PATH)),
109
115
  policy.parts.map((kind) => kind.contentType)
110
116
  )) {
111
117
  return { ok: false, reason: "part-changed", part: CONTENT_TYPES_PATH };
112
118
  }
113
119
  if (aroundTheStory(before) !== aroundTheStory(after)) {
114
- return { ok: false, reason: "part-changed", part: before.mainPartPath };
120
+ return { ok: false, reason: "part-changed", part: was.mainPartPath };
115
121
  }
116
122
  return { ok: true };
117
123
  }
@@ -120,18 +126,22 @@ function partKept(kind, before, after, authorId, options) {
120
126
  const submitted = kind.entriesIn(after.session, "submitted");
121
127
  if (submitted === null) return false;
122
128
  if (!kind.rootKept(before.session, after.session)) return false;
129
+ const unattributed = unattributedCommentAuthors(
130
+ before.session.comments.ordered
131
+ );
123
132
  const stoodBehindNow = kind.referents(after);
124
133
  for (const [id, entry] of submitted) {
125
134
  const original = arrived.get(id);
126
135
  if (original && original.xml === entry.xml) continue;
127
136
  if (!stoodBehindNow.has(id)) return false;
128
137
  if (original && kind.anyonesChange(entry.el, original.el)) continue;
129
- if (!kind.wellFormed(entry.el) || !kind.allowed(
138
+ if (!kind.wellFormed(entry.el, original?.el ?? null) || !kind.allowed(
130
139
  entry.el,
131
140
  original?.el ?? null,
132
141
  authorId,
133
142
  options,
134
- after.session
143
+ after.session,
144
+ unattributed
135
145
  )) {
136
146
  return false;
137
147
  }
@@ -155,7 +165,7 @@ function verifyChange(policy, original, submitted, authorId, { xmlParser, ...opt
155
165
  return withXmlParser(xmlParser, () => {
156
166
  const before = importDocx(original);
157
167
  const after = importDocx(submitted);
158
- const packaged = packageKept(policy, before.session, after.session);
168
+ const packaged = packageKept(policy, before, after);
159
169
  if (!packaged.ok) return packaged;
160
170
  const story = policy.storyKept(before, after, authorId, options);
161
171
  return story.ok ? partsKept(policy, before, after, authorId, options) : story;
@@ -1,20 +1,30 @@
1
1
  /**
2
- * Finds the span each individual body block occupies in the raw document.xml text and slices it out.
2
+ * Finds the span each individual block occupies in the raw text of a part and slices it out.
3
3
  *
4
4
  * A block that was not edited is exported by writing this fragment back out untouched,
5
- * which keeps its original bytes intact.
5
+ * which keeps its original bytes intact. The body of the main part is one such run of blocks;
6
+ * a comment and a footnote hold another (`docx/story`), and all of them are sliced here so that
7
+ * one story cannot end up compared or written by rules another does not follow.
6
8
  */
7
- export interface BodyBlockSlice {
9
+ import { type Tag } from "../ooxml/tagScan";
10
+ export interface BlockSlice {
8
11
  /** The tag name exactly as written (e.g. "w:p", "w:tbl", "w:sectPr") */
9
12
  name: string;
10
13
  /** The original XML fragment. It also carries the whitespace that sat between this block and the one before it */
11
14
  xml: string;
12
15
  }
13
- export interface BodyScan {
14
- /** Everything in the raw text before the first block */
16
+ export interface BlockScan {
17
+ /** Everything in the raw text before the first block, the container's own opening tag included */
15
18
  prefix: string;
16
- blocks: BodyBlockSlice[];
17
- /** Everything in the raw text after the last block */
19
+ blocks: BlockSlice[];
20
+ /** Everything in the raw text after the last block, the container's closing tag included */
18
21
  suffix: string;
19
22
  }
20
- export declare function scanBody(source: string): BodyScan;
23
+ /** The tag whose children are the blocks, asked of every opening tag with the depth it stands at */
24
+ export type ContainerTest = (tag: Tag, depth: number) => boolean;
25
+ /**
26
+ * The blocks the first container this test names holds, and the text on either side of them.
27
+ * null for a source holding no such container; a caller says in its own words what that means.
28
+ */
29
+ export declare function scanBlocksIn(source: string, isContainer: ContainerTest): BlockScan | null;
30
+ export declare function scanBody(source: string): BlockScan;
package/dist/docx/scan.js CHANGED
@@ -13,15 +13,15 @@ function tagAt(source, lt) {
13
13
  return tag;
14
14
  }
15
15
  function isBodyTag(tag, depth) {
16
- return localPart(tag.name) === "body" && depth === 1 && tag.kind === "open";
16
+ return localPart(tag.name) === "body" && depth === 1;
17
17
  }
18
- function scanBody(source) {
18
+ function scanBlocksIn(source, isContainer) {
19
19
  const stack = [];
20
20
  const blocks = [];
21
- let bodyDepth = null;
21
+ let containerDepth = null;
22
22
  let contentStart = -1;
23
23
  let sliceStart = -1;
24
- const atBodyLevel = () => bodyDepth !== null && stack.length === bodyDepth;
24
+ const atBlockLevel = () => containerDepth !== null && stack.length === containerDepth;
25
25
  const takeBlock = (name, end) => {
26
26
  blocks.push({ name, xml: source.slice(sliceStart, end) });
27
27
  sliceStart = end;
@@ -43,24 +43,22 @@ function scanBody(source) {
43
43
  `mismatched tags: ${opened} vs ${tag.name}`
44
44
  );
45
45
  }
46
- if (atBodyLevel()) takeBlock(tag.name, tag.end);
46
+ if (atBlockLevel()) takeBlock(tag.name, tag.end);
47
47
  i = tag.end;
48
48
  continue;
49
49
  }
50
- if (bodyDepth === null && isBodyTag(tag, stack.length)) {
50
+ if (containerDepth === null && tag.kind === "open" && isContainer(tag, stack.length)) {
51
51
  stack.push(tag.name);
52
- bodyDepth = stack.length;
52
+ containerDepth = stack.length;
53
53
  contentStart = tag.end;
54
54
  sliceStart = tag.end;
55
55
  } else {
56
- if (atBodyLevel() && tag.kind === "empty") takeBlock(tag.name, tag.end);
56
+ if (atBlockLevel() && tag.kind === "empty") takeBlock(tag.name, tag.end);
57
57
  if (tag.kind === "open") stack.push(tag.name);
58
58
  }
59
59
  i = tag.end;
60
60
  }
61
- if (bodyDepth === null) {
62
- throw new DocxImportError("missing-body", "document has no w:body");
63
- }
61
+ if (containerDepth === null) return null;
64
62
  if (stack.length > 0) {
65
63
  throw new DocxImportError(
66
64
  "malformed-xml",
@@ -73,6 +71,14 @@ function scanBody(source) {
73
71
  suffix: source.slice(sliceStart)
74
72
  };
75
73
  }
74
+ function scanBody(source) {
75
+ const scan = scanBlocksIn(source, isBodyTag);
76
+ if (scan === null) {
77
+ throw new DocxImportError("missing-body", "document has no w:body");
78
+ }
79
+ return scan;
80
+ }
76
81
  export {
82
+ scanBlocksIn,
77
83
  scanBody
78
84
  };
@@ -0,0 +1,126 @@
1
+ /**
2
+ * What each section of a document lays down, read once out of the one `w:sectPr` that says it.
3
+ *
4
+ * A document is a sequence of sections. Every section but the last is closed by the paragraph that
5
+ * ends it, which carries that section's `w:sectPr` inside its own `w:pPr` (§17.6.17); the last one
6
+ * is closed by the body, whose `w:sectPr` stands after the final block (§17.6.18). The body's is
7
+ * the one the document node carries as `sectPr`, so an edit to it goes through a transaction and
8
+ * select-all + delete cannot take it away along with the blocks.
9
+ *
10
+ * A fragment is only ever sliced, never rebuilt: an untouched section goes back out as the bytes it
11
+ * arrived as, and a rewritten child is swapped into the spot `CT_SectPr` lays down.
12
+ */
13
+ import type { Node as PMNode } from "prosemirror-model";
14
+ import { type PageGeometry } from "./pageGeometry";
15
+ export type HeaderFooterVariant = "default" | "first" | "even";
16
+ /** The variants a `w:headerReference` or a `w:footerReference` may name. §17.6.12, §17.6.5 */
17
+ export declare const HEADER_FOOTER_VARIANTS: readonly HeaderFooterVariant[];
18
+ /** The part each variant of one story kind is drawn from, as the relationship id naming it */
19
+ export type HeaderFooterRefs = Readonly<Record<HeaderFooterVariant, string | null>>;
20
+ /** What one `w:sectPr` says, read once and shared by pagination, headers and editing */
21
+ export interface SectionProperties {
22
+ /**
23
+ * The `<w:sectPr>...</w:sectPr>` fragment exactly as written.
24
+ *
25
+ * "" for a section read out of a parsed document rather than out of text: the only fragments this
26
+ * package writes back are the slices it took (a paragraph's `w:pPr` child, the document node's
27
+ * own attr), so a section read off a DOM has no text of its own to hand back.
28
+ */
29
+ xml: string;
30
+ geometry: PageGeometry;
31
+ headerRefs: HeaderFooterRefs;
32
+ footerRefs: HeaderFooterRefs;
33
+ titlePg: boolean;
34
+ /** §17.6.19 `w:pgNumType/@w:start`; null where the section carries the numbering on */
35
+ pageNumberStart: number | null;
36
+ /** §17.6.22 `w:type/@w:val`; null when omitted (nextPage) */
37
+ type: "continuous" | "evenPage" | "nextPage" | "oddPage" | "nextColumn" | null;
38
+ }
39
+ /** What a section the document does not spell out lays down: the paper every document used to be drawn on */
40
+ export declare const DEFAULT_SECTION: SectionProperties;
41
+ /** Which `w:sectPr` closes a section: the paragraph carrying it, or the body itself */
42
+ export type SectionAnchor = {
43
+ kind: "paragraph";
44
+ pos: number;
45
+ } | {
46
+ kind: "body";
47
+ };
48
+ export interface DocumentSection {
49
+ index: number;
50
+ /** Top-level block indexes this section covers, inclusive */
51
+ firstBlock: number;
52
+ lastBlock: number;
53
+ anchor: SectionAnchor;
54
+ props: SectionProperties;
55
+ }
56
+ /**
57
+ * Every relationship the sections of this body name as a header or a footer story.
58
+ *
59
+ * A `w:headerReference` and a `w:footerReference` stand only inside a `w:sectPr` (§17.6.12,
60
+ * §17.6.5), so the body holds all of them however many sections it is written in. A part no
61
+ * section names is one a producer left behind rather than content this document shows.
62
+ */
63
+ export declare function storyReferenceIds(body: Element): ReadonlySet<string>;
64
+ /**
65
+ * Everything one `w:sectPr` lays down.
66
+ *
67
+ * The reading is deliberately forgiving: it runs against a document already opened, and a value
68
+ * that cannot be read is a reason to fall back on what a section saying nothing lays down, never a
69
+ * reason to refuse the file.
70
+ */
71
+ export declare function readSectionProperties(sectPr: Element): SectionProperties;
72
+ /**
73
+ * The same, off a fragment held as text, which keeps the text as `xml`.
74
+ *
75
+ * null for anything that is not a readable `w:sectPr`, which leaves the caller holding the
76
+ * fragment it already has rather than a section read out of a guess.
77
+ */
78
+ export declare function parseSectionProperties(xml: string): SectionProperties | null;
79
+ /**
80
+ * The first `w:sectPr` of an already parsed body: the one closing the first section, wherever it
81
+ * stands, and the body's own where the document holds a single section.
82
+ */
83
+ export declare function firstSectPrElement(body: Element): Element | null;
84
+ /** The `w:sectPr` this paragraph's properties carry, and null for a paragraph that ends no section */
85
+ export declare function sectionBreakOf(pPr: string | null): string | null;
86
+ /**
87
+ * The paragraph properties without the section break, and null for properties left holding nothing
88
+ * at all. Properties carrying no break, and properties whose shape cannot be made out, are handed
89
+ * back as they stand.
90
+ *
91
+ * `parseProps` lists direct children alone, so the `w:sectPr` inside a `w:pPrChange` - the
92
+ * properties this paragraph wore before a tracked change - is not this paragraph's own break and
93
+ * stays where it stands.
94
+ */
95
+ export declare function withoutSectionBreak(pPr: string | null): string | null;
96
+ /** The paragraph properties with this section break in the spot `CT_PPr` gives it */
97
+ export declare function withSectionBreak(pPr: string | null, sectPr: string): string;
98
+ /**
99
+ * One child of a section swapped for new XML, or taken away with a null.
100
+ *
101
+ * A child that was not there goes into the spot `CT_SectPr` lays down, and every other child of
102
+ * the section keeps the text it was written as, whitespace between them included. A header or
103
+ * footer reference names one variant of one story, so writing one leaves the section's references
104
+ * to the other variants where they stand.
105
+ *
106
+ * A fragment whose shape cannot be made out is handed back untouched, which leaves the caller with
107
+ * the section the document wrote rather than one written over a guess.
108
+ */
109
+ export declare function setSectionChild(sectPrXml: string, name: string, childXml: string | null): string;
110
+ /**
111
+ * Every section of the document in order, the last of them the one the document node carries.
112
+ *
113
+ * A document naming no section at all still has one, drawn on the paper `DEFAULT_SECTION` lays
114
+ * down. The last section covers no block where the final paragraph itself ends a section, which
115
+ * leaves `firstBlock` past `lastBlock`.
116
+ */
117
+ export declare function sectionsOf(doc: PMNode): readonly DocumentSection[];
118
+ /**
119
+ * The same, against a table already read.
120
+ *
121
+ * Reading the table walks every block of the document, so anything asking about more than one
122
+ * position - the pages of a preview, one per section - reads it once and asks here.
123
+ */
124
+ export declare function sectionIn(sections: readonly DocumentSection[], doc: PMNode, pos: number): DocumentSection;
125
+ /** The section the block at this position belongs to */
126
+ export declare function sectionAt(doc: PMNode, pos: number): DocumentSection;
@@ -0,0 +1,207 @@
1
+ // src/docx/sections.ts
2
+ import {
3
+ parseProps,
4
+ parsePropsXml,
5
+ renderElement,
6
+ renderProps,
7
+ setChild
8
+ } from "../ooxml/props.js";
9
+ import { rootTagAt } from "../ooxml/tagScan.js";
10
+ import { isOnElement } from "../ooxml/units.js";
11
+ import {
12
+ attributeByLocalName,
13
+ childByLocalName,
14
+ elementChildren,
15
+ R_NS,
16
+ W_NS
17
+ } from "../ooxml/xml.js";
18
+ import {
19
+ A4_PORTRAIT,
20
+ readPageGeometry
21
+ } from "./pageGeometry.js";
22
+ var HEADER_FOOTER_VARIANTS = [
23
+ "default",
24
+ "first",
25
+ "even"
26
+ ];
27
+ var NO_REFS = { default: null, first: null, even: null };
28
+ var DEFAULT_SECTION = {
29
+ xml: "",
30
+ geometry: A4_PORTRAIT,
31
+ headerRefs: NO_REFS,
32
+ footerRefs: NO_REFS,
33
+ titlePg: false,
34
+ pageNumberStart: null,
35
+ type: null
36
+ };
37
+ var SECTION_TYPES = [
38
+ "continuous",
39
+ "evenPage",
40
+ "nextPage",
41
+ "oddPage",
42
+ "nextColumn"
43
+ ];
44
+ function sectionType(sectPr) {
45
+ const declared = childByLocalName(sectPr, "type");
46
+ const value = declared ? attributeByLocalName(declared, "val") : null;
47
+ return SECTION_TYPES.find((known) => known === value) ?? null;
48
+ }
49
+ function pageNumberStart(sectPr) {
50
+ const pgNumType = childByLocalName(sectPr, "pgNumType");
51
+ const declared = pgNumType ? attributeByLocalName(pgNumType, "start") : null;
52
+ if (declared === null || declared.trim() === "") return null;
53
+ const start = Number(declared);
54
+ return Number.isSafeInteger(start) && start >= 0 ? start : null;
55
+ }
56
+ function storyRefs(sectPr, kind) {
57
+ const refs = { ...NO_REFS };
58
+ for (const reference of elementChildren(sectPr)) {
59
+ if (reference.localName !== `${kind}Reference`) continue;
60
+ const declared = attributeByLocalName(reference, "type") ?? "default";
61
+ const variant = HEADER_FOOTER_VARIANTS.find((known) => known === declared);
62
+ if (variant === void 0 || refs[variant] !== null) continue;
63
+ const id = referenceId(reference);
64
+ if (id) refs[variant] = id;
65
+ }
66
+ return refs;
67
+ }
68
+ function referenceId(reference) {
69
+ return reference.getAttributeNS(R_NS, "id") ?? attributeByLocalName(reference, "id");
70
+ }
71
+ function storyReferenceIds(body) {
72
+ const ids = /* @__PURE__ */ new Set();
73
+ for (const name of ["headerReference", "footerReference"]) {
74
+ for (const reference of body.getElementsByTagNameNS(W_NS, name)) {
75
+ const id = referenceId(reference);
76
+ if (id) ids.add(id);
77
+ }
78
+ }
79
+ return ids;
80
+ }
81
+ function readSectionProperties(sectPr) {
82
+ return {
83
+ xml: "",
84
+ geometry: readPageGeometry(sectPr),
85
+ headerRefs: storyRefs(sectPr, "header"),
86
+ footerRefs: storyRefs(sectPr, "footer"),
87
+ titlePg: isOnElement(childByLocalName(sectPr, "titlePg")),
88
+ pageNumberStart: pageNumberStart(sectPr),
89
+ type: sectionType(sectPr)
90
+ };
91
+ }
92
+ function parseSectionProperties(xml) {
93
+ const el = parsePropsXml(xml);
94
+ if (el?.localName !== "sectPr") return null;
95
+ return { ...readSectionProperties(el), xml };
96
+ }
97
+ function firstSectPrElement(body) {
98
+ const wordSection = body.getElementsByTagNameNS(W_NS, "sectPr")[0];
99
+ if (wordSection) return wordSection;
100
+ for (const el of body.getElementsByTagName("*")) {
101
+ if (el.localName === "sectPr") return el;
102
+ }
103
+ return null;
104
+ }
105
+ function pPrText(node) {
106
+ const pPr = node.attrs.pPr;
107
+ return typeof pPr === "string" ? pPr : null;
108
+ }
109
+ function sectionBreakOf(pPr) {
110
+ if (pPr === null) return null;
111
+ const props = parseProps(pPr);
112
+ return props?.children.find((child) => child.name === "sectPr")?.xml ?? null;
113
+ }
114
+ function withoutSectionBreak(pPr) {
115
+ if (pPr === null) return null;
116
+ const props = parseProps(pPr);
117
+ if (props === null) return pPr;
118
+ const without = setChild(props, "sectPr", null);
119
+ if (without.children.length === props.children.length) return pPr;
120
+ return renderProps(without) || null;
121
+ }
122
+ function withSectionBreak(pPr, sectPr) {
123
+ const props = (pPr === null ? null : parseProps(pPr)) ?? EMPTY_P_PR;
124
+ return renderElement(setChild(props, "sectPr", sectPr));
125
+ }
126
+ var EMPTY_P_PR = { tag: "w:pPr", attrs: null, children: [] };
127
+ var REFERENCE_CHILDREN = [
128
+ "headerReference",
129
+ "footerReference"
130
+ ];
131
+ function referenceVariant(xml) {
132
+ const el = parsePropsXml(xml);
133
+ return (el && attributeByLocalName(el, "type")) ?? "default";
134
+ }
135
+ function withReference(props, name, childXml) {
136
+ const variant = referenceVariant(childXml);
137
+ let last = -1;
138
+ let replaced = false;
139
+ const children = props.children.map((child, index) => {
140
+ if (child.name !== name) return child;
141
+ last = index;
142
+ if (referenceVariant(child.xml) !== variant) return child;
143
+ replaced = true;
144
+ return { ...child, xml: childXml };
145
+ });
146
+ if (replaced) return { ...props, children };
147
+ if (last === -1) return setChild(props, name, childXml);
148
+ children.splice(last + 1, 0, { name, xml: childXml });
149
+ return { ...props, children };
150
+ }
151
+ function setSectionChild(sectPrXml, name, childXml) {
152
+ const start = rootTagAt(sectPrXml);
153
+ if (start === -1) return sectPrXml;
154
+ const gap = sectPrXml.slice(0, start);
155
+ const props = parseProps(sectPrXml.slice(start));
156
+ if (props === null) return sectPrXml;
157
+ const written = childXml !== null && REFERENCE_CHILDREN.includes(name) ? withReference(props, name, childXml) : setChild(props, name, childXml);
158
+ return gap + renderElement(written);
159
+ }
160
+ function sectionsOf(doc) {
161
+ const sections = [];
162
+ let firstBlock = 0;
163
+ doc.forEach((child, offset, index) => {
164
+ const brk = sectionBreakOf(pPrText(child));
165
+ if (brk === null) return;
166
+ sections.push({
167
+ index: sections.length,
168
+ firstBlock,
169
+ lastBlock: index,
170
+ anchor: { kind: "paragraph", pos: offset },
171
+ props: parseSectionProperties(brk) ?? { ...DEFAULT_SECTION, xml: brk }
172
+ });
173
+ firstBlock = index + 1;
174
+ });
175
+ const body = doc.attrs.sectPr;
176
+ const xml = typeof body === "string" ? body : null;
177
+ sections.push({
178
+ index: sections.length,
179
+ firstBlock,
180
+ lastBlock: doc.childCount - 1,
181
+ anchor: { kind: "body" },
182
+ props: xml === null ? DEFAULT_SECTION : parseSectionProperties(xml) ?? { ...DEFAULT_SECTION, xml }
183
+ });
184
+ return sections;
185
+ }
186
+ function sectionIn(sections, doc, pos) {
187
+ const index = doc.resolve(Math.min(Math.max(pos, 0), doc.content.size)).index(0);
188
+ return sections.find((section) => index <= section.lastBlock) ?? sections[sections.length - 1];
189
+ }
190
+ function sectionAt(doc, pos) {
191
+ return sectionIn(sectionsOf(doc), doc, pos);
192
+ }
193
+ export {
194
+ DEFAULT_SECTION,
195
+ HEADER_FOOTER_VARIANTS,
196
+ firstSectPrElement,
197
+ parseSectionProperties,
198
+ readSectionProperties,
199
+ sectionAt,
200
+ sectionBreakOf,
201
+ sectionIn,
202
+ sectionsOf,
203
+ setSectionChild,
204
+ storyReferenceIds,
205
+ withSectionBreak,
206
+ withoutSectionBreak
207
+ };
@@ -1,6 +1,15 @@
1
1
  import type { Node as PMNode } from "prosemirror-model";
2
2
  import { type ExportRefs } from "./exportRefs";
3
- import { type SessionStore } from "./session";
4
- /** Why there is no original to write, which a block that came in from another document answers differently */
5
- export declare function lostOriginal(node: PMNode, session: SessionStore): string;
6
- export declare function serializeBlock(node: PMNode, session: SessionStore, refs?: ExportRefs): string;
3
+ export declare function serializeBlock(node: PMNode, refs?: ExportRefs): string;
4
+ /**
5
+ * An unchanged block is written back as its original XML; only a changed block is rebuilt.
6
+ *
7
+ * Unchanged is judged by `sameSource` rather than by `Node.eq`, because opening a file works the
8
+ * display attrs out again (`schema/attrRoles`) and a block rebuilt over that would lose the markup
9
+ * the writer does not model, the properties of a cell continuing a vertical merge among it.
10
+ *
11
+ * `onlyBlock` says the block stands alone in its story. An original of empty XML belongs to the
12
+ * editable placeholder a story with no block of its own opens with; once another block joins it,
13
+ * that paragraph stands for a real blank line and has to be written.
14
+ */
15
+ export declare function blockXml(node: PMNode, refs: ExportRefs, onlyBlock: boolean): string;
@@ -1,32 +1,30 @@
1
1
  // src/docx/serializeBlock.ts
2
2
  import { DocxExportError } from "../ooxml/errors.js";
3
+ import { sameSource } from "../schema/sourceEquality.js";
3
4
  import { NO_EXPORT_REFS } from "./exportRefs.js";
4
- import { preservedXml, serializeParagraph } from "./serializeParagraph.js";
5
+ import { serializeParagraph } from "./serializeParagraph.js";
6
+ import { serializePreservedBlock } from "./serializePreserved.js";
5
7
  import { serializeTable } from "./serializeTable.js";
6
- import { originalBlock, splitBlockKey } from "./session.js";
7
- function lostOriginal(node, session) {
8
- const srcId = node.attrs.srcId;
9
- const key = typeof srcId === "string" ? splitBlockKey(srcId) : null;
10
- return key === null || key.sessionId === session.sessionId ? "a preserved block has lost its original XML" : `a preserved block comes from another document (${key.sessionId})`;
11
- }
12
- function serializeRaw(node, session) {
13
- const imported = originalBlock(node, session);
14
- if (!imported) {
15
- throw new DocxExportError("lost-original", lostOriginal(node, session));
16
- }
17
- return imported.xml;
18
- }
19
- function serializeBlock(node, session, refs = NO_EXPORT_REFS) {
8
+ import { originalBlock } from "./session.js";
9
+ function serializeBlock(node, refs = NO_EXPORT_REFS) {
20
10
  if (node.type.name === "paragraph") return serializeParagraph(node, refs);
21
11
  if (node.type.name === "table") return serializeTable(node, refs);
22
- if (node.type.name === "rawBlock") return preservedXml(node);
23
- if (node.type.isInGroup("preserved")) return serializeRaw(node, session);
12
+ if (node.type.isInGroup("preserved")) {
13
+ return serializePreservedBlock(node, refs);
14
+ }
24
15
  throw new DocxExportError(
25
16
  "unsupported-content",
26
17
  `block we cannot serialize: ${node.type.name}`
27
18
  );
28
19
  }
20
+ function blockXml(node, refs, onlyBlock) {
21
+ const imported = refs.session === null ? void 0 : originalBlock(node, refs.session);
22
+ if (imported && (imported.xml !== "" || onlyBlock) && sameSource(node, imported.node)) {
23
+ return imported.xml;
24
+ }
25
+ return serializeBlock(node, refs);
26
+ }
29
27
  export {
30
- lostOriginal,
28
+ blockXml,
31
29
  serializeBlock
32
30
  };