@portone/docx-editor 0.5.1 → 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.
- package/CHANGELOG.md +47 -0
- package/CONTRIBUTING.md +8 -2
- package/README.md +1 -1
- package/dist/DocxEditor.js +87 -29
- package/dist/docx/exportDocx.d.ts +2 -1
- package/dist/docx/exportDocx.js +3 -9
- package/dist/docx/headersFooters.js +35 -23
- package/dist/docx/identities.d.ts +28 -0
- package/dist/docx/identities.js +42 -10
- package/dist/docx/importDocx.js +25 -26
- package/dist/docx/importParagraph.d.ts +1 -4
- package/dist/docx/importParagraph.js +4 -18
- package/dist/docx/invariants.d.ts +1 -1
- package/dist/docx/invariants.js +45 -4
- package/dist/docx/notes/newNote.d.ts +39 -0
- package/dist/docx/notes/newNote.js +122 -0
- package/dist/docx/notes/numbering.d.ts +27 -0
- package/dist/docx/notes/numbering.js +120 -0
- package/dist/docx/notes/reading.d.ts +58 -0
- package/dist/docx/notes/reading.js +98 -0
- package/dist/docx/notes/writing.d.ts +19 -0
- package/dist/docx/notes/writing.js +96 -0
- package/dist/docx/partPlan.d.ts +3 -0
- package/dist/docx/partPlanners.d.ts +13 -0
- package/dist/docx/partPlanners.js +48 -0
- package/dist/docx/sections.d.ts +5 -0
- package/dist/docx/sections.js +7 -2
- package/dist/docx/serializeParagraph.js +7 -2
- package/dist/docx/session.d.ts +5 -0
- package/dist/docx/session.js +6 -0
- package/dist/docx/storyParts.d.ts +112 -0
- package/dist/docx/storyParts.js +289 -0
- package/dist/editor/clipboard/htmlReader.d.ts +24 -4
- package/dist/editor/clipboard/htmlReader.js +39 -4
- package/dist/editor/clipboard/internalChannel.d.ts +30 -3
- package/dist/editor/clipboard/internalChannel.js +43 -3
- package/dist/editor/clipboard/normalizers.d.ts +35 -8
- package/dist/editor/clipboard/normalizers.js +60 -11
- package/dist/editor/clipboard/plugin.js +42 -22
- package/dist/editor/commands/comments/editing.d.ts +4 -1
- package/dist/editor/commands/comments/editing.js +1 -3
- package/dist/editor/commands/comments/model.d.ts +7 -0
- package/dist/editor/commands/index.d.ts +1 -0
- package/dist/editor/commands/index.js +18 -0
- package/dist/editor/commands/noteCommands.d.ts +47 -0
- package/dist/editor/commands/noteCommands.js +95 -0
- package/dist/editor/commands/noteQueries.d.ts +22 -3
- package/dist/editor/commands/noteQueries.js +21 -13
- package/dist/editor/createEditor.js +25 -3
- package/dist/editor/documentStyles.d.ts +3 -0
- package/dist/editor/documentStyles.js +2 -1
- package/dist/editor/editorDocument.d.ts +24 -0
- package/dist/editor/editorDocument.js +34 -4
- package/dist/editor/notes/noteSurface.d.ts +53 -0
- package/dist/editor/notes/noteSurface.js +246 -0
- package/dist/editor/plugins/commentDecorations.js +5 -4
- package/dist/editor/plugins/commentRestoration.d.ts +20 -0
- package/dist/editor/plugins/commentRestoration.js +168 -0
- package/dist/editor/plugins/compositionSelection.d.ts +20 -0
- package/dist/editor/plugins/compositionSelection.js +19 -0
- package/dist/editor/plugins/keymap.d.ts +14 -0
- package/dist/editor/plugins/keymap.js +13 -1
- package/dist/editor/plugins/noteLifecycle.d.ts +18 -0
- package/dist/editor/plugins/noteLifecycle.js +117 -0
- package/dist/editor/plugins/noteNavigation.d.ts +45 -0
- package/dist/editor/plugins/noteNavigation.js +86 -0
- package/dist/editor/plugins/noteNumbering.d.ts +13 -0
- package/dist/editor/plugins/noteNumbering.js +58 -0
- package/dist/editor/plugins/rowResize.js +2 -7
- package/dist/editor/plugins/tabLayout.js +2 -7
- package/dist/editor/stories/storyMarkup.d.ts +24 -0
- package/dist/editor/stories/storyMarkup.js +35 -0
- package/dist/editor/stories/storyState.d.ts +34 -0
- package/dist/editor/stories/storyState.js +74 -0
- package/dist/editor/stories/storyView.d.ts +115 -0
- package/dist/editor/stories/storyView.js +144 -0
- package/dist/numbering/spellers.d.ts +1 -1
- package/dist/numbering/spellers.js +6 -1
- package/dist/page/blockKinds.d.ts +23 -4
- package/dist/page/demands/footnoteDemands.d.ts +18 -0
- package/dist/page/demands/footnoteDemands.js +51 -0
- package/dist/page/demands/index.d.ts +43 -0
- package/dist/page/demands/index.js +8 -0
- package/dist/page/kinds/paragraphKind.js +7 -1
- package/dist/page/kinds/tableKind.js +9 -1
- package/dist/page/measureBlocks.d.ts +2 -0
- package/dist/page/measureBlocks.js +31 -10
- package/dist/page/pageDecorations.d.ts +9 -2
- package/dist/page/pageDecorations.js +18 -5
- package/dist/page/pageLayout.d.ts +77 -1
- package/dist/page/pageLayout.js +186 -9
- package/dist/page/usePageLayout.d.ts +57 -2
- package/dist/page/usePageLayout.js +50 -5
- package/dist/schema/attrRoles.js +3 -2
- package/dist/schema/docxSchema.js +5 -2
- package/dist/schema/editGuard.d.ts +1 -1
- package/dist/schema/guards.js +1 -2
- package/dist/schema/preservedGuards.d.ts +4 -6
- package/dist/schema/preservedGuards.js +0 -9
- package/dist/schema/stories.d.ts +34 -2
- package/dist/schema/stories.js +22 -5
- package/dist/styles/classNames.d.ts +23 -6
- package/dist/styles/classNames.js +23 -6
- package/dist/styles/inlineStyle.d.ts +7 -0
- package/dist/styles/inlineStyle.js +11 -5
- package/dist/styles/visualScale.d.ts +19 -0
- package/dist/styles/visualScale.js +26 -0
- package/dist/styles.css +96 -36
- package/dist/ui/CommentsPanel.d.ts +6 -1
- package/dist/ui/CommentsPanel.js +26 -8
- package/dist/ui/InsertImageButton.d.ts +3 -1
- package/dist/ui/InsertImageButton.js +3 -2
- package/dist/ui/TableMenu.d.ts +5 -2
- package/dist/ui/TableMenu.js +18 -1
- package/dist/ui/TextMenu.d.ts +5 -2
- package/dist/ui/TextMenu.js +21 -6
- package/dist/ui/Toolbar.d.ts +17 -2
- package/dist/ui/Toolbar.js +23 -10
- package/dist/ui/comments/useCommentRailLayout.js +2 -4
- package/dist/ui/noteItems.d.ts +21 -0
- package/dist/ui/noteItems.js +30 -0
- package/dist/ui/notes/NoteAreas.d.ts +73 -0
- package/dist/ui/notes/NoteAreas.js +131 -0
- package/dist/ui/notes/NoteList.d.ts +25 -0
- package/dist/ui/notes/NoteList.js +58 -0
- package/dist/ui/notes/StoryRow.d.ts +75 -0
- package/dist/ui/notes/StoryRow.js +122 -0
- package/dist/ui/notes/noteBands.d.ts +100 -0
- package/dist/ui/notes/noteBands.js +142 -0
- package/dist/ui/notes/noteHeights.d.ts +10 -0
- package/dist/ui/notes/noteHeights.js +24 -0
- package/dist/ui/notes/noteSeparator.d.ts +12 -0
- package/dist/ui/notes/noteSeparator.js +9 -0
- package/dist/ui/notes/useStorySurface.d.ts +68 -0
- package/dist/ui/notes/useStorySurface.js +95 -0
- package/dist/ui/runCommand.d.ts +8 -1
- package/dist/ui/runCommand.js +2 -2
- package/dist/ui/shortcutLabels.d.ts +16 -0
- package/dist/ui/shortcutLabels.js +9 -0
- package/dist/ui/usePageRoom.d.ts +13 -0
- package/dist/ui/usePageRoom.js +31 -0
- package/package.json +2 -1
- package/dist/docx/notes.d.ts +0 -27
- package/dist/docx/notes.js +0 -55
- package/dist/ui/NotesPanel.d.ts +0 -6
- package/dist/ui/NotesPanel.js +0 -33
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// src/docx/storyParts.ts
|
|
2
|
+
import { attrsText, openTagXml } from "../ooxml/element.js";
|
|
3
|
+
import { NAMESPACES, wName } from "../ooxml/names.js";
|
|
4
|
+
import {
|
|
5
|
+
ensureRootDeclarations,
|
|
6
|
+
partRootProblem,
|
|
7
|
+
splicePart
|
|
8
|
+
} from "../ooxml/partSplice.js";
|
|
9
|
+
import { ST_DecimalNumber } from "../ooxml/simpleTypes.js";
|
|
10
|
+
import { parseAttrs, readTag, rootTagAt } from "../ooxml/tagScan.js";
|
|
11
|
+
import {
|
|
12
|
+
attributeByLocalName,
|
|
13
|
+
decodeUtf8,
|
|
14
|
+
elementChildren,
|
|
15
|
+
encodeUtf8,
|
|
16
|
+
parseXml
|
|
17
|
+
} from "../ooxml/xml.js";
|
|
18
|
+
import { sameSource } from "../schema/sourceEquality.js";
|
|
19
|
+
import {
|
|
20
|
+
asStoryKey,
|
|
21
|
+
STORY_KINDS,
|
|
22
|
+
storiesOf,
|
|
23
|
+
storyKey,
|
|
24
|
+
storyNodeOf
|
|
25
|
+
} from "../schema/stories.js";
|
|
26
|
+
import { NO_EXPORT_REFS } from "./exportRefs.js";
|
|
27
|
+
import { withUniqueStoryIdentities } from "./identities.js";
|
|
28
|
+
import {
|
|
29
|
+
availablePartPath,
|
|
30
|
+
CONTENT_TYPES_PATH,
|
|
31
|
+
readPart,
|
|
32
|
+
relatedPartPath
|
|
33
|
+
} from "./packageParts.js";
|
|
34
|
+
import { directoryOf } from "./relationships.js";
|
|
35
|
+
import { scanBlocksIn } from "./scan.js";
|
|
36
|
+
import { serializeStory } from "./serializeStory.js";
|
|
37
|
+
var DECIMAL = /^-?\d+$/;
|
|
38
|
+
function byId(a, b) {
|
|
39
|
+
if (DECIMAL.test(a) && DECIMAL.test(b)) return Number(a) - Number(b);
|
|
40
|
+
if (a === b) return 0;
|
|
41
|
+
return a < b ? -1 : 1;
|
|
42
|
+
}
|
|
43
|
+
function storyIdOf(key, kind) {
|
|
44
|
+
return key.slice(kind.length + 1);
|
|
45
|
+
}
|
|
46
|
+
function arrivedChange(doc, imported) {
|
|
47
|
+
const current = storyNodeOf(doc, imported.key);
|
|
48
|
+
if (current === null) return { change: "removed", imported };
|
|
49
|
+
return sameSource(current, imported.doc) ? { change: "kept", imported } : { change: "edited", imported, current };
|
|
50
|
+
}
|
|
51
|
+
function storyChangesOf(doc, session, kind) {
|
|
52
|
+
const arrived = Array.from(session.stories.values()).filter((imported) => imported.kind === kind).map((imported) => arrivedChange(doc, imported));
|
|
53
|
+
const added = Object.keys(storiesOf(doc)).flatMap((text) => {
|
|
54
|
+
const key = asStoryKey(text);
|
|
55
|
+
return key === null || !key.startsWith(`${kind}:`) || session.stories.has(key) ? [] : [key];
|
|
56
|
+
}).sort((a, b) => byId(storyIdOf(a, kind), storyIdOf(b, kind))).flatMap((key) => {
|
|
57
|
+
const current = storyNodeOf(doc, key);
|
|
58
|
+
return current === null ? [] : [{ change: "added", key, current }];
|
|
59
|
+
});
|
|
60
|
+
return [...arrived, ...added];
|
|
61
|
+
}
|
|
62
|
+
function changed(changes) {
|
|
63
|
+
return changes.some((change) => change.change !== "kept");
|
|
64
|
+
}
|
|
65
|
+
var EVERY_STORY_CHANGE = [
|
|
66
|
+
"edited",
|
|
67
|
+
"removed",
|
|
68
|
+
"added"
|
|
69
|
+
];
|
|
70
|
+
var NO_FROZEN_ENTRIES = /* @__PURE__ */ new Set();
|
|
71
|
+
function storyEntriesWriting(part) {
|
|
72
|
+
return {
|
|
73
|
+
kind: part.kind,
|
|
74
|
+
changes: EVERY_STORY_CHANGE,
|
|
75
|
+
frozenEntries: (session) => part.frozenEntries(session)
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function storyWriting(kind, changes) {
|
|
79
|
+
return { kind, changes, frozenEntries: () => NO_FROZEN_ENTRIES };
|
|
80
|
+
}
|
|
81
|
+
function unwrittenStoryChanges(writings, doc, session) {
|
|
82
|
+
const byKind = new Map(writings.map((writing) => [writing.kind, writing]));
|
|
83
|
+
return STORY_KINDS.flatMap((kind) => {
|
|
84
|
+
const writing = byKind.get(kind);
|
|
85
|
+
const frozen = writing?.frozenEntries(session) ?? NO_FROZEN_ENTRIES;
|
|
86
|
+
return storyChangesOf(doc, session, kind).flatMap((entry) => {
|
|
87
|
+
if (entry.change === "kept") return [];
|
|
88
|
+
const key = entry.change === "added" ? entry.key : entry.imported.key;
|
|
89
|
+
const written = writing !== void 0 && writing.changes.includes(entry.change) && !frozen.has(key);
|
|
90
|
+
return written ? [] : [{ key, change: entry.change }];
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function writtenStoryOf(change, frozen) {
|
|
95
|
+
if (change.change === "added") return change.current;
|
|
96
|
+
if (change.change === "kept" || frozen.has(change.imported.key)) {
|
|
97
|
+
return change.imported.doc;
|
|
98
|
+
}
|
|
99
|
+
return change.change === "edited" ? change.current : null;
|
|
100
|
+
}
|
|
101
|
+
function writtenStories(changes, frozen) {
|
|
102
|
+
return changes.flatMap((change) => {
|
|
103
|
+
const story = writtenStoryOf(change, frozen);
|
|
104
|
+
if (story === null) return [];
|
|
105
|
+
return [
|
|
106
|
+
{
|
|
107
|
+
change,
|
|
108
|
+
story,
|
|
109
|
+
frozen: change.change !== "added" && frozen.has(change.imported.key)
|
|
110
|
+
}
|
|
111
|
+
];
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function storyEntriesOf(part, doc, session) {
|
|
115
|
+
const changes = storyChangesOf(doc, session, part.kind);
|
|
116
|
+
return changed(changes) ? writtenStories(changes, part.frozenEntries(session)) : [];
|
|
117
|
+
}
|
|
118
|
+
function addedIdProblems(part, changes) {
|
|
119
|
+
return changes.flatMap(
|
|
120
|
+
(change) => change.change === "added" && ST_DecimalNumber.parse(storyIdOf(change.key, part.kind)) === null ? [
|
|
121
|
+
{
|
|
122
|
+
code: "unsupported-content",
|
|
123
|
+
message: `the ${change.key} story is named by no whole number, and a ${wName(part.entry)} is identified by one`
|
|
124
|
+
}
|
|
125
|
+
] : []
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
function storyEntriesProblems(part, doc, session) {
|
|
129
|
+
const changes = storyChangesOf(doc, session, part.kind);
|
|
130
|
+
if (!changed(changes)) return [];
|
|
131
|
+
const added = addedIdProblems(part, changes);
|
|
132
|
+
if (added.length > 0) return added;
|
|
133
|
+
const xml = readPart(
|
|
134
|
+
session.parts,
|
|
135
|
+
relatedPartPath(session.parts, session.mainPartPath, part.relType)
|
|
136
|
+
);
|
|
137
|
+
if (xml !== null) {
|
|
138
|
+
const problem = partRootProblem(xml, part.root);
|
|
139
|
+
return problem === null ? [] : [{ code: "malformed-xml", message: problem }];
|
|
140
|
+
}
|
|
141
|
+
return session.parts.has(CONTENT_TYPES_PATH) ? [] : [
|
|
142
|
+
{
|
|
143
|
+
code: "missing-content-types",
|
|
144
|
+
message: `cannot add a part to a package that has no ${CONTENT_TYPES_PATH}`
|
|
145
|
+
}
|
|
146
|
+
];
|
|
147
|
+
}
|
|
148
|
+
var ENTRY_MARKUP = { namespaces: { w: NAMESPACES.w } };
|
|
149
|
+
var XML_DECLARATION = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
|
|
150
|
+
function containerOf(part, id) {
|
|
151
|
+
const name = wName(part.entry);
|
|
152
|
+
return {
|
|
153
|
+
open: openTagXml(name, attrsText([[wName("id"), id]])),
|
|
154
|
+
close: `</${name}>`
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function entryXml(part, { change, frozen }, settled, refs) {
|
|
158
|
+
if (change.change === "added") {
|
|
159
|
+
return serializeStory(
|
|
160
|
+
settled,
|
|
161
|
+
null,
|
|
162
|
+
containerOf(part, storyIdOf(change.key, part.kind)),
|
|
163
|
+
refs
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
const { imported } = change;
|
|
167
|
+
return frozen || settled === imported.doc ? imported.xml : serializeStory(settled, imported, imported, refs);
|
|
168
|
+
}
|
|
169
|
+
function beforeClosingTag(suffix) {
|
|
170
|
+
let at = suffix.indexOf("<");
|
|
171
|
+
while (at !== -1) {
|
|
172
|
+
const tag = readTag(suffix, at);
|
|
173
|
+
if (tag === null || tag.kind === "close") return suffix.slice(0, at);
|
|
174
|
+
at = suffix.indexOf("<", tag.end);
|
|
175
|
+
}
|
|
176
|
+
return suffix;
|
|
177
|
+
}
|
|
178
|
+
function rewrittenPart(part, xml, arrived, appended) {
|
|
179
|
+
const scan = scanBlocksIn(xml, (_tag, depth) => depth === 0);
|
|
180
|
+
const seen = /* @__PURE__ */ new Set();
|
|
181
|
+
const entries = elementChildren(parseXml(xml).documentElement).map(
|
|
182
|
+
(el, at) => {
|
|
183
|
+
const slice = scan?.blocks[at]?.xml ?? "";
|
|
184
|
+
const id = el.localName === part.entry ? attributeByLocalName(el, "id") : null;
|
|
185
|
+
if (id === null) return slice;
|
|
186
|
+
if (seen.has(id)) return "";
|
|
187
|
+
seen.add(id);
|
|
188
|
+
const entry = arrived.get(storyKey(part.kind, id));
|
|
189
|
+
return entry === void 0 ? "" : slice.slice(0, slice.indexOf("<")) + entry;
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
return ensureRootDeclarations(
|
|
193
|
+
splicePart(xml, {
|
|
194
|
+
root: part.root,
|
|
195
|
+
replaceChildren: entries.join("") + appended + beforeClosingTag(scan?.suffix ?? "")
|
|
196
|
+
}),
|
|
197
|
+
ENTRY_MARKUP
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
function mainDeclarations(documentPrefix) {
|
|
201
|
+
const at = rootTagAt(documentPrefix);
|
|
202
|
+
const tag = at === -1 ? null : readTag(documentPrefix, at);
|
|
203
|
+
if (tag === null || tag.kind !== "open") return [];
|
|
204
|
+
const attrs = parseAttrs(documentPrefix.slice(tag.nameEnd, tag.end - 1));
|
|
205
|
+
return (attrs ?? []).filter(
|
|
206
|
+
([name]) => name.startsWith("xmlns:") && name !== "xmlns:w" || name === "mc:Ignorable"
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
function createdPart(part, session, children) {
|
|
210
|
+
const name = wName(part.root);
|
|
211
|
+
const declarations = attrsText(mainDeclarations(session.documentPrefix));
|
|
212
|
+
return ensureRootDeclarations(
|
|
213
|
+
`${XML_DECLARATION}${openTagXml(name, declarations === "" ? null : declarations)}${children}</${name}>`,
|
|
214
|
+
ENTRY_MARKUP
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
function planEntries(part, doc, session, context) {
|
|
218
|
+
const changes = storyChangesOf(doc, session, part.kind);
|
|
219
|
+
if (!changed(changes)) return null;
|
|
220
|
+
const refs = {
|
|
221
|
+
...NO_EXPORT_REFS,
|
|
222
|
+
notes: context.notes,
|
|
223
|
+
session
|
|
224
|
+
};
|
|
225
|
+
const written = writtenStories(changes, part.frozenEntries(session));
|
|
226
|
+
const settled = withUniqueStoryIdentities(written);
|
|
227
|
+
const entries = written.map((entry, at) => ({
|
|
228
|
+
change: entry.change,
|
|
229
|
+
xml: entryXml(part, entry, settled[at] ?? entry.story, refs)
|
|
230
|
+
}));
|
|
231
|
+
const appended = entries.flatMap(({ change, xml }) => change.change === "added" ? [xml] : []).join("");
|
|
232
|
+
const related = relatedPartPath(
|
|
233
|
+
session.parts,
|
|
234
|
+
session.mainPartPath,
|
|
235
|
+
part.relType
|
|
236
|
+
);
|
|
237
|
+
const path = related ?? availablePartPath(session.parts, session.mainPartPath, part.stem);
|
|
238
|
+
const bytes = session.parts.get(path);
|
|
239
|
+
if (bytes !== void 0) {
|
|
240
|
+
const { text, hadBom } = decodeUtf8(bytes);
|
|
241
|
+
const arrived = new Map(
|
|
242
|
+
entries.flatMap(
|
|
243
|
+
({ change, xml }) => change.change === "added" ? [] : [[change.imported.key, xml]]
|
|
244
|
+
)
|
|
245
|
+
);
|
|
246
|
+
return /* @__PURE__ */ new Map([
|
|
247
|
+
[path, encodeUtf8(rewrittenPart(part, text, arrived, appended), hadBom)]
|
|
248
|
+
]);
|
|
249
|
+
}
|
|
250
|
+
if (related === null) {
|
|
251
|
+
context.relationships.add({
|
|
252
|
+
type: part.relType,
|
|
253
|
+
target: path.slice(directoryOf(session.mainPartPath).length)
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
context.contentTypes.addOverride(path, part.contentType);
|
|
257
|
+
const taken = /* @__PURE__ */ new Set([
|
|
258
|
+
...changes.map(
|
|
259
|
+
(change) => change.change === "added" ? storyIdOf(change.key, part.kind) : change.imported.id
|
|
260
|
+
),
|
|
261
|
+
...part.referencedIds(doc)
|
|
262
|
+
]);
|
|
263
|
+
return /* @__PURE__ */ new Map([
|
|
264
|
+
[
|
|
265
|
+
path,
|
|
266
|
+
encodeUtf8(
|
|
267
|
+
createdPart(part, session, part.prelude(taken) + appended),
|
|
268
|
+
false
|
|
269
|
+
)
|
|
270
|
+
]
|
|
271
|
+
]);
|
|
272
|
+
}
|
|
273
|
+
function storyEntriesPlanner(part) {
|
|
274
|
+
return {
|
|
275
|
+
name: part.name,
|
|
276
|
+
plan: (doc, session, context) => planEntries(part, doc, session, context)
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
export {
|
|
280
|
+
EVERY_STORY_CHANGE,
|
|
281
|
+
storyChangesOf,
|
|
282
|
+
storyEntriesOf,
|
|
283
|
+
storyEntriesPlanner,
|
|
284
|
+
storyEntriesProblems,
|
|
285
|
+
storyEntriesWriting,
|
|
286
|
+
storyIdOf,
|
|
287
|
+
storyWriting,
|
|
288
|
+
unwrittenStoryChanges
|
|
289
|
+
};
|
|
@@ -2,9 +2,9 @@ import { Slice } from "prosemirror-model";
|
|
|
2
2
|
import type { Transaction } from "prosemirror-state";
|
|
3
3
|
import { type NewLists } from "../../numbering/listRegistry";
|
|
4
4
|
import { type HtmlBlockReader } from "./blockReaders";
|
|
5
|
-
import type
|
|
5
|
+
import { type ListKinds, type NoteStories } from "./internalChannel";
|
|
6
6
|
import type { HtmlReadContext } from "./readContext";
|
|
7
|
-
/** What a paste puts in: the content,
|
|
7
|
+
/** What a paste puts in: the content, the definitions of the lists it started, and its notes */
|
|
8
8
|
export interface PastedContent {
|
|
9
9
|
slice: Slice;
|
|
10
10
|
newLists: NewLists;
|
|
@@ -14,13 +14,33 @@ export interface PastedContent {
|
|
|
14
14
|
* which knows what kind each is from the markup it read them out of.
|
|
15
15
|
*/
|
|
16
16
|
listKinds?: ListKinds;
|
|
17
|
+
/**
|
|
18
|
+
* What the notes this slice's references call said where it was copied (`./internalChannel`).
|
|
19
|
+
* Absent for a slice that arrived any other way, whose references this document drops.
|
|
20
|
+
*/
|
|
21
|
+
noteStories?: NoteStories;
|
|
22
|
+
/**
|
|
23
|
+
* The notes the paste puts in, under the ids its references were given (`./normalizers`).
|
|
24
|
+
* Absent until the content has been normalized against the document it is going into.
|
|
25
|
+
*/
|
|
26
|
+
newStories?: NoteStories;
|
|
17
27
|
}
|
|
18
28
|
/** The content one piece of already parsed markup reads as, or null when it reads as nothing */
|
|
19
29
|
export declare function readHtml(root: ParentNode, context: HtmlReadContext, blockReaders?: readonly HtmlBlockReader[]): PastedContent | null;
|
|
20
30
|
/** The same reading, for a caller holding the markup as a string rather than as elements */
|
|
21
31
|
export declare function readHtmlSlice(context: HtmlReadContext, source: string): PastedContent | null;
|
|
22
32
|
/**
|
|
23
|
-
* The
|
|
24
|
-
*
|
|
33
|
+
* The notes a paste brought, written under the ids its references were given.
|
|
34
|
+
*
|
|
35
|
+
* They go in the transaction the paste itself is in, so the export writes them out and one undo
|
|
36
|
+
* takes the text and its notes back together. A note whose reference did not land - a paste a
|
|
37
|
+
* table or a command turned into something else - would be an entry of the file nothing calls, so
|
|
38
|
+
* only the ones the document now refers to are written.
|
|
39
|
+
*/
|
|
40
|
+
export declare function withPastedStories(tr: Transaction, stories: NoteStories): Transaction;
|
|
41
|
+
/**
|
|
42
|
+
* The transaction a paste is put in with: the content, the definitions of the lists it started
|
|
43
|
+
* recorded on the document node so that the export writes them out and undo takes them back, and
|
|
44
|
+
* the notes its references call.
|
|
25
45
|
*/
|
|
26
46
|
export declare function withPastedContent(tr: Transaction, content: PastedContent): Transaction;
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
withListNumbering,
|
|
8
8
|
withParagraphStyle
|
|
9
9
|
} from "../../docx/paraProps.js";
|
|
10
|
+
import { setStory } from "../../docx/story.js";
|
|
10
11
|
import {
|
|
11
12
|
listsWorn,
|
|
12
13
|
NEW_LISTS_ATTR,
|
|
@@ -20,6 +21,10 @@ import {
|
|
|
20
21
|
import { styleIdOf } from "../../ooxml/props.js";
|
|
21
22
|
import { docxSchema } from "../../schema/index.js";
|
|
22
23
|
import { COPIED_STYLE_ATTRIBUTE } from "../../schema/clipboard.js";
|
|
24
|
+
import {
|
|
25
|
+
EDITABLE_NOTE_KINDS,
|
|
26
|
+
storyKey
|
|
27
|
+
} from "../../schema/stories.js";
|
|
23
28
|
import { editorClassNames } from "../../styles/classNames.js";
|
|
24
29
|
import { numIdsIn } from "../commands/listCommands.js";
|
|
25
30
|
import {
|
|
@@ -32,6 +37,9 @@ import {
|
|
|
32
37
|
marksFor,
|
|
33
38
|
withInlineStyle
|
|
34
39
|
} from "./inlineFormatting.js";
|
|
40
|
+
import {
|
|
41
|
+
NO_NOTE_STORIES
|
|
42
|
+
} from "./internalChannel.js";
|
|
35
43
|
import { detectHtmlSource } from "./source.js";
|
|
36
44
|
var BLOCK_TAGS = /* @__PURE__ */ new Set([
|
|
37
45
|
"ADDRESS",
|
|
@@ -363,14 +371,41 @@ function readHtmlSlice(context, source) {
|
|
|
363
371
|
template.innerHTML = source;
|
|
364
372
|
return readHtml(template.content, context);
|
|
365
373
|
}
|
|
374
|
+
function notesCalledIn(doc) {
|
|
375
|
+
const keys = /* @__PURE__ */ new Set();
|
|
376
|
+
doc.descendants((node) => {
|
|
377
|
+
if (node.type !== docxSchema.nodes.noteReference) return true;
|
|
378
|
+
const kind = EDITABLE_NOTE_KINDS.find(
|
|
379
|
+
(candidate) => candidate === node.attrs.kind
|
|
380
|
+
);
|
|
381
|
+
const id = node.attrs.id;
|
|
382
|
+
if (kind !== void 0 && typeof id === "string") {
|
|
383
|
+
keys.add(storyKey(kind, id));
|
|
384
|
+
}
|
|
385
|
+
return true;
|
|
386
|
+
});
|
|
387
|
+
return keys;
|
|
388
|
+
}
|
|
389
|
+
function withPastedStories(tr, stories) {
|
|
390
|
+
if (stories.size === 0) return tr;
|
|
391
|
+
const called = notesCalledIn(tr.doc);
|
|
392
|
+
for (const [key, story] of stories) {
|
|
393
|
+
if (called.has(key)) setStory(tr, key, story);
|
|
394
|
+
}
|
|
395
|
+
return tr;
|
|
396
|
+
}
|
|
366
397
|
function withPastedContent(tr, content) {
|
|
367
|
-
return
|
|
368
|
-
|
|
369
|
-
|
|
398
|
+
return withPastedStories(
|
|
399
|
+
tr.setDocAttribute(
|
|
400
|
+
NEW_LISTS_ATTR,
|
|
401
|
+
newListsValue(listsWorn(content.newLists, numIdsIn(tr.doc)))
|
|
402
|
+
),
|
|
403
|
+
content.newStories ?? NO_NOTE_STORIES
|
|
370
404
|
);
|
|
371
405
|
}
|
|
372
406
|
export {
|
|
373
407
|
readHtml,
|
|
374
408
|
readHtmlSlice,
|
|
375
|
-
withPastedContent
|
|
409
|
+
withPastedContent,
|
|
410
|
+
withPastedStories
|
|
376
411
|
};
|
|
@@ -16,15 +16,23 @@
|
|
|
16
16
|
* about the copy that only the editor it was copied from can answer: a number says nothing about
|
|
17
17
|
* whether it counted or bulleted, and the definition that said so is the source document's. It is
|
|
18
18
|
* read here because this is where both ends are the same session.
|
|
19
|
+
*
|
|
20
|
+
* The note each reference in it calls is kept the same way, and for a stronger reason: a cut takes
|
|
21
|
+
* a footnote away together with its last reference (`editor/plugins/noteLifecycle`), so by the time
|
|
22
|
+
* the paste happens the document no longer holds what the copy was pointing at.
|
|
19
23
|
*/
|
|
20
|
-
import type { Slice } from "prosemirror-model";
|
|
24
|
+
import type { Node as PMNode, Slice } from "prosemirror-model";
|
|
21
25
|
import type { ListKind } from "../../numbering/listTemplate";
|
|
22
26
|
import type { Numbering } from "../../numbering/parseNumbering";
|
|
27
|
+
import { type NoteKey, type StoryKey } from "../../schema/stories";
|
|
23
28
|
/** Where the copied HTML carries the name of the slice this editor kept */
|
|
24
29
|
export declare const INTERNAL_TOKEN_ATTRIBUTE = "data-docx-clip";
|
|
25
30
|
/** The kind of list each numbering id of a slice stood for where the slice was copied */
|
|
26
31
|
export type ListKinds = ReadonlyMap<number, ListKind>;
|
|
27
32
|
export declare const NO_LIST_KINDS: ListKinds;
|
|
33
|
+
/** The story of each note the slice refers to, as the document copied from held it */
|
|
34
|
+
export type NoteStories = ReadonlyMap<NoteKey, PMNode>;
|
|
35
|
+
export declare const NO_NOTE_STORIES: NoteStories;
|
|
28
36
|
/**
|
|
29
37
|
* The two ways a copy leaves the editor.
|
|
30
38
|
*
|
|
@@ -34,19 +42,38 @@ export declare const NO_LIST_KINDS: ListKinds;
|
|
|
34
42
|
* replaced only by the next copy made the same way.
|
|
35
43
|
*/
|
|
36
44
|
export type CopyRoute = "clipboard" | "drag";
|
|
37
|
-
/** What one name stands for: the slice that was copied, and what
|
|
45
|
+
/** What one name stands for: the slice that was copied, and what it named where it was copied */
|
|
38
46
|
export interface RecalledCopy {
|
|
39
47
|
readonly slice: Slice;
|
|
40
48
|
readonly listKinds: ListKinds;
|
|
49
|
+
readonly noteStories: NoteStories;
|
|
50
|
+
}
|
|
51
|
+
/** The document a copy is made from, which is the only one that can answer what it carries */
|
|
52
|
+
export interface CopySource {
|
|
53
|
+
/** Null for a state built without a document, which has no session to bind a name to */
|
|
54
|
+
readonly sessionId: string | null;
|
|
55
|
+
readonly doc: PMNode;
|
|
56
|
+
readonly numbering: Numbering;
|
|
57
|
+
/** The notes laying a part out rather than saying something, which are never copied */
|
|
58
|
+
readonly specialNotes: ReadonlySet<StoryKey>;
|
|
41
59
|
}
|
|
42
60
|
/**
|
|
43
61
|
* Keeps the copied slice under a fresh name, and answers with the name to write into the markup.
|
|
44
62
|
* Null for a state built without a document: with no session to bind the name to there is nothing
|
|
45
63
|
* to tell this editor's copy from another's, and the copy travels as markup alone.
|
|
46
64
|
*/
|
|
47
|
-
export declare function rememberCopied(route: CopyRoute, slice: Slice,
|
|
65
|
+
export declare function rememberCopied(route: CopyRoute, slice: Slice, source: CopySource): string | null;
|
|
48
66
|
/** The copy that name stands for, and only when the session asking is the one that copied it */
|
|
49
67
|
export declare function recallCopied(token: string | null, sessionId: string | null): RecalledCopy | null;
|
|
68
|
+
/**
|
|
69
|
+
* The notes the last drag out of this editor carried.
|
|
70
|
+
*
|
|
71
|
+
* A drop is handed the slice itself rather than markup (`prosemirror-view`), so it carries no name
|
|
72
|
+
* to look a copy up by; the drag is the one route whose last copy is the copy being dropped. The
|
|
73
|
+
* session must still be the one that dragged, since a drop from another editor arrives as the
|
|
74
|
+
* markup its own reader answers for.
|
|
75
|
+
*/
|
|
76
|
+
export declare function draggedNotes(sessionId: string | null): NoteStories;
|
|
50
77
|
/**
|
|
51
78
|
* The name the pasted markup carries, if it carries one.
|
|
52
79
|
*
|
|
@@ -1,31 +1,50 @@
|
|
|
1
1
|
// src/editor/clipboard/internalChannel.ts
|
|
2
2
|
import { docxSchema } from "../../schema/index.js";
|
|
3
|
+
import {
|
|
4
|
+
EDITABLE_NOTE_KINDS,
|
|
5
|
+
storyKey,
|
|
6
|
+
storyNodeOf
|
|
7
|
+
} from "../../schema/stories.js";
|
|
3
8
|
import { listKindOf, listRefOf } from "../commands/listCommands.js";
|
|
4
9
|
var INTERNAL_TOKEN_ATTRIBUTE = "data-docx-clip";
|
|
5
10
|
var NO_LIST_KINDS = /* @__PURE__ */ new Map();
|
|
11
|
+
var NO_NOTE_STORIES = /* @__PURE__ */ new Map();
|
|
6
12
|
var copied = /* @__PURE__ */ new Map();
|
|
7
13
|
var copies = 0;
|
|
8
14
|
function nextToken() {
|
|
9
15
|
copies += 1;
|
|
10
16
|
return `c${copies}-${Math.random().toString(36).slice(2, 10)}`;
|
|
11
17
|
}
|
|
12
|
-
function rememberCopied(route, slice,
|
|
18
|
+
function rememberCopied(route, slice, source) {
|
|
19
|
+
const { sessionId } = source;
|
|
13
20
|
if (sessionId === null) return null;
|
|
14
21
|
const token = nextToken();
|
|
15
22
|
copied.set(route, {
|
|
16
23
|
token,
|
|
17
24
|
sessionId,
|
|
18
25
|
slice,
|
|
19
|
-
listKinds: listKindsIn(slice, numbering)
|
|
26
|
+
listKinds: listKindsIn(slice, source.numbering),
|
|
27
|
+
noteStories: noteStoriesIn(slice, source)
|
|
20
28
|
});
|
|
21
29
|
return token;
|
|
22
30
|
}
|
|
31
|
+
function recalled(copy) {
|
|
32
|
+
return {
|
|
33
|
+
slice: copy.slice,
|
|
34
|
+
listKinds: copy.listKinds,
|
|
35
|
+
noteStories: copy.noteStories
|
|
36
|
+
};
|
|
37
|
+
}
|
|
23
38
|
function recallCopied(token, sessionId) {
|
|
24
39
|
if (token === null || sessionId === null) return null;
|
|
25
40
|
const found = [...copied.values()].find(
|
|
26
41
|
(copy) => copy.token === token && copy.sessionId === sessionId
|
|
27
42
|
);
|
|
28
|
-
return found === void 0 ? null :
|
|
43
|
+
return found === void 0 ? null : recalled(found);
|
|
44
|
+
}
|
|
45
|
+
function draggedNotes(sessionId) {
|
|
46
|
+
const found = copied.get("drag");
|
|
47
|
+
return found === void 0 || sessionId === null || found.sessionId !== sessionId ? NO_NOTE_STORIES : found.noteStories;
|
|
29
48
|
}
|
|
30
49
|
function listKindsIn(slice, numbering) {
|
|
31
50
|
const kinds = /* @__PURE__ */ new Map();
|
|
@@ -41,6 +60,25 @@ function listKindsIn(slice, numbering) {
|
|
|
41
60
|
slice.content.forEach(read);
|
|
42
61
|
return kinds.size === 0 ? NO_LIST_KINDS : kinds;
|
|
43
62
|
}
|
|
63
|
+
function noteStoriesIn(slice, source) {
|
|
64
|
+
const stories = /* @__PURE__ */ new Map();
|
|
65
|
+
const read = (node) => {
|
|
66
|
+
if (node.type === docxSchema.nodes.noteReference) {
|
|
67
|
+
const kind = EDITABLE_NOTE_KINDS.find(
|
|
68
|
+
(candidate) => candidate === node.attrs.kind
|
|
69
|
+
);
|
|
70
|
+
const id = node.attrs.id;
|
|
71
|
+
if (kind === void 0 || typeof id !== "string") return;
|
|
72
|
+
const key = storyKey(kind, id);
|
|
73
|
+
const story = source.specialNotes.has(key) ? null : storyNodeOf(source.doc, key);
|
|
74
|
+
if (story !== null) stories.set(key, story);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
node.forEach(read);
|
|
78
|
+
};
|
|
79
|
+
slice.content.forEach(read);
|
|
80
|
+
return stories.size === 0 ? NO_NOTE_STORIES : stories;
|
|
81
|
+
}
|
|
44
82
|
function internalTokenOf(dom) {
|
|
45
83
|
if (dom instanceof Element) {
|
|
46
84
|
const own = dom.getAttribute(INTERNAL_TOKEN_ATTRIBUTE);
|
|
@@ -52,6 +90,8 @@ function internalTokenOf(dom) {
|
|
|
52
90
|
export {
|
|
53
91
|
INTERNAL_TOKEN_ATTRIBUTE,
|
|
54
92
|
NO_LIST_KINDS,
|
|
93
|
+
NO_NOTE_STORIES,
|
|
94
|
+
draggedNotes,
|
|
55
95
|
internalTokenOf,
|
|
56
96
|
recallCopied,
|
|
57
97
|
rememberCopied
|
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
import { type Node as PMNode, Slice } from "prosemirror-model";
|
|
17
17
|
import type { EditorState } from "prosemirror-state";
|
|
18
18
|
import { type ListKind } from "../../numbering/listTemplate";
|
|
19
|
+
import { type NoteKind } from "../../schema/stories";
|
|
19
20
|
import type { PastedContent } from "./htmlReader";
|
|
20
|
-
import { type ListKinds } from "./internalChannel";
|
|
21
|
+
import { type ListKinds, type NoteStories } from "./internalChannel";
|
|
21
22
|
/** What one normalizer answers about the document the slice is going into */
|
|
22
23
|
export interface NormalizeContext {
|
|
23
24
|
readonly state: EditorState;
|
|
@@ -40,6 +41,17 @@ export interface NormalizeContext {
|
|
|
40
41
|
* the list button starts one. Null in a document with nowhere to write a definition.
|
|
41
42
|
*/
|
|
42
43
|
startList(kind: ListKind): number | null;
|
|
44
|
+
/**
|
|
45
|
+
* What the notes the slice's references call said where it was copied
|
|
46
|
+
* (`./internalChannel`). Empty for a slice that arrived any other way, whose references name
|
|
47
|
+
* notes of a document this one knows nothing about.
|
|
48
|
+
*/
|
|
49
|
+
readonly noteStories: NoteStories;
|
|
50
|
+
/**
|
|
51
|
+
* A note this paste puts in, under an id no note of this document answers to, registered in
|
|
52
|
+
* `PastedContent.newStories`. It answers with the id the reference is pasted under.
|
|
53
|
+
*/
|
|
54
|
+
startNote(kind: NoteKind, story: PMNode): string;
|
|
43
55
|
}
|
|
44
56
|
export type SliceNormalizer = (slice: Slice, context: NormalizeContext) => Slice;
|
|
45
57
|
/** The slice with every node rewritten depth first, and the ones mapped to null taken out */
|
|
@@ -71,17 +83,31 @@ export declare const rekeyNumbering: SliceNormalizer;
|
|
|
71
83
|
/**
|
|
72
84
|
* Takes the anchors off what is pasted.
|
|
73
85
|
*
|
|
74
|
-
* A comment marker
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
86
|
+
* A comment marker and a bookmark both point at something written elsewhere in the package: the
|
|
87
|
+
* comment, the bookmark's other end. A copy of the marker alone points at the same thing a second
|
|
88
|
+
* time, which is a duplicate identifier the file may not hold and a range the reader cannot close.
|
|
89
|
+
* Nothing here duplicates what they point at, so the anchor goes and the text it stood in stays,
|
|
90
|
+
* which is what Word does with a note it has no body for.
|
|
79
91
|
*
|
|
80
92
|
* A move drop duplicates nothing: the source goes as the drop lands, so the one anchor there was
|
|
81
93
|
* travels with the text it opened. Taking it off would delete a marker the document has no way to
|
|
82
94
|
* write back, which is a change the preserved guard refuses whole, and the drag would do nothing.
|
|
83
95
|
*/
|
|
84
96
|
export declare const detachAnchors: SliceNormalizer;
|
|
97
|
+
/**
|
|
98
|
+
* Gives a pasted note reference a note of its own.
|
|
99
|
+
*
|
|
100
|
+
* A note is written once and called from the text by its id, so a second reference naming that id
|
|
101
|
+
* is one note drawn with one number where the reader made two. The note each reference called was
|
|
102
|
+
* remembered as the slice was copied (`./internalChannel`) - a cut deletes a footnote together
|
|
103
|
+
* with its last reference, so the document no longer holds it by the time the paste lands - and
|
|
104
|
+
* each reference is pasted under a fresh id with a copy of that note beside it.
|
|
105
|
+
*
|
|
106
|
+
* A reference no remembered note answers for points at nothing here: a copy from another editor or
|
|
107
|
+
* another document numbers its notes against its own file, and markup naming one says nothing
|
|
108
|
+
* about what it said. It goes the way the other anchors do.
|
|
109
|
+
*/
|
|
110
|
+
export declare const duplicateNotes: SliceNormalizer;
|
|
85
111
|
/**
|
|
86
112
|
* Works out again what the pasted paragraphs are drawn with.
|
|
87
113
|
*
|
|
@@ -93,7 +119,8 @@ export declare const detachAnchors: SliceNormalizer;
|
|
|
93
119
|
export declare const rederiveDisplay: SliceNormalizer;
|
|
94
120
|
export declare const DEFAULT_NORMALIZERS: readonly SliceNormalizer[];
|
|
95
121
|
/**
|
|
96
|
-
* The pasted content as this document may hold it,
|
|
97
|
-
*
|
|
122
|
+
* The pasted content as this document may hold it, the definitions of the lists the paste starts -
|
|
123
|
+
* the ones a reading started, and the ones a reissued number was taken for - and the notes it puts
|
|
124
|
+
* in beside the references calling them.
|
|
98
125
|
*/
|
|
99
126
|
export declare function normalizePasted(content: PastedContent, state: EditorState, move: boolean, normalizers?: readonly SliceNormalizer[]): PastedContent;
|