@portone/docx-editor 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/CONTRIBUTING.md +2 -0
- 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.js +2 -1
- 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/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/package.json +5 -4
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The inline wrappers this editor takes apart and puts back together, one entry each.
|
|
3
|
+
*
|
|
4
|
+
* A wrapper is an element holding inline content that goes back out around the same content: a
|
|
5
|
+
* content control (`w:sdt`, §17.5.2.17), a hyperlink (`w:hyperlink`, §17.16.22). `EG_PContent`
|
|
6
|
+
* lets them nest in any order and any depth, so import reads the nesting as the file wrote it and
|
|
7
|
+
* export rebuilds it from the marks (`schema/wrappers` holds the order, `docx/importParagraph`
|
|
8
|
+
* reads, `docx/serializeParagraph` writes).
|
|
9
|
+
*
|
|
10
|
+
* A kind knows three things and nothing about either walk: what a mark reads out of one element,
|
|
11
|
+
* what opens it again, and what closes it. Tracked changes (`w:ins`, `w:del`) and simple fields
|
|
12
|
+
* (`w:fldSimple`) are wrappers of the same shape. Registering one is an entry here and a mark spec
|
|
13
|
+
* in `schema/docxSchema` carrying `WRAPPER_ATTRS`; its attrs also want a row in
|
|
14
|
+
* `schema/attrRoles`, and two test lists name the kinds outright (`WRAPPERS_READS` in
|
|
15
|
+
* `schema/attrClasses.test.ts`, the kind list in `schema/wrappers.test.ts`). Neither paragraph walk
|
|
16
|
+
* and neither policy table has to be touched.
|
|
17
|
+
*
|
|
18
|
+
* The opening tag is the verbatim string the file wrote wherever there is one, so a wrapper nobody
|
|
19
|
+
* edited goes back out byte for byte; only the closing tag is written from scratch.
|
|
20
|
+
*/
|
|
21
|
+
import type { Mark } from "prosemirror-model";
|
|
22
|
+
import type { ExportRefs } from "./exportRefs";
|
|
23
|
+
import type { ImportSources } from "./importParagraph";
|
|
24
|
+
/** A wrapper read off the file: the mark its content wears, and the element that content stands in */
|
|
25
|
+
export interface WrapperReading {
|
|
26
|
+
mark: Mark;
|
|
27
|
+
content: Element;
|
|
28
|
+
}
|
|
29
|
+
/** One kind of inline wrapper, as import reads it and export writes it */
|
|
30
|
+
export interface WrapperKind {
|
|
31
|
+
/** The mark name in the schema */
|
|
32
|
+
readonly mark: string;
|
|
33
|
+
/** The local name of the element it comes from */
|
|
34
|
+
readonly element: string;
|
|
35
|
+
/**
|
|
36
|
+
* The mark this element goes on and the content to read on inside it. null for a shape we do
|
|
37
|
+
* not write back ourselves, which leaves the element preserved whole where it stood.
|
|
38
|
+
*/
|
|
39
|
+
read(el: Element, depth: number, sources: ImportSources): WrapperReading | null;
|
|
40
|
+
/** The opening XML this mark goes back out as */
|
|
41
|
+
open(mark: Mark, refs: ExportRefs): string;
|
|
42
|
+
/** What closes it again */
|
|
43
|
+
close(mark: Mark): string;
|
|
44
|
+
/**
|
|
45
|
+
* The same wrapper under a name of its own, for a second stretch claiming one name
|
|
46
|
+
* (`docx/identities`). Absent for a kind that names nothing a document must keep unique.
|
|
47
|
+
*/
|
|
48
|
+
copy?(mark: Mark): Mark;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Every wrapper the editor models, in the order the schema declares their marks: what stands
|
|
52
|
+
* first here stands outside the others where two of them are written at one depth.
|
|
53
|
+
*/
|
|
54
|
+
export declare const WRAPPER_KINDS: readonly WrapperKind[];
|
|
55
|
+
/** The kind this element is one of, undefined for an element no kind reads */
|
|
56
|
+
export declare function wrapperKindFor(el: Element): WrapperKind | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* Whether a wrapper of this kind may be read inside the wrappers already around it.
|
|
59
|
+
*
|
|
60
|
+
* A kind whose mark excludes its own type records one of itself at a time, so the inner of two is
|
|
61
|
+
* left whole where it stood rather than pushing the outer one off the content it wraps: that is a
|
|
62
|
+
* `w:hyperlink` inside a `w:hyperlink`, which OOXML admits and Word does not write. A kind that
|
|
63
|
+
* excludes nothing nests as deeply as the file does (`w:sdt`, §17.5.2.17).
|
|
64
|
+
*/
|
|
65
|
+
export declare function wrapperFits(kind: WrapperKind, wrappers: readonly Mark[]): boolean;
|
|
66
|
+
/** The kind this mark stands for. A wrapper mark nobody registered has no way back out */
|
|
67
|
+
export declare function wrapperKindOf(mark: Mark): WrapperKind;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// src/docx/wrappers.ts
|
|
2
|
+
import { DocxExportError } from "../ooxml/errors.js";
|
|
3
|
+
import { docxSchema } from "../schema/index.js";
|
|
4
|
+
import { readHyperlinkWrapper, relIdIn, withRelId } from "./hyperlink.js";
|
|
5
|
+
import { copiedControlPrefix, newControlId, readSdtWrapper } from "./sdt.js";
|
|
6
|
+
var counts = /* @__PURE__ */ new WeakMap();
|
|
7
|
+
function nextKey(kind, el) {
|
|
8
|
+
const perDocument = counts.get(el.ownerDocument) ?? /* @__PURE__ */ new Map();
|
|
9
|
+
counts.set(el.ownerDocument, perDocument);
|
|
10
|
+
const key = perDocument.get(kind.mark) ?? 0;
|
|
11
|
+
perDocument.set(kind.mark, key + 1);
|
|
12
|
+
return key;
|
|
13
|
+
}
|
|
14
|
+
var SDT = {
|
|
15
|
+
mark: "sdt",
|
|
16
|
+
element: "sdt",
|
|
17
|
+
read(el, depth) {
|
|
18
|
+
const wrapper = readSdtWrapper(el);
|
|
19
|
+
if (!wrapper) return null;
|
|
20
|
+
return {
|
|
21
|
+
mark: docxSchema.marks.sdt.create({
|
|
22
|
+
sdtPrefix: wrapper.prefix,
|
|
23
|
+
depth,
|
|
24
|
+
key: nextKey(SDT, el),
|
|
25
|
+
contentsLocked: wrapper.contentsLocked,
|
|
26
|
+
deletionLocked: wrapper.deletionLocked
|
|
27
|
+
}),
|
|
28
|
+
content: wrapper.content
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
open(mark) {
|
|
32
|
+
const prefix = mark.attrs.sdtPrefix;
|
|
33
|
+
if (typeof prefix !== "string") {
|
|
34
|
+
throw new DocxExportError(
|
|
35
|
+
"lost-original",
|
|
36
|
+
"a content control has lost the opening XML it goes back out as"
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return `${prefix}<w:sdtContent>`;
|
|
40
|
+
},
|
|
41
|
+
close: () => "</w:sdtContent></w:sdt>",
|
|
42
|
+
copy(mark) {
|
|
43
|
+
const prefix = mark.attrs.sdtPrefix;
|
|
44
|
+
if (typeof prefix !== "string") return mark;
|
|
45
|
+
return mark.type.create({
|
|
46
|
+
...mark.attrs,
|
|
47
|
+
sdtPrefix: copiedControlPrefix(prefix, newControlId())
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var LINK = {
|
|
52
|
+
mark: "link",
|
|
53
|
+
element: "hyperlink",
|
|
54
|
+
read(el, depth, sources) {
|
|
55
|
+
const wrapper = readHyperlinkWrapper(el);
|
|
56
|
+
if (!wrapper) return null;
|
|
57
|
+
return {
|
|
58
|
+
mark: docxSchema.marks.link.create({
|
|
59
|
+
linkPrefix: wrapper.prefix,
|
|
60
|
+
href: wrapper.relId === null ? null : sources.links.get(wrapper.relId) ?? null,
|
|
61
|
+
depth,
|
|
62
|
+
key: nextKey(LINK, el)
|
|
63
|
+
}),
|
|
64
|
+
content: el
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
open(mark, refs) {
|
|
68
|
+
const prefix = mark.attrs.linkPrefix;
|
|
69
|
+
const original = typeof prefix === "string" ? prefix : null;
|
|
70
|
+
const href = mark.attrs.href;
|
|
71
|
+
if (typeof href !== "string") {
|
|
72
|
+
if (original !== null) return original;
|
|
73
|
+
throw new DocxExportError(
|
|
74
|
+
"lost-original",
|
|
75
|
+
"a hyperlink carries neither an address nor the opening XML it goes back out as"
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
const relId = refs.links.relIdOf(
|
|
79
|
+
href,
|
|
80
|
+
original === null ? null : relIdIn(original)
|
|
81
|
+
);
|
|
82
|
+
if (relId === void 0) {
|
|
83
|
+
if (original !== null) return original;
|
|
84
|
+
throw new DocxExportError(
|
|
85
|
+
"unsupported-content",
|
|
86
|
+
"an inserted hyperlink has no relationship to point at; export it through exportDocx"
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return withRelId(original ?? "<w:hyperlink>", relId);
|
|
90
|
+
},
|
|
91
|
+
close: () => "</w:hyperlink>"
|
|
92
|
+
};
|
|
93
|
+
var WRAPPER_KINDS = [SDT, LINK];
|
|
94
|
+
function wrapperKindFor(el) {
|
|
95
|
+
return WRAPPER_KINDS.find((kind) => kind.element === el.localName);
|
|
96
|
+
}
|
|
97
|
+
function wrapperFits(kind, wrappers) {
|
|
98
|
+
const type = docxSchema.marks[kind.mark];
|
|
99
|
+
if (!type.excludes(type)) return true;
|
|
100
|
+
return !wrappers.some((mark) => mark.type === type);
|
|
101
|
+
}
|
|
102
|
+
function wrapperKindOf(mark) {
|
|
103
|
+
const kind = WRAPPER_KINDS.find((entry) => entry.mark === mark.type.name);
|
|
104
|
+
if (!kind) {
|
|
105
|
+
throw new DocxExportError(
|
|
106
|
+
"unsupported-content",
|
|
107
|
+
`inline wrapper we cannot serialize: ${mark.type.name}`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return kind;
|
|
111
|
+
}
|
|
112
|
+
export {
|
|
113
|
+
WRAPPER_KINDS,
|
|
114
|
+
wrapperFits,
|
|
115
|
+
wrapperKindFor,
|
|
116
|
+
wrapperKindOf
|
|
117
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The elements a reading takes as blocks of their own.
|
|
3
|
+
*
|
|
4
|
+
* Everything else in a piece of clipboard HTML is read by the shape of the markup alone: an
|
|
5
|
+
* element either names a block or it is text in the paragraph around it. A block reader is how an
|
|
6
|
+
* element that means more than its shape says - a table, a Word paragraph that is really a list
|
|
7
|
+
* item - is read as what it means, without the reading itself learning about the applications
|
|
8
|
+
* one by one.
|
|
9
|
+
*/
|
|
10
|
+
import type { Node as PMNode } from "prosemirror-model";
|
|
11
|
+
import { type ListKind } from "../../numbering/listTemplate";
|
|
12
|
+
import { type InlineContext } from "./inlineFormatting";
|
|
13
|
+
import type { HtmlReadContext } from "./readContext";
|
|
14
|
+
/** Where one paragraph being read stands in a list */
|
|
15
|
+
export interface ListPlacement {
|
|
16
|
+
kind: ListKind;
|
|
17
|
+
/** The number the list wears here, or null where this document may start no list */
|
|
18
|
+
numId: number | null;
|
|
19
|
+
/** How deep the item sits, counted from zero */
|
|
20
|
+
level: number;
|
|
21
|
+
}
|
|
22
|
+
/** What a block reader may ask of the reading it is part of */
|
|
23
|
+
export interface HtmlBlockHost {
|
|
24
|
+
/** What the markup is being read against, including the application that wrote it */
|
|
25
|
+
readonly context: HtmlReadContext;
|
|
26
|
+
/** Whether the reading already stands inside a cell of a table it is reading */
|
|
27
|
+
readonly inTable: boolean;
|
|
28
|
+
/** The blocks the content of one cell reads as. None where the cell holds nothing */
|
|
29
|
+
readCell(cell: ParentNode, inline: InlineContext): readonly PMNode[];
|
|
30
|
+
/** The inline nodes these nodes read as */
|
|
31
|
+
readInline(nodes: Iterable<Node>, inline: InlineContext): readonly PMNode[];
|
|
32
|
+
/** One paragraph of the reading, as an item of the list it belongs to when it is one */
|
|
33
|
+
paragraph(content: readonly PMNode[], list?: ListPlacement | null): PMNode;
|
|
34
|
+
/**
|
|
35
|
+
* The number the pasted list this key names takes here. The same key answers the same number,
|
|
36
|
+
* so the paragraphs of one list join it rather than each starting a list of their own.
|
|
37
|
+
*/
|
|
38
|
+
listNumber(key: string, kind: ListKind): number | null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* One kind of element read as blocks of its own rather than as text where it stands.
|
|
42
|
+
*
|
|
43
|
+
* A reader answers null for an element it has nothing to say about, so that deciding whether an
|
|
44
|
+
* element is its own and reading it are one pass rather than two.
|
|
45
|
+
*/
|
|
46
|
+
export interface HtmlBlockReader {
|
|
47
|
+
read(element: HTMLElement, inline: InlineContext, host: HtmlBlockHost): readonly PMNode[] | null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* An HTML table, read as a table of this document.
|
|
51
|
+
*
|
|
52
|
+
* The cells hold what their own markup reads as, one paragraph per block they hold, and the table
|
|
53
|
+
* is as wide as a table inserted here would be: a pasted table is drawn and written out exactly
|
|
54
|
+
* like one the toolbar put there, since nothing of the width a foreign document measured its own
|
|
55
|
+
* paper against would mean the same here.
|
|
56
|
+
*
|
|
57
|
+
* A table inside a cell is not read as a table, since the model only makes the outer one editable,
|
|
58
|
+
* and neither is one larger than the model holds. Both are read as their rows instead, so that a
|
|
59
|
+
* cell of the inner table still stands apart from the next one. A caption stands as the paragraph
|
|
60
|
+
* above whichever of the two the table is read as.
|
|
61
|
+
*/
|
|
62
|
+
export declare const tableBlockReader: HtmlBlockReader;
|
|
63
|
+
/**
|
|
64
|
+
* A Word paragraph that is an item of a list, read as an item of a list here.
|
|
65
|
+
*
|
|
66
|
+
* Word puts no `<ul>` or `<ol>` on the clipboard: every item is a paragraph saying which list it
|
|
67
|
+
* belongs to, so the paragraphs of one list are joined by that name rather than by standing inside
|
|
68
|
+
* one element. Whether the list counts its items is only visible in the marker Word drew, which
|
|
69
|
+
* is taken out of the paragraph wherever it is: marked as a marker, or standing at the front of
|
|
70
|
+
* the content as anything else would.
|
|
71
|
+
*/
|
|
72
|
+
export declare const wordListReader: HtmlBlockReader;
|
|
73
|
+
/** The readers every reading consults, in the order they are tried */
|
|
74
|
+
export declare const DEFAULT_BLOCK_READERS: readonly HtmlBlockReader[];
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// src/editor/clipboard/blockReaders.ts
|
|
2
|
+
import {
|
|
3
|
+
createTableNodeFrom,
|
|
4
|
+
isTableSide,
|
|
5
|
+
MAX_TABLE_SIDE
|
|
6
|
+
} from "../../docx/tableTemplate.js";
|
|
7
|
+
import { MAX_ILVL } from "../../numbering/listTemplate.js";
|
|
8
|
+
import { docxSchema } from "../../schema/index.js";
|
|
9
|
+
import { contextFor } from "./inlineFormatting.js";
|
|
10
|
+
var CELL_TAGS = /* @__PURE__ */ new Set(["TD", "TH"]);
|
|
11
|
+
var ROW_GROUP_TAGS = /* @__PURE__ */ new Set(["THEAD", "TBODY", "TFOOT"]);
|
|
12
|
+
function rowsOf(table) {
|
|
13
|
+
return [...table.children].flatMap((child) => {
|
|
14
|
+
if (!(child instanceof HTMLElement)) return [];
|
|
15
|
+
if (child.tagName === "TR") return [child];
|
|
16
|
+
if (!ROW_GROUP_TAGS.has(child.tagName)) return [];
|
|
17
|
+
return [...child.children].flatMap(
|
|
18
|
+
(row) => row instanceof HTMLElement && row.tagName === "TR" ? [row] : []
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function cellsOf(row) {
|
|
23
|
+
return [...row.children].flatMap(
|
|
24
|
+
(cell) => cell instanceof HTMLElement && CELL_TAGS.has(cell.tagName) ? [cell] : []
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
function spanOf(element, name, limit) {
|
|
28
|
+
const value = Number.parseInt(element.getAttribute(name) ?? "", 10);
|
|
29
|
+
return Number.isFinite(value) && value > 1 ? Math.min(value, limit) : 1;
|
|
30
|
+
}
|
|
31
|
+
function gridOf(table) {
|
|
32
|
+
const rows = rowsOf(table).filter((row) => cellsOf(row).length > 0);
|
|
33
|
+
if (!isTableSide(rows.length)) return null;
|
|
34
|
+
const covered = rows.map(() => /* @__PURE__ */ new Set());
|
|
35
|
+
const cells = rows.map(() => []);
|
|
36
|
+
let cols = 0;
|
|
37
|
+
rows.forEach((row, top) => {
|
|
38
|
+
let left = 0;
|
|
39
|
+
for (const element of cellsOf(row)) {
|
|
40
|
+
while (covered[top]?.has(left)) left += 1;
|
|
41
|
+
const colspan = spanOf(element, "colspan", MAX_TABLE_SIDE);
|
|
42
|
+
const rowspan = Math.min(
|
|
43
|
+
spanOf(element, "rowspan", MAX_TABLE_SIDE),
|
|
44
|
+
rows.length - top
|
|
45
|
+
);
|
|
46
|
+
for (let covering = top; covering < top + rowspan; covering += 1) {
|
|
47
|
+
for (let column = left; column < left + colspan; column += 1) {
|
|
48
|
+
covered[covering]?.add(column);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
cells[top]?.push({
|
|
52
|
+
element,
|
|
53
|
+
rect: { top, bottom: top + rowspan, left, right: left + colspan }
|
|
54
|
+
});
|
|
55
|
+
left += colspan;
|
|
56
|
+
cols = Math.max(cols, left);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
return isTableSide(cols) ? cells : null;
|
|
60
|
+
}
|
|
61
|
+
function captionOf(table) {
|
|
62
|
+
for (const child of table.children) {
|
|
63
|
+
if (child instanceof HTMLElement && child.tagName === "CAPTION") {
|
|
64
|
+
return child;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
function rowParagraphs(table, inline, host) {
|
|
70
|
+
return rowsOf(table).filter((row) => cellsOf(row).length > 0).map(
|
|
71
|
+
(row) => host.paragraph(
|
|
72
|
+
cellsOf(row).flatMap((cell, index) => [
|
|
73
|
+
...index === 0 ? [] : [docxSchema.text(" ", [docxSchema.marks.tab.create()])],
|
|
74
|
+
...host.readInline(cell.childNodes, contextFor(inline, cell))
|
|
75
|
+
])
|
|
76
|
+
)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
var tableBlockReader = {
|
|
80
|
+
read: (element, inline, host) => {
|
|
81
|
+
if (element.tagName !== "TABLE") return null;
|
|
82
|
+
const inside = contextFor(inline, element);
|
|
83
|
+
const caption = captionOf(element);
|
|
84
|
+
const above = caption === null ? [] : [
|
|
85
|
+
host.paragraph(
|
|
86
|
+
host.readInline(caption.childNodes, contextFor(inside, caption))
|
|
87
|
+
)
|
|
88
|
+
];
|
|
89
|
+
const grid = host.inTable ? null : gridOf(element);
|
|
90
|
+
if (grid === null) {
|
|
91
|
+
return [...above, ...rowParagraphs(element, inside, host)];
|
|
92
|
+
}
|
|
93
|
+
const cells = grid.map(
|
|
94
|
+
(row) => row.map(({ element: cell, rect }) => ({
|
|
95
|
+
rect,
|
|
96
|
+
content: host.readCell(cell, contextFor(inside, cell))
|
|
97
|
+
}))
|
|
98
|
+
);
|
|
99
|
+
return [...above, createTableNodeFrom(cells, host.context.geometry)];
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
var WORD_LIST = /mso-list:\s*l(?<list>\d+)\s+level(?<level>\d+)\s+lfo(?<applied>\d+)/i;
|
|
103
|
+
var IGNORED_MARKER = /mso-list:\s*Ignore/i;
|
|
104
|
+
var MARKER_START = /^\[if\s+!supportLists\]/i;
|
|
105
|
+
var MARKER_END = /^\[endif\]/i;
|
|
106
|
+
var COUNTED_MARKER = /^\s*(?:\d+|[A-Za-z]+)\s*[.)]\s*/;
|
|
107
|
+
function wordListItemOf(element) {
|
|
108
|
+
const found = WORD_LIST.exec(element.getAttribute("style") ?? "")?.groups;
|
|
109
|
+
if (found === void 0) return null;
|
|
110
|
+
const level = Number.parseInt(found.level ?? "", 10);
|
|
111
|
+
return {
|
|
112
|
+
key: `${found.list}/${found.applied}`,
|
|
113
|
+
// Word counts its levels from one, and no document has more levels than the model holds
|
|
114
|
+
level: Number.isFinite(level) ? Math.min(Math.max(level, 1), MAX_ILVL + 1) - 1 : 0
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function withoutMarker(element) {
|
|
118
|
+
const content = [];
|
|
119
|
+
let marker = "";
|
|
120
|
+
let inMarker = false;
|
|
121
|
+
for (const child of element.childNodes) {
|
|
122
|
+
if (child.nodeType === child.COMMENT_NODE) {
|
|
123
|
+
const data = child.nodeValue ?? "";
|
|
124
|
+
if (MARKER_START.test(data)) inMarker = true;
|
|
125
|
+
else if (MARKER_END.test(data)) inMarker = false;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const ignored = child instanceof HTMLElement && IGNORED_MARKER.test(child.getAttribute("style") ?? "");
|
|
129
|
+
if (inMarker || ignored) marker += child.textContent ?? "";
|
|
130
|
+
else content.push(child);
|
|
131
|
+
}
|
|
132
|
+
return { marker, content };
|
|
133
|
+
}
|
|
134
|
+
var SYMBOL_FONTS = /symbol|wingdings|webdings/i;
|
|
135
|
+
function leadingNode(element) {
|
|
136
|
+
for (const child of element.childNodes) {
|
|
137
|
+
const blank = child.nodeType === child.TEXT_NODE && (child.nodeValue ?? "").trim() === "";
|
|
138
|
+
if (!blank) return child;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
function firstText(element) {
|
|
143
|
+
for (const child of element.childNodes) {
|
|
144
|
+
if (child.nodeType === child.TEXT_NODE) {
|
|
145
|
+
if ((child.nodeValue ?? "").trim() !== "") return child;
|
|
146
|
+
} else if (child instanceof HTMLElement) {
|
|
147
|
+
const found = firstText(child);
|
|
148
|
+
if (found !== null) return found;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
function leadingMarker(element) {
|
|
154
|
+
const copy = element.cloneNode(true);
|
|
155
|
+
if (!(copy instanceof HTMLElement)) return null;
|
|
156
|
+
const lead = leadingNode(copy);
|
|
157
|
+
if (lead instanceof HTMLElement && SYMBOL_FONTS.test(lead.getAttribute("style") ?? "")) {
|
|
158
|
+
const marker = lead.textContent ?? "";
|
|
159
|
+
lead.remove();
|
|
160
|
+
return { marker, content: [...copy.childNodes] };
|
|
161
|
+
}
|
|
162
|
+
const text = firstText(copy);
|
|
163
|
+
const counted = COUNTED_MARKER.exec(text?.nodeValue ?? "");
|
|
164
|
+
if (text === null || counted === null) return null;
|
|
165
|
+
text.nodeValue = (text.nodeValue ?? "").slice(counted[0].length);
|
|
166
|
+
return { marker: counted[0], content: [...copy.childNodes] };
|
|
167
|
+
}
|
|
168
|
+
var wordListReader = {
|
|
169
|
+
read: (element, inline, host) => {
|
|
170
|
+
if (host.context.source !== "word") return null;
|
|
171
|
+
const item = wordListItemOf(element);
|
|
172
|
+
if (item === null) return null;
|
|
173
|
+
const marked = withoutMarker(element);
|
|
174
|
+
const { marker, content } = marked.marker === "" ? leadingMarker(element) ?? marked : marked;
|
|
175
|
+
const kind = COUNTED_MARKER.test(marker) ? "numbered" : "bullet";
|
|
176
|
+
return [
|
|
177
|
+
host.paragraph(host.readInline(content, contextFor(inline, element)), {
|
|
178
|
+
kind,
|
|
179
|
+
numId: host.listNumber(item.key, kind),
|
|
180
|
+
level: item.level
|
|
181
|
+
})
|
|
182
|
+
];
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
var DEFAULT_BLOCK_READERS = [
|
|
186
|
+
tableBlockReader,
|
|
187
|
+
wordListReader
|
|
188
|
+
];
|
|
189
|
+
export {
|
|
190
|
+
DEFAULT_BLOCK_READERS,
|
|
191
|
+
tableBlockReader,
|
|
192
|
+
wordListReader
|
|
193
|
+
};
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import { Slice } from "prosemirror-model";
|
|
2
2
|
import type { Transaction } from "prosemirror-state";
|
|
3
3
|
import { type NewLists } from "../../numbering/listRegistry";
|
|
4
|
+
import { type HtmlBlockReader } from "./blockReaders";
|
|
4
5
|
import type { ListKinds } from "./internalChannel";
|
|
5
6
|
import type { HtmlReadContext } from "./readContext";
|
|
6
|
-
/**
|
|
7
|
-
* The paragraph style a copy carries, which is the one thing a paste needs that the drawing does
|
|
8
|
-
* not already say. It is the style's id alone, where the editor draws the whole `w:pPr`.
|
|
9
|
-
*/
|
|
10
|
-
export declare const COPIED_STYLE_ATTRIBUTE = "data-style";
|
|
11
7
|
/** What a paste puts in: the content, and the definitions of the lists it started */
|
|
12
8
|
export interface PastedContent {
|
|
13
9
|
slice: Slice;
|
|
@@ -20,7 +16,7 @@ export interface PastedContent {
|
|
|
20
16
|
listKinds?: ListKinds;
|
|
21
17
|
}
|
|
22
18
|
/** The content one piece of already parsed markup reads as, or null when it reads as nothing */
|
|
23
|
-
export declare function readHtml(root: ParentNode, context: HtmlReadContext): PastedContent | null;
|
|
19
|
+
export declare function readHtml(root: ParentNode, context: HtmlReadContext, blockReaders?: readonly HtmlBlockReader[]): PastedContent | null;
|
|
24
20
|
/** The same reading, for a caller holding the markup as a string rather than as elements */
|
|
25
21
|
export declare function readHtmlSlice(context: HtmlReadContext, source: string): PastedContent | null;
|
|
26
22
|
/**
|