@portone/docx-editor 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/DocxEditor.js +84 -26
  3. package/dist/docx/exportDocx.d.ts +2 -1
  4. package/dist/docx/exportDocx.js +3 -9
  5. package/dist/docx/headersFooters.js +35 -23
  6. package/dist/docx/identities.d.ts +28 -0
  7. package/dist/docx/identities.js +42 -10
  8. package/dist/docx/importDocx.js +25 -26
  9. package/dist/docx/importParagraph.d.ts +1 -4
  10. package/dist/docx/importParagraph.js +4 -18
  11. package/dist/docx/invariants.d.ts +1 -1
  12. package/dist/docx/invariants.js +45 -4
  13. package/dist/docx/notes/newNote.d.ts +39 -0
  14. package/dist/docx/notes/newNote.js +122 -0
  15. package/dist/docx/notes/numbering.d.ts +27 -0
  16. package/dist/docx/notes/numbering.js +120 -0
  17. package/dist/docx/notes/reading.d.ts +58 -0
  18. package/dist/docx/notes/reading.js +98 -0
  19. package/dist/docx/notes/writing.d.ts +19 -0
  20. package/dist/docx/notes/writing.js +96 -0
  21. package/dist/docx/partPlan.d.ts +3 -0
  22. package/dist/docx/partPlanners.d.ts +13 -0
  23. package/dist/docx/partPlanners.js +48 -0
  24. package/dist/docx/sections.d.ts +5 -0
  25. package/dist/docx/sections.js +7 -2
  26. package/dist/docx/serializeParagraph.js +7 -2
  27. package/dist/docx/session.d.ts +5 -0
  28. package/dist/docx/session.js +6 -0
  29. package/dist/docx/storyParts.d.ts +112 -0
  30. package/dist/docx/storyParts.js +289 -0
  31. package/dist/editor/clipboard/htmlReader.d.ts +24 -4
  32. package/dist/editor/clipboard/htmlReader.js +39 -4
  33. package/dist/editor/clipboard/internalChannel.d.ts +30 -3
  34. package/dist/editor/clipboard/internalChannel.js +43 -3
  35. package/dist/editor/clipboard/normalizers.d.ts +35 -8
  36. package/dist/editor/clipboard/normalizers.js +60 -11
  37. package/dist/editor/clipboard/plugin.js +42 -22
  38. package/dist/editor/commands/index.d.ts +1 -0
  39. package/dist/editor/commands/index.js +18 -0
  40. package/dist/editor/commands/noteCommands.d.ts +47 -0
  41. package/dist/editor/commands/noteCommands.js +95 -0
  42. package/dist/editor/commands/noteQueries.d.ts +22 -3
  43. package/dist/editor/commands/noteQueries.js +21 -13
  44. package/dist/editor/createEditor.js +21 -3
  45. package/dist/editor/documentStyles.d.ts +3 -0
  46. package/dist/editor/documentStyles.js +2 -1
  47. package/dist/editor/editorDocument.d.ts +24 -0
  48. package/dist/editor/editorDocument.js +34 -4
  49. package/dist/editor/notes/noteSurface.d.ts +53 -0
  50. package/dist/editor/notes/noteSurface.js +246 -0
  51. package/dist/editor/plugins/compositionSelection.d.ts +20 -0
  52. package/dist/editor/plugins/compositionSelection.js +19 -0
  53. package/dist/editor/plugins/keymap.d.ts +14 -0
  54. package/dist/editor/plugins/keymap.js +13 -1
  55. package/dist/editor/plugins/noteLifecycle.d.ts +18 -0
  56. package/dist/editor/plugins/noteLifecycle.js +117 -0
  57. package/dist/editor/plugins/noteNavigation.d.ts +45 -0
  58. package/dist/editor/plugins/noteNavigation.js +86 -0
  59. package/dist/editor/plugins/noteNumbering.d.ts +13 -0
  60. package/dist/editor/plugins/noteNumbering.js +58 -0
  61. package/dist/editor/plugins/rowResize.js +2 -7
  62. package/dist/editor/plugins/tabLayout.js +2 -7
  63. package/dist/editor/stories/storyMarkup.d.ts +24 -0
  64. package/dist/editor/stories/storyMarkup.js +35 -0
  65. package/dist/editor/stories/storyState.d.ts +34 -0
  66. package/dist/editor/stories/storyState.js +74 -0
  67. package/dist/editor/stories/storyView.d.ts +115 -0
  68. package/dist/editor/stories/storyView.js +144 -0
  69. package/dist/numbering/spellers.d.ts +1 -1
  70. package/dist/numbering/spellers.js +6 -1
  71. package/dist/page/blockKinds.d.ts +23 -4
  72. package/dist/page/demands/footnoteDemands.d.ts +18 -0
  73. package/dist/page/demands/footnoteDemands.js +51 -0
  74. package/dist/page/demands/index.d.ts +43 -0
  75. package/dist/page/demands/index.js +8 -0
  76. package/dist/page/kinds/paragraphKind.js +7 -1
  77. package/dist/page/kinds/tableKind.js +9 -1
  78. package/dist/page/measureBlocks.d.ts +2 -0
  79. package/dist/page/measureBlocks.js +31 -10
  80. package/dist/page/pageDecorations.d.ts +9 -2
  81. package/dist/page/pageDecorations.js +18 -5
  82. package/dist/page/pageLayout.d.ts +77 -1
  83. package/dist/page/pageLayout.js +186 -9
  84. package/dist/page/usePageLayout.d.ts +57 -2
  85. package/dist/page/usePageLayout.js +50 -5
  86. package/dist/schema/attrRoles.js +3 -2
  87. package/dist/schema/docxSchema.js +5 -2
  88. package/dist/schema/editGuard.d.ts +1 -1
  89. package/dist/schema/guards.js +1 -2
  90. package/dist/schema/preservedGuards.d.ts +4 -6
  91. package/dist/schema/preservedGuards.js +0 -9
  92. package/dist/schema/stories.d.ts +34 -2
  93. package/dist/schema/stories.js +22 -5
  94. package/dist/styles/classNames.d.ts +21 -6
  95. package/dist/styles/classNames.js +21 -6
  96. package/dist/styles/inlineStyle.d.ts +7 -0
  97. package/dist/styles/inlineStyle.js +11 -5
  98. package/dist/styles/visualScale.d.ts +19 -0
  99. package/dist/styles/visualScale.js +26 -0
  100. package/dist/styles.css +89 -32
  101. package/dist/ui/InsertImageButton.d.ts +3 -1
  102. package/dist/ui/InsertImageButton.js +3 -2
  103. package/dist/ui/TableMenu.d.ts +5 -2
  104. package/dist/ui/TableMenu.js +18 -1
  105. package/dist/ui/TextMenu.d.ts +5 -2
  106. package/dist/ui/TextMenu.js +21 -6
  107. package/dist/ui/Toolbar.d.ts +17 -2
  108. package/dist/ui/Toolbar.js +23 -10
  109. package/dist/ui/comments/useCommentRailLayout.js +2 -4
  110. package/dist/ui/noteItems.d.ts +21 -0
  111. package/dist/ui/noteItems.js +30 -0
  112. package/dist/ui/notes/NoteAreas.d.ts +73 -0
  113. package/dist/ui/notes/NoteAreas.js +131 -0
  114. package/dist/ui/notes/NoteList.d.ts +25 -0
  115. package/dist/ui/notes/NoteList.js +58 -0
  116. package/dist/ui/notes/StoryRow.d.ts +75 -0
  117. package/dist/ui/notes/StoryRow.js +122 -0
  118. package/dist/ui/notes/noteBands.d.ts +100 -0
  119. package/dist/ui/notes/noteBands.js +142 -0
  120. package/dist/ui/notes/noteHeights.d.ts +10 -0
  121. package/dist/ui/notes/noteHeights.js +24 -0
  122. package/dist/ui/notes/noteSeparator.d.ts +12 -0
  123. package/dist/ui/notes/noteSeparator.js +9 -0
  124. package/dist/ui/notes/useStorySurface.d.ts +68 -0
  125. package/dist/ui/notes/useStorySurface.js +95 -0
  126. package/dist/ui/runCommand.d.ts +8 -1
  127. package/dist/ui/runCommand.js +2 -2
  128. package/dist/ui/shortcutLabels.d.ts +16 -0
  129. package/dist/ui/shortcutLabels.js +9 -0
  130. package/dist/ui/usePageRoom.d.ts +13 -0
  131. package/dist/ui/usePageRoom.js +31 -0
  132. package/package.json +1 -1
  133. package/dist/docx/notes.d.ts +0 -27
  134. package/dist/docx/notes.js +0 -55
  135. package/dist/ui/NotesPanel.d.ts +0 -6
  136. package/dist/ui/NotesPanel.js +0 -33
@@ -0,0 +1,144 @@
1
+ // src/editor/stories/storyView.ts
2
+ import {
3
+ TextSelection
4
+ } from "prosemirror-state";
5
+ import { EditorView } from "prosemirror-view";
6
+ import { editingProtection, editsShut } from "../../schema/protectionState.js";
7
+ import { sameStory, storyNodeOf } from "../../schema/stories.js";
8
+ import { editorClassNames } from "../../styles/classNames.js";
9
+ import { runMarkView } from "../views/runMarkView.js";
10
+ import { storyEditorState } from "./storyState.js";
11
+ var SURFACE_CAPABILITIES = [
12
+ "list",
13
+ "table",
14
+ "image",
15
+ "link",
16
+ "note",
17
+ "comment"
18
+ ];
19
+ var EVERY_CAPABILITY = new Set(
20
+ SURFACE_CAPABILITIES
21
+ );
22
+ var NO_CAPABILITY = /* @__PURE__ */ new Set();
23
+ function caretOf(state) {
24
+ return {
25
+ kind: "at",
26
+ anchor: state.selection.anchor,
27
+ head: state.selection.head
28
+ };
29
+ }
30
+ function placed(state, at, to) {
31
+ const size = state.doc.content.size;
32
+ const $anchor = state.doc.resolve(Math.min(Math.max(at, 0), size));
33
+ const $head = state.doc.resolve(Math.min(Math.max(to, 0), size));
34
+ return state.apply(
35
+ state.tr.setSelection(TextSelection.between($anchor, $head))
36
+ );
37
+ }
38
+ function endComposition(view) {
39
+ if (!view.composing) return;
40
+ const marks = view.state.storedMarks ?? view.state.selection.$from.marks();
41
+ view.dispatch(
42
+ view.state.tr.setStoredMarks(marks).setMeta("addToHistory", false)
43
+ );
44
+ }
45
+ function createStoryView({
46
+ mount,
47
+ host,
48
+ key,
49
+ document,
50
+ fontFallbacks,
51
+ extensions,
52
+ caret = null,
53
+ onStateChange
54
+ }) {
55
+ const standing = () => host.shut(key) ? "readOnly" : editingProtection(host.state());
56
+ let protection = standing();
57
+ const keys = {
58
+ "Mod-z": () => host.undo(),
59
+ "Mod-y": () => host.redo(),
60
+ "Shift-Mod-z": () => host.redo(),
61
+ Escape: () => {
62
+ host.leave(key);
63
+ return true;
64
+ }
65
+ };
66
+ const built = (story) => storyEditorState({
67
+ story,
68
+ document,
69
+ protection,
70
+ keys,
71
+ plugins: extensions.plugins,
72
+ normalizers: extensions.normalizers,
73
+ takes: extensions.takes
74
+ });
75
+ const storyNow = () => storyNodeOf(host.state().doc, key);
76
+ const opening = storyNow();
77
+ if (opening === null) {
78
+ throw new Error(`the document holds no story named ${key}`);
79
+ }
80
+ const view = new EditorView(mount, {
81
+ state: built(opening),
82
+ // A standing that shuts the story shuts typing in it, and is read off the state so that a
83
+ // mode switched while the story is open takes effect without a new view
84
+ editable: (current) => !editsShut(current),
85
+ attributes: { class: editorClassNames.storyBody },
86
+ // The schema can only draw a run with the default fallbacks; this view draws the host's
87
+ markViews: { run: runMarkView(fontFallbacks) },
88
+ nodeViews: extensions.nodeViews,
89
+ dispatchTransaction(transaction) {
90
+ const next = view.state.applyTransaction(transaction);
91
+ if (next.state.doc === view.state.doc) {
92
+ show(next.state);
93
+ return;
94
+ }
95
+ const before = view.state;
96
+ show(next.state);
97
+ if (host.write(key, next.state.doc)) return;
98
+ if (sameStory(storyNow(), next.state.doc)) return;
99
+ show(before);
100
+ endComposition(view);
101
+ }
102
+ });
103
+ const show = (next) => {
104
+ view.updateState(next);
105
+ onStateChange?.(next);
106
+ };
107
+ const open = caret ?? null;
108
+ if (open === null) {
109
+ const end = view.state.doc.content.size;
110
+ show(placed(view.state, end, end));
111
+ } else if (open.kind === "at") {
112
+ show(placed(view.state, open.anchor, open.head));
113
+ } else {
114
+ const at = view.posAtCoords({ left: open.left, top: open.top });
115
+ const pos = at?.pos ?? view.state.doc.content.size;
116
+ show(placed(view.state, pos, pos));
117
+ }
118
+ const registerFocus = () => host.activate(key, view);
119
+ view.dom.addEventListener("focus", registerFocus);
120
+ return {
121
+ view,
122
+ sync() {
123
+ const story = storyNow();
124
+ if (story === null) return;
125
+ const wanted = standing();
126
+ if (wanted === protection && sameStory(story, view.state.doc)) return;
127
+ const { anchor, head } = view.state.selection;
128
+ protection = wanted;
129
+ show(placed(built(story), anchor, head));
130
+ },
131
+ destroy() {
132
+ view.dom.removeEventListener("focus", registerFocus);
133
+ host.activate(key, null);
134
+ view.destroy();
135
+ }
136
+ };
137
+ }
138
+ export {
139
+ EVERY_CAPABILITY,
140
+ NO_CAPABILITY,
141
+ SURFACE_CAPABILITIES,
142
+ caretOf,
143
+ createStoryView
144
+ };
@@ -6,7 +6,7 @@
6
6
  * because it has a speller here, so a marker can never be counted in a format nothing can write.
7
7
  */
8
8
  /** A `w:numFmt` this editor spells out. Every other one is drawn as a decimal */
9
- export type NumberFormat = "decimal" | "decimalZero" | "bullet" | "lowerLetter" | "upperLetter" | "lowerRoman" | "upperRoman" | "ganada" | "koreanDigital" | "chineseCounting";
9
+ export type NumberFormat = "decimal" | "decimalZero" | "bullet" | "lowerLetter" | "upperLetter" | "lowerRoman" | "upperRoman" | "ganada" | "koreanDigital" | "chineseCounting" | "chicago";
10
10
  export declare function isNumberFormat(value: string | null): value is NumberFormat;
11
11
  /**
12
12
  * The count as this format writes it, and as a decimal where spelling it out would run past the
@@ -31,6 +31,7 @@ function repeated(symbols, count) {
31
31
  return (symbols[index] ?? "").repeat(times);
32
32
  }
33
33
  var LATIN = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
34
+ var CHICAGO = "*\u2020\u2021\xA7";
34
35
  var GANADA = "\uAC00\uB098\uB2E4\uB77C\uB9C8\uBC14\uC0AC\uC544\uC790\uCC28\uCE74\uD0C0\uD30C\uD558";
35
36
  var KOREAN_DIGITS = "\uC601\uC77C\uC774\uC0BC\uC0AC\uC624\uC721\uCE60\uD314\uAD6C";
36
37
  function digits(symbols, count) {
@@ -80,7 +81,11 @@ var NUMBER_SPELLERS = {
80
81
  spell: (count) => digits(KOREAN_DIGITS, count),
81
82
  countsPerChar: POSITIONAL
82
83
  },
83
- chineseCounting: { spell: chineseCounting, countsPerChar: POSITIONAL }
84
+ chineseCounting: { spell: chineseCounting, countsPerChar: POSITIONAL },
85
+ chicago: {
86
+ spell: (count) => repeated(CHICAGO, count),
87
+ countsPerChar: CHICAGO.length
88
+ }
84
89
  };
85
90
  function isNumberFormat(value) {
86
91
  return value !== null && Object.hasOwn(NUMBER_SPELLERS, value);
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import type { Node as PMNode } from "prosemirror-model";
12
12
  import type { Decoration, EditorView } from "prosemirror-view";
13
+ import type { PageDemand } from "./demands";
13
14
  /** A place inside a block where the next page may, or must, start */
14
15
  export interface BreakCandidate {
15
16
  /**
@@ -41,13 +42,21 @@ export interface MeasuredBlock {
41
42
  minFirstPiece: number;
42
43
  /** The document asks for this block to stand on the same page as the start of the block after it */
43
44
  keepWithNext: boolean;
45
+ /**
46
+ * The places in the block that ask the page they land on for room at its foot, their offsets read
47
+ * like a candidate's with no space in the block. A block asking for none may leave it out
48
+ */
49
+ demands?: readonly PageDemand[];
44
50
  }
45
51
  /** A page cut the layout decided on: the space opened before the continued piece */
46
52
  export interface PageCut {
47
53
  at: number;
48
54
  height: number;
49
55
  }
50
- /** One block as it stands on the sheet, handed to the kind about to measure it */
56
+ /**
57
+ * One block as it stands on the sheet, handed to the kind about to measure it and then to every
58
+ * demand source (`page/demands`)
59
+ */
51
60
  export interface MeasureTarget {
52
61
  view: EditorView;
53
62
  node: PMNode;
@@ -72,6 +81,15 @@ export interface KindMeasure {
72
81
  breakAfter: boolean;
73
82
  /** Height the engine's own marks add inside this block, taken off its measured bottom */
74
83
  appliedHeight: number;
84
+ /**
85
+ * What the engine's marks have opened at each place this block was cut, as the browser drew it,
86
+ * by the candidate's position. What is drawn there can come out taller than the cut asked for -
87
+ * a table's spacer row takes half of a collapsed border on each side - and everything else a
88
+ * kind reports is read off the sheet as drawn, so a place below a cut is taken back to its
89
+ * natural offset by this rather than by what the layout asked for (`page/measureBlocks`).
90
+ * A kind that opens nothing leaves it out
91
+ */
92
+ opened?: ReadonlyMap<number, number>;
75
93
  /**
76
94
  * Whether the document asks for this block to stand on the same page as the start of the block
77
95
  * after it. A kind whose blocks never do leaves it out
@@ -82,9 +100,10 @@ export interface KindMeasure {
82
100
  * Everything the engine does with one shape of breakable block: how one is measured, and how the
83
101
  * cuts the layout gave it are drawn.
84
102
  *
85
- * A shape the engine learns later - a footnote area, a paragraph with a floating object hanging
86
- * out of it - is one more kind rather than another branch in the measurer, in the decorations
87
- * and in the plugin state at once.
103
+ * A shape the engine learns later is one more kind rather than another branch in the measurer, in
104
+ * the decorations and in the plugin state at once. Room a place inside a block asks of the page it
105
+ * lands on is not a shape: a block has one kind while such a place can stand inside a block of any
106
+ * kind, so it is asked of every block by a demand source (`page/demands`) instead.
88
107
  */
89
108
  export interface BlockKind {
90
109
  readonly name: string;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The room a footnote reference asks of the page it lands on: its footnote, drawn at the foot of
3
+ * that page (`ui/notes`).
4
+ *
5
+ * The sheet is measured on every layout pass and nearly every block holds no reference, so which
6
+ * references a block holds is read off the block node and remembered against it. A node an edit
7
+ * did not touch is the same node afterwards, so a pass reads nothing off the page for a block with
8
+ * no reference and walks only the blocks an edit rebuilt.
9
+ */
10
+ import type { DemandSource } from "./index";
11
+ /** The band footnotes are kept in, which the layout's caller hands the heights of */
12
+ export declare const FOOTNOTE_BAND = "footnote";
13
+ /**
14
+ * Where footnotes stand among the bands a page keeps (`DemandBand.order`). Word sets them at the
15
+ * very foot of the body, under anything else the page holds there, so this is the last place
16
+ */
17
+ export declare const FOOTNOTE_BAND_ORDER = 0;
18
+ export declare const footnoteDemands: DemandSource;
@@ -0,0 +1,51 @@
1
+ // src/page/demands/footnoteDemands.ts
2
+ import { docxSchema } from "../../schema/index.js";
3
+ import { storyKey } from "../../schema/stories.js";
4
+ var FOOTNOTE_BAND = "footnote";
5
+ var FOOTNOTE_BAND_ORDER = 0;
6
+ var BAND_NOTE_KINDS = ["footnote"];
7
+ var NO_REFERENCES = [];
8
+ var NO_DEMANDS = [];
9
+ var held = /* @__PURE__ */ new WeakMap();
10
+ function footnoteReferencesIn(block) {
11
+ const remembered = held.get(block);
12
+ if (remembered) return remembered;
13
+ const found = [];
14
+ block.descendants((node, offset) => {
15
+ if (node.type !== docxSchema.nodes.noteReference) return true;
16
+ const id = node.attrs.id;
17
+ const kind = BAND_NOTE_KINDS.find(
18
+ (candidate) => candidate === node.attrs.kind
19
+ );
20
+ if (kind !== void 0 && typeof id === "string") {
21
+ found.push({ offset, key: storyKey(kind, id) });
22
+ }
23
+ return false;
24
+ });
25
+ const references = found.length === 0 ? NO_REFERENCES : found;
26
+ held.set(block, references);
27
+ return references;
28
+ }
29
+ var footnoteDemands = {
30
+ name: "footnote",
31
+ demandsIn({ view, node, pos, sheetY, top }) {
32
+ const references = footnoteReferencesIn(node);
33
+ if (references.length === 0) return NO_DEMANDS;
34
+ return references.flatMap(({ offset, key }) => {
35
+ const drawn = view.nodeDOM(pos + 1 + offset);
36
+ if (!(drawn instanceof Element)) return [];
37
+ return [
38
+ {
39
+ offset: sheetY(drawn.getBoundingClientRect().top) - top,
40
+ id: key,
41
+ band: FOOTNOTE_BAND
42
+ }
43
+ ];
44
+ });
45
+ }
46
+ };
47
+ export {
48
+ FOOTNOTE_BAND,
49
+ FOOTNOTE_BAND_ORDER,
50
+ footnoteDemands
51
+ };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Room a place inside a block asks the page it lands on to keep at the foot of its body.
3
+ *
4
+ * A block has one kind (`page/blockKinds`), and a place asking for room can stand inside a block of
5
+ * any kind: a paragraph, or a paragraph in a table cell. So what asks is not a kind but a source,
6
+ * and every registered source is asked about every block whatever its kind. The layout
7
+ * (`page/pageLayout`) knows a demand by its band alone, never by what put it there.
8
+ */
9
+ import type { MeasureTarget } from "../blockKinds";
10
+ /** Room one place in a block asks the page it lands on to keep at the foot */
11
+ export interface PageDemand {
12
+ /**
13
+ * The place's top from the block's natural top. A source reads it off the sheet as drawn, the
14
+ * way a kind reads a candidate (`target.sheetY(viewportY) - target.top`), and the measurement
15
+ * takes back off the spaces the engine opened inside the block above that place
16
+ * (`page/measureBlocks`), so no source has to know what any kind draws
17
+ */
18
+ readonly offset: number;
19
+ /** What is kept. One id is kept once on a page, however many places on it ask for it */
20
+ readonly id: string;
21
+ /** The band at the foot of the page that keeps it (`DemandBand`) */
22
+ readonly band: string;
23
+ }
24
+ export interface DemandSource {
25
+ readonly name: string;
26
+ demandsIn(target: MeasureTarget): readonly PageDemand[];
27
+ }
28
+ export interface DemandBand {
29
+ /**
30
+ * Where this band stands among the bands one page keeps, counted up from the foot of the body:
31
+ * 0 stands at the very foot and a higher number above it, between the text and the band below.
32
+ * Without it a page would stack its bands in the order its text happened to reach them, so the
33
+ * same two bands could come out one way round on one page and the other way round on the next.
34
+ * Two bands a page may hold together name two different places
35
+ */
36
+ readonly order: number;
37
+ /** Height a page adds once when it holds any demand of this band */
38
+ readonly overhead: number;
39
+ /** The height each id takes in the band. An id not measured yet takes none */
40
+ readonly heights: ReadonlyMap<string, number>;
41
+ }
42
+ /** The sources the editor asks about every block, in the order their demands are listed */
43
+ export declare const DEFAULT_DEMAND_SOURCES: readonly DemandSource[];
@@ -0,0 +1,8 @@
1
+ // src/page/demands/index.ts
2
+ import { footnoteDemands } from "./footnoteDemands.js";
3
+ var DEFAULT_DEMAND_SOURCES = [
4
+ footnoteDemands
5
+ ];
6
+ export {
7
+ DEFAULT_DEMAND_SOURCES
8
+ };
@@ -28,9 +28,11 @@ var paragraphKind = {
28
28
  const breaks = pageBreaksIn(node, pos);
29
29
  const candidates = [];
30
30
  let appliedHeight = 0;
31
+ const opened = /* @__PURE__ */ new Map();
31
32
  dom.querySelectorAll(BREAK_SPACE).forEach((space, index) => {
32
33
  const box = space.getBoundingClientRect();
33
34
  const found = breaks.at(index);
35
+ const drawn = box.height / scale;
34
36
  if (found) {
35
37
  candidates.push({
36
38
  at: found.at,
@@ -38,8 +40,9 @@ var paragraphKind = {
38
40
  forced: true,
39
41
  repeatHeight: 0
40
42
  });
43
+ if (drawn > 0) opened.set(found.at, drawn);
41
44
  }
42
- appliedHeight += box.height / scale;
45
+ appliedHeight += drawn;
43
46
  });
44
47
  return {
45
48
  candidates,
@@ -48,6 +51,7 @@ var paragraphKind = {
48
51
  // still start a new page, but only after the whole block
49
52
  breakAfter: dom.querySelectorAll(PAGE_BREAK_BR).length > candidates.length,
50
53
  appliedHeight,
54
+ opened,
51
55
  keepWithNext: toParagraphFormat(node.attrs.format)?.keepNext === true
52
56
  };
53
57
  },
@@ -59,6 +63,8 @@ var paragraphKind = {
59
63
  into.push(
60
64
  Decoration.inline(at, at + size, {
61
65
  nodeName: "span",
66
+ // Chrome can enter the enlarged break and insert text outside the document model.
67
+ contenteditable: "false",
62
68
  style: spaceStyle(height),
63
69
  [editorAttributes.pageBreakSpace]: `${height}`
64
70
  })
@@ -34,7 +34,8 @@ function rowEntries(view, tableNode, tablePos, tableDom, appliedRows, scale) {
34
34
  node: row,
35
35
  pos,
36
36
  top: (rect.top - tableTop) / scale - appliedBefore,
37
- height: rect.height / scale
37
+ height: rect.height / scale,
38
+ appliedAbove: appliedBefore
38
39
  });
39
40
  });
40
41
  return entries;
@@ -132,6 +133,12 @@ var tableKind = {
132
133
  });
133
134
  }
134
135
  }
136
+ const opened = /* @__PURE__ */ new Map();
137
+ rows.forEach((row, index) => {
138
+ const above = index === 0 ? 0 : rows[index - 1]?.appliedAbove ?? 0;
139
+ const here = row.appliedAbove - above;
140
+ if (here > 0) opened.set(row.pos, here);
141
+ });
135
142
  const firstBodyBoundary = candidates.find(
136
143
  (candidate) => candidate.offset > repeatHeaderHeight + 0.5
137
144
  );
@@ -141,6 +148,7 @@ var tableKind = {
141
148
  );
142
149
  return {
143
150
  candidates,
151
+ opened,
144
152
  // The smallest useful first piece: the headers followed by one body row group
145
153
  minFirstPiece: firstBodyBoundary?.offset ?? dom.getBoundingClientRect().height / scale - appliedHeight,
146
154
  // A space cannot be opened inside a cell, so a break standing in one is answered after the
@@ -7,6 +7,8 @@
7
7
  * The engine's own marks are taken back off as they are read, so a block already pushed and a
8
8
  * break already given its space read as they would with neither applied. A measurement that read
9
9
  * them in would add to what is already there, and the layout would creep on every pass.
10
+ * Every demand source (`page/demands`) is asked about every block as well, after its kind, and the
11
+ * places it reads have the spaces the engine opened inside the block taken back off the same way.
10
12
  * Where there is no layout (in tests) everything is 0, so the result is a single page.
11
13
  */
12
14
  import type { EditorView } from "prosemirror-view";
@@ -1,15 +1,15 @@
1
1
  // src/page/measureBlocks.ts
2
2
  import { editorAttributes } from "../styles/classNames.js";
3
- import { blockKindFor } from "./blockKinds.js";
4
- import { blockKindsOf } from "./pageDecorations.js";
3
+ import { visualScaleOf } from "../styles/visualScale.js";
4
+ import {
5
+ blockKindFor
6
+ } from "./blockKinds.js";
7
+ import { blockKindsOf, demandSourcesOf } from "./pageDecorations.js";
8
+ var NOTHING_OPENED = /* @__PURE__ */ new Map();
5
9
  function pixels(value) {
6
10
  const parsed = Number.parseFloat(value);
7
11
  return Number.isFinite(parsed) ? parsed : 0;
8
12
  }
9
- function visualScale(element) {
10
- const scale = pixels(getComputedStyle(element).zoom);
11
- return scale > 0 ? scale : 1;
12
- }
13
13
  function appliedPush(element) {
14
14
  return pixels(element.getAttribute(editorAttributes.pagePush) ?? "");
15
15
  }
@@ -23,16 +23,28 @@ function drawnBlocks(view) {
23
23
  });
24
24
  return found;
25
25
  }
26
+ function naturalOffset(candidates, opened, drawn) {
27
+ let shift = 0;
28
+ for (const candidate of candidates) {
29
+ const space = opened.get(candidate.at);
30
+ if (space === void 0) continue;
31
+ const start = candidate.offset + shift;
32
+ if (drawn < start) break;
33
+ shift += Math.min(space, drawn - start);
34
+ }
35
+ return drawn - shift;
36
+ }
26
37
  function measureSheet(view, layer) {
27
38
  const sheet = view.dom;
28
39
  const sheetRect = sheet.getBoundingClientRect();
29
40
  const layerRect = layer.getBoundingClientRect();
30
41
  const style = getComputedStyle(sheet);
31
- const scale = visualScale(layer);
42
+ const scale = visualScaleOf(layer);
32
43
  const contentTop = pixels(style.paddingTop);
33
44
  const contentBottom = pixels(style.paddingBottom);
34
45
  const sheetY = (viewportY) => (viewportY - sheetRect.top) / scale;
35
46
  const kinds = blockKindsOf(view.state);
47
+ const sources = demandSourcesOf(view.state);
36
48
  const blocks = [];
37
49
  let previousBottom = contentTop;
38
50
  let applied = 0;
@@ -42,7 +54,7 @@ function measureSheet(view, layer) {
42
54
  const above = applied;
43
55
  const blockY = (viewportY) => sheetY(viewportY) - above;
44
56
  const top = blockY(rect.top);
45
- const measured = blockKindFor(kinds, node).measure({
57
+ const target = {
46
58
  view,
47
59
  node,
48
60
  pos,
@@ -50,7 +62,8 @@ function measureSheet(view, layer) {
50
62
  sheetY: blockY,
51
63
  top,
52
64
  scale
53
- });
65
+ };
66
+ const measured = blockKindFor(kinds, node).measure(target);
54
67
  applied += measured.appliedHeight;
55
68
  const bottom = sheetY(rect.bottom) - applied;
56
69
  blocks.push({
@@ -61,7 +74,15 @@ function measureSheet(view, layer) {
61
74
  breakAfter: measured.breakAfter,
62
75
  candidates: measured.candidates,
63
76
  minFirstPiece: measured.minFirstPiece,
64
- keepWithNext: measured.keepWithNext ?? false
77
+ keepWithNext: measured.keepWithNext ?? false,
78
+ demands: sources.flatMap((source) => source.demandsIn(target)).map((demand) => ({
79
+ ...demand,
80
+ offset: naturalOffset(
81
+ measured.candidates,
82
+ measured.opened ?? NOTHING_OPENED,
83
+ demand.offset
84
+ )
85
+ }))
65
86
  });
66
87
  previousBottom = bottom;
67
88
  }
@@ -3,13 +3,15 @@
3
3
  * own kind draws the cuts inside it as (`page/kinds`).
4
4
  *
5
5
  * The kinds the editor was built with are held here, so the measurement and the decorations read
6
- * one registry and a block is measured by the same kind that draws it.
6
+ * one registry and a block is measured by the same kind that draws it. The demand sources
7
+ * (`page/demands`) the measurement asks about every block are held beside them.
7
8
  * The document model is left untouched, so no trace of any of it is left in the exported XML or
8
9
  * in the edit history.
9
10
  */
10
11
  import { type EditorState, Plugin } from "prosemirror-state";
11
12
  import { DecorationSet, type EditorView } from "prosemirror-view";
12
13
  import { type BlockKind, type PageCut } from "./blockKinds";
14
+ import { type DemandSource } from "./demands";
13
15
  import type { BlockPush } from "./pageLayout";
14
16
  /** Everything one measurement has to say about the page (`page/pageLayout`) */
15
17
  export interface PageMarksInput {
@@ -25,11 +27,16 @@ export interface PageMarksInput {
25
27
  }
26
28
  interface PageMarks extends PageMarksInput {
27
29
  kinds: readonly BlockKind[];
30
+ sources: readonly DemandSource[];
28
31
  decorations: DecorationSet;
29
32
  }
30
- export declare function pageDecorations(kinds?: readonly BlockKind[]): Plugin<PageMarks>;
33
+ export declare function pageDecorations(kinds?: readonly BlockKind[], sources?: readonly DemandSource[]): Plugin<PageMarks>;
31
34
  /** The kinds the editor was built with, which the measurement reads to match them */
32
35
  export declare function blockKindsOf(state: EditorState): readonly BlockKind[];
36
+ /** The demand sources the editor was built with, which the measurement asks about every block */
37
+ export declare function demandSourcesOf(state: EditorState): readonly DemandSource[];
38
+ /** The cuts the sheet is drawn with, whose spaces the measurement takes back off a demand's place */
39
+ export declare function pageCutsOf(state: EditorState): readonly PageCut[];
33
40
  /** One transaction, or none when the same marks are already applied */
34
41
  export declare function setPageMarks(view: EditorView, next: PageMarksInput): void;
35
42
  export {};
@@ -6,6 +6,7 @@ import {
6
6
  import { Decoration, DecorationSet } from "prosemirror-view";
7
7
  import { editorAttributes } from "../styles/classNames.js";
8
8
  import { blockKindFor } from "./blockKinds.js";
9
+ import { DEFAULT_DEMAND_SOURCES } from "./demands/index.js";
9
10
  import { DEFAULT_BLOCK_KINDS } from "./kinds/index.js";
10
11
  var marksKey = new PluginKey("docxPageDecorations");
11
12
  function pushStyle(marginTop) {
@@ -46,9 +47,10 @@ function decorationsFor(doc, kinds, pushes, cuts) {
46
47
  });
47
48
  return DecorationSet.create(doc, decorations);
48
49
  }
49
- function marksFor(doc, kinds, pushes, cuts) {
50
+ function marksFor(doc, { kinds, sources }, pushes, cuts) {
50
51
  return {
51
52
  kinds,
53
+ sources,
52
54
  pushes,
53
55
  cuts,
54
56
  decorations: decorationsFor(doc, kinds, pushes, cuts)
@@ -75,18 +77,18 @@ function samePageMarks(a, b) {
75
77
  return other !== void 0 && other.at === cut.at && other.height === cut.height;
76
78
  });
77
79
  }
78
- function pageDecorations(kinds = DEFAULT_BLOCK_KINDS) {
80
+ function pageDecorations(kinds = DEFAULT_BLOCK_KINDS, sources = DEFAULT_DEMAND_SOURCES) {
79
81
  return new Plugin({
80
82
  key: marksKey,
81
83
  state: {
82
- init: (_config, state) => marksFor(state.doc, kinds, [], []),
84
+ init: (_config, state) => marksFor(state.doc, { kinds, sources }, [], []),
83
85
  apply(tr, value) {
84
86
  const next = tr.getMeta(marksKey);
85
87
  if (next) return next;
86
88
  if (!tr.docChanged) return value;
87
89
  return marksFor(
88
90
  tr.doc,
89
- value.kinds,
91
+ value,
90
92
  value.pushes.map((push) => ({
91
93
  ...push,
92
94
  pos: tr.mapping.map(push.pos)
@@ -106,6 +108,12 @@ function pageDecorations(kinds = DEFAULT_BLOCK_KINDS) {
106
108
  function blockKindsOf(state) {
107
109
  return marksKey.getState(state)?.kinds ?? DEFAULT_BLOCK_KINDS;
108
110
  }
111
+ function demandSourcesOf(state) {
112
+ return marksKey.getState(state)?.sources ?? DEFAULT_DEMAND_SOURCES;
113
+ }
114
+ function pageCutsOf(state) {
115
+ return marksKey.getState(state)?.cuts ?? [];
116
+ }
109
117
  function current(view) {
110
118
  return marksKey.getState(view.state) ?? { pushes: [], cuts: [] };
111
119
  }
@@ -116,7 +124,10 @@ function setPageMarks(view, next) {
116
124
  marksKey,
117
125
  marksFor(
118
126
  view.state.doc,
119
- blockKindsOf(view.state),
127
+ {
128
+ kinds: blockKindsOf(view.state),
129
+ sources: demandSourcesOf(view.state)
130
+ },
120
131
  next.pushes,
121
132
  next.cuts
122
133
  )
@@ -125,6 +136,8 @@ function setPageMarks(view, next) {
125
136
  }
126
137
  export {
127
138
  blockKindsOf,
139
+ demandSourcesOf,
140
+ pageCutsOf,
128
141
  pageDecorations,
129
142
  setPageMarks
130
143
  };