@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,343 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HeaderFooterFragment,
|
|
3
|
+
HeaderFooterRegistry,
|
|
4
|
+
} from "../parse/header_footer";
|
|
5
|
+
import type { Section } from "../model/sections";
|
|
6
|
+
import type { HeaderFooterKind, LayoutResult, SectionGeometry } from "../layout/types";
|
|
7
|
+
import { renderHeaderFooterFragment } from "./header_footer_view";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Visual gap between page rectangles. Word's "page setup" reserves
|
|
11
|
+
* the page margin around content; the gap is purely aesthetic to
|
|
12
|
+
* distinguish one page from the next.
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_PAGE_GAP_PX = 24;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Optional factory the caller can use to override how a header or footer
|
|
18
|
+
* is mounted into a given page. View.ts uses this to attach an editable
|
|
19
|
+
* ProseMirror EditorView to page 1 in editable mode while the chrome
|
|
20
|
+
* manager continues to mount read-only DOM for pages 2..N.
|
|
21
|
+
*
|
|
22
|
+
* Return null to use the default (read-only DOM clone).
|
|
23
|
+
*/
|
|
24
|
+
export type HeaderFooterFactory = (args: {
|
|
25
|
+
pageNumber: number;
|
|
26
|
+
kind: "hdr" | "ftr";
|
|
27
|
+
fragment: HeaderFooterFragment;
|
|
28
|
+
relationshipId: string | null;
|
|
29
|
+
/** True if this is the very first page of the document (pageNumber === 1). */
|
|
30
|
+
isFirstPage: boolean;
|
|
31
|
+
/** True if this page is the first page of its section. */
|
|
32
|
+
isFirstInSection: boolean;
|
|
33
|
+
/** Which header/footer variant the layout engine selected. */
|
|
34
|
+
headerKind: HeaderFooterKind;
|
|
35
|
+
}) => HTMLElement | null;
|
|
36
|
+
|
|
37
|
+
export type PageChromeOptions = {
|
|
38
|
+
host: HTMLElement;
|
|
39
|
+
sections: readonly Section[];
|
|
40
|
+
sectionGeometries: readonly SectionGeometry[];
|
|
41
|
+
headerFooterRegistry: HeaderFooterRegistry | null;
|
|
42
|
+
/** Visual gap between page rectangles in CSS pixels. Defaults to 24. */
|
|
43
|
+
pageGapPx?: number;
|
|
44
|
+
/**
|
|
45
|
+
* Override how header/footer DOM is built per page. If not provided or
|
|
46
|
+
* the override returns null, the manager falls back to read-only DOM.
|
|
47
|
+
*/
|
|
48
|
+
renderHeaderFooter?: HeaderFooterFactory;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type PageChromeManager = {
|
|
52
|
+
/**
|
|
53
|
+
* Reconcile the page rectangles to match the supplied layout. Diffs against
|
|
54
|
+
* the previous layout so unchanged pages are not re-rendered.
|
|
55
|
+
*/
|
|
56
|
+
setLayout(layout: LayoutResult): void;
|
|
57
|
+
/**
|
|
58
|
+
* Replace the per-page footnote DOM. The map's key is the page number
|
|
59
|
+
* (1-based); the value is the DOM the manager should mount above that
|
|
60
|
+
* page's footer band. Pass an empty map to clear all footnotes.
|
|
61
|
+
*/
|
|
62
|
+
setFootnotes(footnotesByPage: ReadonlyMap<number, HTMLElement>): void;
|
|
63
|
+
/**
|
|
64
|
+
* Total Y height occupied by the page chrome layer.
|
|
65
|
+
*/
|
|
66
|
+
getTotalHeight(): number;
|
|
67
|
+
/**
|
|
68
|
+
* Per-page-boundary gap height in px. Used by the pagination plugin to
|
|
69
|
+
* emit widget decorations that visually separate page content.
|
|
70
|
+
*
|
|
71
|
+
* Returns an array of length `pageCount - 1`. Entry `i` is the gap that
|
|
72
|
+
* sits between page `i+1` and page `i+2`.
|
|
73
|
+
*/
|
|
74
|
+
getGapHeights(): readonly number[];
|
|
75
|
+
destroy(): void;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Build the absolutely-positioned page rectangles that sit behind the
|
|
80
|
+
* editor content. The editor's contentDOM overlays this layer at the
|
|
81
|
+
* same Y origin; matching headerBand padding on the editor content keeps
|
|
82
|
+
* paragraphs aligned with each page's content area.
|
|
83
|
+
*/
|
|
84
|
+
export function createPageChromeManager(
|
|
85
|
+
options: PageChromeOptions,
|
|
86
|
+
): PageChromeManager {
|
|
87
|
+
const gapPx = options.pageGapPx ?? DEFAULT_PAGE_GAP_PX;
|
|
88
|
+
const layerEl = document.createElement("div");
|
|
89
|
+
layerEl.className = "docx-pages-layer";
|
|
90
|
+
layerEl.setAttribute("data-docx-pages-layer", "true");
|
|
91
|
+
options.host.appendChild(layerEl);
|
|
92
|
+
|
|
93
|
+
// Reconciliation key: pageNumber. We diff by pageNumber so unchanged
|
|
94
|
+
// pages retain their DOM (and any focus/selection state on editable
|
|
95
|
+
// header sub-views).
|
|
96
|
+
const mounted = new Map<number, MountedPage>();
|
|
97
|
+
let lastTotalHeight = 0;
|
|
98
|
+
let lastGapHeights: readonly number[] = [];
|
|
99
|
+
|
|
100
|
+
const setLayout = (layout: LayoutResult): void => {
|
|
101
|
+
const seen = new Set<number>();
|
|
102
|
+
let nextY = 0;
|
|
103
|
+
const gaps: number[] = [];
|
|
104
|
+
|
|
105
|
+
for (let i = 0; i < layout.pages.length; i++) {
|
|
106
|
+
const page = layout.pages[i];
|
|
107
|
+
const geometry = options.sectionGeometries[page.sectionIndex];
|
|
108
|
+
if (!geometry) continue;
|
|
109
|
+
|
|
110
|
+
let mountedPage = mounted.get(page.pageNumber);
|
|
111
|
+
if (
|
|
112
|
+
!mountedPage ||
|
|
113
|
+
mountedPage.sectionIndex !== page.sectionIndex ||
|
|
114
|
+
mountedPage.headerKind !== page.headerKind
|
|
115
|
+
) {
|
|
116
|
+
// Mount or remount.
|
|
117
|
+
if (mountedPage) {
|
|
118
|
+
mountedPage.element.remove();
|
|
119
|
+
mounted.delete(page.pageNumber);
|
|
120
|
+
}
|
|
121
|
+
mountedPage = mountPage({
|
|
122
|
+
page,
|
|
123
|
+
geometry,
|
|
124
|
+
section: options.sections[page.sectionIndex],
|
|
125
|
+
registry: options.headerFooterRegistry,
|
|
126
|
+
factory: options.renderHeaderFooter,
|
|
127
|
+
isFirstPage: page.pageNumber === 1,
|
|
128
|
+
});
|
|
129
|
+
mounted.set(page.pageNumber, mountedPage);
|
|
130
|
+
layerEl.appendChild(mountedPage.element);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
mountedPage.element.style.top = `${nextY}px`;
|
|
134
|
+
mountedPage.element.style.left = "0";
|
|
135
|
+
mountedPage.element.style.width = `${geometry.pageWidth}px`;
|
|
136
|
+
mountedPage.element.style.height = `${geometry.pageHeight}px`;
|
|
137
|
+
|
|
138
|
+
seen.add(page.pageNumber);
|
|
139
|
+
|
|
140
|
+
if (i < layout.pages.length - 1) {
|
|
141
|
+
// Gap between this page and the next.
|
|
142
|
+
const nextGeometry =
|
|
143
|
+
options.sectionGeometries[layout.pages[i + 1].sectionIndex] ??
|
|
144
|
+
geometry;
|
|
145
|
+
const sameSection =
|
|
146
|
+
layout.pages[i + 1].sectionIndex === page.sectionIndex;
|
|
147
|
+
// Always use the same gap height so the editor content's gap
|
|
148
|
+
// decoration is a fixed value. Section-boundary aesthetics are
|
|
149
|
+
// handled by the canvas, not the gap.
|
|
150
|
+
gaps.push(gapPx);
|
|
151
|
+
nextY += geometry.pageHeight + gapPx;
|
|
152
|
+
// Reference nextGeometry to avoid an unused-symbol lint when the
|
|
153
|
+
// next section has different page dimensions but we still use the
|
|
154
|
+
// shared gap.
|
|
155
|
+
void nextGeometry;
|
|
156
|
+
void sameSection;
|
|
157
|
+
} else {
|
|
158
|
+
nextY += geometry.pageHeight;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Unmount pages no longer in the layout.
|
|
163
|
+
for (const [pageNum, mountedPage] of mounted) {
|
|
164
|
+
if (!seen.has(pageNum)) {
|
|
165
|
+
mountedPage.element.remove();
|
|
166
|
+
mounted.delete(pageNum);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
layerEl.style.height = `${nextY}px`;
|
|
171
|
+
lastTotalHeight = nextY;
|
|
172
|
+
lastGapHeights = gaps;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const setFootnotes = (
|
|
176
|
+
footnotesByPage: ReadonlyMap<number, HTMLElement>,
|
|
177
|
+
): void => {
|
|
178
|
+
for (const [pageNumber, mountedPage] of mounted) {
|
|
179
|
+
// Drop any existing footnote section, then re-mount the new one.
|
|
180
|
+
// NOTE: footnotes are read-only DOM today, so a plain `.remove()`
|
|
181
|
+
// is sufficient. If editable footnotes land in a later phase
|
|
182
|
+
// (Phase 5+), this needs to destroy any ProseMirror EditorView
|
|
183
|
+
// inside the removed section before unmounting, or every layout
|
|
184
|
+
// recompute will leak a PM view.
|
|
185
|
+
const existing = mountedPage.element.querySelector(
|
|
186
|
+
".docx-page-footnotes",
|
|
187
|
+
);
|
|
188
|
+
existing?.remove();
|
|
189
|
+
const content = footnotesByPage.get(pageNumber);
|
|
190
|
+
if (!content) continue;
|
|
191
|
+
const wrapper = document.createElement("div");
|
|
192
|
+
wrapper.className = "docx-page-footnotes";
|
|
193
|
+
wrapper.appendChild(content);
|
|
194
|
+
// Insert just before the footer band so footnotes sit above the
|
|
195
|
+
// section footer at the bottom of the body slot.
|
|
196
|
+
const footerBand = mountedPage.element.querySelector(
|
|
197
|
+
".docx-page-footer-band",
|
|
198
|
+
);
|
|
199
|
+
if (footerBand) {
|
|
200
|
+
mountedPage.element.insertBefore(wrapper, footerBand);
|
|
201
|
+
} else {
|
|
202
|
+
mountedPage.element.appendChild(wrapper);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
setLayout,
|
|
209
|
+
setFootnotes,
|
|
210
|
+
getTotalHeight: () => lastTotalHeight,
|
|
211
|
+
getGapHeights: () => lastGapHeights,
|
|
212
|
+
destroy: () => {
|
|
213
|
+
for (const mountedPage of mounted.values()) {
|
|
214
|
+
mountedPage.element.remove();
|
|
215
|
+
}
|
|
216
|
+
mounted.clear();
|
|
217
|
+
layerEl.remove();
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// =========================================================================
|
|
223
|
+
// Internal: per-page mount
|
|
224
|
+
// =========================================================================
|
|
225
|
+
|
|
226
|
+
type MountedPage = {
|
|
227
|
+
element: HTMLElement;
|
|
228
|
+
sectionIndex: number;
|
|
229
|
+
headerKind: HeaderFooterKind;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
function mountPage(args: {
|
|
233
|
+
page: LayoutResult["pages"][number];
|
|
234
|
+
geometry: SectionGeometry;
|
|
235
|
+
section: Section;
|
|
236
|
+
registry: HeaderFooterRegistry | null;
|
|
237
|
+
factory: HeaderFooterFactory | undefined;
|
|
238
|
+
isFirstPage: boolean;
|
|
239
|
+
}): MountedPage {
|
|
240
|
+
const pageEl = document.createElement("div");
|
|
241
|
+
pageEl.className = "docx-page";
|
|
242
|
+
pageEl.setAttribute("data-docx-page", String(args.page.pageNumber));
|
|
243
|
+
pageEl.setAttribute("data-section-index", String(args.page.sectionIndex));
|
|
244
|
+
pageEl.style.position = "absolute";
|
|
245
|
+
|
|
246
|
+
// Header band.
|
|
247
|
+
const headerEl = document.createElement("div");
|
|
248
|
+
headerEl.className = "docx-page-header-band";
|
|
249
|
+
headerEl.style.height = `${args.geometry.headerBand}px`;
|
|
250
|
+
pageEl.appendChild(headerEl);
|
|
251
|
+
|
|
252
|
+
// Body slot (transparent — editor content shows through).
|
|
253
|
+
const bodyEl = document.createElement("div");
|
|
254
|
+
bodyEl.className = "docx-page-body";
|
|
255
|
+
bodyEl.style.height = `${args.geometry.pageContentHeight}px`;
|
|
256
|
+
pageEl.appendChild(bodyEl);
|
|
257
|
+
|
|
258
|
+
// Footer band.
|
|
259
|
+
const footerEl = document.createElement("div");
|
|
260
|
+
footerEl.className = "docx-page-footer-band";
|
|
261
|
+
footerEl.style.height = `${args.geometry.footerBand}px`;
|
|
262
|
+
pageEl.appendChild(footerEl);
|
|
263
|
+
|
|
264
|
+
// Resolve and mount the header/footer for this page.
|
|
265
|
+
if (args.registry) {
|
|
266
|
+
const headerFragment = pickFragmentForKind(
|
|
267
|
+
args.section.properties.headerRefs,
|
|
268
|
+
args.page.headerKind,
|
|
269
|
+
args.registry,
|
|
270
|
+
);
|
|
271
|
+
if (headerFragment) {
|
|
272
|
+
const headerRef = pickRefForKind(
|
|
273
|
+
args.section.properties.headerRefs,
|
|
274
|
+
args.page.headerKind,
|
|
275
|
+
);
|
|
276
|
+
const mountedHeader =
|
|
277
|
+
args.factory?.({
|
|
278
|
+
pageNumber: args.page.pageNumber,
|
|
279
|
+
kind: "hdr",
|
|
280
|
+
fragment: headerFragment.fragment,
|
|
281
|
+
relationshipId: headerRef?.relationshipId ?? null,
|
|
282
|
+
isFirstPage: args.isFirstPage,
|
|
283
|
+
isFirstInSection: args.page.isFirstInSection,
|
|
284
|
+
headerKind: args.page.headerKind,
|
|
285
|
+
}) ?? renderHeaderFooterFragment(headerFragment.fragment);
|
|
286
|
+
mountedHeader.setAttribute("data-docx-header", "true");
|
|
287
|
+
headerEl.appendChild(mountedHeader);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const footerFragment = pickFragmentForKind(
|
|
291
|
+
args.section.properties.footerRefs,
|
|
292
|
+
args.page.headerKind,
|
|
293
|
+
args.registry,
|
|
294
|
+
);
|
|
295
|
+
if (footerFragment) {
|
|
296
|
+
const footerRef = pickRefForKind(
|
|
297
|
+
args.section.properties.footerRefs,
|
|
298
|
+
args.page.headerKind,
|
|
299
|
+
);
|
|
300
|
+
const mountedFooter =
|
|
301
|
+
args.factory?.({
|
|
302
|
+
pageNumber: args.page.pageNumber,
|
|
303
|
+
kind: "ftr",
|
|
304
|
+
fragment: footerFragment.fragment,
|
|
305
|
+
relationshipId: footerRef?.relationshipId ?? null,
|
|
306
|
+
isFirstPage: args.isFirstPage,
|
|
307
|
+
isFirstInSection: args.page.isFirstInSection,
|
|
308
|
+
headerKind: args.page.headerKind,
|
|
309
|
+
}) ?? renderHeaderFooterFragment(footerFragment.fragment);
|
|
310
|
+
mountedFooter.setAttribute("data-docx-footer", "true");
|
|
311
|
+
footerEl.appendChild(mountedFooter);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
element: pageEl,
|
|
317
|
+
sectionIndex: args.page.sectionIndex,
|
|
318
|
+
headerKind: args.page.headerKind,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function pickRefForKind(
|
|
323
|
+
refs: readonly { type: string; relationshipId: string }[],
|
|
324
|
+
kind: HeaderFooterKind,
|
|
325
|
+
): { type: string; relationshipId: string } | null {
|
|
326
|
+
const exact = refs.find((r) => r.type === kind);
|
|
327
|
+
if (exact) return exact;
|
|
328
|
+
// Fallback to default when the section doesn't define the requested variant.
|
|
329
|
+
const fallback = refs.find((r) => r.type === "default");
|
|
330
|
+
return fallback ?? null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function pickFragmentForKind(
|
|
334
|
+
refs: readonly { type: string; relationshipId: string }[],
|
|
335
|
+
kind: HeaderFooterKind,
|
|
336
|
+
registry: HeaderFooterRegistry,
|
|
337
|
+
): { fragment: HeaderFooterFragment } | null {
|
|
338
|
+
const ref = pickRefForKind(refs, kind);
|
|
339
|
+
if (!ref) return null;
|
|
340
|
+
const fragment = registry.byRelationshipId.get(ref.relationshipId);
|
|
341
|
+
if (!fragment) return null;
|
|
342
|
+
return { fragment };
|
|
343
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import type { FontRegistry, FontResource } from "../fonts/types";
|
|
2
|
+
import type { SectionProperties } from "../model/sections";
|
|
3
|
+
import { dxaToPx } from "./units";
|
|
4
|
+
|
|
5
|
+
export type PageStyle = {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
paddingTop: number;
|
|
9
|
+
paddingRight: number;
|
|
10
|
+
paddingBottom: number;
|
|
11
|
+
paddingLeft: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function pageStyleFromSection(props: SectionProperties): PageStyle {
|
|
15
|
+
return {
|
|
16
|
+
width: dxaToPx(props.pageSize.width),
|
|
17
|
+
height: dxaToPx(props.pageSize.height),
|
|
18
|
+
paddingTop: dxaToPx(props.margins.top),
|
|
19
|
+
paddingRight: dxaToPx(props.margins.right),
|
|
20
|
+
paddingBottom: dxaToPx(props.margins.bottom),
|
|
21
|
+
paddingLeft: dxaToPx(props.margins.left),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function fontFaceRule(resource: FontResource): string {
|
|
26
|
+
if (resource.source.kind !== "bundled") {
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
29
|
+
const weight = resource.weight;
|
|
30
|
+
const style = resource.style;
|
|
31
|
+
return [
|
|
32
|
+
"@font-face {",
|
|
33
|
+
` font-family: "${resource.family}";`,
|
|
34
|
+
` font-weight: ${weight};`,
|
|
35
|
+
` font-style: ${style};`,
|
|
36
|
+
` font-display: swap;`,
|
|
37
|
+
` src: url("${resource.source.assetUrl}") format("woff2");`,
|
|
38
|
+
"}",
|
|
39
|
+
].join("\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function fontFaceStylesheet(registry: FontRegistry): string {
|
|
43
|
+
const rules: string[] = [];
|
|
44
|
+
for (const set of registry.listVariantSets()) {
|
|
45
|
+
for (const variant of [set.regular, set.bold, set.italic, set.boldItalic]) {
|
|
46
|
+
if (variant === null) continue;
|
|
47
|
+
const rule = fontFaceRule(variant);
|
|
48
|
+
if (rule.length > 0) rules.push(rule);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return rules.join("\n");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const EDITOR_BASE_CSS = `
|
|
55
|
+
.docx-canvas {
|
|
56
|
+
background: #f1f3f4;
|
|
57
|
+
padding: 24px 0 80px;
|
|
58
|
+
display: flex;
|
|
59
|
+
justify-content: center;
|
|
60
|
+
min-height: 100%;
|
|
61
|
+
}
|
|
62
|
+
.docx-canvas-inner {
|
|
63
|
+
transform-origin: top center;
|
|
64
|
+
transition: transform 120ms ease-out;
|
|
65
|
+
}
|
|
66
|
+
.docx-page-stack {
|
|
67
|
+
position: relative;
|
|
68
|
+
}
|
|
69
|
+
.docx-pages-layer {
|
|
70
|
+
position: relative;
|
|
71
|
+
width: 100%;
|
|
72
|
+
}
|
|
73
|
+
.docx-page {
|
|
74
|
+
background: #ffffff;
|
|
75
|
+
box-shadow: 0 1px 3px rgba(60,64,67,0.15), 0 4px 8px rgba(60,64,67,0.10);
|
|
76
|
+
box-sizing: border-box;
|
|
77
|
+
display: flex;
|
|
78
|
+
flex-direction: column;
|
|
79
|
+
}
|
|
80
|
+
.docx-page-header-band {
|
|
81
|
+
flex-shrink: 0;
|
|
82
|
+
box-sizing: border-box;
|
|
83
|
+
padding: 0;
|
|
84
|
+
overflow: hidden;
|
|
85
|
+
}
|
|
86
|
+
.docx-page-body {
|
|
87
|
+
flex: 1 1 auto;
|
|
88
|
+
}
|
|
89
|
+
.docx-page-footer-band {
|
|
90
|
+
flex-shrink: 0;
|
|
91
|
+
box-sizing: border-box;
|
|
92
|
+
padding: 0;
|
|
93
|
+
overflow: hidden;
|
|
94
|
+
}
|
|
95
|
+
.docx-page-gap {
|
|
96
|
+
display: block;
|
|
97
|
+
width: 100%;
|
|
98
|
+
pointer-events: none;
|
|
99
|
+
user-select: none;
|
|
100
|
+
}
|
|
101
|
+
.docx-editor-mount {
|
|
102
|
+
box-sizing: border-box;
|
|
103
|
+
}
|
|
104
|
+
.docx-editor-content {
|
|
105
|
+
outline: none;
|
|
106
|
+
caret-color: #1a73e8;
|
|
107
|
+
font-feature-settings: "kern", "liga", "calt";
|
|
108
|
+
text-rendering: optimizeLegibility;
|
|
109
|
+
-webkit-font-smoothing: antialiased;
|
|
110
|
+
-moz-osx-font-smoothing: grayscale;
|
|
111
|
+
background: transparent;
|
|
112
|
+
}
|
|
113
|
+
.docx-editor-content::selection,
|
|
114
|
+
.docx-editor-content *::selection {
|
|
115
|
+
background: rgba(166, 206, 255, 0.55);
|
|
116
|
+
}
|
|
117
|
+
.docx-editor-content p { margin: 0; }
|
|
118
|
+
.docx-editor-content table {
|
|
119
|
+
border-collapse: collapse;
|
|
120
|
+
/* auto layout matches Word AutoFit: cells expand to content. PM-table
|
|
121
|
+
conversion passes null colwidth for tblW=auto so this engages. Fixed
|
|
122
|
+
widths still apply via the PM colwidth attribute / inline col widths. */
|
|
123
|
+
table-layout: auto;
|
|
124
|
+
width: auto;
|
|
125
|
+
margin: 8px 0;
|
|
126
|
+
}
|
|
127
|
+
.docx-editor-content td, .docx-editor-content th {
|
|
128
|
+
vertical-align: top;
|
|
129
|
+
position: relative;
|
|
130
|
+
/* Word's TableGrid default: single 0.5pt black on every cell edge.
|
|
131
|
+
OOXML tblBorders/tcBorders parse later — for now this guarantees
|
|
132
|
+
every table renders visible borders rather than appearing as plain
|
|
133
|
+
text columns. Cell-specific border overrides win via specificity. */
|
|
134
|
+
border: 1px solid #000000;
|
|
135
|
+
padding: 4px 8px;
|
|
136
|
+
min-width: 24px;
|
|
137
|
+
}
|
|
138
|
+
.docx-editor-content .selectedCell {
|
|
139
|
+
background: rgba(26, 115, 232, 0.12);
|
|
140
|
+
outline: 1px solid rgba(26, 115, 232, 0.35);
|
|
141
|
+
}
|
|
142
|
+
.docx-editor-content .column-resize-handle {
|
|
143
|
+
position: absolute;
|
|
144
|
+
right: -2px;
|
|
145
|
+
top: 0;
|
|
146
|
+
bottom: 0;
|
|
147
|
+
width: 4px;
|
|
148
|
+
background: #1a73e8;
|
|
149
|
+
pointer-events: none;
|
|
150
|
+
z-index: 20;
|
|
151
|
+
}
|
|
152
|
+
.docx-editor-content.resize-cursor { cursor: ew-resize; cursor: col-resize; }
|
|
153
|
+
.docx-editor-content .ProseMirror-gapcursor { display: none; pointer-events: none; position: absolute; }
|
|
154
|
+
.docx-editor-content .ProseMirror-gapcursor::after {
|
|
155
|
+
content: "";
|
|
156
|
+
display: block;
|
|
157
|
+
position: absolute;
|
|
158
|
+
top: -2px;
|
|
159
|
+
width: 20px;
|
|
160
|
+
border-top: 1px solid #1a73e8;
|
|
161
|
+
}
|
|
162
|
+
.docx-editor-content.ProseMirror-focused .ProseMirror-gapcursor { display: block; }
|
|
163
|
+
.docx-editor-content .docx-template-marker { cursor: default; }
|
|
164
|
+
.docx-placeholder-empty::before {
|
|
165
|
+
content: attr(data-placeholder);
|
|
166
|
+
color: #9aa0a6;
|
|
167
|
+
pointer-events: none;
|
|
168
|
+
position: absolute;
|
|
169
|
+
}
|
|
170
|
+
.ProseMirror-search-match { background-color: rgba(255, 235, 59, 0.45); }
|
|
171
|
+
.ProseMirror-active-search-match { background-color: rgba(255, 152, 0, 0.55); }
|
|
172
|
+
|
|
173
|
+
.docx-image-inline { position: relative; display: inline-block; }
|
|
174
|
+
.docx-image-inline img { display: block; user-select: none; }
|
|
175
|
+
.docx-image-inline:hover .docx-image-resize-handle,
|
|
176
|
+
.docx-image-inline:focus-within .docx-image-resize-handle {
|
|
177
|
+
opacity: 1;
|
|
178
|
+
}
|
|
179
|
+
.docx-image-resize-handle {
|
|
180
|
+
position: absolute;
|
|
181
|
+
width: 10px;
|
|
182
|
+
height: 10px;
|
|
183
|
+
background: #ffffff;
|
|
184
|
+
border: 1px solid #1a73e8;
|
|
185
|
+
border-radius: 50%;
|
|
186
|
+
opacity: 0;
|
|
187
|
+
transition: opacity 100ms ease-out;
|
|
188
|
+
z-index: 10;
|
|
189
|
+
}
|
|
190
|
+
.docx-image-resize-handle--nw { top: -5px; left: -5px; cursor: nwse-resize; }
|
|
191
|
+
.docx-image-resize-handle--ne { top: -5px; right: -5px; cursor: nesw-resize; }
|
|
192
|
+
.docx-image-resize-handle--sw { bottom: -5px; left: -5px; cursor: nesw-resize; }
|
|
193
|
+
.docx-image-resize-handle--se { bottom: -5px; right: -5px; cursor: nwse-resize; }
|
|
194
|
+
|
|
195
|
+
.docx-numbering-marker {
|
|
196
|
+
display: inline-block;
|
|
197
|
+
min-width: 1.5em;
|
|
198
|
+
margin-right: 0.4em;
|
|
199
|
+
text-align: left;
|
|
200
|
+
user-select: none;
|
|
201
|
+
}
|
|
202
|
+
.docx-editor-content [data-list-ilvl="0"] { padding-left: 24px; }
|
|
203
|
+
.docx-editor-content [data-list-ilvl="1"] { padding-left: 48px; }
|
|
204
|
+
.docx-editor-content [data-list-ilvl="2"] { padding-left: 72px; }
|
|
205
|
+
.docx-editor-content [data-list-ilvl="3"] { padding-left: 96px; }
|
|
206
|
+
.docx-editor-content [data-list-ilvl="4"] { padding-left: 120px; }
|
|
207
|
+
.docx-editor-content [data-list-ilvl="5"] { padding-left: 144px; }
|
|
208
|
+
.docx-editor-content [data-list-ilvl="6"] { padding-left: 168px; }
|
|
209
|
+
.docx-editor-content [data-list-ilvl="7"] { padding-left: 192px; }
|
|
210
|
+
.docx-editor-content [data-list-ilvl="8"] { padding-left: 216px; }
|
|
211
|
+
|
|
212
|
+
.docx-page-footnotes {
|
|
213
|
+
flex-shrink: 0;
|
|
214
|
+
padding: 0 96px;
|
|
215
|
+
border-top: 1px solid #dadce0;
|
|
216
|
+
font-size: 10pt;
|
|
217
|
+
color: #3c4043;
|
|
218
|
+
box-sizing: border-box;
|
|
219
|
+
}
|
|
220
|
+
.docx-footnotes__list {
|
|
221
|
+
margin: 8px 0;
|
|
222
|
+
padding-left: 24px;
|
|
223
|
+
}
|
|
224
|
+
.docx-footnote {
|
|
225
|
+
margin-bottom: 4px;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.docx-header-footer { color: #5f6368; padding: 8px 96px; box-sizing: border-box; height: 100%; }
|
|
229
|
+
.docx-header-footer--editable { outline: none; cursor: text; }
|
|
230
|
+
.docx-header-footer--editable:focus { color: #1f1f1f; background: rgba(26,115,232,0.04); }
|
|
231
|
+
.docx-page-stack--body-dimmed .docx-editor-content {
|
|
232
|
+
opacity: 0.5;
|
|
233
|
+
}
|
|
234
|
+
`;
|