@portone/docx-editor 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +174 -0
- package/CONTRIBUTING.md +9 -1
- package/dist/DocxEditor.d.ts +6 -0
- package/dist/DocxEditor.js +12 -29
- package/dist/core.d.ts +23 -8
- package/dist/core.js +9 -4
- package/dist/docx/cloning.d.ts +38 -0
- package/dist/docx/cloning.js +74 -0
- package/dist/docx/commentOnlyChange.d.ts +14 -7
- package/dist/docx/commentOnlyChange.js +9 -139
- package/dist/docx/comments/constants.d.ts +3 -4
- package/dist/docx/comments/constants.js +4 -5
- package/dist/docx/comments/grammar.d.ts +86 -0
- package/dist/docx/comments/grammar.js +223 -0
- package/dist/docx/comments/parts.d.ts +52 -0
- package/dist/docx/comments/parts.js +279 -0
- package/dist/docx/comments/people.d.ts +11 -2
- package/dist/docx/comments/people.js +28 -106
- package/dist/docx/comments/policy.d.ts +7 -0
- package/dist/docx/comments/policy.js +43 -0
- package/dist/docx/comments/reading.d.ts +5 -3
- package/dist/docx/comments/reading.js +30 -29
- package/dist/docx/comments/writing.d.ts +19 -7
- package/dist/docx/comments/writing.js +105 -137
- package/dist/docx/documentSettings.d.ts +7 -0
- package/dist/docx/documentSettings.js +10 -2
- package/dist/docx/exportDocx.d.ts +40 -5
- package/dist/docx/exportDocx.js +71 -72
- package/dist/docx/exportRefs.d.ts +5 -2
- package/dist/docx/exportRefs.js +3 -1
- package/dist/docx/fidelity.d.ts +43 -0
- package/dist/docx/fidelity.js +64 -0
- package/dist/docx/formatting/attrs.d.ts +27 -0
- package/dist/docx/formatting/attrs.js +31 -0
- package/dist/docx/formatting/context.d.ts +31 -0
- package/dist/docx/formatting/context.js +42 -0
- package/dist/docx/formatting/direct.d.ts +6 -5
- package/dist/docx/formatting/direct.js +52 -81
- package/dist/docx/formatting/resolve.d.ts +45 -0
- package/dist/docx/formatting/resolve.js +140 -0
- package/dist/docx/formatting/runProperties.d.ts +93 -0
- package/dist/docx/formatting/runProperties.js +316 -0
- package/dist/docx/formatting/styles.js +3 -3
- package/dist/docx/formatting/tabStops.js +7 -22
- package/dist/docx/formatting.d.ts +4 -1
- package/dist/docx/formatting.js +4 -1
- package/dist/docx/headersFooters.js +13 -22
- package/dist/docx/identities.d.ts +67 -0
- package/dist/docx/identities.js +174 -0
- package/dist/docx/importDocx.d.ts +12 -1
- package/dist/docx/importDocx.js +72 -79
- package/dist/docx/importParagraph.d.ts +1 -1
- package/dist/docx/importParagraph.js +4 -3
- package/dist/docx/importTable.d.ts +1 -1
- package/dist/docx/importTable.js +17 -1
- package/dist/docx/invariants.d.ts +33 -0
- package/dist/docx/invariants.js +256 -0
- package/dist/docx/media.d.ts +6 -4
- package/dist/docx/media.js +6 -37
- package/dist/docx/newLists.d.ts +20 -0
- package/dist/docx/newLists.js +36 -0
- package/dist/docx/notes.js +6 -12
- package/dist/docx/numberingPlanner.d.ts +8 -0
- package/dist/docx/numberingPlanner.js +19 -0
- package/dist/docx/packageParts.d.ts +42 -0
- package/dist/docx/packageParts.js +135 -0
- package/dist/docx/pageGeometry.d.ts +2 -0
- package/dist/docx/pageGeometry.js +18 -9
- package/dist/docx/paraProps.d.ts +10 -22
- package/dist/docx/paraProps.js +75 -99
- package/dist/docx/partPlan.d.ts +36 -0
- package/dist/docx/partPlan.js +59 -0
- package/dist/docx/protectionPolicy.d.ts +127 -0
- package/dist/docx/protectionPolicy.js +169 -0
- package/dist/docx/relationships.d.ts +1 -1
- package/dist/docx/relationships.js +15 -15
- package/dist/docx/runProps.d.ts +9 -22
- package/dist/docx/runProps.js +15 -168
- package/dist/docx/scan.js +20 -52
- package/dist/docx/sdt.js +14 -37
- package/dist/docx/sdtProps.d.ts +8 -1
- package/dist/docx/sdtProps.js +10 -0
- package/dist/docx/serializeBlock.d.ts +2 -0
- package/dist/docx/serializeBlock.js +9 -7
- package/dist/docx/serializeParagraph.d.ts +2 -1
- package/dist/docx/serializeParagraph.js +25 -14
- package/dist/docx/serializeTable.js +63 -32
- package/dist/docx/session.d.ts +42 -14
- package/dist/docx/session.js +43 -17
- package/dist/docx/storyProjection.d.ts +36 -0
- package/dist/docx/storyProjection.js +23 -0
- package/dist/docx/tableFormatting/editing.js +130 -185
- package/dist/docx/tableFormatting/reading.d.ts +16 -6
- package/dist/docx/tableFormatting/reading.js +40 -20
- package/dist/docx/tableTemplate.js +37 -5
- package/dist/docx/theme.d.ts +0 -6
- package/dist/docx/theme.js +0 -8
- package/dist/download.d.ts +8 -5
- package/dist/download.js +2 -0
- package/dist/editor/clipboard/images.js +17 -3
- package/dist/editor/clipboard/inlineFormatting.d.ts +7 -0
- package/dist/editor/clipboard/inlineFormatting.js +20 -30
- package/dist/editor/commands/breakCommands.js +4 -3
- package/dist/editor/commands/canRunCommand.d.ts +2 -2
- package/dist/editor/commands/canRunCommand.js +1 -1
- package/dist/editor/commands/comments/editing.d.ts +6 -2
- package/dist/editor/commands/comments/editing.js +21 -36
- package/dist/editor/commands/exportQueries.d.ts +15 -0
- package/dist/editor/commands/exportQueries.js +14 -0
- package/dist/editor/commands/fidelityQueries.d.ts +12 -0
- package/dist/editor/commands/fidelityQueries.js +8 -0
- package/dist/editor/commands/formatting/editing.d.ts +2 -2
- package/dist/editor/commands/formatting/editing.js +26 -108
- package/dist/editor/commands/formatting/propertyCommands.d.ts +21 -0
- package/dist/editor/commands/formatting/propertyCommands.js +103 -0
- package/dist/editor/commands/formatting/shared.d.ts +3 -3
- package/dist/editor/commands/formatting/shared.js +5 -2
- package/dist/editor/commands/historyCommands.js +2 -1
- package/dist/editor/commands/indentCommands.js +5 -4
- package/dist/editor/commands/index.d.ts +11 -0
- package/dist/editor/commands/index.js +5 -0
- package/dist/editor/commands/linkCommands.js +5 -6
- package/dist/editor/commands/listCommands.js +8 -10
- package/dist/editor/commands/lockCommands.d.ts +7 -1
- package/dist/editor/commands/lockCommands.js +35 -37
- package/dist/editor/commands/paragraphCommands.js +28 -31
- package/dist/editor/commands/spacingCommands.js +1 -1
- package/dist/editor/commands/tabCommands.js +4 -3
- package/dist/editor/createEditor.d.ts +17 -29
- package/dist/editor/createEditor.js +61 -67
- package/dist/editor/documentStyles.d.ts +11 -35
- package/dist/editor/documentStyles.js +9 -52
- package/dist/editor/editorDocument.d.ts +56 -0
- package/dist/editor/editorDocument.js +74 -0
- package/dist/editor/externalClipboard.js +166 -33
- package/dist/editor/insertImage.js +4 -3
- package/dist/editor/insertTable.js +4 -3
- package/dist/editor/paragraphEdits.d.ts +11 -18
- package/dist/editor/paragraphEdits.js +7 -16
- package/dist/editor/plugins/displayDerivation.d.ts +47 -0
- package/dist/editor/plugins/displayDerivation.js +78 -0
- package/dist/editor/plugins/documentProtection.d.ts +3 -3
- package/dist/editor/plugins/imagePaste.js +4 -3
- package/dist/editor/plugins/keymap.js +18 -3
- package/dist/editor/plugins/lockedContent.d.ts +4 -4
- package/dist/editor/plugins/lockedContent.js +1 -1
- package/dist/editor/plugins/numberingDecorations.d.ts +5 -11
- package/dist/editor/plugins/numberingDecorations.js +7 -17
- package/dist/editor/plugins/paragraphDisplay.d.ts +7 -0
- package/dist/editor/plugins/paragraphDisplay.js +51 -0
- package/dist/editor/plugins/tabLayout.js +1 -1
- package/dist/editor/plugins/tableDisplay.d.ts +6 -0
- package/dist/editor/plugins/tableDisplay.js +16 -0
- package/dist/index.d.ts +2 -0
- package/dist/model/format.d.ts +30 -8
- package/dist/model/format.js +35 -22
- package/dist/model/tabStops.d.ts +9 -0
- package/dist/model/tabStops.js +18 -0
- package/dist/numbering/listTemplate.js +49 -10
- package/dist/numbering/parseNumbering.d.ts +14 -1
- package/dist/numbering/parseNumbering.js +35 -22
- package/dist/numbering/writeNumbering.d.ts +3 -4
- package/dist/numbering/writeNumbering.js +14 -25
- package/dist/ooxml/childOrder.d.ts +34 -0
- package/dist/ooxml/childOrder.js +496 -0
- package/dist/ooxml/element.d.ts +65 -0
- package/dist/ooxml/element.js +68 -0
- package/dist/ooxml/errors.d.ts +3 -2
- package/dist/ooxml/fragment.d.ts +53 -0
- package/dist/ooxml/fragment.js +76 -0
- package/dist/ooxml/image.d.ts +4 -0
- package/dist/ooxml/image.js +3 -5
- package/dist/ooxml/names.d.ts +27 -0
- package/dist/ooxml/names.js +29 -0
- package/dist/ooxml/partSplice.d.ts +67 -0
- package/dist/ooxml/partSplice.js +169 -0
- package/dist/ooxml/precedence.d.ts +31 -0
- package/dist/ooxml/precedence.js +51 -0
- package/dist/ooxml/props.d.ts +112 -0
- package/dist/ooxml/props.js +214 -0
- package/dist/ooxml/simpleTypes.d.ts +103 -0
- package/dist/ooxml/simpleTypes.js +182 -0
- package/dist/ooxml/tabStops.js +8 -27
- package/dist/ooxml/tagScan.d.ts +34 -0
- package/dist/ooxml/tagScan.js +108 -0
- package/dist/ooxml/units.d.ts +25 -13
- package/dist/ooxml/units.js +65 -31
- package/dist/ooxml/xml.d.ts +52 -0
- package/dist/ooxml/xml.js +64 -5
- package/dist/page/blockKinds.d.ts +103 -0
- package/dist/page/blockKinds.js +11 -0
- package/dist/page/kinds/index.d.ts +6 -0
- package/dist/page/kinds/index.js +10 -0
- package/dist/page/kinds/paragraphKind.d.ts +10 -0
- package/dist/page/kinds/paragraphKind.js +71 -0
- package/dist/page/kinds/tableKind.d.ts +10 -0
- package/dist/page/kinds/tableKind.js +178 -0
- package/dist/page/measureBlocks.d.ts +3 -1
- package/dist/page/measureBlocks.js +24 -25
- package/dist/page/pageDecorations.d.ts +27 -28
- package/dist/page/pageDecorations.js +70 -151
- package/dist/page/pageLayout.d.ts +10 -62
- package/dist/page/pageLayout.js +60 -53
- package/dist/page/usePageLayout.d.ts +2 -2
- package/dist/page/usePageLayout.js +3 -11
- package/dist/schema/attrRoles.d.ts +42 -0
- package/dist/schema/attrRoles.js +141 -0
- package/dist/schema/displayDerivation.d.ts +82 -0
- package/dist/schema/displayDerivation.js +130 -0
- package/dist/schema/docxSchema.d.ts +9 -0
- package/dist/schema/docxSchema.js +237 -114
- package/dist/schema/editGuard.d.ts +99 -0
- package/dist/schema/editGuard.js +43 -0
- package/dist/schema/guards.d.ts +91 -0
- package/dist/schema/guards.js +108 -0
- package/dist/schema/index.d.ts +2 -0
- package/dist/schema/index.js +2 -0
- package/dist/schema/locks.d.ts +17 -77
- package/dist/schema/locks.js +26 -70
- package/dist/schema/preservedGuards.d.ts +50 -0
- package/dist/schema/preservedGuards.js +80 -0
- package/dist/schema/protection.d.ts +17 -4
- package/dist/schema/protection.js +4 -2
- package/dist/schema/sourceEquality.d.ts +19 -0
- package/dist/schema/sourceEquality.js +39 -0
- package/dist/styles/inlineStyle.js +15 -6
- package/dist/table/cellFormatting.d.ts +8 -1
- package/dist/table/cellFormatting.js +10 -17
- package/dist/table/commands.d.ts +2 -2
- package/dist/table/commands.js +20 -18
- package/dist/table/format.d.ts +3 -0
- package/dist/table/format.js +3 -9
- package/dist/table/gridBorders.d.ts +14 -16
- package/dist/table/gridBorders.js +3 -41
- package/dist/table/index.d.ts +2 -2
- package/dist/table/merge.d.ts +4 -8
- package/dist/table/merge.js +5 -5
- package/package.json +10 -3
- package/dist/docx/comments/contentTypes.d.ts +0 -7
- package/dist/docx/comments/contentTypes.js +0 -38
- package/dist/docx/formatting/effectiveParagraph.d.ts +0 -15
- package/dist/docx/formatting/effectiveParagraph.js +0 -81
- package/dist/docx/propsXml.d.ts +0 -49
- package/dist/docx/propsXml.js +0 -302
- package/dist/docx/uniqueControls.d.ts +0 -14
- package/dist/docx/uniqueControls.js +0 -62
- package/dist/editor/plugins/bookmarkProtection.d.ts +0 -3
- package/dist/editor/plugins/bookmarkProtection.js +0 -28
- package/dist/editor/plugins/commentReservations.d.ts +0 -5
- package/dist/editor/plugins/commentReservations.js +0 -26
- package/dist/editor/plugins/noteProtection.d.ts +0 -3
- package/dist/editor/plugins/noteProtection.js +0 -25
- package/dist/editor/plugins/styledParagraphs.d.ts +0 -15
- package/dist/editor/plugins/styledParagraphs.js +0 -65
- package/dist/page/tableMeasurements.d.ts +0 -10
- package/dist/page/tableMeasurements.js +0 -115
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// src/docx/comments/parts.ts
|
|
2
|
+
import { NAMESPACES } from "../../ooxml/names.js";
|
|
3
|
+
import {
|
|
4
|
+
attributeByLocalName,
|
|
5
|
+
isElement,
|
|
6
|
+
parseXml,
|
|
7
|
+
serializeXml,
|
|
8
|
+
W_NS
|
|
9
|
+
} from "../../ooxml/xml.js";
|
|
10
|
+
import { availablePartPath } from "../packageParts.js";
|
|
11
|
+
import {
|
|
12
|
+
COMMENTS_CONTENT_TYPE,
|
|
13
|
+
COMMENTS_EXTENDED_CONTENT_TYPE,
|
|
14
|
+
COMMENTS_EXTENDED_REL_TYPE,
|
|
15
|
+
COMMENTS_REL_TYPE,
|
|
16
|
+
PEOPLE_CONTENT_TYPE,
|
|
17
|
+
PEOPLE_REL_TYPE,
|
|
18
|
+
W14_NS,
|
|
19
|
+
W15_NS
|
|
20
|
+
} from "./constants.js";
|
|
21
|
+
import {
|
|
22
|
+
attributesWithin,
|
|
23
|
+
COMMENT_ATTRIBUTES,
|
|
24
|
+
declarationWritten,
|
|
25
|
+
lastBodyParagraph,
|
|
26
|
+
readStrictCommentBody,
|
|
27
|
+
recordedIdentity,
|
|
28
|
+
wellFormedCommentExtension,
|
|
29
|
+
wellFormedPerson
|
|
30
|
+
} from "./grammar.js";
|
|
31
|
+
import { commentReferencesIn } from "./model.js";
|
|
32
|
+
import { commentAuthorId } from "./people.js";
|
|
33
|
+
import { lastParagraphId } from "./reading.js";
|
|
34
|
+
var COMMENT_IDENTITY = [
|
|
35
|
+
"id",
|
|
36
|
+
"author",
|
|
37
|
+
"date",
|
|
38
|
+
"initials"
|
|
39
|
+
];
|
|
40
|
+
function sameAttributes(entry, original, names) {
|
|
41
|
+
return names.every(
|
|
42
|
+
(name) => attributeByLocalName(entry, name) === attributeByLocalName(original, name)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
function wellFormedEntry(entry) {
|
|
46
|
+
if (entry.namespaceURI === W_NS && entry.localName === "comment") {
|
|
47
|
+
return attributesWithin(entry, COMMENT_ATTRIBUTES) && readStrictCommentBody(entry) !== null;
|
|
48
|
+
}
|
|
49
|
+
if (entry.namespaceURI === W15_NS && entry.localName === "commentEx") {
|
|
50
|
+
return wellFormedCommentExtension(entry);
|
|
51
|
+
}
|
|
52
|
+
if (entry.namespaceURI === W15_NS && entry.localName === "person") {
|
|
53
|
+
return wellFormedPerson(entry);
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
function withoutThreadKey(entry) {
|
|
58
|
+
const copy = entry.cloneNode(true);
|
|
59
|
+
if (!isElement(copy)) return serializeXml(entry);
|
|
60
|
+
const last = lastBodyParagraph(copy);
|
|
61
|
+
last?.removeAttributeNS(W14_NS, "paraId");
|
|
62
|
+
return serializeXml(copy);
|
|
63
|
+
}
|
|
64
|
+
function threadKeyAlone(entry, original) {
|
|
65
|
+
const before = lastParagraphId(original);
|
|
66
|
+
const after = lastParagraphId(entry);
|
|
67
|
+
const kept = before === null || after === before;
|
|
68
|
+
return kept && withoutThreadKey(entry) === withoutThreadKey(original);
|
|
69
|
+
}
|
|
70
|
+
function entryAllowed(entry, original, authorId, editableComments, people) {
|
|
71
|
+
if (entry.namespaceURI === W_NS && entry.localName === "comment") {
|
|
72
|
+
const author = attributeByLocalName(entry, "author");
|
|
73
|
+
const recorded = author === null ? null : commentAuthorId(people, author);
|
|
74
|
+
if (original === null) return recorded === authorId;
|
|
75
|
+
const paraId = lastParagraphId(original);
|
|
76
|
+
if (!sameAttributes(entry, original, COMMENT_IDENTITY) || paraId !== null && lastParagraphId(entry) !== paraId) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
return editableComments === "all" || recorded === null || recorded === authorId;
|
|
80
|
+
}
|
|
81
|
+
if (entry.namespaceURI === W15_NS && entry.localName === "commentEx") {
|
|
82
|
+
if (original === null) return true;
|
|
83
|
+
return sameAttributes(entry, original, ["paraId", "paraIdParent"]);
|
|
84
|
+
}
|
|
85
|
+
if (entry.namespaceURI === W15_NS && entry.localName === "person") {
|
|
86
|
+
return original === null && recordedIdentity(entry) === authorId;
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
var TEXT_NODE = 3;
|
|
91
|
+
var COMPATIBILITY = /* @__PURE__ */ new Map([
|
|
92
|
+
["xmlns:w14", NAMESPACES.w14],
|
|
93
|
+
["xmlns:mc", NAMESPACES.mc]
|
|
94
|
+
]);
|
|
95
|
+
var IGNORABLE = "mc:Ignorable";
|
|
96
|
+
var IGNORED = "w14";
|
|
97
|
+
function isLayout(node) {
|
|
98
|
+
return node.nodeType === TEXT_NODE && /^[ \t\r\n]*$/.test(node.nodeValue ?? "");
|
|
99
|
+
}
|
|
100
|
+
function surroundingNodes(parent) {
|
|
101
|
+
return parent === null ? [] : Array.from(parent.childNodes).filter(
|
|
102
|
+
(node) => !isElement(node) && !isLayout(node)
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
function sameNodes(now, was) {
|
|
106
|
+
return now.length === was.length && now.every((node, at) => {
|
|
107
|
+
const original = was[at];
|
|
108
|
+
return node.nodeType === original.nodeType && node.nodeName === original.nodeName && node.nodeValue === original.nodeValue;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function interEntryContentKept(now, was, rewritesContents) {
|
|
112
|
+
const current = surroundingNodes(now);
|
|
113
|
+
if (sameNodes(current, surroundingNodes(was))) return true;
|
|
114
|
+
return rewritesContents && current.length === 0 && was !== null && Array.from(now.children, serializeXml).join("") !== Array.from(was.children, serializeXml).join("");
|
|
115
|
+
}
|
|
116
|
+
function ignorableTokens(value) {
|
|
117
|
+
return new Set(value.split(/\s+/).filter(Boolean));
|
|
118
|
+
}
|
|
119
|
+
function ignorableKept(now, was) {
|
|
120
|
+
const gained = ignorableTokens(now);
|
|
121
|
+
const had = was === null ? /* @__PURE__ */ new Set() : ignorableTokens(was);
|
|
122
|
+
return Array.from(had).every((token) => gained.has(token)) && Array.from(gained).every((token) => had.has(token) || token === IGNORED);
|
|
123
|
+
}
|
|
124
|
+
function writtenRoot(root, shape) {
|
|
125
|
+
if (root.namespaceURI !== shape.namespace) return false;
|
|
126
|
+
if (root.localName !== shape.rootName) return false;
|
|
127
|
+
return Array.from(root.attributes).every(
|
|
128
|
+
(attr) => attr.name === IGNORABLE ? ignorableKept(attr.value, null) : declarationWritten(attr) || COMPATIBILITY.get(attr.name) === attr.value
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
function rootKeptAgainst(now, was, shape) {
|
|
132
|
+
if (was === null) return writtenRoot(now, shape);
|
|
133
|
+
if (now.nodeName !== was.nodeName) return false;
|
|
134
|
+
const had = new Map(
|
|
135
|
+
Array.from(was.attributes, (attr) => [attr.name, attr.value])
|
|
136
|
+
);
|
|
137
|
+
const kept = Array.from(now.attributes).every((attr) => {
|
|
138
|
+
const before = had.get(attr.name) ?? null;
|
|
139
|
+
if (attr.name === IGNORABLE) return ignorableKept(attr.value, before);
|
|
140
|
+
if (before === null) return COMPATIBILITY.get(attr.name) === attr.value;
|
|
141
|
+
return before === attr.value;
|
|
142
|
+
});
|
|
143
|
+
return kept && Array.from(had.keys()).every((name) => now.hasAttribute(name));
|
|
144
|
+
}
|
|
145
|
+
function entriesOf(shape, xml, reading) {
|
|
146
|
+
const entries = /* @__PURE__ */ new Map();
|
|
147
|
+
if (xml === null) return entries;
|
|
148
|
+
const strict = reading === "submitted";
|
|
149
|
+
for (const node of Array.from(parseXml(xml).documentElement.childNodes)) {
|
|
150
|
+
if (!isElement(node)) continue;
|
|
151
|
+
const el = node;
|
|
152
|
+
const named = strict ? el.namespaceURI === shape.namespace && el.localName === shape.localName : el.localName === shape.localName;
|
|
153
|
+
if (!named) {
|
|
154
|
+
if (strict) return null;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const id = attributeByLocalName(el, shape.idAttr);
|
|
158
|
+
if (id === null || entries.has(id)) {
|
|
159
|
+
if (strict) return null;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
entries.set(id, { el, xml: serializeXml(el) });
|
|
163
|
+
}
|
|
164
|
+
return entries;
|
|
165
|
+
}
|
|
166
|
+
function arrivedIn(shape, session) {
|
|
167
|
+
return entriesOf(shape, shape.xmlIn(session), "arrived") ?? /* @__PURE__ */ new Map();
|
|
168
|
+
}
|
|
169
|
+
function storyPart(shape) {
|
|
170
|
+
return {
|
|
171
|
+
relType: shape.relType,
|
|
172
|
+
contentType: shape.contentType,
|
|
173
|
+
pathIn: shape.pathIn,
|
|
174
|
+
writePathIn: (session) => shape.pathIn(session) ?? availablePartPath(session.parts, session.mainPartPath, shape.baseName),
|
|
175
|
+
entriesIn: (session, reading) => entriesOf(shape, shape.xmlIn(session), reading),
|
|
176
|
+
rootKept: (before, after) => {
|
|
177
|
+
const submitted = shape.xmlIn(after);
|
|
178
|
+
if (submitted === null) return true;
|
|
179
|
+
const arrived = shape.xmlIn(before);
|
|
180
|
+
const now = parseXml(submitted);
|
|
181
|
+
const was = arrived === null ? null : parseXml(arrived);
|
|
182
|
+
return rootKeptAgainst(
|
|
183
|
+
now.documentElement,
|
|
184
|
+
was?.documentElement ?? null,
|
|
185
|
+
shape
|
|
186
|
+
) && sameNodes(surroundingNodes(now), surroundingNodes(was)) && interEntryContentKept(
|
|
187
|
+
now.documentElement,
|
|
188
|
+
was?.documentElement ?? null,
|
|
189
|
+
shape.rewritesContents
|
|
190
|
+
);
|
|
191
|
+
},
|
|
192
|
+
referents: shape.referents,
|
|
193
|
+
wellFormed: wellFormedEntry,
|
|
194
|
+
anyonesChange: threadKeyAlone,
|
|
195
|
+
allowed: (entry, original, authorId, options, session) => entryAllowed(
|
|
196
|
+
entry,
|
|
197
|
+
original,
|
|
198
|
+
authorId,
|
|
199
|
+
options.editableComments,
|
|
200
|
+
session.comments.people
|
|
201
|
+
)
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function referencedCommentIds(doc) {
|
|
205
|
+
const ids = /* @__PURE__ */ new Set();
|
|
206
|
+
for (const [id, comment] of commentReferencesIn(doc)) {
|
|
207
|
+
ids.add(id);
|
|
208
|
+
for (const reply of comment.replies) ids.add(reply.id);
|
|
209
|
+
}
|
|
210
|
+
return ids;
|
|
211
|
+
}
|
|
212
|
+
var commentsShape = {
|
|
213
|
+
relType: COMMENTS_REL_TYPE,
|
|
214
|
+
contentType: COMMENTS_CONTENT_TYPE,
|
|
215
|
+
baseName: "comments",
|
|
216
|
+
namespace: W_NS,
|
|
217
|
+
rootName: "comments",
|
|
218
|
+
rewritesContents: true,
|
|
219
|
+
localName: "comment",
|
|
220
|
+
idAttr: "id",
|
|
221
|
+
xmlIn: (session) => session.comments.xml,
|
|
222
|
+
pathIn: (session) => session.comments.partPath,
|
|
223
|
+
referents: (story) => referencedCommentIds(story.doc)
|
|
224
|
+
};
|
|
225
|
+
function commentEntries(story) {
|
|
226
|
+
return Array.from(
|
|
227
|
+
arrivedIn(commentsShape, story.session).values(),
|
|
228
|
+
(entry) => entry.el
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
var commentsPart = storyPart(commentsShape);
|
|
232
|
+
var commentsExtendedPart = storyPart({
|
|
233
|
+
relType: COMMENTS_EXTENDED_REL_TYPE,
|
|
234
|
+
contentType: COMMENTS_EXTENDED_CONTENT_TYPE,
|
|
235
|
+
baseName: "commentsExtended",
|
|
236
|
+
namespace: W15_NS,
|
|
237
|
+
rootName: "commentsEx",
|
|
238
|
+
rewritesContents: true,
|
|
239
|
+
localName: "commentEx",
|
|
240
|
+
idAttr: "paraId",
|
|
241
|
+
xmlIn: (session) => session.comments.extendedXml,
|
|
242
|
+
pathIn: (session) => session.comments.extendedPartPath,
|
|
243
|
+
// Thread state stands for a comment, and it is keyed by the comment's own thread key
|
|
244
|
+
referents: (story) => new Set(
|
|
245
|
+
commentEntries(story).flatMap((el) => {
|
|
246
|
+
const key = lastParagraphId(el);
|
|
247
|
+
return key === null ? [] : [key];
|
|
248
|
+
})
|
|
249
|
+
)
|
|
250
|
+
});
|
|
251
|
+
var peoplePart = storyPart({
|
|
252
|
+
relType: PEOPLE_REL_TYPE,
|
|
253
|
+
contentType: PEOPLE_CONTENT_TYPE,
|
|
254
|
+
baseName: "people",
|
|
255
|
+
namespace: W15_NS,
|
|
256
|
+
rootName: "people",
|
|
257
|
+
rewritesContents: false,
|
|
258
|
+
localName: "person",
|
|
259
|
+
idAttr: "author",
|
|
260
|
+
xmlIn: (session) => session.comments.people.xml,
|
|
261
|
+
pathIn: (session) => session.comments.people.partPath,
|
|
262
|
+
// An identity stands for a name somebody writes comments under
|
|
263
|
+
referents: (story) => new Set(
|
|
264
|
+
commentEntries(story).flatMap((el) => {
|
|
265
|
+
const author = attributeByLocalName(el, "author");
|
|
266
|
+
return author === null ? [] : [author];
|
|
267
|
+
})
|
|
268
|
+
)
|
|
269
|
+
});
|
|
270
|
+
var COMMENT_STORY_PARTS = [commentsPart, commentsExtendedPart, peoplePart];
|
|
271
|
+
export {
|
|
272
|
+
COMMENT_STORY_PARTS,
|
|
273
|
+
commentsExtendedPart,
|
|
274
|
+
commentsPart,
|
|
275
|
+
entryAllowed,
|
|
276
|
+
peoplePart,
|
|
277
|
+
threadKeyAlone,
|
|
278
|
+
wellFormedEntry
|
|
279
|
+
};
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* it stands and never appended to. Appending a second person for it would leave a file naming two
|
|
10
10
|
* identities for one name, where a reader keying by name hands one author's comments to the other.
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
12
|
+
import type { PartPlanContext } from "../partPlan";
|
|
13
|
+
import type { StoryPartKind } from "../protectionPolicy";
|
|
13
14
|
import type { SessionStore } from "../session";
|
|
14
15
|
import type { CommentReferenceData, CommentReplyData } from "./model";
|
|
15
16
|
export interface ImportedPeople {
|
|
@@ -31,9 +32,17 @@ export declare function readPeople(parts: Map<string, Uint8Array>, mainPartPath:
|
|
|
31
32
|
* record, and for one the part records under more than one identity.
|
|
32
33
|
*/
|
|
33
34
|
export declare function commentAuthorId(people: ImportedPeople, author: string): string | null;
|
|
35
|
+
/**
|
|
36
|
+
* The authors the part records no person for at all, each under the name they write as. A name it
|
|
37
|
+
* already records is left as it stands, whichever provider recorded it and whatever it resolves to.
|
|
38
|
+
*/
|
|
39
|
+
export declare function unrecordedAuthors(bodies: Iterable<CommentReferenceData | CommentReplyData>, people: ImportedPeople): Map<string, string>;
|
|
34
40
|
/**
|
|
35
41
|
* Plans the people part, its relationship and its content type for every identity the current
|
|
36
42
|
* comments carry that the document has not recorded. Null when it has recorded them all, which
|
|
37
43
|
* leaves the part as it arrived.
|
|
44
|
+
*
|
|
45
|
+
* `part` is the description the verifier reads the part back through (`./parts`), so where this
|
|
46
|
+
* writes it and what it declares it as are the same facts on both sides.
|
|
38
47
|
*/
|
|
39
|
-
export declare function planPeoplePart(bodies: Iterable<CommentReferenceData | CommentReplyData>, session: SessionStore,
|
|
48
|
+
export declare function planPeoplePart(part: StoryPartKind, bodies: Iterable<CommentReferenceData | CommentReplyData>, session: SessionStore, context: PartPlanContext): ReadonlyMap<string, Uint8Array> | null;
|
|
@@ -1,42 +1,28 @@
|
|
|
1
1
|
// src/docx/comments/people.ts
|
|
2
|
-
import {
|
|
2
|
+
import { xmlnsAttr } from "../../ooxml/element.js";
|
|
3
|
+
import { xmlnsDecl } from "../../ooxml/names.js";
|
|
4
|
+
import { splicePart } from "../../ooxml/partSplice.js";
|
|
3
5
|
import {
|
|
6
|
+
attributeByLocalName,
|
|
4
7
|
childByLocalName,
|
|
5
8
|
decodeUtf8,
|
|
6
9
|
elementChildren,
|
|
7
10
|
encodeUtf8,
|
|
8
|
-
escapeXml,
|
|
9
11
|
parseXml
|
|
10
12
|
} from "../../ooxml/xml.js";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
resolveTarget
|
|
16
|
-
} from "../relationships.js";
|
|
17
|
-
import {
|
|
18
|
-
COMMENT_AUTHOR_PROVIDER,
|
|
19
|
-
CONTENT_TYPES_PATH,
|
|
20
|
-
PEOPLE_CONTENT_TYPE,
|
|
21
|
-
PEOPLE_REL_TYPE,
|
|
22
|
-
W15_NS
|
|
23
|
-
} from "./constants.js";
|
|
24
|
-
import { withContentType } from "./contentTypes.js";
|
|
13
|
+
import { relatedPartPath } from "../packageParts.js";
|
|
14
|
+
import { directoryOf } from "../relationships.js";
|
|
15
|
+
import { COMMENT_AUTHOR_PROVIDER, PEOPLE_REL_TYPE, W15_NS } from "./constants.js";
|
|
16
|
+
import { renderPerson } from "./grammar.js";
|
|
25
17
|
var NO_PEOPLE = {
|
|
26
18
|
partPath: null,
|
|
27
19
|
xml: null,
|
|
28
20
|
hadBom: false,
|
|
29
21
|
byAuthor: /* @__PURE__ */ new Map()
|
|
30
22
|
};
|
|
31
|
-
function attribute(el, localName) {
|
|
32
|
-
return Array.from(el.attributes).find((entry) => entry.localName === localName)?.value ?? null;
|
|
33
|
-
}
|
|
34
23
|
function readPeople(parts, mainPartPath) {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
);
|
|
38
|
-
if (!relationship) return NO_PEOPLE;
|
|
39
|
-
const partPath = resolveTarget(mainPartPath, relationship.target);
|
|
24
|
+
const partPath = relatedPartPath(parts, mainPartPath, PEOPLE_REL_TYPE);
|
|
25
|
+
if (partPath === null) return NO_PEOPLE;
|
|
40
26
|
const bytes = parts.get(partPath);
|
|
41
27
|
if (!bytes) return { ...NO_PEOPLE, partPath };
|
|
42
28
|
const { text, hadBom } = decodeUtf8(bytes);
|
|
@@ -45,14 +31,15 @@ function readPeople(parts, mainPartPath) {
|
|
|
45
31
|
const byAuthor = /* @__PURE__ */ new Map();
|
|
46
32
|
for (const el of elementChildren(root)) {
|
|
47
33
|
if (el.localName !== "person") continue;
|
|
48
|
-
const author =
|
|
34
|
+
const author = attributeByLocalName(el, "author");
|
|
49
35
|
if (author === null) continue;
|
|
50
36
|
byAuthor.set(author, null);
|
|
51
37
|
const presence = childByLocalName(el, "presenceInfo");
|
|
52
38
|
if (presence === null) continue;
|
|
53
|
-
if (
|
|
39
|
+
if (attributeByLocalName(presence, "providerId") !== COMMENT_AUTHOR_PROVIDER)
|
|
40
|
+
continue;
|
|
54
41
|
const ids = ourIds.get(author) ?? /* @__PURE__ */ new Set();
|
|
55
|
-
ids.add(
|
|
42
|
+
ids.add(attributeByLocalName(presence, "userId"));
|
|
56
43
|
ourIds.set(author, ids);
|
|
57
44
|
}
|
|
58
45
|
for (const [author, ids] of ourIds) {
|
|
@@ -64,9 +51,6 @@ function readPeople(parts, mainPartPath) {
|
|
|
64
51
|
function commentAuthorId(people, author) {
|
|
65
52
|
return people.byAuthor.get(author) ?? null;
|
|
66
53
|
}
|
|
67
|
-
function personXml(author, userId, prefix, declaration) {
|
|
68
|
-
return `<${prefix}person${declaration} ${prefix}author="${escapeXml(author)}"><${prefix}presenceInfo ${prefix}providerId="${COMMENT_AUTHOR_PROVIDER}" ${prefix}userId="${escapeXml(userId)}"/></${prefix}person>`;
|
|
69
|
-
}
|
|
70
54
|
function unrecordedAuthors(bodies, people) {
|
|
71
55
|
const unrecorded = /* @__PURE__ */ new Map();
|
|
72
56
|
for (const body of bodies) {
|
|
@@ -87,106 +71,43 @@ function w15Prefix(root) {
|
|
|
87
71
|
const prefix = declaration.name.slice("xmlns:".length);
|
|
88
72
|
return prefix === "" ? "" : `${prefix}:`;
|
|
89
73
|
}
|
|
90
|
-
function rootTagStart(xml) {
|
|
91
|
-
let at = 0;
|
|
92
|
-
for (; ; ) {
|
|
93
|
-
const opens = xml.indexOf("<", at);
|
|
94
|
-
if (opens === -1) return -1;
|
|
95
|
-
if (xml.startsWith("<!--", opens)) {
|
|
96
|
-
at = xml.indexOf("-->", opens + 4) + 3;
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
if (xml.startsWith("<?", opens)) {
|
|
100
|
-
at = xml.indexOf("?>", opens + 2) + 2;
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
return opens;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
function openTagEnd(xml, at) {
|
|
107
|
-
let quote = null;
|
|
108
|
-
for (let i = at; i < xml.length; i += 1) {
|
|
109
|
-
const character = xml[i];
|
|
110
|
-
if (quote !== null) {
|
|
111
|
-
if (character === quote) quote = null;
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
if (character === '"' || character === "'") {
|
|
115
|
-
quote = character;
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
if (character === ">") {
|
|
119
|
-
return { end: i + 1, selfClosing: xml[i - 1] === "/" };
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
function malformed(detail) {
|
|
125
|
-
return new DocxExportError("malformed-xml", `the people part ${detail}`);
|
|
126
|
-
}
|
|
127
74
|
function peopleXml(people, added) {
|
|
128
75
|
const xml = people.xml;
|
|
129
76
|
if (xml === null) {
|
|
130
77
|
const persons2 = Array.from(
|
|
131
78
|
added,
|
|
132
|
-
([author, userId]) =>
|
|
79
|
+
([author, userId]) => renderPerson(author, userId, "w15:", null)
|
|
133
80
|
);
|
|
134
|
-
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w15:people
|
|
81
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w15:people ${xmlnsDecl("w15")}>${persons2.join("")}</w15:people>`;
|
|
135
82
|
}
|
|
136
|
-
const
|
|
137
|
-
if (root.localName !== "people") {
|
|
138
|
-
throw malformed("has no people root element");
|
|
139
|
-
}
|
|
140
|
-
const prefix = w15Prefix(root);
|
|
83
|
+
const prefix = w15Prefix(parseXml(xml).documentElement);
|
|
141
84
|
const persons = Array.from(
|
|
142
85
|
added,
|
|
143
|
-
([author, userId]) =>
|
|
86
|
+
([author, userId]) => renderPerson(
|
|
144
87
|
author,
|
|
145
88
|
userId,
|
|
146
89
|
prefix ?? "w15:",
|
|
147
|
-
prefix === null ?
|
|
90
|
+
prefix === null ? xmlnsAttr("w15") : null
|
|
148
91
|
)
|
|
149
92
|
).join("");
|
|
150
|
-
|
|
151
|
-
const open = start === -1 ? null : openTagEnd(xml, start);
|
|
152
|
-
if (open === null) throw malformed("has no people root element");
|
|
153
|
-
if (open.selfClosing) {
|
|
154
|
-
return `${xml.slice(0, open.end - 2)}>${persons}</${root.nodeName}>` + xml.slice(open.end);
|
|
155
|
-
}
|
|
156
|
-
const close = xml.lastIndexOf(`</${root.nodeName}`);
|
|
157
|
-
if (close === -1) throw malformed("has no closing people tag");
|
|
158
|
-
return xml.slice(0, close) + persons + xml.slice(close);
|
|
93
|
+
return splicePart(xml, { root: "people", append: persons });
|
|
159
94
|
}
|
|
160
|
-
function
|
|
161
|
-
const directory = directoryOf(session.mainPartPath);
|
|
162
|
-
for (let suffix = 0; ; suffix += 1) {
|
|
163
|
-
const name = suffix === 0 ? "people.xml" : `people${suffix + 1}.xml`;
|
|
164
|
-
const path = directory + name;
|
|
165
|
-
if (!session.parts.has(path)) return path;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
function planPeoplePart(bodies, session, relationships, currentContentTypes) {
|
|
95
|
+
function planPeoplePart(part, bodies, session, context) {
|
|
169
96
|
const people = session.comments.people;
|
|
170
97
|
const added = unrecordedAuthors(bodies, people);
|
|
171
98
|
if (added.size === 0) return null;
|
|
172
99
|
const parts = /* @__PURE__ */ new Map();
|
|
173
|
-
const addingPart =
|
|
174
|
-
const partPath =
|
|
100
|
+
const addingPart = part.pathIn(session) === null;
|
|
101
|
+
const partPath = part.writePathIn(session);
|
|
175
102
|
if (addingPart) {
|
|
176
|
-
relationships.add({
|
|
177
|
-
type:
|
|
103
|
+
context.relationships.add({
|
|
104
|
+
type: part.relType,
|
|
178
105
|
target: partPath.slice(directoryOf(session.mainPartPath).length)
|
|
179
106
|
});
|
|
180
107
|
}
|
|
181
108
|
parts.set(partPath, encodeUtf8(peopleXml(people, added), people.hadBom));
|
|
182
109
|
if (addingPart || people.xml === null) {
|
|
183
|
-
|
|
184
|
-
session.parts,
|
|
185
|
-
partPath,
|
|
186
|
-
PEOPLE_CONTENT_TYPE,
|
|
187
|
-
currentContentTypes
|
|
188
|
-
);
|
|
189
|
-
if (contentTypes) parts.set(CONTENT_TYPES_PATH, contentTypes);
|
|
110
|
+
context.contentTypes.addOverride(partPath, part.contentType);
|
|
190
111
|
}
|
|
191
112
|
return parts;
|
|
192
113
|
}
|
|
@@ -194,5 +115,6 @@ export {
|
|
|
194
115
|
NO_PEOPLE,
|
|
195
116
|
commentAuthorId,
|
|
196
117
|
planPeoplePart,
|
|
197
|
-
readPeople
|
|
118
|
+
readPeople,
|
|
119
|
+
unrecordedAuthors
|
|
198
120
|
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The protection that takes comments and nothing else, as the policy both the part planners and
|
|
3
|
+
* the server verifier read (`../protectionPolicy`).
|
|
4
|
+
*/
|
|
5
|
+
type StoryReason = "body-changed" | "comment-not-owned" | "comment-author-forged";
|
|
6
|
+
export declare const commentsPolicy: import("../protectionPolicy").ProtectionPolicy<StoryReason, "comment-markup-rejected">;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// src/docx/comments/policy.ts
|
|
2
|
+
import {
|
|
3
|
+
commentAdditionsBy,
|
|
4
|
+
commentEditsOwned,
|
|
5
|
+
commentIdentitiesKept,
|
|
6
|
+
withoutComments
|
|
7
|
+
} from "../../schema/protection.js";
|
|
8
|
+
import {
|
|
9
|
+
registerProtectionPolicy
|
|
10
|
+
} from "../protectionPolicy.js";
|
|
11
|
+
import { comparableStory } from "../storyProjection.js";
|
|
12
|
+
import { COMMENT_STORY_PARTS } from "./parts.js";
|
|
13
|
+
var refused = (reason) => ({ ok: false, reason });
|
|
14
|
+
function sameBody(before, after) {
|
|
15
|
+
const was = comparableStory(before, withoutComments);
|
|
16
|
+
const now = comparableStory(after, withoutComments);
|
|
17
|
+
return was !== null && now !== null && was.length === now.length && was.every((block, at) => block === now[at]);
|
|
18
|
+
}
|
|
19
|
+
function commentsStoryKept(before, after, authorId, { editableComments }) {
|
|
20
|
+
if (!sameBody(before, after)) return refused("body-changed");
|
|
21
|
+
if (!commentIdentitiesKept(before.doc, after.doc) || !commentAdditionsBy(before.doc, after.doc, authorId)) {
|
|
22
|
+
return refused("comment-author-forged");
|
|
23
|
+
}
|
|
24
|
+
if (!commentEditsOwned(before.doc, after.doc, {
|
|
25
|
+
protection: "comments",
|
|
26
|
+
authorId,
|
|
27
|
+
editableComments
|
|
28
|
+
})) {
|
|
29
|
+
return refused("comment-not-owned");
|
|
30
|
+
}
|
|
31
|
+
return { ok: true };
|
|
32
|
+
}
|
|
33
|
+
var commentsPolicy = registerProtectionPolicy(
|
|
34
|
+
{
|
|
35
|
+
level: "comments",
|
|
36
|
+
parts: COMMENT_STORY_PARTS,
|
|
37
|
+
rejectedMarkup: "comment-markup-rejected",
|
|
38
|
+
storyKept: commentsStoryKept
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
export {
|
|
42
|
+
commentsPolicy
|
|
43
|
+
};
|
|
@@ -41,11 +41,13 @@ export interface ImportedCommentExtension {
|
|
|
41
41
|
resolved: boolean;
|
|
42
42
|
xml: string;
|
|
43
43
|
}
|
|
44
|
+
/** The thread key of a comment, which the writer puts on the last paragraph of its body */
|
|
45
|
+
export declare function lastParagraphId(comment: Element): string | null;
|
|
44
46
|
/**
|
|
45
47
|
* Reads the Comments part related from the main document story.
|
|
46
48
|
*
|
|
47
|
-
* The people part
|
|
48
|
-
* comment left, and a comment added to it then has to be
|
|
49
|
-
* second one.
|
|
49
|
+
* The people part and the extended part are read whether or not there are comments: a document may
|
|
50
|
+
* carry either with no comment left, and a comment added to it then has to be written into the
|
|
51
|
+
* part it already has rather than into a second one beside it.
|
|
50
52
|
*/
|
|
51
53
|
export declare function readComments(parts: Map<string, Uint8Array>, mainPartPath: string): ImportedComments;
|