@lotics/docx 0.1.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/package.json +40 -0
- package/src/fixtures/.gitkeep +0 -0
- package/src/fixtures/lotics_generated_contract.docx +0 -0
- package/src/fonts/bundled.ts +123 -0
- package/src/fonts/registry.test.ts +233 -0
- package/src/fonts/registry.ts +219 -0
- package/src/fonts/types.ts +83 -0
- package/src/index.ts +16 -0
- package/src/layout/engine.test.ts +430 -0
- package/src/layout/engine.ts +566 -0
- package/src/layout/page_geometry.ts +43 -0
- package/src/layout/types.ts +159 -0
- package/src/load.test.ts +144 -0
- package/src/load.ts +142 -0
- package/src/model/default_numbering.ts +101 -0
- package/src/model/default_styles.ts +201 -0
- package/src/model/numbering_table.ts +52 -0
- package/src/model/properties.ts +328 -0
- package/src/model/sections.ts +94 -0
- package/src/model/style_resolution.test.ts +219 -0
- package/src/model/style_resolution.ts +113 -0
- package/src/model/style_table.ts +22 -0
- package/src/model/theme.ts +156 -0
- package/src/model/types.ts +55 -0
- package/src/parse/drawing.ts +157 -0
- package/src/parse/font_table.ts +132 -0
- package/src/parse/footnotes.ts +60 -0
- package/src/parse/header_footer.test.ts +264 -0
- package/src/parse/header_footer.ts +66 -0
- package/src/parse/numbering.ts +187 -0
- package/src/parse/parser.ts +184 -0
- package/src/parse/relationships.ts +83 -0
- package/src/parse/sections.test.ts +192 -0
- package/src/parse/sections.ts +182 -0
- package/src/parse/styles.ts +149 -0
- package/src/parse/theme.test.ts +86 -0
- package/src/parse/theme.ts +112 -0
- package/src/pm/bubble_menu.ts +117 -0
- package/src/pm/commands.test.ts +185 -0
- package/src/pm/commands.ts +697 -0
- package/src/pm/commands_insert.test.ts +183 -0
- package/src/pm/docx_to_pm.test.ts +330 -0
- package/src/pm/docx_to_pm.ts +643 -0
- package/src/pm/drag_handle.ts +166 -0
- package/src/pm/format_painter.test.ts +91 -0
- package/src/pm/format_painter.ts +109 -0
- package/src/pm/header_footer_doc.ts +24 -0
- package/src/pm/hyperlinks.test.ts +234 -0
- package/src/pm/image_registry.test.ts +81 -0
- package/src/pm/image_registry.ts +100 -0
- package/src/pm/images.test.ts +257 -0
- package/src/pm/link_popover.ts +159 -0
- package/src/pm/mark_commands.ts +60 -0
- package/src/pm/marks.ts +169 -0
- package/src/pm/nodes.ts +258 -0
- package/src/pm/numbering.test.ts +210 -0
- package/src/pm/numbering_plugin.test.ts +71 -0
- package/src/pm/numbering_plugin.ts +96 -0
- package/src/pm/outline.ts +41 -0
- package/src/pm/page_break.test.ts +80 -0
- package/src/pm/page_layout.test.ts +87 -0
- package/src/pm/pagination_plugin.test.ts +155 -0
- package/src/pm/pagination_plugin.ts +590 -0
- package/src/pm/phase5.test.ts +271 -0
- package/src/pm/phase6.test.ts +215 -0
- package/src/pm/placeholder_plugin.ts +24 -0
- package/src/pm/plugins.ts +91 -0
- package/src/pm/pm_to_docx.ts +0 -0
- package/src/pm/roundtrip.test.ts +332 -0
- package/src/pm/schema.test.ts +188 -0
- package/src/pm/schema.ts +79 -0
- package/src/pm/search.ts +46 -0
- package/src/pm/table_attrs.ts +48 -0
- package/src/pm/table_borders.test.ts +117 -0
- package/src/pm/table_borders.ts +130 -0
- package/src/pm/table_convert.test.ts +221 -0
- package/src/pm/table_convert.ts +541 -0
- package/src/pm/table_decorations.ts +132 -0
- package/src/pm/table_handles.ts +163 -0
- package/src/pm/template_marker.ts +47 -0
- package/src/pm/template_plugin.ts +65 -0
- package/src/pm/templates.test.ts +162 -0
- package/src/render/clipboard.test.ts +115 -0
- package/src/render/clipboard.ts +200 -0
- package/src/render/editable_view.test.ts +173 -0
- package/src/render/footnotes_view.ts +94 -0
- package/src/render/header_footer_view.ts +95 -0
- package/src/render/link_mark_view.ts +26 -0
- package/src/render/media_resolver.ts +61 -0
- package/src/render/node_views.ts +296 -0
- package/src/render/numbering_counter.ts +149 -0
- package/src/render/page_chrome.test.ts +262 -0
- package/src/render/page_chrome.ts +343 -0
- package/src/render/page_styles.ts +234 -0
- package/src/render/paragraph_view.test.ts +162 -0
- package/src/render/paragraph_view.ts +141 -0
- package/src/render/ruler.ts +110 -0
- package/src/render/style_registry.ts +33 -0
- package/src/render/table_dom.test.ts +171 -0
- package/src/render/table_dom.ts +288 -0
- package/src/render/units.ts +18 -0
- package/src/render/view.test.ts +165 -0
- package/src/render/view.ts +607 -0
- package/src/roundtrip.test.ts +179 -0
- package/src/serialize/default_parts.ts +128 -0
- package/src/serialize/header_footer_pm.ts +82 -0
- package/src/serialize/serializer.ts +114 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getAttr,
|
|
3
|
+
getChildren,
|
|
4
|
+
getTagName,
|
|
5
|
+
getTextContent,
|
|
6
|
+
type XmlElement,
|
|
7
|
+
} from "@lotics/ooxml/xml";
|
|
8
|
+
import {
|
|
9
|
+
extractParagraphProperties,
|
|
10
|
+
extractRunProperties,
|
|
11
|
+
} from "../model/properties";
|
|
12
|
+
import type { ParagraphProperties, RunProperties } from "../model/properties";
|
|
13
|
+
import { halfPointsToPx } from "./units";
|
|
14
|
+
|
|
15
|
+
function alignmentCss(align: ParagraphProperties["alignment"]): string | null {
|
|
16
|
+
if (align === null) return null;
|
|
17
|
+
if (align === "both") return "justify";
|
|
18
|
+
if (align === "start") return "left";
|
|
19
|
+
if (align === "end") return "right";
|
|
20
|
+
return align;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function buildParagraphInlineStyle(props: ParagraphProperties): string {
|
|
24
|
+
const parts: string[] = [];
|
|
25
|
+
const align = alignmentCss(props.alignment);
|
|
26
|
+
if (align) parts.push(`text-align: ${align}`);
|
|
27
|
+
return parts.join("; ");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildRunInlineStyle(props: RunProperties): string {
|
|
31
|
+
const parts: string[] = [];
|
|
32
|
+
const family = props.fontAscii ?? props.fontHAnsi ?? props.fontEastAsia;
|
|
33
|
+
if (family) parts.push(`font-family: "${family}"`);
|
|
34
|
+
if (props.size !== null) parts.push(`font-size: ${halfPointsToPx(props.size).toFixed(2)}px`);
|
|
35
|
+
if (props.color !== null) parts.push(`color: #${props.color}`);
|
|
36
|
+
if (props.highlight !== null) parts.push(`background-color: ${props.highlight}`);
|
|
37
|
+
return parts.join("; ");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function renderRun(runEl: XmlElement): HTMLElement {
|
|
41
|
+
const span = document.createElement("span");
|
|
42
|
+
let runProps: RunProperties | null = null;
|
|
43
|
+
|
|
44
|
+
for (const child of getChildren(runEl)) {
|
|
45
|
+
const tag = getTagName(child);
|
|
46
|
+
if (tag === "w:rPr") {
|
|
47
|
+
runProps = extractRunProperties(getChildren(child));
|
|
48
|
+
const style = buildRunInlineStyle(runProps);
|
|
49
|
+
if (style.length > 0) span.setAttribute("style", style);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (tag === "w:t") {
|
|
53
|
+
span.appendChild(document.createTextNode(getTextContent(child)));
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (tag === "w:tab") {
|
|
57
|
+
span.appendChild(document.createTextNode("\t"));
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (tag === "w:br") {
|
|
61
|
+
span.appendChild(document.createElement("br"));
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let inner: HTMLElement = span;
|
|
67
|
+
if (runProps) {
|
|
68
|
+
if (runProps.bold === true) {
|
|
69
|
+
const strong = document.createElement("strong");
|
|
70
|
+
while (inner.firstChild) strong.appendChild(inner.firstChild);
|
|
71
|
+
inner.appendChild(strong);
|
|
72
|
+
inner = strong;
|
|
73
|
+
}
|
|
74
|
+
if (runProps.italic === true) {
|
|
75
|
+
const em = document.createElement("em");
|
|
76
|
+
while (inner.firstChild) em.appendChild(inner.firstChild);
|
|
77
|
+
inner.appendChild(em);
|
|
78
|
+
inner = em;
|
|
79
|
+
}
|
|
80
|
+
if (runProps.underline !== null) {
|
|
81
|
+
const u = document.createElement("u");
|
|
82
|
+
while (inner.firstChild) u.appendChild(inner.firstChild);
|
|
83
|
+
inner.appendChild(u);
|
|
84
|
+
inner = u;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return span;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function renderHyperlink(hyperlinkEl: XmlElement): HTMLElement {
|
|
92
|
+
const a = document.createElement("a");
|
|
93
|
+
const anchor = getAttr(hyperlinkEl, "w:anchor");
|
|
94
|
+
if (anchor) a.setAttribute("href", `#${anchor}`);
|
|
95
|
+
else a.setAttribute("href", "#");
|
|
96
|
+
for (const child of getChildren(hyperlinkEl)) {
|
|
97
|
+
if (getTagName(child) === "w:r") {
|
|
98
|
+
a.appendChild(renderRun(child));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return a;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function renderParagraph(pEl: XmlElement): HTMLElement {
|
|
105
|
+
const p = document.createElement("p");
|
|
106
|
+
p.className = "docx-paragraph";
|
|
107
|
+
|
|
108
|
+
let pPrChildren: XmlElement[] | null = null;
|
|
109
|
+
for (const child of getChildren(pEl)) {
|
|
110
|
+
const tag = getTagName(child);
|
|
111
|
+
if (tag === "w:pPr") {
|
|
112
|
+
pPrChildren = getChildren(child);
|
|
113
|
+
const props = extractParagraphProperties(pPrChildren);
|
|
114
|
+
const style = buildParagraphInlineStyle(props);
|
|
115
|
+
if (style.length > 0) p.setAttribute("style", style);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (tag === "w:r") {
|
|
119
|
+
p.appendChild(renderRun(child));
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (tag === "w:hyperlink") {
|
|
123
|
+
p.appendChild(renderHyperlink(child));
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return p;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function pickBorderColor(value: string | undefined): string | null {
|
|
131
|
+
if (!value || value === "auto") return null;
|
|
132
|
+
return `#${value}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function renderCellBorder(side: string, borderEl: XmlElement | null): string | null {
|
|
136
|
+
if (!borderEl) return null;
|
|
137
|
+
const styleVal = getAttr(borderEl, "w:val") ?? "single";
|
|
138
|
+
const color = pickBorderColor(getAttr(borderEl, "w:color"));
|
|
139
|
+
const sizePts = (() => {
|
|
140
|
+
const v = getAttr(borderEl, "w:sz");
|
|
141
|
+
if (!v) return 0.5;
|
|
142
|
+
const n = Number.parseInt(v, 10);
|
|
143
|
+
return Number.isFinite(n) ? n / 8 : 0.5;
|
|
144
|
+
})();
|
|
145
|
+
if (styleVal === "nil" || styleVal === "none") return `border-${side}: 0`;
|
|
146
|
+
const cssStyle = styleVal === "double" ? "double" : "solid";
|
|
147
|
+
const colorStr = color ?? "#888";
|
|
148
|
+
return `border-${side}: ${sizePts}pt ${cssStyle} ${colorStr}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function applyCellBorders(td: HTMLElement, tcPr: XmlElement | null): void {
|
|
152
|
+
if (!tcPr) return;
|
|
153
|
+
let borders: XmlElement | null = null;
|
|
154
|
+
for (const child of getChildren(tcPr)) {
|
|
155
|
+
if (getTagName(child) === "w:tcBorders") {
|
|
156
|
+
borders = child;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (!borders) return;
|
|
161
|
+
const sides: Array<[string, string]> = [
|
|
162
|
+
["w:top", "top"],
|
|
163
|
+
["w:bottom", "bottom"],
|
|
164
|
+
["w:left", "left"],
|
|
165
|
+
["w:right", "right"],
|
|
166
|
+
];
|
|
167
|
+
const styles: string[] = [];
|
|
168
|
+
for (const [borderTag, cssSide] of sides) {
|
|
169
|
+
let borderEl: XmlElement | null = null;
|
|
170
|
+
for (const child of getChildren(borders)) {
|
|
171
|
+
if (getTagName(child) === borderTag) {
|
|
172
|
+
borderEl = child;
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const css = renderCellBorder(cssSide, borderEl);
|
|
177
|
+
if (css) styles.push(css);
|
|
178
|
+
}
|
|
179
|
+
const existing = td.getAttribute("style") ?? "";
|
|
180
|
+
td.setAttribute(
|
|
181
|
+
"style",
|
|
182
|
+
[existing, ...styles].filter((s) => s.length > 0).join("; "),
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function renderCell(tcEl: XmlElement, vMergeOpen: number[], colIdx: number): HTMLElement | null {
|
|
187
|
+
let tcPr: XmlElement | null = null;
|
|
188
|
+
let gridSpan = 1;
|
|
189
|
+
let vMerge: "restart" | "continue" | null = null;
|
|
190
|
+
|
|
191
|
+
for (const child of getChildren(tcEl)) {
|
|
192
|
+
const tag = getTagName(child);
|
|
193
|
+
if (tag === "w:tcPr") {
|
|
194
|
+
tcPr = child;
|
|
195
|
+
for (const propChild of getChildren(tcPr)) {
|
|
196
|
+
const propTag = getTagName(propChild);
|
|
197
|
+
if (propTag === "w:gridSpan") {
|
|
198
|
+
const v = getAttr(propChild, "w:val");
|
|
199
|
+
if (v) {
|
|
200
|
+
const n = Number.parseInt(v, 10);
|
|
201
|
+
if (Number.isFinite(n) && n > 0) gridSpan = n;
|
|
202
|
+
}
|
|
203
|
+
} else if (propTag === "w:vMerge") {
|
|
204
|
+
const v = getAttr(propChild, "w:val");
|
|
205
|
+
vMerge = v === "restart" ? "restart" : "continue";
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (vMerge === "continue") {
|
|
212
|
+
if (vMergeOpen[colIdx] !== undefined) vMergeOpen[colIdx] += 1;
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const td = document.createElement("td");
|
|
217
|
+
if (gridSpan > 1) td.setAttribute("colspan", String(gridSpan));
|
|
218
|
+
if (vMerge === "restart") {
|
|
219
|
+
vMergeOpen[colIdx] = 1;
|
|
220
|
+
td.setAttribute("data-vmerge", "restart");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
applyCellBorders(td, tcPr);
|
|
224
|
+
|
|
225
|
+
for (const child of getChildren(tcEl)) {
|
|
226
|
+
const tag = getTagName(child);
|
|
227
|
+
if (tag === "w:p") td.appendChild(renderParagraph(child));
|
|
228
|
+
else if (tag === "w:tbl") td.appendChild(renderTable(child));
|
|
229
|
+
}
|
|
230
|
+
return td;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function finalizeRowSpans(rows: HTMLTableRowElement[]): void {
|
|
234
|
+
for (const row of rows) {
|
|
235
|
+
for (let i = 0; i < row.cells.length; i++) {
|
|
236
|
+
const cell = row.cells[i];
|
|
237
|
+
const spanAttr = cell.getAttribute("data-row-span");
|
|
238
|
+
if (spanAttr !== null) cell.setAttribute("rowspan", spanAttr);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function renderTable(tableEl: XmlElement): HTMLElement {
|
|
244
|
+
const table = document.createElement("table");
|
|
245
|
+
table.className = "docx-table";
|
|
246
|
+
table.style.borderCollapse = "collapse";
|
|
247
|
+
|
|
248
|
+
const tbody = document.createElement("tbody");
|
|
249
|
+
table.appendChild(tbody);
|
|
250
|
+
|
|
251
|
+
const rows: HTMLTableRowElement[] = [];
|
|
252
|
+
const vMergeOpen: number[] = [];
|
|
253
|
+
const vMergeOriginCell: Array<HTMLTableCellElement | null> = [];
|
|
254
|
+
|
|
255
|
+
for (const child of getChildren(tableEl)) {
|
|
256
|
+
if (getTagName(child) !== "w:tr") continue;
|
|
257
|
+
const tr = document.createElement("tr");
|
|
258
|
+
let colIdx = 0;
|
|
259
|
+
for (const tc of getChildren(child)) {
|
|
260
|
+
if (getTagName(tc) !== "w:tc") continue;
|
|
261
|
+
const cell = renderCell(tc, vMergeOpen, colIdx);
|
|
262
|
+
if (cell) {
|
|
263
|
+
if (cell.getAttribute("data-vmerge") === "restart") {
|
|
264
|
+
vMergeOriginCell[colIdx] = cell as HTMLTableCellElement;
|
|
265
|
+
}
|
|
266
|
+
tr.appendChild(cell);
|
|
267
|
+
} else if (vMergeOriginCell[colIdx]) {
|
|
268
|
+
const origin = vMergeOriginCell[colIdx]!;
|
|
269
|
+
const current = origin.getAttribute("data-row-span");
|
|
270
|
+
const next = current ? Number.parseInt(current, 10) + 1 : 2;
|
|
271
|
+
origin.setAttribute("data-row-span", String(next));
|
|
272
|
+
}
|
|
273
|
+
const cellSpanAttr = cell?.getAttribute("colspan");
|
|
274
|
+
const span = cellSpanAttr ? Number.parseInt(cellSpanAttr, 10) : 1;
|
|
275
|
+
colIdx += span;
|
|
276
|
+
}
|
|
277
|
+
tbody.appendChild(tr);
|
|
278
|
+
rows.push(tr);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
finalizeRowSpans(rows);
|
|
282
|
+
return table;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function isTableXml(xml: unknown): xml is XmlElement {
|
|
286
|
+
if (typeof xml !== "object" || xml === null) return false;
|
|
287
|
+
return getTagName(xml as XmlElement) === "w:tbl";
|
|
288
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const DXA_PER_INCH = 1440;
|
|
2
|
+
export const PX_PER_INCH = 96;
|
|
3
|
+
|
|
4
|
+
export function dxaToPx(dxa: number): number {
|
|
5
|
+
return (dxa * PX_PER_INCH) / DXA_PER_INCH;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function halfPointsToPx(halfPoints: number): number {
|
|
9
|
+
return (halfPoints / 2) * (PX_PER_INCH / 72);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function emuToPx(emu: number): number {
|
|
13
|
+
return (emu * PX_PER_INCH) / 914400;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function pxToEmu(px: number): number {
|
|
17
|
+
return Math.round((px * 914400) / PX_PER_INCH);
|
|
18
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
3
|
+
import { docxSchema } from "../pm/schema";
|
|
4
|
+
import { buildFontRegistry } from "../fonts/registry";
|
|
5
|
+
import { DEFAULT_SECTION_PROPERTIES } from "../model/sections";
|
|
6
|
+
import { createReadOnlyView } from "./view";
|
|
7
|
+
import type { Section } from "../model/sections";
|
|
8
|
+
|
|
9
|
+
const fontRegistry = buildFontRegistry({
|
|
10
|
+
embeddedFonts: [],
|
|
11
|
+
workspaceFonts: [],
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const defaultSections: Section[] = [
|
|
15
|
+
{
|
|
16
|
+
properties: { ...DEFAULT_SECTION_PROPERTIES },
|
|
17
|
+
blockStartIndex: 0,
|
|
18
|
+
blockEndIndex: 0,
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
function paraWithText(text: string, marks: string[] = []) {
|
|
23
|
+
const { paragraph } = docxSchema.nodes;
|
|
24
|
+
const markInstances = marks.map((m) => docxSchema.marks[m].create());
|
|
25
|
+
return paragraph.create({}, [docxSchema.text(text, markInstances)]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let host: HTMLElement;
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
document.body.innerHTML = "";
|
|
32
|
+
host = document.createElement("div");
|
|
33
|
+
document.body.appendChild(host);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("createReadOnlyView", () => {
|
|
37
|
+
it("mounts an EditorView inside the page stack", () => {
|
|
38
|
+
const doc = docxSchema.nodes.doc.create({}, [paraWithText("Hello")]);
|
|
39
|
+
const view = createReadOnlyView(host, doc, {
|
|
40
|
+
fontRegistry,
|
|
41
|
+
sections: defaultSections,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(host.querySelector(".docx-page-stack")).not.toBeNull();
|
|
45
|
+
expect(host.querySelector("[data-docx-editor]")).not.toBeNull();
|
|
46
|
+
expect(host.textContent).toContain("Hello");
|
|
47
|
+
view.destroy();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("sizes the canvas inner from section page geometry (8.5\" x 11\")", () => {
|
|
51
|
+
const doc = docxSchema.nodes.doc.create({}, [paraWithText("page sizing")]);
|
|
52
|
+
const view = createReadOnlyView(host, doc, {
|
|
53
|
+
fontRegistry,
|
|
54
|
+
sections: defaultSections,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const canvasInner = host.querySelector(".docx-canvas-inner") as HTMLElement;
|
|
58
|
+
// 8.5 inch * 96 dpi = 816 px
|
|
59
|
+
expect(canvasInner.style.width).toBe("816px");
|
|
60
|
+
view.destroy();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("renders bold marks as <strong>", () => {
|
|
64
|
+
const doc = docxSchema.nodes.doc.create({}, [
|
|
65
|
+
paraWithText("emphasized", ["bold"]),
|
|
66
|
+
]);
|
|
67
|
+
const view = createReadOnlyView(host, doc, {
|
|
68
|
+
fontRegistry,
|
|
69
|
+
sections: defaultSections,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(host.querySelector("strong")).not.toBeNull();
|
|
73
|
+
expect(host.querySelector("strong")!.textContent).toBe("emphasized");
|
|
74
|
+
view.destroy();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("renders italic marks as <em>", () => {
|
|
78
|
+
const doc = docxSchema.nodes.doc.create({}, [
|
|
79
|
+
paraWithText("italicized", ["italic"]),
|
|
80
|
+
]);
|
|
81
|
+
const view = createReadOnlyView(host, doc, {
|
|
82
|
+
fontRegistry,
|
|
83
|
+
sections: defaultSections,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(host.querySelector("em")).not.toBeNull();
|
|
87
|
+
view.destroy();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("renders opaque_block via custom nodeView with placeholder text for unsupported XML", () => {
|
|
91
|
+
const { doc, paragraph, opaque_block } = docxSchema.nodes;
|
|
92
|
+
const node = doc.create({}, [
|
|
93
|
+
paragraph.create({}, []),
|
|
94
|
+
opaque_block.create({ xml: { "w:sdt": [] } }),
|
|
95
|
+
]);
|
|
96
|
+
const view = createReadOnlyView(host, node, {
|
|
97
|
+
fontRegistry,
|
|
98
|
+
sections: defaultSections,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const placeholder = host.querySelector("[data-opaque-block]");
|
|
102
|
+
expect(placeholder).not.toBeNull();
|
|
103
|
+
expect(placeholder!.textContent).toContain("unsupported");
|
|
104
|
+
view.destroy();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("renders section_break via custom nodeView (hidden indicator)", () => {
|
|
108
|
+
const { doc, paragraph, section_break } = docxSchema.nodes;
|
|
109
|
+
const node = doc.create({}, [
|
|
110
|
+
paragraph.create({}, []),
|
|
111
|
+
section_break.create({
|
|
112
|
+
properties: { ...DEFAULT_SECTION_PROPERTIES },
|
|
113
|
+
isFinal: true,
|
|
114
|
+
}),
|
|
115
|
+
]);
|
|
116
|
+
const view = createReadOnlyView(host, node, {
|
|
117
|
+
fontRegistry,
|
|
118
|
+
sections: defaultSections,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const breakEl = host.querySelector("[data-section-break]");
|
|
122
|
+
expect(breakEl).not.toBeNull();
|
|
123
|
+
expect(breakEl!.getAttribute("data-final")).toBe("true");
|
|
124
|
+
view.destroy();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("is non-editable", () => {
|
|
128
|
+
const doc = docxSchema.nodes.doc.create({}, [paraWithText("locked")]);
|
|
129
|
+
const view = createReadOnlyView(host, doc, {
|
|
130
|
+
fontRegistry,
|
|
131
|
+
sections: defaultSections,
|
|
132
|
+
});
|
|
133
|
+
expect(view.editorView.editable).toBe(false);
|
|
134
|
+
const editorContent = host.querySelector(".ProseMirror") as HTMLElement;
|
|
135
|
+
expect(editorContent.getAttribute("contenteditable")).toBe("false");
|
|
136
|
+
view.destroy();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("injects a stylesheet with @font-face rules for bundled fonts", () => {
|
|
140
|
+
const doc = docxSchema.nodes.doc.create({}, [paraWithText("fonts")]);
|
|
141
|
+
const view = createReadOnlyView(host, doc, {
|
|
142
|
+
fontRegistry,
|
|
143
|
+
sections: defaultSections,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const styleEl = host.querySelector(
|
|
147
|
+
"style[data-docx-editor-styles]",
|
|
148
|
+
) as HTMLStyleElement;
|
|
149
|
+
expect(styleEl).not.toBeNull();
|
|
150
|
+
expect(styleEl.textContent).toContain("@font-face");
|
|
151
|
+
expect(styleEl.textContent).toContain("Liberation Serif");
|
|
152
|
+
view.destroy();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("destroy() removes the editor and styles", () => {
|
|
156
|
+
const doc = docxSchema.nodes.doc.create({}, [paraWithText("destroy")]);
|
|
157
|
+
const view = createReadOnlyView(host, doc, {
|
|
158
|
+
fontRegistry,
|
|
159
|
+
sections: defaultSections,
|
|
160
|
+
});
|
|
161
|
+
view.destroy();
|
|
162
|
+
expect(host.querySelector(".docx-canvas")).toBeNull();
|
|
163
|
+
expect(host.querySelector("style[data-docx-editor-styles]")).toBeNull();
|
|
164
|
+
});
|
|
165
|
+
});
|