@portone/docx-editor 0.6.0 → 0.6.2
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 +40 -0
- package/dist/DocxEditor.js +84 -26
- package/dist/core.d.ts +4 -3
- package/dist/docx/exportDocx.d.ts +3 -2
- package/dist/docx/exportDocx.js +6 -10
- package/dist/docx/headersFooters.js +35 -23
- package/dist/docx/identities.d.ts +30 -0
- package/dist/docx/identities.js +43 -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 +13 -10
- package/dist/docx/invariants.js +300 -116
- package/dist/docx/newLists.js +12 -3
- 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 +113 -0
- package/dist/docx/notes/reading.d.ts +58 -0
- package/dist/docx/notes/reading.js +98 -0
- package/dist/docx/notes/references.d.ts +33 -0
- package/dist/docx/notes/references.js +37 -0
- package/dist/docx/notes/writing.d.ts +19 -0
- package/dist/docx/notes/writing.js +95 -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/serializePreserved.d.ts +5 -1
- package/dist/docx/serializePreserved.js +24 -7
- package/dist/docx/session.d.ts +5 -0
- package/dist/docx/session.js +6 -0
- package/dist/docx/storyParts.d.ts +133 -0
- package/dist/docx/storyParts.js +314 -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/exportQueries.d.ts +4 -3
- package/dist/editor/commands/index.d.ts +2 -1
- 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 +21 -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/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/index.d.ts +2 -2
- package/dist/numbering/spellers.d.ts +1 -1
- package/dist/numbering/spellers.js +6 -1
- package/dist/ooxml/errors.d.ts +135 -2
- package/dist/ooxml/errors.js +6 -0
- 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 +42 -2
- package/dist/schema/stories.js +28 -5
- package/dist/styles/classNames.d.ts +21 -6
- package/dist/styles/classNames.js +21 -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 +89 -32
- 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 +1 -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,122 @@
|
|
|
1
|
+
// src/docx/notes/newNote.ts
|
|
2
|
+
import { Fragment } from "prosemirror-model";
|
|
3
|
+
import { elementXml } from "../../ooxml/element.js";
|
|
4
|
+
import { wName } from "../../ooxml/names.js";
|
|
5
|
+
import { docxSchema } from "../../schema/index.js";
|
|
6
|
+
import {
|
|
7
|
+
NOTE_KINDS,
|
|
8
|
+
storiesOf,
|
|
9
|
+
storyKey
|
|
10
|
+
} from "../../schema/stories.js";
|
|
11
|
+
import { CLONE_POLICIES } from "../cloning.js";
|
|
12
|
+
import { originalBlock } from "../session.js";
|
|
13
|
+
import { withStyleFormats } from "../story.js";
|
|
14
|
+
var NOTE_MARKUP = {
|
|
15
|
+
footnote: {
|
|
16
|
+
textStyle: "FootnoteText",
|
|
17
|
+
referenceStyle: "FootnoteReference",
|
|
18
|
+
numberElement: "footnoteRef"
|
|
19
|
+
},
|
|
20
|
+
endnote: {
|
|
21
|
+
textStyle: "EndnoteText",
|
|
22
|
+
referenceStyle: "EndnoteReference",
|
|
23
|
+
numberElement: "endnoteRef"
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var NOTE_NUMBER_ELEMENTS = NOTE_KINDS.map(
|
|
27
|
+
(kind) => NOTE_MARKUP[kind].numberElement
|
|
28
|
+
);
|
|
29
|
+
function noteNumberRunProps(kind, formatting) {
|
|
30
|
+
const { referenceStyle } = NOTE_MARKUP[kind];
|
|
31
|
+
const property = formatting.styles.get(referenceStyle)?.type === "character" ? elementXml(wName("rStyle"), [[wName("val"), referenceStyle]]) : elementXml(wName("vertAlign"), [[wName("val"), "superscript"]]);
|
|
32
|
+
return elementXml(wName("rPr"), [], [property]);
|
|
33
|
+
}
|
|
34
|
+
function noteTextProps(kind, formatting) {
|
|
35
|
+
const { textStyle } = NOTE_MARKUP[kind];
|
|
36
|
+
return formatting.styles.get(textStyle)?.type === "paragraph" ? elementXml(
|
|
37
|
+
wName("pPr"),
|
|
38
|
+
[],
|
|
39
|
+
[elementXml(wName("pStyle"), [[wName("val"), textStyle]])]
|
|
40
|
+
) : null;
|
|
41
|
+
}
|
|
42
|
+
function newNoteStory(kind, formatting) {
|
|
43
|
+
const { numberElement } = NOTE_MARKUP[kind];
|
|
44
|
+
const number = docxSchema.nodes.rawRunContent.create(
|
|
45
|
+
{
|
|
46
|
+
xml: elementXml(wName(numberElement), []),
|
|
47
|
+
element: numberElement,
|
|
48
|
+
display: "chip",
|
|
49
|
+
text: null,
|
|
50
|
+
guarded: false
|
|
51
|
+
},
|
|
52
|
+
null,
|
|
53
|
+
[
|
|
54
|
+
docxSchema.marks.run.create({
|
|
55
|
+
rPr: noteNumberRunProps(kind, formatting)
|
|
56
|
+
})
|
|
57
|
+
]
|
|
58
|
+
);
|
|
59
|
+
const paragraph = docxSchema.nodes.paragraph.create(
|
|
60
|
+
{ pPr: noteTextProps(kind, formatting) },
|
|
61
|
+
number
|
|
62
|
+
);
|
|
63
|
+
return withStyleFormats(
|
|
64
|
+
docxSchema.nodes.doc.create(null, [paragraph]),
|
|
65
|
+
formatting
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
var DECIMAL = /^-?\d+$/;
|
|
69
|
+
function takenNoteIds(doc, kind, reserved) {
|
|
70
|
+
const prefix = storyKey(kind, "");
|
|
71
|
+
const ids = new Set(
|
|
72
|
+
[...reserved, ...Object.keys(storiesOf(doc))].flatMap(
|
|
73
|
+
(key) => key.startsWith(prefix) ? [key.slice(prefix.length)] : []
|
|
74
|
+
)
|
|
75
|
+
);
|
|
76
|
+
doc.descendants((node) => {
|
|
77
|
+
const id = node.attrs.id;
|
|
78
|
+
if (node.type === docxSchema.nodes.noteReference && node.attrs.kind === kind && typeof id === "string") {
|
|
79
|
+
ids.add(id);
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
});
|
|
83
|
+
return ids;
|
|
84
|
+
}
|
|
85
|
+
function nextNoteId(taken) {
|
|
86
|
+
const largest = Array.from(taken).filter((id) => DECIMAL.test(id)).map((id) => BigInt(id)).reduce((max, id) => id > max ? id : max, 0n);
|
|
87
|
+
return (largest + 1n).toString();
|
|
88
|
+
}
|
|
89
|
+
function copiedBlock(node, session) {
|
|
90
|
+
if (node.isInline) return node;
|
|
91
|
+
if (node.type === docxSchema.nodes.paragraph) {
|
|
92
|
+
return node.type.create(
|
|
93
|
+
CLONE_POLICIES.paragraph.attrs(node, "copy"),
|
|
94
|
+
node.content,
|
|
95
|
+
node.marks
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (node.type === docxSchema.nodes.rawBlock) {
|
|
99
|
+
const held = node.attrs.xml;
|
|
100
|
+
const xml = typeof held === "string" ? held : session === null ? void 0 : originalBlock(node, session)?.xml;
|
|
101
|
+
return xml === void 0 ? node : node.type.create({ ...node.attrs, xml, srcId: null }, null, node.marks);
|
|
102
|
+
}
|
|
103
|
+
const attrs = node.attrs.srcId === void 0 || node.attrs.srcId === null ? node.attrs : { ...node.attrs, srcId: null };
|
|
104
|
+
return node.type.create(
|
|
105
|
+
attrs,
|
|
106
|
+
Fragment.fromArray(
|
|
107
|
+
node.children.map((child) => copiedBlock(child, session))
|
|
108
|
+
),
|
|
109
|
+
node.marks
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
function copiedNoteStory(story, session) {
|
|
113
|
+
return copiedBlock(story, session);
|
|
114
|
+
}
|
|
115
|
+
export {
|
|
116
|
+
NOTE_NUMBER_ELEMENTS,
|
|
117
|
+
copiedNoteStory,
|
|
118
|
+
newNoteStory,
|
|
119
|
+
nextNoteId,
|
|
120
|
+
noteNumberRunProps,
|
|
121
|
+
takenNoteIds
|
|
122
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Node as PMNode } from "prosemirror-model";
|
|
2
|
+
import { type StoryKey } from "../../schema/stories";
|
|
3
|
+
import { type NoteNumbering } from "./reading";
|
|
4
|
+
/**
|
|
5
|
+
* The label each note reference of the body is drawn with, by position.
|
|
6
|
+
*
|
|
7
|
+
* Each kind is counted on its own, in the order its references stand: a repeated id takes the
|
|
8
|
+
* label of its first reference since the count last started, a reference whose custom mark follows
|
|
9
|
+
* takes neither a label nor a number (§17.11.14), and one calling a separator entry is no note to
|
|
10
|
+
* count. A section's own properties lay over the settings, and a section that restarts each section
|
|
11
|
+
* counts from its start again (§17.11.19, §17.11.20), so a note referred to in an earlier section
|
|
12
|
+
* and again in this one is drawn with this section's own number, as Word draws it. A restart on
|
|
13
|
+
* each page is counted straight through, since the label would then depend on the pages, which
|
|
14
|
+
* depend in turn on how wide the labels are drawn.
|
|
15
|
+
*
|
|
16
|
+
* It reads the document and what it is handed and nothing else, so the file an edited document
|
|
17
|
+
* exports opens with the labels the editor drew.
|
|
18
|
+
*/
|
|
19
|
+
export declare function noteLabelsIn(doc: PMNode, numbering: NoteNumbering, special: ReadonlySet<StoryKey>): ReadonlyMap<number, string>;
|
|
20
|
+
/**
|
|
21
|
+
* What `noteLabelsIn` reads off the document, as one string: the kind, id, and custom mark of each
|
|
22
|
+
* reference, which section it stands in, and what each section lays over the settings. Two
|
|
23
|
+
* documents with the same signature are labelled alike under the same numbering.
|
|
24
|
+
*/
|
|
25
|
+
export declare function noteLabelSignature(doc: PMNode): string;
|
|
26
|
+
/** The document with every note reference labelled, and the same node where every label already stood */
|
|
27
|
+
export declare function withNoteLabels(doc: PMNode, numbering: NoteNumbering, special: ReadonlySet<StoryKey>): PMNode;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// src/docx/notes/numbering.ts
|
|
2
|
+
import { Fragment } from "prosemirror-model";
|
|
3
|
+
import { spellNumber } from "../../numbering/spellers.js";
|
|
4
|
+
import {
|
|
5
|
+
NOTE_KINDS,
|
|
6
|
+
storyKey
|
|
7
|
+
} from "../../schema/stories.js";
|
|
8
|
+
import { sectionsOf } from "../sections.js";
|
|
9
|
+
import {
|
|
10
|
+
withNoteProps
|
|
11
|
+
} from "./reading.js";
|
|
12
|
+
import { eachNoteReference } from "./references.js";
|
|
13
|
+
var MAX_LABEL_CHARS = 64;
|
|
14
|
+
function referencesBySection(doc) {
|
|
15
|
+
const found = [];
|
|
16
|
+
eachNoteReference(doc, ({ node, pos, block }) => {
|
|
17
|
+
found.push({ block, reference: { pos, node } });
|
|
18
|
+
});
|
|
19
|
+
if (found.length === 0) return [];
|
|
20
|
+
const sections = sectionsOf(doc);
|
|
21
|
+
const references = sections.map(() => []);
|
|
22
|
+
let at = 0;
|
|
23
|
+
for (const { block, reference } of found) {
|
|
24
|
+
while (block > sections[at].lastBlock) at += 1;
|
|
25
|
+
references[at].push(reference);
|
|
26
|
+
}
|
|
27
|
+
return sections.map((section, index) => ({
|
|
28
|
+
section,
|
|
29
|
+
references: references[index]
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
function kindOf(node) {
|
|
33
|
+
return node.attrs.kind === "endnote" ? "endnote" : "footnote";
|
|
34
|
+
}
|
|
35
|
+
function idOf(node) {
|
|
36
|
+
const id = node.attrs.id;
|
|
37
|
+
return typeof id === "string" ? id : null;
|
|
38
|
+
}
|
|
39
|
+
function propsIn(section, numbering) {
|
|
40
|
+
return {
|
|
41
|
+
footnote: withNoteProps(numbering.footnote, section.props.footnotePr),
|
|
42
|
+
endnote: withNoteProps(numbering.endnote, section.props.endnotePr)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function restarted(count, props) {
|
|
46
|
+
count.next = props.start;
|
|
47
|
+
count.taken.clear();
|
|
48
|
+
}
|
|
49
|
+
function labelOf(node, count, props, special) {
|
|
50
|
+
if (node.attrs.customMarkFollows === true) return "";
|
|
51
|
+
const id = idOf(node);
|
|
52
|
+
if (id === null || special.has(storyKey(kindOf(node), id))) return "?";
|
|
53
|
+
const taken = count.taken.get(id);
|
|
54
|
+
if (taken !== void 0) return taken;
|
|
55
|
+
const label = spellNumber(count.next, props.format, MAX_LABEL_CHARS);
|
|
56
|
+
count.next += 1;
|
|
57
|
+
count.taken.set(id, label);
|
|
58
|
+
return label;
|
|
59
|
+
}
|
|
60
|
+
function noteLabelsIn(doc, numbering, special) {
|
|
61
|
+
const labels = /* @__PURE__ */ new Map();
|
|
62
|
+
const counts = {
|
|
63
|
+
footnote: { next: 1, taken: /* @__PURE__ */ new Map() },
|
|
64
|
+
endnote: { next: 1, taken: /* @__PURE__ */ new Map() }
|
|
65
|
+
};
|
|
66
|
+
referencesBySection(doc).forEach(({ section, references }, index) => {
|
|
67
|
+
const props = propsIn(section, numbering);
|
|
68
|
+
for (const kind of NOTE_KINDS) {
|
|
69
|
+
if (index === 0 || props[kind].restart === "eachSect") {
|
|
70
|
+
restarted(counts[kind], props[kind]);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const { pos, node } of references) {
|
|
74
|
+
const kind = kindOf(node);
|
|
75
|
+
labels.set(pos, labelOf(node, counts[kind], props[kind], special));
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
return labels;
|
|
79
|
+
}
|
|
80
|
+
function noteLabelSignature(doc) {
|
|
81
|
+
return JSON.stringify(
|
|
82
|
+
referencesBySection(doc).map(({ section, references }) => [
|
|
83
|
+
section.props.footnotePr,
|
|
84
|
+
section.props.endnotePr,
|
|
85
|
+
references.map(({ node }) => [
|
|
86
|
+
kindOf(node),
|
|
87
|
+
idOf(node),
|
|
88
|
+
node.attrs.customMarkFollows === true
|
|
89
|
+
])
|
|
90
|
+
])
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
function relabelled(node, contentStart, labels) {
|
|
94
|
+
const children = [];
|
|
95
|
+
let changed = false;
|
|
96
|
+
node.forEach((child, offset) => {
|
|
97
|
+
const at = contentStart + offset;
|
|
98
|
+
const label = labels.get(at);
|
|
99
|
+
const next = label !== void 0 && child.attrs.label !== label ? child.type.create({ ...child.attrs, label }, null, child.marks) : child.isLeaf ? child : relabelled(child, at + 1, labels);
|
|
100
|
+
changed ||= next !== child;
|
|
101
|
+
children.push(next);
|
|
102
|
+
});
|
|
103
|
+
return changed ? node.copy(Fragment.from(children)) : node;
|
|
104
|
+
}
|
|
105
|
+
function withNoteLabels(doc, numbering, special) {
|
|
106
|
+
const labels = noteLabelsIn(doc, numbering, special);
|
|
107
|
+
return labels.size === 0 ? doc : relabelled(doc, 0, labels);
|
|
108
|
+
}
|
|
109
|
+
export {
|
|
110
|
+
noteLabelSignature,
|
|
111
|
+
noteLabelsIn,
|
|
112
|
+
withNoteLabels
|
|
113
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads the footnotes and endnotes parts the main part relates to: the entry each holds, which of
|
|
3
|
+
* them lays the page out rather than says anything, and how each kind is numbered.
|
|
4
|
+
*
|
|
5
|
+
* What a note says is a story of its own, read by `docx/story` out of the same part text.
|
|
6
|
+
*/
|
|
7
|
+
import { type NumberFormat } from "../../numbering/spellers";
|
|
8
|
+
import { type NoteKind, type StoryKey } from "../../schema/stories";
|
|
9
|
+
export type { NoteKind } from "../../schema/stories";
|
|
10
|
+
/**
|
|
11
|
+
* One entry of a notes part as it arrived. What the note says is a story of its own, held on the
|
|
12
|
+
* document node under `footnote:<id>` or `endnote:<id>` (`docx/story`).
|
|
13
|
+
*/
|
|
14
|
+
export interface ImportedNote {
|
|
15
|
+
kind: NoteKind;
|
|
16
|
+
id: string;
|
|
17
|
+
type: string | null;
|
|
18
|
+
}
|
|
19
|
+
export interface ImportedNotePart {
|
|
20
|
+
partPath: string | null;
|
|
21
|
+
/** The raw text of the part, which is what the note stories are sliced out of. null for none */
|
|
22
|
+
xml: string | null;
|
|
23
|
+
ordered: readonly ImportedNote[];
|
|
24
|
+
byId: ReadonlyMap<string, ImportedNote>;
|
|
25
|
+
}
|
|
26
|
+
export interface ImportedNotes {
|
|
27
|
+
footnotes: ImportedNotePart;
|
|
28
|
+
endnotes: ImportedNotePart;
|
|
29
|
+
}
|
|
30
|
+
/** Reads both note parts without modifying their original package bytes. */
|
|
31
|
+
export declare function readNotes(parts: Map<string, Uint8Array>, mainPartPath: string): ImportedNotes;
|
|
32
|
+
/**
|
|
33
|
+
* The entries a notes part lays the page out with rather than numbers - a separator, a
|
|
34
|
+
* continuation separator, a continuation notice (§17.11.1, §17.11.23) - as the keys their stories
|
|
35
|
+
* stand under.
|
|
36
|
+
*/
|
|
37
|
+
export declare function specialNotesOf(notes: ImportedNotes): ReadonlySet<StoryKey>;
|
|
38
|
+
/** Where the count of one kind of note goes back to its start (§17.18.74) */
|
|
39
|
+
export type NoteRestart = "continuous" | "eachSect" | "eachPage";
|
|
40
|
+
/** How one kind of note is counted (§17.11.17-20) */
|
|
41
|
+
export interface NoteNumberingProps {
|
|
42
|
+
readonly format: NumberFormat;
|
|
43
|
+
readonly start: number;
|
|
44
|
+
readonly restart: NoteRestart;
|
|
45
|
+
}
|
|
46
|
+
export type NoteNumbering = Readonly<Record<NoteKind, NoteNumberingProps>>;
|
|
47
|
+
export declare const DEFAULT_NOTE_NUMBERING: NoteNumbering;
|
|
48
|
+
/**
|
|
49
|
+
* What one `w:footnotePr` or `w:endnotePr` names, leaving out every property it does not, and null
|
|
50
|
+
* for no element at all.
|
|
51
|
+
*
|
|
52
|
+
* A format this editor has no speller for is counted in decimal, as a list marker is.
|
|
53
|
+
*/
|
|
54
|
+
export declare function readNoteProps(props: Element | null): Partial<NoteNumberingProps> | null;
|
|
55
|
+
/** The properties with whatever an override names laid over them (§17.11.5, §17.11.11) */
|
|
56
|
+
export declare function withNoteProps(base: NoteNumberingProps, override: Partial<NoteNumberingProps> | null): NoteNumberingProps;
|
|
57
|
+
/** The document-wide note properties settings.xml names (§17.11.4, §17.11.12) */
|
|
58
|
+
export declare function readNoteNumbering(settings: Document | null): NoteNumbering;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// src/docx/notes/reading.ts
|
|
2
|
+
import { isNumberFormat } from "../../numbering/spellers.js";
|
|
3
|
+
import { ST_DecimalNumber } from "../../ooxml/simpleTypes.js";
|
|
4
|
+
import {
|
|
5
|
+
attributeByLocalName,
|
|
6
|
+
childByLocalName,
|
|
7
|
+
decodeUtf8,
|
|
8
|
+
elementChildren,
|
|
9
|
+
parseXml,
|
|
10
|
+
R_NS
|
|
11
|
+
} from "../../ooxml/xml.js";
|
|
12
|
+
import { storyKey } from "../../schema/stories.js";
|
|
13
|
+
import { relatedPartPath } from "../packageParts.js";
|
|
14
|
+
var EMPTY_PART = {
|
|
15
|
+
partPath: null,
|
|
16
|
+
xml: null,
|
|
17
|
+
ordered: [],
|
|
18
|
+
byId: /* @__PURE__ */ new Map()
|
|
19
|
+
};
|
|
20
|
+
function notePart(parts, mainPartPath, kind) {
|
|
21
|
+
const partPath = relatedPartPath(parts, mainPartPath, `${R_NS}/${kind}s`);
|
|
22
|
+
if (partPath === null) return EMPTY_PART;
|
|
23
|
+
const bytes = parts.get(partPath);
|
|
24
|
+
if (!bytes) return { ...EMPTY_PART, partPath };
|
|
25
|
+
const xml = decodeUtf8(bytes).text;
|
|
26
|
+
const root = parseXml(xml).documentElement;
|
|
27
|
+
const elements = elementChildren(root).filter((el) => el.localName === kind);
|
|
28
|
+
const ordered = elements.flatMap((el) => {
|
|
29
|
+
const id = attributeByLocalName(el, "id");
|
|
30
|
+
if (id === null) return [];
|
|
31
|
+
return [{ kind, id, type: attributeByLocalName(el, "type") }];
|
|
32
|
+
});
|
|
33
|
+
const byId = /* @__PURE__ */ new Map();
|
|
34
|
+
for (const note of ordered) {
|
|
35
|
+
if (!byId.has(note.id)) byId.set(note.id, note);
|
|
36
|
+
}
|
|
37
|
+
return { partPath, xml, ordered, byId };
|
|
38
|
+
}
|
|
39
|
+
function readNotes(parts, mainPartPath) {
|
|
40
|
+
return {
|
|
41
|
+
footnotes: notePart(parts, mainPartPath, "footnote"),
|
|
42
|
+
endnotes: notePart(parts, mainPartPath, "endnote")
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function isSpecial(note) {
|
|
46
|
+
return note.type !== null && note.type !== "normal";
|
|
47
|
+
}
|
|
48
|
+
function specialNotesOf(notes) {
|
|
49
|
+
return new Set(
|
|
50
|
+
[...notes.footnotes.byId.values(), ...notes.endnotes.byId.values()].filter(isSpecial).map((note) => storyKey(note.kind, note.id))
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
var UNNAMED_PROPS = {
|
|
54
|
+
format: "decimal",
|
|
55
|
+
start: 1,
|
|
56
|
+
restart: "continuous"
|
|
57
|
+
};
|
|
58
|
+
var DEFAULT_NOTE_NUMBERING = {
|
|
59
|
+
footnote: UNNAMED_PROPS,
|
|
60
|
+
endnote: UNNAMED_PROPS
|
|
61
|
+
};
|
|
62
|
+
var RESTARTS = ["continuous", "eachSect", "eachPage"];
|
|
63
|
+
function valOf(props, name) {
|
|
64
|
+
const child = childByLocalName(props, name);
|
|
65
|
+
return child === null ? null : attributeByLocalName(child, "val");
|
|
66
|
+
}
|
|
67
|
+
function readNoteProps(props) {
|
|
68
|
+
if (props === null) return null;
|
|
69
|
+
const format = valOf(props, "numFmt");
|
|
70
|
+
const start = ST_DecimalNumber.parse(valOf(props, "numStart"));
|
|
71
|
+
const restart = RESTARTS.find(
|
|
72
|
+
(known) => known === valOf(props, "numRestart")
|
|
73
|
+
);
|
|
74
|
+
return {
|
|
75
|
+
...format === null ? {} : { format: isNumberFormat(format) ? format : "decimal" },
|
|
76
|
+
...start === null ? {} : { start },
|
|
77
|
+
...restart === void 0 ? {} : { restart }
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function withNoteProps(base, override) {
|
|
81
|
+
return override === null ? base : { ...base, ...override };
|
|
82
|
+
}
|
|
83
|
+
function readNoteNumbering(settings) {
|
|
84
|
+
const root = settings?.documentElement ?? null;
|
|
85
|
+
const named = (name) => readNoteProps(root === null ? null : childByLocalName(root, name));
|
|
86
|
+
return {
|
|
87
|
+
footnote: withNoteProps(UNNAMED_PROPS, named("footnotePr")),
|
|
88
|
+
endnote: withNoteProps(UNNAMED_PROPS, named("endnotePr"))
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export {
|
|
92
|
+
DEFAULT_NOTE_NUMBERING,
|
|
93
|
+
readNoteNumbering,
|
|
94
|
+
readNoteProps,
|
|
95
|
+
readNotes,
|
|
96
|
+
specialNotesOf,
|
|
97
|
+
withNoteProps
|
|
98
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the body refers to its notes.
|
|
3
|
+
*
|
|
4
|
+
* Three readers ask: the notes writers collect the ids each part is referred to by, the numbering
|
|
5
|
+
* counts the references section by section (`./numbering`), and the export invariants look up the
|
|
6
|
+
* first reference to one note, which is where a problem about a note's own text is reported
|
|
7
|
+
* (`docx/invariants`). One walk here is what keeps the three reading the same nodes in the same
|
|
8
|
+
* order.
|
|
9
|
+
*/
|
|
10
|
+
import type { Node as PMNode } from "prosemirror-model";
|
|
11
|
+
import { type NoteKey } from "../../schema/stories";
|
|
12
|
+
/** One reference the body makes to a note */
|
|
13
|
+
export interface PlacedNoteReference {
|
|
14
|
+
readonly node: PMNode;
|
|
15
|
+
/** Where the reference stands in the document */
|
|
16
|
+
readonly pos: number;
|
|
17
|
+
/** The top-level block it stands in, by index, which is what a section covers a run of */
|
|
18
|
+
readonly block: number;
|
|
19
|
+
/** The note it names, and null where it names no kind of note or carries no id */
|
|
20
|
+
readonly key: NoteKey | null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Every `noteReference` of the body in document order, until `visit` answers false.
|
|
24
|
+
*
|
|
25
|
+
* Only the document node is walked: WordprocessingML puts a reference in the body and a note's own
|
|
26
|
+
* text in a part of its own, so a side story holds none.
|
|
27
|
+
*/
|
|
28
|
+
export declare function eachNoteReference(doc: PMNode, visit: (reference: PlacedNoteReference) => boolean | void): void;
|
|
29
|
+
/**
|
|
30
|
+
* Where the body first refers to each of these notes, and nothing for one it refers to nowhere.
|
|
31
|
+
* The walk stops as soon as every note asked about has been placed.
|
|
32
|
+
*/
|
|
33
|
+
export declare function firstNoteReferences(doc: PMNode, wanted: ReadonlySet<NoteKey>): ReadonlyMap<NoteKey, number>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// src/docx/notes/references.ts
|
|
2
|
+
import { NOTE_KINDS, storyKey } from "../../schema/stories.js";
|
|
3
|
+
function keyOf(node) {
|
|
4
|
+
const kind = NOTE_KINDS.find((candidate) => candidate === node.attrs.kind);
|
|
5
|
+
const id = node.attrs.id;
|
|
6
|
+
return kind === void 0 || typeof id !== "string" ? null : storyKey(kind, id);
|
|
7
|
+
}
|
|
8
|
+
function eachNoteReference(doc, visit) {
|
|
9
|
+
let running = true;
|
|
10
|
+
doc.forEach((block, offset, index) => {
|
|
11
|
+
if (!running) return;
|
|
12
|
+
block.descendants((node, pos) => {
|
|
13
|
+
if (!running) return false;
|
|
14
|
+
if (node.type.name !== "noteReference") return true;
|
|
15
|
+
running = visit({
|
|
16
|
+
node,
|
|
17
|
+
pos: offset + 1 + pos,
|
|
18
|
+
block: index,
|
|
19
|
+
key: keyOf(node)
|
|
20
|
+
}) !== false;
|
|
21
|
+
return false;
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function firstNoteReferences(doc, wanted) {
|
|
26
|
+
const first = /* @__PURE__ */ new Map();
|
|
27
|
+
if (wanted.size === 0) return first;
|
|
28
|
+
eachNoteReference(doc, ({ key, pos }) => {
|
|
29
|
+
if (key !== null && wanted.has(key) && !first.has(key)) first.set(key, pos);
|
|
30
|
+
return first.size < wanted.size;
|
|
31
|
+
});
|
|
32
|
+
return first;
|
|
33
|
+
}
|
|
34
|
+
export {
|
|
35
|
+
eachNoteReference,
|
|
36
|
+
firstNoteReferences
|
|
37
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writes an edited, added, or removed note back into the part of its kind, one entry per story
|
|
3
|
+
* (`docx/storyParts`).
|
|
4
|
+
*
|
|
5
|
+
* The two parts differ in nothing but the names they are written under, so both are laid out by
|
|
6
|
+
* one description read off the kind: a `w:footnotes` part holds a `w:footnote` apiece and a
|
|
7
|
+
* `w:endnotes` part a `w:endnote` (§17.11.15, §17.11.16).
|
|
8
|
+
*
|
|
9
|
+
* A part this writer creates opens with the separator and continuation-separator entries Word lays
|
|
10
|
+
* the line above the notes out with (§17.11.1, §17.11.23). `settings.xml` is left alone: the
|
|
11
|
+
* `w:footnote` and `w:endnote` references there are optional (`CT_FtnDocProps`, `CT_EdnDocProps`),
|
|
12
|
+
* and a Google Docs export writes none.
|
|
13
|
+
*/
|
|
14
|
+
import type { PartPlanner } from "../partPlan";
|
|
15
|
+
import { type StoryEntriesPart } from "../storyParts";
|
|
16
|
+
export declare const FOOTNOTES_PART: StoryEntriesPart;
|
|
17
|
+
export declare const ENDNOTES_PART: StoryEntriesPart;
|
|
18
|
+
export declare const footnotesPlanner: PartPlanner;
|
|
19
|
+
export declare const endnotesPlanner: PartPlanner;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// src/docx/notes/writing.ts
|
|
2
|
+
import { elementXml } from "../../ooxml/element.js";
|
|
3
|
+
import { wName } from "../../ooxml/names.js";
|
|
4
|
+
import { R_NS } from "../../ooxml/xml.js";
|
|
5
|
+
import { noteKeyOf } from "../../schema/stories.js";
|
|
6
|
+
import { storyEntriesPlanner } from "../storyParts.js";
|
|
7
|
+
import { eachNoteReference } from "./references.js";
|
|
8
|
+
var NOTE_CONTENT_TYPES = {
|
|
9
|
+
footnote: "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml",
|
|
10
|
+
endnote: "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml"
|
|
11
|
+
};
|
|
12
|
+
var SEPARATORS = [
|
|
13
|
+
{ type: "separator", id: -1 },
|
|
14
|
+
{ type: "continuationSeparator", id: 0 }
|
|
15
|
+
];
|
|
16
|
+
var DECIMAL = /^-?\d+$/;
|
|
17
|
+
function separatorIds(taken) {
|
|
18
|
+
const held = Array.from(taken).filter((id) => DECIMAL.test(id)).map(Number);
|
|
19
|
+
return SEPARATORS.reduce((chosen, { id }) => {
|
|
20
|
+
const used = [...held, ...chosen];
|
|
21
|
+
return [...chosen, used.includes(id) ? Math.min(0, ...used) - 1 : id];
|
|
22
|
+
}, []);
|
|
23
|
+
}
|
|
24
|
+
function separatorXml(kind, type, id) {
|
|
25
|
+
const paragraph = elementXml(
|
|
26
|
+
wName("p"),
|
|
27
|
+
[],
|
|
28
|
+
[
|
|
29
|
+
elementXml(
|
|
30
|
+
wName("pPr"),
|
|
31
|
+
[],
|
|
32
|
+
[
|
|
33
|
+
elementXml(wName("spacing"), [
|
|
34
|
+
[wName("after"), "0"],
|
|
35
|
+
[wName("line"), "240"],
|
|
36
|
+
[wName("lineRule"), "auto"]
|
|
37
|
+
])
|
|
38
|
+
]
|
|
39
|
+
),
|
|
40
|
+
elementXml(wName("r"), [], [elementXml(wName(type), [])])
|
|
41
|
+
]
|
|
42
|
+
);
|
|
43
|
+
return elementXml(
|
|
44
|
+
wName(kind),
|
|
45
|
+
[
|
|
46
|
+
[wName("type"), type],
|
|
47
|
+
[wName("id"), String(id)]
|
|
48
|
+
],
|
|
49
|
+
[paragraph]
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
function separatorsXml(kind, taken) {
|
|
53
|
+
const ids = separatorIds(taken);
|
|
54
|
+
return SEPARATORS.map(
|
|
55
|
+
({ type }, at) => separatorXml(kind, type, ids[at] ?? 0)
|
|
56
|
+
).join("");
|
|
57
|
+
}
|
|
58
|
+
function frozenNotes(session, kind) {
|
|
59
|
+
return new Set(
|
|
60
|
+
Array.from(session.specialNotes).filter((key) => key.startsWith(`${kind}:`))
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
function referenceIds(doc, kind) {
|
|
64
|
+
const ids = /* @__PURE__ */ new Set();
|
|
65
|
+
eachNoteReference(doc, ({ key }) => {
|
|
66
|
+
const note = key === null ? null : noteKeyOf(key);
|
|
67
|
+
if (note?.kind === kind) ids.add(note.id);
|
|
68
|
+
});
|
|
69
|
+
return ids;
|
|
70
|
+
}
|
|
71
|
+
function notesPart(kind) {
|
|
72
|
+
const plural = `${kind}s`;
|
|
73
|
+
return {
|
|
74
|
+
name: plural,
|
|
75
|
+
kind,
|
|
76
|
+
relType: `${R_NS}/${plural}`,
|
|
77
|
+
contentType: NOTE_CONTENT_TYPES[kind],
|
|
78
|
+
stem: plural,
|
|
79
|
+
root: plural,
|
|
80
|
+
entry: kind,
|
|
81
|
+
referencedIds: (doc) => referenceIds(doc, kind),
|
|
82
|
+
frozenEntries: (session) => frozenNotes(session, kind),
|
|
83
|
+
prelude: (taken) => separatorsXml(kind, taken)
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
var FOOTNOTES_PART = notesPart("footnote");
|
|
87
|
+
var ENDNOTES_PART = notesPart("endnote");
|
|
88
|
+
var footnotesPlanner = storyEntriesPlanner(FOOTNOTES_PART);
|
|
89
|
+
var endnotesPlanner = storyEntriesPlanner(ENDNOTES_PART);
|
|
90
|
+
export {
|
|
91
|
+
ENDNOTES_PART,
|
|
92
|
+
FOOTNOTES_PART,
|
|
93
|
+
endnotesPlanner,
|
|
94
|
+
footnotesPlanner
|
|
95
|
+
};
|
package/dist/docx/partPlan.d.ts
CHANGED
|
@@ -7,12 +7,15 @@
|
|
|
7
7
|
* for, so no planner writes either of them itself and none can write over what another declared.
|
|
8
8
|
*/
|
|
9
9
|
import type { Node as PMNode } from "prosemirror-model";
|
|
10
|
+
import type { FidelityCollector } from "./fidelity";
|
|
10
11
|
import { type ContentTypeWriter } from "./packageParts";
|
|
11
12
|
import { type RelationshipWriter } from "./relationships";
|
|
12
13
|
import type { SessionStore } from "./session";
|
|
13
14
|
export interface PartPlanContext {
|
|
14
15
|
readonly relationships: RelationshipWriter;
|
|
15
16
|
readonly contentTypes: ContentTypeWriter;
|
|
17
|
+
/** Where a story writer records an approximation it had to make, beside the body writer's */
|
|
18
|
+
readonly notes: FidelityCollector;
|
|
16
19
|
}
|
|
17
20
|
export interface PartPlanner {
|
|
18
21
|
readonly name: string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The parts written beside the body, and what each of their planners carries into the file.
|
|
3
|
+
*
|
|
4
|
+
* `docx/exportDocx` runs these planners and `docx/invariants` reads what they write off the same
|
|
5
|
+
* list, so the two cannot disagree about which change has a writer.
|
|
6
|
+
*/
|
|
7
|
+
import type { PartPlanner } from "./partPlan";
|
|
8
|
+
import { type StoryEntriesPart, type StoryWriting } from "./storyParts";
|
|
9
|
+
export declare const PART_PLANNERS: readonly PartPlanner[];
|
|
10
|
+
/** What every planner writes back of the side stories the document holds */
|
|
11
|
+
export declare const STORY_WRITINGS: readonly StoryWriting[];
|
|
12
|
+
/** The parts written one entry per story, which the identity pass walks as one part apiece */
|
|
13
|
+
export declare const STORY_ENTRIES_PARTS: readonly StoryEntriesPart[];
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// src/docx/partPlanners.ts
|
|
2
|
+
import { HEADER_FOOTER_KINDS } from "../schema/stories.js";
|
|
3
|
+
import { commentsPlanner } from "./comments.js";
|
|
4
|
+
import { headerFooterPlanner } from "./headersFooters.js";
|
|
5
|
+
import {
|
|
6
|
+
ENDNOTES_PART,
|
|
7
|
+
endnotesPlanner,
|
|
8
|
+
FOOTNOTES_PART,
|
|
9
|
+
footnotesPlanner
|
|
10
|
+
} from "./notes/writing.js";
|
|
11
|
+
import { numberingPlanner } from "./numberingPlanner.js";
|
|
12
|
+
import {
|
|
13
|
+
EVERY_STORY_CHANGE,
|
|
14
|
+
storyEntriesWriting,
|
|
15
|
+
storyWriting
|
|
16
|
+
} from "./storyParts.js";
|
|
17
|
+
function entriesWriter(planner, part) {
|
|
18
|
+
return { planner, writes: [storyEntriesWriting(part)], entriesPart: part };
|
|
19
|
+
}
|
|
20
|
+
var PART_WRITERS = [
|
|
21
|
+
{ planner: numberingPlanner, writes: [], entriesPart: null },
|
|
22
|
+
{
|
|
23
|
+
planner: commentsPlanner,
|
|
24
|
+
writes: [storyWriting("comment", EVERY_STORY_CHANGE)],
|
|
25
|
+
entriesPart: null
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
planner: headerFooterPlanner,
|
|
29
|
+
writes: HEADER_FOOTER_KINDS.map((kind) => storyWriting(kind, ["edited"])),
|
|
30
|
+
entriesPart: null
|
|
31
|
+
},
|
|
32
|
+
entriesWriter(footnotesPlanner, FOOTNOTES_PART),
|
|
33
|
+
entriesWriter(endnotesPlanner, ENDNOTES_PART)
|
|
34
|
+
];
|
|
35
|
+
var PART_PLANNERS = PART_WRITERS.map(
|
|
36
|
+
({ planner }) => planner
|
|
37
|
+
);
|
|
38
|
+
var STORY_WRITINGS = PART_WRITERS.flatMap(
|
|
39
|
+
({ writes }) => writes
|
|
40
|
+
);
|
|
41
|
+
var STORY_ENTRIES_PARTS = PART_WRITERS.flatMap(
|
|
42
|
+
({ entriesPart }) => entriesPart === null ? [] : [entriesPart]
|
|
43
|
+
);
|
|
44
|
+
export {
|
|
45
|
+
PART_PLANNERS,
|
|
46
|
+
STORY_ENTRIES_PARTS,
|
|
47
|
+
STORY_WRITINGS
|
|
48
|
+
};
|