@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
@@ -13,31 +13,45 @@ function elementOf(node) {
13
13
  return stringAttr(node.attrs.name) ?? elementNameOf(stringAttr(node.attrs.xml) ?? "");
14
14
  }
15
15
  function demotedBlock(element) {
16
- const name = element === null ? null : localPart(element);
16
+ const name = localNameOf(element);
17
17
  if (name === "tbl") return { severity: "placeholder", code: "table-demoted" };
18
18
  if (name === "p") {
19
19
  return { severity: "placeholder", code: "paragraph-demoted" };
20
20
  }
21
21
  return { severity: "placeholder", code: "preserved-block" };
22
22
  }
23
+ function severityOf(display) {
24
+ return display === "chip" ? "placeholder" : "hidden";
25
+ }
23
26
  function preservedBy(node, element) {
24
27
  switch (node.type.name) {
25
- case "docxRaw":
26
- return demotedBlock(element);
27
28
  case "rawBlock":
28
- return { severity: "placeholder", code: "preserved-block" };
29
- case "bookmarkBlock":
30
- return { severity: "hidden", code: "range-marker" };
29
+ return preservedBlockNote(node, element);
30
+ case "rawRunContent":
31
+ return {
32
+ severity: severityOf(node.attrs.display),
33
+ code: "preserved-run-content"
34
+ };
31
35
  case "rawInline":
32
- return preservedInline(element);
36
+ return preservedInline(node, element);
33
37
  default:
34
38
  return null;
35
39
  }
36
40
  }
37
- function preservedInline(element) {
38
- const name = element === null ? null : localPart(element);
41
+ function preservedBlockNote(node, element) {
42
+ if (severityOf(node.attrs.display) === "placeholder") {
43
+ return demotedBlock(element);
44
+ }
45
+ return node.attrs.guarded === true ? { severity: "hidden", code: "range-marker" } : { severity: "hidden", code: "preserved-block" };
46
+ }
47
+ function preservedInline(node, element) {
48
+ const name = stringAttr(node.attrs.element) ?? localNameOf(element);
39
49
  if (name === "r") return null;
40
- return name === "bookmarkStart" || name === "bookmarkEnd" ? { severity: "hidden", code: "range-marker" } : { severity: "hidden", code: "preserved-inline" };
50
+ const severity = severityOf(node.attrs.display);
51
+ return severity === "hidden" && node.attrs.guarded === true ? { severity, code: "range-marker" } : { severity, code: "preserved-inline" };
52
+ }
53
+ function localNameOf(element) {
54
+ return element === null ? null : localPart(element);
41
55
  }
42
56
  function blockNumberOf(node) {
43
57
  const srcId = stringAttr(node.attrs.srcId);
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Where the fields of a paragraph stand, read back off the fragments the editor keeps as they came.
3
+ *
4
+ * A complex field is not one element. It is a begin character, the instruction text, an optional
5
+ * separate character, the result its producer cached, and an end character, all standing loose
6
+ * among the runs of the paragraph in that order (§17.16.18). Nothing in the model pairs them up, so
7
+ * anything that wants to know what a field was told to print has to read the characters back. A
8
+ * simple field says the same in one element and carries its instruction as an attribute (§17.16.19).
9
+ *
10
+ * What a field means, and how to work it out again, belongs to the fields feature. This answers the
11
+ * one question a projection has: where each field stands and what its instruction says.
12
+ */
13
+ import type { Node as PMNode } from "prosemirror-model";
14
+ export interface FieldSpan {
15
+ /** The instruction text between begin and separate (or end), whitespace-trimmed */
16
+ instr: string;
17
+ /** Where the begin character stands, and the simple field itself for one written as a single element */
18
+ begin: number;
19
+ /** Where the separate character stands, and null for a field that cached no result */
20
+ separate: number | null;
21
+ /** Where the end character stands, which is `begin` again for a simple field */
22
+ end: number;
23
+ /** The half-open range of positions the cached result covers, and null for a field carrying none */
24
+ result: [number, number] | null;
25
+ }
26
+ /**
27
+ * Whether the node is one of the pieces a complex field is written out of rather than something
28
+ * the field prints. A projection drops these whether or not they pair up into a field.
29
+ */
30
+ export declare function isFieldCharacter(node: PMNode): boolean;
31
+ /**
32
+ * The fields this paragraph holds, in the order they begin, each position offset by `offset`.
33
+ *
34
+ * Positions are counted inside the paragraph's content, so `offset` is where that content starts:
35
+ * a caller reading a paragraph standing on its own passes 0, and one reading a paragraph of a
36
+ * document passes the position just inside it.
37
+ *
38
+ * A begin character with no end is no field: the file says a field starts there and never says
39
+ * what it prints, so there is nothing to pair it with and it is passed over. `documentFidelity`
40
+ * still reports the characters themselves as content kept rather than modelled.
41
+ */
42
+ export declare function fieldSpans(paragraph: PMNode, offset: number): FieldSpan[];
@@ -0,0 +1,76 @@
1
+ // src/docx/fields.ts
2
+ import { parsePropsXml } from "../ooxml/props.js";
3
+ import { attributeByLocalName } from "../ooxml/xml.js";
4
+ var FIELD_CHARACTER = "fldChar";
5
+ var INSTRUCTION = "instrText";
6
+ var SIMPLE_FIELD = "fldSimple";
7
+ function attributeOf(node, name) {
8
+ const xml = node.attrs.xml;
9
+ if (typeof xml !== "string") return null;
10
+ const el = parsePropsXml(xml);
11
+ return el === null ? null : attributeByLocalName(el, name);
12
+ }
13
+ function chipText(node) {
14
+ const text = node.attrs.text;
15
+ return typeof text === "string" ? text : "";
16
+ }
17
+ var DELETED_INSTRUCTION = "delInstrText";
18
+ var FIELD_PLUMBING = /* @__PURE__ */ new Set([
19
+ FIELD_CHARACTER,
20
+ INSTRUCTION,
21
+ DELETED_INSTRUCTION
22
+ ]);
23
+ function isFieldCharacter(node) {
24
+ const element = node.attrs.element;
25
+ return node.type.name === "rawRunContent" && typeof element === "string" && FIELD_PLUMBING.has(element);
26
+ }
27
+ function fieldSpans(paragraph, offset) {
28
+ const spans = [];
29
+ const open = [];
30
+ paragraph.forEach((child, at) => {
31
+ const pos = offset + at;
32
+ const element = child.attrs.element;
33
+ if (child.type.name === "rawInline" && element === SIMPLE_FIELD) {
34
+ spans.push({
35
+ instr: (attributeOf(child, "instr") ?? "").trim(),
36
+ begin: pos,
37
+ separate: null,
38
+ end: pos,
39
+ result: null
40
+ });
41
+ return;
42
+ }
43
+ if (child.type.name !== "rawRunContent") return;
44
+ if (element === INSTRUCTION) {
45
+ const active2 = open.at(-1);
46
+ if (active2 && active2.separate === null) active2.instr += chipText(child);
47
+ return;
48
+ }
49
+ if (element !== FIELD_CHARACTER) return;
50
+ const kind = attributeOf(child, "fldCharType");
51
+ if (kind === "begin") {
52
+ open.push({ instr: "", begin: pos, separate: null });
53
+ return;
54
+ }
55
+ const active = open.at(-1);
56
+ if (active === void 0) return;
57
+ if (kind === "separate") {
58
+ if (active.separate === null) active.separate = pos;
59
+ return;
60
+ }
61
+ if (kind !== "end") return;
62
+ open.pop();
63
+ spans.push({
64
+ instr: active.instr.trim(),
65
+ begin: active.begin,
66
+ separate: active.separate,
67
+ end: pos,
68
+ result: active.separate === null ? null : [active.separate + 1, pos]
69
+ });
70
+ });
71
+ return spans.sort((a, b) => a.begin - b.begin);
72
+ }
73
+ export {
74
+ fieldSpans,
75
+ isFieldCharacter
76
+ };
@@ -1,17 +1,25 @@
1
- /** Reads the first section's header and footer stories for the page preview. */
2
- import type { ParagraphAlign } from "../model/format";
3
- export type HeaderFooterVariant = "default" | "first" | "even";
4
- export type PageField = "PAGE" | "NUMPAGES";
5
- export type HeaderFooterSegment = {
6
- kind: "text";
7
- value: string;
8
- } | {
9
- kind: "field";
10
- field: PageField;
11
- };
12
- /** A display-only projection. The original part remains untouched in the package. */
1
+ /**
2
+ * The header and footer stories of a document: where each one is written, which section shows it,
3
+ * what it reads as on a given page, and where an edited one goes back out.
4
+ *
5
+ * A header part is a story of the same schema as the body (`./story`): its blocks are read by the
6
+ * same readers and sliced verbatim by the same scanner, so an edit to one rides a transaction and
7
+ * the part it stands in goes back out as the bytes it arrived as until somebody changes it. What
8
+ * the page preview draws is a projection of that story rather than a reading of its own.
9
+ *
10
+ * Which story a section shows is the section's question (`./sections`): every section names its own
11
+ * variants, so the second section of a document draws its own header and not the first section's.
12
+ */
13
+ import type { Node as PMNode } from "prosemirror-model";
14
+ import { type ParagraphAlign } from "../model/format";
15
+ import { type StoryKey } from "../schema/stories";
16
+ import type { PartPlanner } from "./partPlan";
17
+ import { type DocumentSection } from "./sections";
18
+ import { type ImportedStory, type StoryDeps } from "./story";
19
+ /** One story a section may show, as the document currently says it */
13
20
  export interface HeaderFooterContent {
14
- segments: readonly HeaderFooterSegment[];
21
+ story: PMNode;
22
+ /** What the first paragraph of the story is aligned to, which is what the preview is drawn with */
15
23
  align: ParagraphAlign | null;
16
24
  }
17
25
  export interface HeaderFooterVariants {
@@ -26,11 +34,72 @@ export interface HeadersFooters {
26
34
  evenAndOdd: boolean;
27
35
  pageNumberStart: number;
28
36
  }
29
- export declare const NO_HEADERS_FOOTERS: HeadersFooters;
30
- /** Reads the first section's display stories and section-level selection switches. */
31
- export declare function readHeadersFooters(parts: Map<string, Uint8Array>, mainPartPath: string, body: Element): HeadersFooters;
37
+ /**
38
+ * What picking one section's stories takes beyond the section itself.
39
+ *
40
+ * A section names a relationship, a relationship names a part, and a part is where a story stands,
41
+ * so the two ends are joined once when the document is opened. `w:evenAndOddHeaders` is a
42
+ * document-wide setting rather than a section's own (§17.15.1.29), so it rides along here.
43
+ */
44
+ export interface HeaderFooterStories {
45
+ /**
46
+ * The story each header or footer relationship of the main part names. The key says which of the
47
+ * two kinds it is, so a `w:headerReference` naming a footer relationship names no story.
48
+ */
49
+ readonly keyByRelId: ReadonlyMap<string, StoryKey>;
50
+ readonly evenAndOdd: boolean;
51
+ }
52
+ /**
53
+ * Every header and footer part a section of the document names, read as a story apiece, and the
54
+ * relationship each one answers to.
55
+ *
56
+ * A part is read once however many relationships point at it, and a part whose root is not the
57
+ * element its relationship promises is passed over: the reference names nothing this can draw, and
58
+ * the bytes stay where they are. A part no section names is not read at all, so an orphan a
59
+ * producer left behind - which may hold anything, a document this one never draws included - is
60
+ * neither a story of this document nor a reason to refuse the file, and its bytes go back out as
61
+ * they came.
62
+ */
63
+ export declare function readHeaderFooterStories(parts: Map<string, Uint8Array>, mainPartPath: string, referenced: ReadonlySet<string>, depsFor: (partPath: string) => StoryDeps): {
64
+ stories: readonly ImportedStory[];
65
+ refs: HeaderFooterStories;
66
+ };
67
+ /**
68
+ * The stories this section shows and the switches that pick between them.
69
+ *
70
+ * `storyOf` answers with what the document currently says rather than what the package arrived
71
+ * holding, so a header the editor rewrote is the one the preview draws.
72
+ */
73
+ export declare function variantsFor(section: DocumentSection, refs: HeaderFooterStories, storyOf: (key: StoryKey) => PMNode | null): HeadersFooters;
74
+ /**
75
+ * The number this visual page shows, counted from the start its section declares.
76
+ *
77
+ * `page` is the page's place in the whole document: the count is not restarted at a section
78
+ * boundary, which is what the editor's documented page numbering says.
79
+ */
32
80
  export declare function displayPageNumber(headersFooters: HeadersFooters, page: number): number;
33
- /** Resolves the section variant and evaluates PAGE and NUMPAGES for one visual page. */
34
- export declare function headerFooterText(variants: HeaderFooterVariants, headersFooters: HeadersFooters, page: number, totalPages: number): string | null;
35
- /** Resolves the direct alignment of the first paragraph in the selected story. */
36
- export declare function headerFooterAlign(variants: HeaderFooterVariants, headersFooters: HeadersFooters, page: number): ParagraphAlign | null;
81
+ /**
82
+ * The story this page of a section shows, and null where the section declares none for it.
83
+ *
84
+ * `pageInSection` is the page's place within its own section rather than in the document:
85
+ * `w:titlePg` selects the `first` story for the first page of every section it is written in
86
+ * (§17.10.1), and the odd or even story follows the number that page would carry counting from
87
+ * the section's own start (§17.10.6), so a second section opening halfway down a document draws
88
+ * its own first page as a first page.
89
+ */
90
+ export declare function headerFooterOn(variants: HeaderFooterVariants, headersFooters: HeadersFooters, pageInSection: number): HeaderFooterContent | null;
91
+ /**
92
+ * What the story reads as on one visual page: its paragraphs joined by newlines, with `PAGE` and
93
+ * `NUMPAGES` replaced by the numbers this page actually carries.
94
+ *
95
+ * `page` is the number the page shows rather than its place on the sheet, since that is what a
96
+ * `PAGE` field prints (§17.16.5.45).
97
+ */
98
+ export declare function headerFooterText(story: PMNode, page: number, totalPages: number): string;
99
+ /**
100
+ * Writes back the header and footer parts an edit changed, and no others.
101
+ *
102
+ * A story nobody touched is not written at all, so a document opened and exported hands every
103
+ * header part back as the bytes it arrived as, and editing one header leaves the rest untouched.
104
+ */
105
+ export declare const headerFooterPlanner: PartPlanner;
@@ -1,131 +1,32 @@
1
1
  // src/docx/headersFooters.ts
2
- import { ALIGN_BY_JC, isOnElement } from "../ooxml/units.js";
2
+ import { toParagraphFormat } from "../model/format.js";
3
+ import { NAMESPACES } from "../ooxml/names.js";
3
4
  import {
4
- attributeByLocalName,
5
- decodeUtf8,
6
- elementChildren,
7
- parseXml,
8
- R_NS,
9
- W_NS
10
- } from "../ooxml/xml.js";
5
+ ensureRootDeclarations
6
+ } from "../ooxml/partSplice.js";
7
+ import { isOnElement } from "../ooxml/units.js";
8
+ import { decodeUtf8, encodeUtf8, parseXml, R_NS, W_NS } from "../ooxml/xml.js";
9
+ import { docxSchema } from "../schema/index.js";
10
+ import { sameSource } from "../schema/sourceEquality.js";
11
+ import { storyKey, storyNodeOf } from "../schema/stories.js";
12
+ import { NO_EXPORT_REFS } from "./exportRefs.js";
13
+ import { fieldSpans, isFieldCharacter } from "./fields.js";
11
14
  import { relatedPartPath } from "./packageParts.js";
12
15
  import { readRelationships, relsPathOf, resolveTarget } from "./relationships.js";
16
+ import {
17
+ HEADER_FOOTER_VARIANTS
18
+ } from "./sections.js";
19
+ import { serializeStory } from "./serializeStory.js";
20
+ import {
21
+ readStory,
22
+ storyLeafText
23
+ } from "./story.js";
13
24
  var EMPTY_VARIANTS = {
14
25
  default: null,
15
26
  first: null,
16
27
  even: null
17
28
  };
18
- var NO_HEADERS_FOOTERS = {
19
- headers: EMPTY_VARIANTS,
20
- footers: EMPTY_VARIANTS,
21
- firstPageDifferent: false,
22
- evenAndOdd: false,
23
- pageNumberStart: 1
24
- };
25
- function firstSectPrIn(root) {
26
- const namespaced = root.getElementsByTagNameNS(W_NS, "sectPr").item(0);
27
- if (namespaced) return namespaced;
28
- for (const el of root.getElementsByTagName("*")) {
29
- if (el.localName === "sectPr") return el;
30
- }
31
- return null;
32
- }
33
- function pageField(instruction) {
34
- const name = instruction.trim().split(/\s+/, 1)[0]?.toUpperCase();
35
- return name === "PAGE" || name === "NUMPAGES" ? name : null;
36
- }
37
- function paragraphSegments(paragraph) {
38
- const segments = [];
39
- const fields = [];
40
- const suppressed = () => fields.some((field) => !field.separated || field.field !== null);
41
- const text = (value) => {
42
- if (!value || suppressed()) return;
43
- const previous = segments.at(-1);
44
- if (previous?.kind === "text") previous.value += value;
45
- else segments.push({ kind: "text", value });
46
- };
47
- const dynamic = (field) => {
48
- if (!suppressed()) segments.push({ kind: "field", field });
49
- };
50
- const visit = (el) => {
51
- if (el.namespaceURI === W_NS && (el.localName === "drawing" || el.localName === "pict" || el.localName === "object" || el.localName === "tbl" || el.localName === "txbxContent")) {
52
- return;
53
- }
54
- if (el.namespaceURI === W_NS && el.localName === "fldSimple") {
55
- const field = pageField(attributeByLocalName(el, "instr") ?? "");
56
- if (field) dynamic(field);
57
- else for (const child of elementChildren(el)) visit(child);
58
- return;
59
- }
60
- if (el.namespaceURI === W_NS && el.localName === "fldChar") {
61
- const kind = attributeByLocalName(el, "fldCharType");
62
- if (kind === "begin") {
63
- fields.push({ instruction: "", separated: false, field: null });
64
- } else if (kind === "separate") {
65
- const active = fields.at(-1);
66
- if (active) {
67
- active.field = pageField(active.instruction);
68
- active.separated = true;
69
- if (active.field) {
70
- const outerSuppresses = fields.slice(0, -1).some((field) => !field.separated || field.field !== null);
71
- if (!outerSuppresses) {
72
- segments.push({ kind: "field", field: active.field });
73
- }
74
- }
75
- }
76
- } else if (kind === "end") {
77
- const active = fields.pop();
78
- if (active && !active.separated) {
79
- const field = pageField(active.instruction);
80
- if (field) dynamic(field);
81
- }
82
- }
83
- return;
84
- }
85
- if (el.namespaceURI === W_NS && el.localName === "instrText") {
86
- const active = fields.at(-1);
87
- if (active && !active.separated)
88
- active.instruction += el.textContent ?? "";
89
- return;
90
- }
91
- if (el.namespaceURI === W_NS && el.localName === "t") {
92
- text(el.textContent ?? "");
93
- return;
94
- }
95
- if (el.namespaceURI === W_NS && el.localName === "tab") {
96
- text(" ");
97
- return;
98
- }
99
- if (el.namespaceURI === W_NS && (el.localName === "br" || el.localName === "cr")) {
100
- text("\n");
101
- return;
102
- }
103
- for (const child of elementChildren(el)) visit(child);
104
- };
105
- for (const child of elementChildren(paragraph)) visit(child);
106
- return segments;
107
- }
108
- function readContent(bytes) {
109
- const root = parseXml(decodeUtf8(bytes).text).documentElement;
110
- const paragraphs = elementChildren(root).filter(
111
- (child) => child.namespaceURI === W_NS && child.localName === "p"
112
- );
113
- const segments = [];
114
- paragraphs.forEach((paragraph, index) => {
115
- if (index > 0) segments.push({ kind: "text", value: "\n" });
116
- segments.push(...paragraphSegments(paragraph));
117
- });
118
- const pPr = paragraphs[0] ? elementChildren(paragraphs[0]).find(
119
- (child) => child.namespaceURI === W_NS && child.localName === "pPr"
120
- ) : void 0;
121
- const jc = pPr ? elementChildren(pPr).find(
122
- (child) => child.namespaceURI === W_NS && child.localName === "jc"
123
- ) : void 0;
124
- return {
125
- segments,
126
- align: jc ? ALIGN_BY_JC[attributeByLocalName(jc, "val") ?? ""] ?? null : null
127
- };
128
- }
29
+ var PART_ROOT = { header: "hdr", footer: "ftr" };
129
30
  function settingsEvenAndOdd(parts, mainPartPath) {
130
31
  const path = relatedPartPath(parts, mainPartPath, `${R_NS}/settings`);
131
32
  const bytes = path === null ? void 0 : parts.get(path);
@@ -134,76 +35,166 @@ function settingsEvenAndOdd(parts, mainPartPath) {
134
35
  const setting = root.getElementsByTagNameNS(W_NS, "evenAndOddHeaders").item(0);
135
36
  return isOnElement(setting);
136
37
  }
137
- function readVariants(parts, mainPartPath, sectPr, kind) {
138
- const relationships = new Map(
139
- readRelationships(parts, relsPathOf(mainPartPath)).map((entry) => [
140
- entry.id,
141
- entry
142
- ])
143
- );
144
- const variants = { ...EMPTY_VARIANTS };
145
- for (const reference of elementChildren(sectPr)) {
146
- if (reference.localName !== `${kind}Reference`) continue;
147
- const type = attributeByLocalName(reference, "type") ?? "default";
148
- if (type !== "default" && type !== "first" && type !== "even") continue;
149
- const id = reference.getAttributeNS(R_NS, "id") ?? attributeByLocalName(reference, "id");
150
- if (!id) continue;
151
- const relationship = relationships.get(id);
152
- if (!relationship || relationship.external || relationship.type !== `${R_NS}/${kind}`) {
38
+ function kindOf(relationshipType) {
39
+ if (relationshipType === `${R_NS}/header`) return "header";
40
+ if (relationshipType === `${R_NS}/footer`) return "footer";
41
+ return null;
42
+ }
43
+ function readHeaderFooterStories(parts, mainPartPath, referenced, depsFor) {
44
+ const keyByRelId = /* @__PURE__ */ new Map();
45
+ const read = /* @__PURE__ */ new Map();
46
+ for (const relationship of readRelationships(
47
+ parts,
48
+ relsPathOf(mainPartPath)
49
+ )) {
50
+ if (!referenced.has(relationship.id)) continue;
51
+ const kind = relationship.external ? null : kindOf(relationship.type);
52
+ if (kind === null) continue;
53
+ const partPath = resolveTarget(mainPartPath, relationship.target);
54
+ const key = storyKey(kind, partPath);
55
+ if (read.has(key)) {
56
+ keyByRelId.set(relationship.id, key);
57
+ continue;
58
+ }
59
+ const bytes = parts.get(partPath);
60
+ if (!bytes) continue;
61
+ const xml = decodeUtf8(bytes).text;
62
+ const root = parseXml(xml).documentElement;
63
+ if (root.namespaceURI !== W_NS || root.localName !== PART_ROOT[kind]) {
153
64
  continue;
154
65
  }
155
- const bytes = parts.get(resolveTarget(mainPartPath, relationship.target));
156
- if (bytes) variants[type] = readContent(bytes);
66
+ read.set(
67
+ key,
68
+ readStory(
69
+ { kind, id: partPath, partPath },
70
+ { el: root, xml },
71
+ depsFor(partPath)
72
+ )
73
+ );
74
+ keyByRelId.set(relationship.id, key);
157
75
  }
158
- return variants;
76
+ return {
77
+ stories: Array.from(read.values()),
78
+ refs: { keyByRelId, evenAndOdd: settingsEvenAndOdd(parts, mainPartPath) }
79
+ };
159
80
  }
160
- function pageNumberStart(sectPr) {
161
- const pgNumType = elementChildren(sectPr).find(
162
- (child) => child.localName === "pgNumType"
163
- );
164
- const declared = pgNumType ? attributeByLocalName(pgNumType, "start") : null;
165
- if (declared === null) return 1;
166
- const start = Number(declared);
167
- return Number.isSafeInteger(start) && start >= 0 ? start : 1;
81
+ function firstAlign(story) {
82
+ for (let at = 0; at < story.childCount; at += 1) {
83
+ const block = story.child(at);
84
+ if (block.type !== docxSchema.nodes.paragraph) continue;
85
+ return toParagraphFormat(block.attrs.format)?.align ?? null;
86
+ }
87
+ return null;
168
88
  }
169
- function readHeadersFooters(parts, mainPartPath, body) {
170
- const sectPr = firstSectPrIn(body);
171
- if (!sectPr) return NO_HEADERS_FOOTERS;
89
+ function variantContent(ids, kind, refs, storyOf) {
90
+ const variants = { ...EMPTY_VARIANTS };
91
+ for (const variant of HEADER_FOOTER_VARIANTS) {
92
+ const id = ids[variant];
93
+ const key = id === null ? void 0 : refs.keyByRelId.get(id);
94
+ if (key === void 0 || !key.startsWith(`${kind}:`)) continue;
95
+ const story = storyOf(key);
96
+ if (story !== null) variants[variant] = { story, align: firstAlign(story) };
97
+ }
98
+ return variants;
99
+ }
100
+ function variantsFor(section, refs, storyOf) {
172
101
  return {
173
- headers: readVariants(parts, mainPartPath, sectPr, "header"),
174
- footers: readVariants(parts, mainPartPath, sectPr, "footer"),
175
- firstPageDifferent: isOnElement(
176
- elementChildren(sectPr).find((child) => child.localName === "titlePg") ?? null
177
- ),
178
- evenAndOdd: settingsEvenAndOdd(parts, mainPartPath),
179
- pageNumberStart: pageNumberStart(sectPr)
102
+ headers: variantContent(section.props.headerRefs, "header", refs, storyOf),
103
+ footers: variantContent(section.props.footerRefs, "footer", refs, storyOf),
104
+ firstPageDifferent: section.props.titlePg,
105
+ evenAndOdd: refs.evenAndOdd,
106
+ pageNumberStart: section.props.pageNumberStart ?? 1
180
107
  };
181
108
  }
182
109
  function displayPageNumber(headersFooters, page) {
183
110
  return headersFooters.pageNumberStart + page - 1;
184
111
  }
185
- function contentForPage(variants, headersFooters, page) {
186
- if (page === 1 && headersFooters.firstPageDifferent) return variants.first;
187
- if (headersFooters.evenAndOdd && displayPageNumber(headersFooters, page) % 2 === 0) {
188
- return variants.even;
112
+ function headerFooterOn(variants, headersFooters, pageInSection) {
113
+ if (pageInSection === 1 && headersFooters.firstPageDifferent) {
114
+ return variants.first;
189
115
  }
116
+ const number = headersFooters.pageNumberStart + pageInSection - 1;
117
+ if (headersFooters.evenAndOdd && number % 2 === 0) return variants.even;
190
118
  return variants.default;
191
119
  }
192
- function headerFooterText(variants, headersFooters, page, totalPages) {
193
- const content = contentForPage(variants, headersFooters, page);
194
- if (!content) return null;
195
- return content.segments.map((segment) => {
196
- if (segment.kind === "text") return segment.value;
197
- return segment.field === "PAGE" ? `${displayPageNumber(headersFooters, page)}` : `${totalPages}`;
198
- }).join("");
120
+ function pageField(instruction) {
121
+ const name = instruction.split(/\s+/, 1)[0]?.toUpperCase();
122
+ return name === "PAGE" || name === "NUMPAGES" ? name : null;
123
+ }
124
+ function hiddenBy(field) {
125
+ if (pageField(field.instr) !== null) {
126
+ return { field, from: field.begin, to: field.end };
127
+ }
128
+ if (field.begin === field.end) return null;
129
+ return { field, from: field.begin, to: field.separate ?? field.end };
130
+ }
131
+ function nested(hidden, field) {
132
+ return hidden.some(
133
+ (other) => other.field !== field && field.begin >= other.from && field.begin <= other.to
134
+ );
135
+ }
136
+ function paragraphText(paragraph, page, totalPages) {
137
+ const fields = fieldSpans(paragraph, 0);
138
+ const hidden = fields.flatMap((field) => {
139
+ const range = hiddenBy(field);
140
+ return range === null ? [] : [range];
141
+ });
142
+ const numbers = new Map(
143
+ fields.flatMap((field) => {
144
+ const name = pageField(field.instr);
145
+ if (name === null || nested(hidden, field)) return [];
146
+ return [[field.begin, name === "PAGE" ? `${page}` : `${totalPages}`]];
147
+ })
148
+ );
149
+ const pieces = [];
150
+ paragraph.forEach((child, at) => {
151
+ const number = numbers.get(at);
152
+ if (number !== void 0) pieces.push(number);
153
+ if (isFieldCharacter(child)) return;
154
+ if (hidden.some(({ from, to }) => at >= from && at <= to)) return;
155
+ pieces.push(storyLeafText(child));
156
+ });
157
+ return pieces.join("");
158
+ }
159
+ function headerFooterText(story, page, totalPages) {
160
+ const lines = [];
161
+ story.forEach((block) => {
162
+ if (block.type === docxSchema.nodes.paragraph) {
163
+ lines.push(paragraphText(block, page, totalPages));
164
+ }
165
+ });
166
+ return lines.join("\n");
199
167
  }
200
- function headerFooterAlign(variants, headersFooters, page) {
201
- return contentForPage(variants, headersFooters, page)?.align ?? null;
168
+ var HEADER_MARKUP = { namespaces: { w: NAMESPACES.w } };
169
+ function rewrittenParts(doc, session) {
170
+ const parts = /* @__PURE__ */ new Map();
171
+ for (const [key, imported] of session.stories) {
172
+ if (imported.kind !== "header" && imported.kind !== "footer") continue;
173
+ const current = storyNodeOf(doc, key);
174
+ if (current === null || sameSource(current, imported.doc)) continue;
175
+ const written = serializeStory(current, imported, imported, {
176
+ ...NO_EXPORT_REFS,
177
+ session
178
+ });
179
+ const hadBom = decodeUtf8(
180
+ session.parts.get(imported.partPath) ?? new Uint8Array()
181
+ ).hadBom;
182
+ parts.set(
183
+ imported.partPath,
184
+ encodeUtf8(ensureRootDeclarations(written, HEADER_MARKUP), hadBom)
185
+ );
186
+ }
187
+ return parts.size === 0 ? null : parts;
202
188
  }
189
+ var headerFooterPlanner = {
190
+ name: "headers and footers",
191
+ plan: (doc, session) => rewrittenParts(doc, session)
192
+ };
203
193
  export {
204
- NO_HEADERS_FOOTERS,
205
194
  displayPageNumber,
206
- headerFooterAlign,
195
+ headerFooterOn,
196
+ headerFooterPlanner,
207
197
  headerFooterText,
208
- readHeadersFooters
198
+ readHeaderFooterStories,
199
+ variantsFor
209
200
  };