@portone/docx-editor 0.5.0 → 0.6.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 +65 -0
- package/CONTRIBUTING.md +10 -2
- package/README.md +1 -1
- package/dist/DocxEditor.js +3 -3
- package/dist/docx/formatting/styles.d.ts +0 -2
- package/dist/docx/formatting/styles.js +2 -15
- package/dist/docx/identities.js +17 -26
- package/dist/docx/importParagraph.d.ts +6 -5
- package/dist/docx/importParagraph.js +15 -62
- package/dist/docx/importPolicy.js +10 -5
- package/dist/docx/serializeParagraph.d.ts +5 -4
- package/dist/docx/serializeParagraph.js +22 -58
- package/dist/docx/tableTemplate.d.ts +25 -0
- package/dist/docx/tableTemplate.js +30 -16
- package/dist/docx/wrappers.d.ts +67 -0
- package/dist/docx/wrappers.js +117 -0
- package/dist/editor/clipboard/blockReaders.d.ts +74 -0
- package/dist/editor/clipboard/blockReaders.js +193 -0
- package/dist/editor/clipboard/htmlReader.d.ts +2 -6
- package/dist/editor/clipboard/htmlReader.js +98 -24
- package/dist/editor/clipboard/inlineFormatting.d.ts +0 -7
- package/dist/editor/clipboard/inlineFormatting.js +1 -7
- package/dist/editor/clipboard/plugin.js +7 -139
- package/dist/editor/clipboard/readContext.d.ts +10 -0
- package/dist/editor/clipboard/readContext.js +8 -2
- package/dist/editor/clipboard/source.d.ts +12 -0
- package/dist/editor/clipboard/source.js +21 -0
- package/dist/editor/commands/comments/editing.d.ts +4 -1
- package/dist/editor/commands/comments/editing.js +3 -4
- package/dist/editor/commands/comments/model.d.ts +7 -0
- package/dist/editor/commands/linkCommands.js +20 -5
- package/dist/editor/commands/lockCommands.js +59 -16
- package/dist/editor/commands/paragraphCommands.js +2 -2
- package/dist/editor/createEditor.js +4 -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/ooxml/props.d.ts +2 -0
- package/dist/ooxml/props.js +14 -0
- package/dist/schema/attrRoles.js +8 -3
- package/dist/schema/clipboard.d.ts +72 -0
- package/dist/schema/clipboard.js +288 -0
- package/dist/schema/docxSchema.js +27 -27
- package/dist/schema/locks.d.ts +3 -0
- package/dist/schema/locks.js +29 -12
- package/dist/schema/rendering.d.ts +14 -0
- package/dist/schema/rendering.js +24 -11
- package/dist/schema/wrappers.d.ts +58 -0
- package/dist/schema/wrappers.js +77 -0
- package/dist/styles/classNames.d.ts +2 -0
- package/dist/styles/classNames.js +2 -0
- package/dist/styles.css +7 -4
- package/dist/ui/CommentsPanel.d.ts +6 -1
- package/dist/ui/CommentsPanel.js +26 -8
- package/package.json +6 -4
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// src/schema/clipboard.ts
|
|
2
|
+
import {
|
|
3
|
+
DOMSerializer,
|
|
4
|
+
Fragment
|
|
5
|
+
} from "prosemirror-model";
|
|
6
|
+
import {
|
|
7
|
+
spanCount,
|
|
8
|
+
toCellFormat,
|
|
9
|
+
toGridCols,
|
|
10
|
+
toParagraphFormat,
|
|
11
|
+
toRowFormat,
|
|
12
|
+
toRunFormat,
|
|
13
|
+
toTableFormat,
|
|
14
|
+
toTableWidth
|
|
15
|
+
} from "../model/format.js";
|
|
16
|
+
import { styleIdOf } from "../ooxml/props.js";
|
|
17
|
+
import { editorClassNames } from "../styles/classNames.js";
|
|
18
|
+
import { DEFAULT_FONT_FALLBACKS } from "../styles/fontStack.js";
|
|
19
|
+
import {
|
|
20
|
+
cellStyle,
|
|
21
|
+
columnWidthPx,
|
|
22
|
+
paragraphStyle,
|
|
23
|
+
rowStyle,
|
|
24
|
+
tableStyle
|
|
25
|
+
} from "../styles/inlineStyle.js";
|
|
26
|
+
import { isPageBreak } from "./docxSchema.js";
|
|
27
|
+
import { imageAttrs, runAttrs } from "./rendering.js";
|
|
28
|
+
var COPIED_STYLE_ATTRIBUTE = "data-style";
|
|
29
|
+
function safeHref(value) {
|
|
30
|
+
const href = value?.trim() ?? "";
|
|
31
|
+
if (href === "") return null;
|
|
32
|
+
if (/^(?:https?|mailto|tel):/i.test(href)) return href;
|
|
33
|
+
return /^(?:[./]|#)/.test(href) ? href : null;
|
|
34
|
+
}
|
|
35
|
+
function textAttr(value) {
|
|
36
|
+
return typeof value === "string" ? value : void 0;
|
|
37
|
+
}
|
|
38
|
+
function noteLabel(node) {
|
|
39
|
+
return node.attrs.customMarkFollows === true ? "" : textAttr(node.attrs.label) ?? "";
|
|
40
|
+
}
|
|
41
|
+
function oneLine(text) {
|
|
42
|
+
return text.replace(/[\n\f]+/g, " ");
|
|
43
|
+
}
|
|
44
|
+
function spanAttribute(count) {
|
|
45
|
+
const span = spanCount(count);
|
|
46
|
+
return span > 1 ? `${span}` : void 0;
|
|
47
|
+
}
|
|
48
|
+
function preservedText(node) {
|
|
49
|
+
const display = node.attrs.display;
|
|
50
|
+
if (display === "break") return "\n";
|
|
51
|
+
if (display !== "text" && display !== "chip") return null;
|
|
52
|
+
const drawn = textAttr(node.attrs.text);
|
|
53
|
+
return drawn === void 0 || drawn === "" ? null : drawn;
|
|
54
|
+
}
|
|
55
|
+
var PRESERVED_SPEC = {
|
|
56
|
+
toClipboardDOM: (node) => {
|
|
57
|
+
const said = preservedText(node);
|
|
58
|
+
if (said === null) return null;
|
|
59
|
+
return said === "\n" ? ["br"] : ["span", said];
|
|
60
|
+
},
|
|
61
|
+
toClipboardText: preservedText
|
|
62
|
+
};
|
|
63
|
+
var WRITES_NOTHING = {
|
|
64
|
+
toClipboardDOM: null,
|
|
65
|
+
toClipboardText: null
|
|
66
|
+
};
|
|
67
|
+
var CLIPBOARD_NODES = {
|
|
68
|
+
// A slice carries blocks, never the document itself, so neither answer is ever asked for
|
|
69
|
+
doc: WRITES_NOTHING,
|
|
70
|
+
paragraph: {
|
|
71
|
+
toClipboardDOM: (node) => [
|
|
72
|
+
"p",
|
|
73
|
+
{
|
|
74
|
+
class: editorClassNames.paragraph,
|
|
75
|
+
style: paragraphStyle(
|
|
76
|
+
toParagraphFormat(node.attrs.format),
|
|
77
|
+
toRunFormat(node.attrs.styleRun)
|
|
78
|
+
),
|
|
79
|
+
[COPIED_STYLE_ATTRIBUTE]: styleIdOf(node.attrs.pPr) ?? void 0
|
|
80
|
+
},
|
|
81
|
+
0
|
|
82
|
+
],
|
|
83
|
+
toClipboardText: (_node, children) => children.join("")
|
|
84
|
+
},
|
|
85
|
+
table: {
|
|
86
|
+
toClipboardDOM: (node) => {
|
|
87
|
+
const attrs = {
|
|
88
|
+
class: editorClassNames.table,
|
|
89
|
+
style: tableStyle(
|
|
90
|
+
toTableFormat(node.attrs.format),
|
|
91
|
+
toTableWidth(node.attrs.tblW)
|
|
92
|
+
)
|
|
93
|
+
};
|
|
94
|
+
const body = ["tbody", 0];
|
|
95
|
+
const columns = toGridCols(node.attrs.gridCols).map((dxa) => [
|
|
96
|
+
"col",
|
|
97
|
+
{ style: `width:${columnWidthPx(dxa)}px` }
|
|
98
|
+
]);
|
|
99
|
+
if (columns.length === 0) return ["table", attrs, body];
|
|
100
|
+
return ["table", attrs, ["colgroup", ...columns], body];
|
|
101
|
+
},
|
|
102
|
+
toClipboardText: (_node, children) => children.join("\n")
|
|
103
|
+
},
|
|
104
|
+
tableRow: {
|
|
105
|
+
toClipboardDOM: (node) => [
|
|
106
|
+
"tr",
|
|
107
|
+
{
|
|
108
|
+
class: editorClassNames.tableRow,
|
|
109
|
+
style: rowStyle(toRowFormat(node.attrs.format))
|
|
110
|
+
},
|
|
111
|
+
0
|
|
112
|
+
],
|
|
113
|
+
toClipboardText: (_node, children) => children.join(" ")
|
|
114
|
+
},
|
|
115
|
+
tableCell: {
|
|
116
|
+
toClipboardDOM: (node) => [
|
|
117
|
+
"td",
|
|
118
|
+
{
|
|
119
|
+
class: editorClassNames.tableCell,
|
|
120
|
+
style: cellStyle(toCellFormat(node.attrs.format)),
|
|
121
|
+
colspan: spanAttribute(node.attrs.colspan),
|
|
122
|
+
rowspan: spanAttribute(node.attrs.rowspan)
|
|
123
|
+
},
|
|
124
|
+
0
|
|
125
|
+
],
|
|
126
|
+
// A tab stands between two cells and a line between two rows wherever a table is pasted as
|
|
127
|
+
// text, so nothing inside a cell may write a line: several paragraphs, a line break of their
|
|
128
|
+
// own and a nested table all say what they hold on the one line this cell stands on
|
|
129
|
+
toClipboardText: (_node, children) => oneLine(children.join(" "))
|
|
130
|
+
},
|
|
131
|
+
rawBlock: WRITES_NOTHING,
|
|
132
|
+
hardBreak: {
|
|
133
|
+
toClipboardDOM: () => ["br"],
|
|
134
|
+
// A page break ends more than a line, and the form feed is what says so in plain text
|
|
135
|
+
toClipboardText: (node) => isPageBreak(node.attrs.brAttrs) ? "\f" : "\n"
|
|
136
|
+
},
|
|
137
|
+
image: {
|
|
138
|
+
// The measure the document keeps stays behind. The pixels it was drawn at are what another
|
|
139
|
+
// application reads a size from, and what a paste back into this editor reads one from
|
|
140
|
+
toClipboardDOM: (node) => ["img", imageAttrs(node.attrs)],
|
|
141
|
+
// A picture is not text. But a copy may be read where no picture can follow, and there the
|
|
142
|
+
// words the document gave it for that very case are better than a gap
|
|
143
|
+
toClipboardText: (node) => textAttr(node.attrs.alt) ?? ""
|
|
144
|
+
},
|
|
145
|
+
commentStart: WRITES_NOTHING,
|
|
146
|
+
commentEnd: WRITES_NOTHING,
|
|
147
|
+
commentReference: WRITES_NOTHING,
|
|
148
|
+
noteReference: {
|
|
149
|
+
// A note whose own mark follows draws no number, and an empty superscript would stand for
|
|
150
|
+
// nothing wherever the copy lands
|
|
151
|
+
toClipboardDOM: (node) => {
|
|
152
|
+
const label = noteLabel(node);
|
|
153
|
+
if (label === "") return null;
|
|
154
|
+
const kind = node.attrs.kind === "endnote" ? "Endnote" : "Footnote";
|
|
155
|
+
return ["sup", { "aria-label": `${kind} ${label}` }, label];
|
|
156
|
+
},
|
|
157
|
+
toClipboardText: noteLabel
|
|
158
|
+
},
|
|
159
|
+
rawRunContent: PRESERVED_SPEC,
|
|
160
|
+
rawInline: PRESERVED_SPEC,
|
|
161
|
+
text: {
|
|
162
|
+
// `DOMSerializer` writes a text node itself and never asks a spec for one, so there is no
|
|
163
|
+
// drawing to declare. The characters travel as they stand
|
|
164
|
+
toClipboardDOM: null,
|
|
165
|
+
toClipboardText: (node) => node.text ?? ""
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
var CLIPBOARD_MARKS = {
|
|
169
|
+
// A content control is a wrapper the file keeps and nothing outside this editor reads back, so
|
|
170
|
+
// the text it held leaves on its own rather than inside a span standing for nothing
|
|
171
|
+
sdt: { toClipboardDOM: null },
|
|
172
|
+
link: {
|
|
173
|
+
// The editor draws the address as data so that a click inside the text places the caret.
|
|
174
|
+
// Anywhere else an anchor is what a link is: it follows in mail or a document, and it comes
|
|
175
|
+
// back as a link when it is pasted here again
|
|
176
|
+
toClipboardDOM: (mark) => {
|
|
177
|
+
const href = safeHref(textAttr(mark.attrs.href) ?? null);
|
|
178
|
+
const attrs = { class: editorClassNames.link, href: href ?? void 0 };
|
|
179
|
+
return href === null ? ["span", attrs, 0] : ["a", attrs, 0];
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
run: {
|
|
183
|
+
// The built-in fallbacks rather than the ones this editor was given: a copy is read where the
|
|
184
|
+
// host's font list means nothing, and the schema cannot see which editor it is drawing for
|
|
185
|
+
toClipboardDOM: (mark) => [
|
|
186
|
+
"span",
|
|
187
|
+
runAttrs(mark.attrs, DEFAULT_FONT_FALLBACKS),
|
|
188
|
+
0
|
|
189
|
+
]
|
|
190
|
+
},
|
|
191
|
+
tab: { toClipboardDOM: () => ["span", { class: editorClassNames.tab }, 0] }
|
|
192
|
+
};
|
|
193
|
+
var clipboardSpecs = { nodes: CLIPBOARD_NODES, marks: CLIPBOARD_MARKS };
|
|
194
|
+
var NODES_BY_NAME = CLIPBOARD_NODES;
|
|
195
|
+
function nodeSpec(node) {
|
|
196
|
+
return NODES_BY_NAME[node.type.name];
|
|
197
|
+
}
|
|
198
|
+
function clipboardDOMOf(node) {
|
|
199
|
+
const spec = nodeSpec(node);
|
|
200
|
+
if (spec === void 0 || spec.toClipboardDOM === null) return null;
|
|
201
|
+
return spec.toClipboardDOM(node);
|
|
202
|
+
}
|
|
203
|
+
function written(fragment) {
|
|
204
|
+
const kept = [];
|
|
205
|
+
fragment.forEach((child) => {
|
|
206
|
+
if (child.isText || clipboardDOMOf(child) !== null) kept.push(child);
|
|
207
|
+
});
|
|
208
|
+
return kept.length === fragment.childCount ? fragment : Fragment.from(kept);
|
|
209
|
+
}
|
|
210
|
+
var ClipboardSerializer = class extends DOMSerializer {
|
|
211
|
+
constructor(stampCopy) {
|
|
212
|
+
super(clipboardNodes(), clipboardMarks());
|
|
213
|
+
this.stampCopy = stampCopy;
|
|
214
|
+
}
|
|
215
|
+
stampCopy;
|
|
216
|
+
serializeFragment(fragment, options, target) {
|
|
217
|
+
const dom = super.serializeFragment(written(fragment), options, target);
|
|
218
|
+
if (target !== void 0) return dom;
|
|
219
|
+
if (dom.firstChild === null && fragment.childCount > 0) {
|
|
220
|
+
carrier(dom, options?.document);
|
|
221
|
+
}
|
|
222
|
+
this.stampCopy?.(dom);
|
|
223
|
+
return dom;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
function carrier(copy, document) {
|
|
227
|
+
const owner = document ?? copy.ownerDocument;
|
|
228
|
+
if (owner !== null) copy.appendChild(owner.createElement("span"));
|
|
229
|
+
}
|
|
230
|
+
function clipboardNodes() {
|
|
231
|
+
return Object.fromEntries(
|
|
232
|
+
Object.entries(CLIPBOARD_NODES).flatMap(([name, spec]) => {
|
|
233
|
+
const draw = spec.toClipboardDOM;
|
|
234
|
+
return draw === null ? [] : [
|
|
235
|
+
[
|
|
236
|
+
name,
|
|
237
|
+
(node) => {
|
|
238
|
+
const drawn = draw(node);
|
|
239
|
+
if (drawn === null) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`${name} was serialized after drawing nothing`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return drawn;
|
|
245
|
+
}
|
|
246
|
+
]
|
|
247
|
+
];
|
|
248
|
+
})
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
function clipboardMarks() {
|
|
252
|
+
return Object.fromEntries(
|
|
253
|
+
Object.entries(CLIPBOARD_MARKS).flatMap(
|
|
254
|
+
([name, spec]) => (
|
|
255
|
+
// A mark with no drawing of its own is left out, and `DOMSerializer` writes the content it
|
|
256
|
+
// covered without a wrapper
|
|
257
|
+
spec.toClipboardDOM === null ? [] : [[name, spec.toClipboardDOM]]
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
function clipboardSerializer(stampCopy) {
|
|
263
|
+
return new ClipboardSerializer(stampCopy);
|
|
264
|
+
}
|
|
265
|
+
function textPieces(fragment) {
|
|
266
|
+
const pieces = [];
|
|
267
|
+
fragment.forEach((child) => {
|
|
268
|
+
const piece = clipboardTextOf(child);
|
|
269
|
+
if (piece !== null) pieces.push(piece);
|
|
270
|
+
});
|
|
271
|
+
return pieces;
|
|
272
|
+
}
|
|
273
|
+
function clipboardTextOf(node) {
|
|
274
|
+
const spec = nodeSpec(node);
|
|
275
|
+
if (spec === void 0 || spec.toClipboardText === null) return null;
|
|
276
|
+
return spec.toClipboardText(node, textPieces(node.content));
|
|
277
|
+
}
|
|
278
|
+
function clipboardText(slice) {
|
|
279
|
+
const pieces = textPieces(slice.content);
|
|
280
|
+
return pieces.join(slice.content.firstChild?.isInline === true ? "" : "\n");
|
|
281
|
+
}
|
|
282
|
+
export {
|
|
283
|
+
COPIED_STYLE_ATTRIBUTE,
|
|
284
|
+
clipboardSerializer,
|
|
285
|
+
clipboardSpecs,
|
|
286
|
+
clipboardText,
|
|
287
|
+
safeHref
|
|
288
|
+
};
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
tableStyle
|
|
36
36
|
} from "../styles/inlineStyle.js";
|
|
37
37
|
import { imageNodeSpec, runMarkSpec } from "./rendering.js";
|
|
38
|
+
import { WRAPPER_ATTRS, WRAPPER_GROUP } from "./wrappers.js";
|
|
38
39
|
import { imageNodeSpec as imageNodeSpec2, runMarkSpec as runMarkSpec2 } from "./rendering.js";
|
|
39
40
|
var NO_STORIES = Object.freeze({});
|
|
40
41
|
function isRecord(value) {
|
|
@@ -572,7 +573,7 @@ var docxSchema = new Schema({
|
|
|
572
573
|
hardBreak: {
|
|
573
574
|
group: "inline",
|
|
574
575
|
inline: true,
|
|
575
|
-
marks: "run
|
|
576
|
+
marks: "run wrapper",
|
|
576
577
|
attrs: { brAttrs: { default: null } },
|
|
577
578
|
toDOM(node) {
|
|
578
579
|
return [
|
|
@@ -605,7 +606,7 @@ var docxSchema = new Schema({
|
|
|
605
606
|
group: "inline",
|
|
606
607
|
inline: true,
|
|
607
608
|
draggable: true,
|
|
608
|
-
marks: "run
|
|
609
|
+
marks: "run wrapper",
|
|
609
610
|
attrs: {
|
|
610
611
|
/** The image bytes as a data URL */
|
|
611
612
|
src: { default: null },
|
|
@@ -640,7 +641,7 @@ var docxSchema = new Schema({
|
|
|
640
641
|
inline: true,
|
|
641
642
|
atom: true,
|
|
642
643
|
selectable: false,
|
|
643
|
-
marks: "
|
|
644
|
+
marks: "wrapper",
|
|
644
645
|
attrs: {
|
|
645
646
|
id: { default: null },
|
|
646
647
|
xml: { default: null }
|
|
@@ -672,7 +673,7 @@ var docxSchema = new Schema({
|
|
|
672
673
|
inline: true,
|
|
673
674
|
atom: true,
|
|
674
675
|
selectable: false,
|
|
675
|
-
marks: "
|
|
676
|
+
marks: "wrapper",
|
|
676
677
|
attrs: {
|
|
677
678
|
id: { default: null },
|
|
678
679
|
xml: { default: null }
|
|
@@ -704,7 +705,7 @@ var docxSchema = new Schema({
|
|
|
704
705
|
inline: true,
|
|
705
706
|
atom: true,
|
|
706
707
|
selectable: false,
|
|
707
|
-
marks: "run
|
|
708
|
+
marks: "run wrapper",
|
|
708
709
|
attrs: {
|
|
709
710
|
id: { default: null },
|
|
710
711
|
referenceXml: { default: null },
|
|
@@ -780,7 +781,7 @@ var docxSchema = new Schema({
|
|
|
780
781
|
inline: true,
|
|
781
782
|
atom: true,
|
|
782
783
|
selectable: false,
|
|
783
|
-
marks: "run
|
|
784
|
+
marks: "run wrapper",
|
|
784
785
|
attrs: {
|
|
785
786
|
kind: { default: "footnote" },
|
|
786
787
|
id: { default: null },
|
|
@@ -836,7 +837,7 @@ var docxSchema = new Schema({
|
|
|
836
837
|
group: "inline",
|
|
837
838
|
inline: true,
|
|
838
839
|
atom: true,
|
|
839
|
-
marks: "run
|
|
840
|
+
marks: "run wrapper",
|
|
840
841
|
attrs: {
|
|
841
842
|
xml: { default: null },
|
|
842
843
|
/** The local name of the element, for example `fldChar` */
|
|
@@ -862,7 +863,7 @@ var docxSchema = new Schema({
|
|
|
862
863
|
group: "inline",
|
|
863
864
|
inline: true,
|
|
864
865
|
atom: true,
|
|
865
|
-
marks: "
|
|
866
|
+
marks: "wrapper",
|
|
866
867
|
attrs: {
|
|
867
868
|
xml: { default: null },
|
|
868
869
|
/** The local name of the element; `r` for a run with no content, kept whole */
|
|
@@ -889,20 +890,20 @@ var docxSchema = new Schema({
|
|
|
889
890
|
*
|
|
890
891
|
* It is declared ahead of the run mark on purpose: the mark declared first is drawn
|
|
891
892
|
* outside on screen, so one control can hold the several runs it wrapped in the file.
|
|
893
|
+
* It is declared ahead of the other wrappers for the same reason, which is what settles
|
|
894
|
+
* the nesting of two wrappers written at one and the same depth (`./wrappers`).
|
|
892
895
|
*/
|
|
893
896
|
sdt: {
|
|
897
|
+
group: WRAPPER_GROUP,
|
|
894
898
|
// A character typed against either edge of the control belongs outside it
|
|
895
899
|
inclusive: false,
|
|
900
|
+
// §17.5.2.17 has a control hold a control, so a second one does not replace the first:
|
|
901
|
+
// which stands inside which is what `depth` says (`./wrappers`)
|
|
902
|
+
excludes: "",
|
|
896
903
|
attrs: {
|
|
897
904
|
/** The opening XML of the `<w:sdt>`, the same string a wrapped cell carries */
|
|
898
905
|
sdtPrefix: { default: null },
|
|
899
|
-
|
|
900
|
-
* Which control this is, counted through the document as it was opened, and never
|
|
901
|
-
* written back to the file. Two controls whose XML is identical would otherwise wear
|
|
902
|
-
* the very same mark and their text would run into one. The first control gets 0, so
|
|
903
|
-
* a control made during editing needs no number of its own.
|
|
904
|
-
*/
|
|
905
|
-
sdtKey: { default: 0 },
|
|
906
|
+
...WRAPPER_ATTRS,
|
|
906
907
|
/**
|
|
907
908
|
* The two clauses of the control's lock: whether its contents may not be edited, and
|
|
908
909
|
* whether the control itself may not be deleted, not even whole. The `w:lock` inside
|
|
@@ -921,7 +922,8 @@ var docxSchema = new Schema({
|
|
|
921
922
|
{
|
|
922
923
|
class: locked ? `${editorClassNames.sdt} ${editorClassNames.sdtLocked}` : editorClassNames.sdt,
|
|
923
924
|
"data-sdt-prefix": text(mark.attrs.sdtPrefix),
|
|
924
|
-
"data-
|
|
925
|
+
"data-key": numberText(mark.attrs.key),
|
|
926
|
+
"data-depth": numberText(mark.attrs.depth),
|
|
925
927
|
"data-sdt-contents-locked": locked ? "1" : void 0,
|
|
926
928
|
"data-sdt-deletion-locked": mark.attrs.deletionLocked === true ? "1" : void 0
|
|
927
929
|
},
|
|
@@ -936,7 +938,8 @@ var docxSchema = new Schema({
|
|
|
936
938
|
if (prefix === null || prefix === false) return false;
|
|
937
939
|
return {
|
|
938
940
|
sdtPrefix: prefix,
|
|
939
|
-
|
|
941
|
+
key: parseInt10(dom.getAttribute("data-key"), 0),
|
|
942
|
+
depth: parseInt10(dom.getAttribute("data-depth"), 0),
|
|
940
943
|
contentsLocked: dom.getAttribute("data-sdt-contents-locked") === "1",
|
|
941
944
|
deletionLocked: dom.getAttribute("data-sdt-deletion-locked") === "1"
|
|
942
945
|
};
|
|
@@ -952,10 +955,12 @@ var docxSchema = new Schema({
|
|
|
952
955
|
* back out inside it (`docx/serializeParagraph`).
|
|
953
956
|
*/
|
|
954
957
|
link: {
|
|
958
|
+
group: WRAPPER_GROUP,
|
|
955
959
|
// A character typed against either edge of a link belongs outside it, and that is also what
|
|
956
960
|
// decides whether a caret counts as standing in one (`editor/commands/linkCommands`)
|
|
957
961
|
inclusive: false,
|
|
958
962
|
attrs: {
|
|
963
|
+
...WRAPPER_ATTRS,
|
|
959
964
|
/**
|
|
960
965
|
* The opening XML of the `<w:hyperlink>`, which carries everything about the link we never
|
|
961
966
|
* read: `w:tooltip`, `w:history`, `w:docLocation`, `w:anchor`. null for a link made in the
|
|
@@ -967,14 +972,7 @@ var docxSchema = new Schema({
|
|
|
967
972
|
* a bookmark alone, or one whose relationship leads nowhere we can follow: the wrapper still
|
|
968
973
|
* travels, and no address is offered for it.
|
|
969
974
|
*/
|
|
970
|
-
href: { default: null }
|
|
971
|
-
/**
|
|
972
|
-
* Which link this is, counted through the document as it was opened, and never written back
|
|
973
|
-
* to the file. Two links written exactly alike would otherwise wear the very same mark and
|
|
974
|
-
* the text they cover would run into one wrapper. The first link gets 0, so a link made
|
|
975
|
-
* during editing needs no number of its own.
|
|
976
|
-
*/
|
|
977
|
-
linkKey: { default: 0 }
|
|
975
|
+
href: { default: null }
|
|
978
976
|
},
|
|
979
977
|
toDOM(mark) {
|
|
980
978
|
return [
|
|
@@ -986,7 +984,8 @@ var docxSchema = new Schema({
|
|
|
986
984
|
// by the link panel instead
|
|
987
985
|
"data-href": text(mark.attrs.href),
|
|
988
986
|
"data-link-prefix": text(mark.attrs.linkPrefix),
|
|
989
|
-
"data-
|
|
987
|
+
"data-key": numberText(mark.attrs.key),
|
|
988
|
+
"data-depth": numberText(mark.attrs.depth)
|
|
990
989
|
},
|
|
991
990
|
0
|
|
992
991
|
];
|
|
@@ -1002,7 +1001,8 @@ var docxSchema = new Schema({
|
|
|
1002
1001
|
return {
|
|
1003
1002
|
linkPrefix: prefix,
|
|
1004
1003
|
href,
|
|
1005
|
-
|
|
1004
|
+
key: parseInt10(dom.getAttribute("data-key"), 0),
|
|
1005
|
+
depth: parseInt10(dom.getAttribute("data-depth"), 0)
|
|
1006
1006
|
};
|
|
1007
1007
|
}
|
|
1008
1008
|
}
|
package/dist/schema/locks.d.ts
CHANGED
|
@@ -56,6 +56,9 @@ export interface ControlSpan extends StepRange {
|
|
|
56
56
|
* A control that wrapped several runs comes in as several inlines wearing the very same mark, and
|
|
57
57
|
* what a lock answers for is the control rather than the run: a stretch covering one run of a
|
|
58
58
|
* control whole still covers only a part of the control.
|
|
59
|
+
*
|
|
60
|
+
* A control standing inside another gives a span of its own, the outer one first, and the two
|
|
61
|
+
* overlap: each locks on its own terms.
|
|
59
62
|
*/
|
|
60
63
|
export declare function controlSpans(block: Textblock): ControlSpan[];
|
|
61
64
|
/**
|
package/dist/schema/locks.js
CHANGED
|
@@ -10,11 +10,12 @@ import {
|
|
|
10
10
|
ReplaceStep
|
|
11
11
|
} from "prosemirror-transform";
|
|
12
12
|
import { docxSchema } from "./index.js";
|
|
13
|
+
import { wrappersOf } from "./wrappers.js";
|
|
13
14
|
var unlockAllowed = new PluginKey("docxEditorUnlockAllowed");
|
|
14
15
|
var historyReplay = new PluginKey("docxEditorHistoryReplay");
|
|
15
16
|
var OPEN = { contents: false, deletion: false };
|
|
16
|
-
function
|
|
17
|
-
return node
|
|
17
|
+
function sdtMarksOf(node) {
|
|
18
|
+
return node ? wrappersOf(node, docxSchema.marks.sdt) : [];
|
|
18
19
|
}
|
|
19
20
|
function markLocks(mark) {
|
|
20
21
|
return {
|
|
@@ -23,8 +24,7 @@ function markLocks(mark) {
|
|
|
23
24
|
};
|
|
24
25
|
}
|
|
25
26
|
function lockedMarkOf(node) {
|
|
26
|
-
|
|
27
|
-
return mark !== null && markLocks(mark).contents ? mark : null;
|
|
27
|
+
return sdtMarksOf(node).find((mark) => markLocks(mark).contents) ?? null;
|
|
28
28
|
}
|
|
29
29
|
var CELL_CONTENTS_ATTR = "sdtContentsLocked";
|
|
30
30
|
var CELL_DELETION_ATTR = "sdtDeletionLocked";
|
|
@@ -42,20 +42,37 @@ function isLockedCell(node) {
|
|
|
42
42
|
return cellLocks(node).contents;
|
|
43
43
|
}
|
|
44
44
|
function carriesLock(node) {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
const marks = node.isInline ? sdtMarksOf(node) : [];
|
|
46
|
+
if (marks.length === 0) {
|
|
47
|
+
const locks = cellLocks(node);
|
|
48
|
+
return locks.contents || locks.deletion;
|
|
49
|
+
}
|
|
50
|
+
return marks.some((mark) => {
|
|
51
|
+
const locks = markLocks(mark);
|
|
52
|
+
return locks.contents || locks.deletion;
|
|
53
|
+
});
|
|
48
54
|
}
|
|
49
55
|
function controlSpans(block) {
|
|
50
56
|
const spans = [];
|
|
57
|
+
let reaching = [];
|
|
51
58
|
block.node.forEach((child, offset) => {
|
|
52
|
-
const mark = sdtMarkOf(child);
|
|
53
|
-
if (!mark) return;
|
|
54
59
|
const from = block.start + offset;
|
|
55
60
|
const to = from + child.nodeSize;
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
const standing = [];
|
|
62
|
+
for (const mark of sdtMarksOf(child)) {
|
|
63
|
+
const carried = reaching.find(
|
|
64
|
+
(span) => span.to === from && span.mark.eq(mark)
|
|
65
|
+
);
|
|
66
|
+
if (carried) {
|
|
67
|
+
carried.to = to;
|
|
68
|
+
standing.push(carried);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const started = { from, to, mark };
|
|
72
|
+
spans.push(started);
|
|
73
|
+
standing.push(started);
|
|
74
|
+
}
|
|
75
|
+
reaching = standing;
|
|
59
76
|
});
|
|
60
77
|
return spans;
|
|
61
78
|
}
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import type { Attrs, DOMOutputSpec } from "prosemirror-model";
|
|
2
2
|
import type { FontFallbacks } from "../styles/fontStack";
|
|
3
|
+
/**
|
|
4
|
+
* What a run says to anyone reading it, the page and a copy of it alike.
|
|
5
|
+
*
|
|
6
|
+
* The rest of the drawing is this editor talking to itself - the original XML, the worked-out
|
|
7
|
+
* formatting it was drawn from - and `schema/clipboard` publishes only what is here.
|
|
8
|
+
*/
|
|
9
|
+
export declare function runAttrs(attrs: Attrs, fontFallbacks: FontFallbacks): Record<string, string | undefined>;
|
|
3
10
|
/** The DOM representation shared by the schema and the editor's custom run mark view. */
|
|
4
11
|
export declare function runMarkSpec(attrs: Attrs, fontFallbacks: FontFallbacks): DOMOutputSpec;
|
|
12
|
+
/**
|
|
13
|
+
* What an image says to anyone reading it, the page and a copy of it alike.
|
|
14
|
+
*
|
|
15
|
+
* The size is given in the pixels it is drawn at rather than as the document's own measure in EMU,
|
|
16
|
+
* which is what makes it the half another application can read.
|
|
17
|
+
*/
|
|
18
|
+
export declare function imageAttrs(attrs: Attrs): Record<string, string | undefined>;
|
|
5
19
|
/** The image element shared by the schema and the resizable image node view. */
|
|
6
20
|
export declare function imageNodeSpec(attrs: Attrs): DOMOutputSpec;
|
package/dist/schema/rendering.js
CHANGED
|
@@ -9,15 +9,21 @@ function text(value) {
|
|
|
9
9
|
function formatJson(format) {
|
|
10
10
|
return format ? JSON.stringify(format) : void 0;
|
|
11
11
|
}
|
|
12
|
+
function runAttrs(attrs, fontFallbacks) {
|
|
13
|
+
const format = toRunFormat(attrs.format);
|
|
14
|
+
return {
|
|
15
|
+
class: editorClassNames.run,
|
|
16
|
+
style: runStyle(format, fontFallbacks),
|
|
17
|
+
// Which shape of a Han character the browser draws is decided by this and nothing else
|
|
18
|
+
lang: format?.lang
|
|
19
|
+
};
|
|
20
|
+
}
|
|
12
21
|
function runMarkSpec(attrs, fontFallbacks) {
|
|
13
22
|
const format = toRunFormat(attrs.format);
|
|
14
23
|
return [
|
|
15
24
|
"span",
|
|
16
25
|
{
|
|
17
|
-
|
|
18
|
-
style: runStyle(format, fontFallbacks),
|
|
19
|
-
// Which shape of a Han character the browser draws is decided by this and nothing else
|
|
20
|
-
lang: format?.lang,
|
|
26
|
+
...runAttrs(attrs, fontFallbacks),
|
|
21
27
|
"data-rattrs": text(attrs.rAttrs),
|
|
22
28
|
"data-rpr": text(attrs.rPr),
|
|
23
29
|
"data-fmt": formatJson(format),
|
|
@@ -27,22 +33,29 @@ function runMarkSpec(attrs, fontFallbacks) {
|
|
|
27
33
|
0
|
|
28
34
|
];
|
|
29
35
|
}
|
|
30
|
-
function
|
|
36
|
+
function imageAttrs(attrs) {
|
|
31
37
|
const extent = toImageExtent(attrs.extent);
|
|
38
|
+
return {
|
|
39
|
+
class: editorClassNames.image,
|
|
40
|
+
src: toImageSrc(attrs.src) ?? void 0,
|
|
41
|
+
alt: text(attrs.alt) ?? "",
|
|
42
|
+
width: extent ? `${Math.round(emuToPx(extent.cx))}` : void 0,
|
|
43
|
+
height: extent ? `${Math.round(emuToPx(extent.cy))}` : void 0
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function imageNodeSpec(attrs) {
|
|
32
47
|
return [
|
|
33
48
|
"img",
|
|
34
49
|
{
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
alt: text(attrs.alt) ?? "",
|
|
38
|
-
width: extent ? `${Math.round(emuToPx(extent.cx))}` : void 0,
|
|
39
|
-
height: extent ? `${Math.round(emuToPx(extent.cy))}` : void 0,
|
|
40
|
-
"data-extent": formatJson(extent),
|
|
50
|
+
...imageAttrs(attrs),
|
|
51
|
+
"data-extent": formatJson(toImageExtent(attrs.extent)),
|
|
41
52
|
"data-xml": text(attrs.xml)
|
|
42
53
|
}
|
|
43
54
|
];
|
|
44
55
|
}
|
|
45
56
|
export {
|
|
57
|
+
imageAttrs,
|
|
46
58
|
imageNodeSpec,
|
|
59
|
+
runAttrs,
|
|
47
60
|
runMarkSpec
|
|
48
61
|
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The inline wrappers a stretch of content stands inside, as the schema records them.
|
|
3
|
+
*
|
|
4
|
+
* A wrapper is an element that holds inline content and goes back out around the same content:
|
|
5
|
+
* a content control (`w:sdt`), a hyperlink (`w:hyperlink`). Each is a mark of its own, so each
|
|
6
|
+
* keeps the `inclusive` rule and the attrs its kind needs, and all of them share the `wrapper`
|
|
7
|
+
* group, so a node's `marks:` whitelist names the group rather than every kind in it and a new
|
|
8
|
+
* kind arrives as one mark spec.
|
|
9
|
+
*
|
|
10
|
+
* The nesting is recorded on the marks themselves: `depth` counts from the outermost wrapper the
|
|
11
|
+
* file wrote inwards. The order of a mark set cannot carry it - `Mark.addToSet` keeps insertion
|
|
12
|
+
* order only among marks of different types, `parseDOM` rebuilds a set from the page, and two
|
|
13
|
+
* wrappers of one kind may stand one inside the other - so `depth` is the source of truth and
|
|
14
|
+
* `wrapperMarks` is the one place that reads the order off it.
|
|
15
|
+
*
|
|
16
|
+
* What each kind reads out of the file and writes back into it is `docx/wrappers`.
|
|
17
|
+
*/
|
|
18
|
+
import type { Mark, MarkType, Node as PMNode } from "prosemirror-model";
|
|
19
|
+
/** The mark group every inline wrapper belongs to */
|
|
20
|
+
export declare const WRAPPER_GROUP = "wrapper";
|
|
21
|
+
/** What every wrapper mark spec carries besides the attrs of its own kind */
|
|
22
|
+
export declare const WRAPPER_ATTRS: {
|
|
23
|
+
/** Outer-to-inner order as read from the file; equal depths fall back to declaration rank */
|
|
24
|
+
readonly depth: {
|
|
25
|
+
readonly default: 0;
|
|
26
|
+
};
|
|
27
|
+
/** Which occurrence this is in the opened document, so two wrappers written alike stay apart */
|
|
28
|
+
readonly key: {
|
|
29
|
+
readonly default: 0;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
/** Whether marks of this type wrap the content they cover (`WRAPPER_GROUP`) */
|
|
33
|
+
export declare function isWrapperType(type: MarkType): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The wrappers this node stands inside, outermost first.
|
|
36
|
+
*
|
|
37
|
+
* Two wrappers written at the same depth - a control laid over a link during editing, say - fall
|
|
38
|
+
* back to the order the schema declares them in, which is the nesting the marks meant before
|
|
39
|
+
* `depth` existed.
|
|
40
|
+
*/
|
|
41
|
+
export declare function wrapperMarks(node: PMNode): readonly Mark[];
|
|
42
|
+
/** Every wrapper of this kind the node stands inside, outermost first */
|
|
43
|
+
export declare function wrappersOf(node: PMNode, kind: MarkType): readonly Mark[];
|
|
44
|
+
/** The outermost wrapper of this kind the node stands inside. null where it stands in none */
|
|
45
|
+
export declare function wrapperOf(node: PMNode, kind: MarkType): Mark | null;
|
|
46
|
+
/**
|
|
47
|
+
* The depth of the innermost of these wrappers, and -1 where there is none, so that
|
|
48
|
+
* `innermostDepth(marks) + 1` is the depth a wrapper laid inside every one of them takes.
|
|
49
|
+
*/
|
|
50
|
+
export declare function innermostDepth(marks: readonly Mark[]): number;
|
|
51
|
+
/**
|
|
52
|
+
* The wrappers all of these nodes stand inside alike, outermost first: the longest run of
|
|
53
|
+
* wrappers, from the outside in, that every one of them wears the very same one of.
|
|
54
|
+
*
|
|
55
|
+
* A wrapper laid across the whole stretch goes inside these and outside everything else, which is
|
|
56
|
+
* what keeps it one element in the file rather than one per stretch the nesting differs over.
|
|
57
|
+
*/
|
|
58
|
+
export declare function sharedWrappers(nodes: readonly PMNode[]): readonly Mark[];
|