@sofereditor/export-pdf 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +13 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/package.json +20 -14
- package/src/__tests__/caption.test.ts +0 -126
- package/src/__tests__/html.test.ts +0 -226
- package/src/download.ts +0 -26
- package/src/html.ts +0 -474
- package/src/index.ts +0 -17
- package/src/pdf.ts +0 -223
- package/tsconfig.json +0 -8
- package/tsup.config.ts +0 -11
package/src/html.ts
DELETED
|
@@ -1,474 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AlignValue,
|
|
3
|
-
BlockAttrs,
|
|
4
|
-
DeltaOp,
|
|
5
|
-
DirValue,
|
|
6
|
-
ImageEmbed,
|
|
7
|
-
LegacySerializedDocument,
|
|
8
|
-
ListKind,
|
|
9
|
-
MarkAttrs,
|
|
10
|
-
PageSettings,
|
|
11
|
-
SerializedBlock,
|
|
12
|
-
SerializedCell,
|
|
13
|
-
SerializedDocument,
|
|
14
|
-
} from "@sofereditor/core";
|
|
15
|
-
import { isImageEmbed, isLegacySerializedDocument, pxToMm } from "@sofereditor/core";
|
|
16
|
-
|
|
17
|
-
function normalize(doc: SerializedDocument | LegacySerializedDocument): SerializedDocument {
|
|
18
|
-
return isLegacySerializedDocument(doc) ? { blocks: doc } : doc;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface DocumentToHtmlOptions {
|
|
22
|
-
/** Document title (set on `<title>` and used as the file name hint). Default "Documento". */
|
|
23
|
-
title?: string;
|
|
24
|
-
/** When true (default) emits a full `<html>` document. When false emits only the body fragment. */
|
|
25
|
-
standalone?: boolean;
|
|
26
|
-
/** Extra CSS appended after the default print stylesheet. Only honored when `standalone`. */
|
|
27
|
-
extraCss?: string;
|
|
28
|
-
/** Page geometry hints injected into `@page`. Defaults to A4. */
|
|
29
|
-
page?: {
|
|
30
|
-
width: string;
|
|
31
|
-
height: string;
|
|
32
|
-
marginTop: string;
|
|
33
|
-
marginBottom: string;
|
|
34
|
-
marginLeft: string;
|
|
35
|
-
marginRight: string;
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const DEFAULT_PAGE = {
|
|
40
|
-
width: "210mm",
|
|
41
|
-
height: "297mm",
|
|
42
|
-
marginTop: "25mm",
|
|
43
|
-
marginBottom: "25mm",
|
|
44
|
-
marginLeft: "25mm",
|
|
45
|
-
marginRight: "25mm",
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Serialize a `SerializedDocument` to standalone HTML.
|
|
50
|
-
*
|
|
51
|
-
* Output structure mirrors what `@sofereditor/react` paints at runtime: each
|
|
52
|
-
* top-level block becomes one element (`<p>`, `<h1..6>`, `<blockquote>`, `<pre>`,
|
|
53
|
-
* `<table>`), consecutive `listItem` blocks are reconstructed into nested
|
|
54
|
-
* `<ul>`/`<ol>` trees, and image embeds are emitted as `<img>` tags carrying the
|
|
55
|
-
* embed's data URL (so the export is self-contained).
|
|
56
|
-
*
|
|
57
|
-
* NOT a fragment of the live editor DOM — this walks the model so the output is
|
|
58
|
-
* editor-state agnostic (no caret artifacts, no zero-width phantom anchors).
|
|
59
|
-
*/
|
|
60
|
-
export function documentToHtml(
|
|
61
|
-
doc: SerializedDocument | LegacySerializedDocument,
|
|
62
|
-
options: DocumentToHtmlOptions = {},
|
|
63
|
-
): string {
|
|
64
|
-
const normalized = normalize(doc);
|
|
65
|
-
const body = documentToHtmlFragment(normalized);
|
|
66
|
-
if (options.standalone === false) return body;
|
|
67
|
-
const page = options.page ?? pageFromSettings(normalized.pageSettings);
|
|
68
|
-
const title = escapeHtml(options.title ?? "Documento");
|
|
69
|
-
const extra = options.extraCss ?? "";
|
|
70
|
-
return [
|
|
71
|
-
"<!doctype html>",
|
|
72
|
-
`<html lang="pt-BR"><head><meta charset="utf-8"><title>${title}</title>`,
|
|
73
|
-
`<style>${baseStylesheet(page)}${extra}</style>`,
|
|
74
|
-
"</head><body>",
|
|
75
|
-
body,
|
|
76
|
-
"</body></html>",
|
|
77
|
-
].join("");
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function pageFromSettings(s: PageSettings | undefined): NonNullable<DocumentToHtmlOptions["page"]> {
|
|
81
|
-
if (!s) return DEFAULT_PAGE;
|
|
82
|
-
const fmt = (px: number) => `${pxToMm(px).toFixed(2)}mm`;
|
|
83
|
-
return {
|
|
84
|
-
width: fmt(s.width),
|
|
85
|
-
height: fmt(s.height),
|
|
86
|
-
marginTop: fmt(s.marginTop),
|
|
87
|
-
marginBottom: fmt(s.marginBottom),
|
|
88
|
-
marginLeft: fmt(s.marginLeft),
|
|
89
|
-
marginRight: fmt(s.marginRight),
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Body-only HTML for embedding in other documents. */
|
|
94
|
-
export function documentToHtmlFragment(
|
|
95
|
-
doc: SerializedDocument | LegacySerializedDocument,
|
|
96
|
-
): string {
|
|
97
|
-
const blocks = normalize(doc).blocks;
|
|
98
|
-
const out: string[] = [];
|
|
99
|
-
let i = 0;
|
|
100
|
-
while (i < blocks.length) {
|
|
101
|
-
const block = blocks[i];
|
|
102
|
-
if (block.type === "listItem") {
|
|
103
|
-
const groupStart = i;
|
|
104
|
-
const kind: ListKind = block.attrs.listKind === "ordered" ? "ordered" : "bullet";
|
|
105
|
-
const items: SerializedBlock[] = [];
|
|
106
|
-
while (
|
|
107
|
-
i < blocks.length &&
|
|
108
|
-
blocks[i].type === "listItem" &&
|
|
109
|
-
(blocks[i].attrs.listKind ?? "bullet") === kind
|
|
110
|
-
) {
|
|
111
|
-
items.push(blocks[i]);
|
|
112
|
-
i++;
|
|
113
|
-
}
|
|
114
|
-
out.push(renderListGroup(items, kind, groupStart));
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
out.push(renderBlock(block, i));
|
|
118
|
-
i++;
|
|
119
|
-
}
|
|
120
|
-
return out.join("");
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** JSON export — a stable serialization for save/load round-trips. */
|
|
124
|
-
export function documentToJson(
|
|
125
|
-
doc: SerializedDocument | LegacySerializedDocument,
|
|
126
|
-
pretty = true,
|
|
127
|
-
): string {
|
|
128
|
-
return JSON.stringify(normalize(doc), null, pretty ? 2 : 0);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// ---------- blocks ----------
|
|
132
|
-
|
|
133
|
-
function renderBlock(block: SerializedBlock, index: number): string {
|
|
134
|
-
switch (block.type) {
|
|
135
|
-
case "paragraph":
|
|
136
|
-
return `<p${blockAttrs("ed-paragraph", block.attrs)}>${renderInline(block.delta)}</p>`;
|
|
137
|
-
case "heading": {
|
|
138
|
-
const level = clampHeadingLevel(block.attrs.level);
|
|
139
|
-
return `<h${level}${blockAttrs(`ed-heading ed-heading-${level}`, block.attrs)}>${renderInline(
|
|
140
|
-
block.delta,
|
|
141
|
-
)}</h${level}>`;
|
|
142
|
-
}
|
|
143
|
-
case "blockquote":
|
|
144
|
-
return `<blockquote${blockAttrs("ed-blockquote", block.attrs)}>${renderInline(
|
|
145
|
-
block.delta,
|
|
146
|
-
)}</blockquote>`;
|
|
147
|
-
case "codeBlock": {
|
|
148
|
-
const lang = block.attrs.language;
|
|
149
|
-
const codeAttrs = lang ? ` class="language-${escapeAttr(lang)}"` : "";
|
|
150
|
-
return `<pre${blockAttrs("ed-codeblock", block.attrs)}><code${codeAttrs}>${renderInline(
|
|
151
|
-
block.delta,
|
|
152
|
-
)}</code></pre>`;
|
|
153
|
-
}
|
|
154
|
-
case "table":
|
|
155
|
-
return renderTable(block);
|
|
156
|
-
default:
|
|
157
|
-
return `<p${blockAttrs("ed-paragraph", block.attrs)}>${renderInline(block.delta)}</p>`;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function renderListGroup(items: SerializedBlock[], kind: ListKind, key: number): string {
|
|
162
|
-
// Tree-build using listLevel just like Editor.buildListTree.
|
|
163
|
-
interface Node { item: SerializedBlock; children: Node[]; }
|
|
164
|
-
const roots: Node[] = [];
|
|
165
|
-
const stack: { children: Node[]; level: number }[] = [{ children: roots, level: -1 }];
|
|
166
|
-
for (const it of items) {
|
|
167
|
-
const level = clampListLevel(it.attrs.listLevel);
|
|
168
|
-
while (stack.length > 1 && stack[stack.length - 1].level >= level) stack.pop();
|
|
169
|
-
const node: Node = { item: it, children: [] };
|
|
170
|
-
stack[stack.length - 1].children.push(node);
|
|
171
|
-
stack.push({ children: node.children, level });
|
|
172
|
-
}
|
|
173
|
-
return renderListTree(roots, kind, `list-${key}`);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function renderListTree(nodes: { item: SerializedBlock; children: any[] }[], kind: ListKind, _key: string): string {
|
|
177
|
-
const Tag = kind === "ordered" ? "ol" : "ul";
|
|
178
|
-
const cls = `ed-list ed-list-${kind}`;
|
|
179
|
-
const items = nodes
|
|
180
|
-
.map((n) => {
|
|
181
|
-
const inner = renderInline(n.item.delta);
|
|
182
|
-
const nested =
|
|
183
|
-
n.children.length > 0 ? renderListTree(n.children, kind, _key + "n") : "";
|
|
184
|
-
return `<li${blockAttrs("ed-listitem", n.item.attrs)}>${inner}${nested}</li>`;
|
|
185
|
-
})
|
|
186
|
-
.join("");
|
|
187
|
-
return `<${Tag} class="${cls}" data-list-kind="${kind}">${items}</${Tag}>`;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function renderTable(block: SerializedBlock): string {
|
|
191
|
-
const rows = typeof block.attrs.rows === "number" ? block.attrs.rows : 0;
|
|
192
|
-
const cols = typeof block.attrs.cols === "number" ? block.attrs.cols : 0;
|
|
193
|
-
const cells = block.cells ?? [];
|
|
194
|
-
if (rows <= 0 || cols <= 0) return "";
|
|
195
|
-
|
|
196
|
-
const colGroup = renderColGroup(block.attrs.colWidths, cols);
|
|
197
|
-
const trs: string[] = [];
|
|
198
|
-
for (let r = 0; r < rows; r++) {
|
|
199
|
-
const tds: string[] = [];
|
|
200
|
-
for (let c = 0; c < cols; c++) {
|
|
201
|
-
const cell = cells[r * cols + c];
|
|
202
|
-
if (!cell) {
|
|
203
|
-
tds.push("<td class=\"ed-cell\"></td>");
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
if (cell.attrs?.covered) continue;
|
|
207
|
-
tds.push(renderCell(cell));
|
|
208
|
-
}
|
|
209
|
-
trs.push(`<tr>${tds.join("")}</tr>`);
|
|
210
|
-
}
|
|
211
|
-
return `<div class="ed-table-wrap"><table class="ed-table">${colGroup}<tbody>${trs.join("")}</tbody></table></div>`;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function renderColGroup(widths: number[] | undefined, cols: number): string {
|
|
215
|
-
if (!widths || widths.length === 0) return "";
|
|
216
|
-
const cells: string[] = [];
|
|
217
|
-
for (let i = 0; i < cols; i++) {
|
|
218
|
-
const w = widths[i];
|
|
219
|
-
cells.push(typeof w === "number" ? `<col style="width:${w}px">` : "<col>");
|
|
220
|
-
}
|
|
221
|
-
return `<colgroup>${cells.join("")}</colgroup>`;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function renderCell(cell: SerializedCell): string {
|
|
225
|
-
const rs = cell.attrs?.rowspan && cell.attrs.rowspan > 1
|
|
226
|
-
? ` rowspan="${cell.attrs.rowspan}"`
|
|
227
|
-
: "";
|
|
228
|
-
const cs = cell.attrs?.colspan && cell.attrs.colspan > 1
|
|
229
|
-
? ` colspan="${cell.attrs.colspan}"`
|
|
230
|
-
: "";
|
|
231
|
-
return `<td class="ed-cell"${rs}${cs}>${renderInline(cell.delta)}</td>`;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// ---------- inline ----------
|
|
235
|
-
|
|
236
|
-
function renderInline(delta: DeltaOp[]): string {
|
|
237
|
-
if (delta.length === 0) return "<br>";
|
|
238
|
-
const parts: string[] = [];
|
|
239
|
-
// Floating wrap-left/wrap-right embeds are emitted FIRST so CSS float wraps
|
|
240
|
-
// the following inline text — mirrors the in-editor renderer.
|
|
241
|
-
const wraps: string[] = [];
|
|
242
|
-
for (const op of delta) {
|
|
243
|
-
if (typeof op.insert === "string") {
|
|
244
|
-
if (op.insert.length === 0) continue;
|
|
245
|
-
parts.push(applyMarks(escapeHtml(op.insert), op.attributes));
|
|
246
|
-
} else if (isImageEmbed(op.insert)) {
|
|
247
|
-
const layout = op.insert.layout ?? "inline";
|
|
248
|
-
if (layout === "wrap-left" || layout === "wrap-right") {
|
|
249
|
-
wraps.push(renderImg(op.insert));
|
|
250
|
-
} else {
|
|
251
|
-
parts.push(renderImg(op.insert));
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
if (wraps.length === 0 && parts.length === 0) return "<br>";
|
|
256
|
-
return wraps.join("") + parts.join("");
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function applyMarks(text: string, attrs: MarkAttrs | undefined): string {
|
|
260
|
-
if (!attrs) return text;
|
|
261
|
-
let html = text;
|
|
262
|
-
if (attrs.bold) html = `<strong>${html}</strong>`;
|
|
263
|
-
if (attrs.italic) html = `<em>${html}</em>`;
|
|
264
|
-
if (attrs.underline) html = `<u>${html}</u>`;
|
|
265
|
-
if (attrs.strike) html = `<s>${html}</s>`;
|
|
266
|
-
const styles: string[] = [];
|
|
267
|
-
if (attrs.color) styles.push(`color:${cssValue(attrs.color)}`);
|
|
268
|
-
if (attrs.fontFamily) styles.push(`font-family:${cssValue(attrs.fontFamily)}`);
|
|
269
|
-
if (attrs.fontSize) styles.push(`font-size:${cssValue(attrs.fontSize)}`);
|
|
270
|
-
if (styles.length > 0) html = `<span style="${styles.join(";")}">${html}</span>`;
|
|
271
|
-
if (attrs.link?.href) {
|
|
272
|
-
const t = attrs.link.title ? ` title="${escapeAttr(attrs.link.title)}"` : "";
|
|
273
|
-
html = `<a href="${escapeAttr(attrs.link.href)}"${t} rel="noopener noreferrer" target="_blank">${html}</a>`;
|
|
274
|
-
}
|
|
275
|
-
if (attrs.comment?.markId) {
|
|
276
|
-
html = `<span data-comment-id="${escapeAttr(attrs.comment.markId)}">${html}</span>`;
|
|
277
|
-
}
|
|
278
|
-
return html;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function renderImg(embed: ImageEmbed): string {
|
|
282
|
-
const layout = embed.layout ?? "inline";
|
|
283
|
-
const align = embed.align;
|
|
284
|
-
const ox = embed.offsetX ?? 0;
|
|
285
|
-
const oy = embed.offsetY ?? 0;
|
|
286
|
-
const hasCaption = !!embed.caption && embed.caption.length > 0;
|
|
287
|
-
|
|
288
|
-
// The wrapper carries the positioning (float/absolute/inline-block/block).
|
|
289
|
-
// The img keeps the intrinsic size when wrapped; when there's no caption we
|
|
290
|
-
// keep the historical bare `<img>` output for unchanged DOM/CSS.
|
|
291
|
-
const wrapperStyles: string[] = [];
|
|
292
|
-
switch (layout) {
|
|
293
|
-
case "wrap-left":
|
|
294
|
-
wrapperStyles.push(
|
|
295
|
-
"float:left",
|
|
296
|
-
`width:${embed.width}px`,
|
|
297
|
-
`margin-right:${Math.max(8, ox)}px`,
|
|
298
|
-
`margin-top:${Math.max(0, oy)}px`,
|
|
299
|
-
"margin-bottom:4px",
|
|
300
|
-
);
|
|
301
|
-
break;
|
|
302
|
-
case "wrap-right":
|
|
303
|
-
wrapperStyles.push(
|
|
304
|
-
"float:right",
|
|
305
|
-
`width:${embed.width}px`,
|
|
306
|
-
`margin-left:${Math.max(8, ox)}px`,
|
|
307
|
-
`margin-top:${Math.max(0, oy)}px`,
|
|
308
|
-
"margin-bottom:4px",
|
|
309
|
-
);
|
|
310
|
-
break;
|
|
311
|
-
case "behind":
|
|
312
|
-
case "front":
|
|
313
|
-
wrapperStyles.push(
|
|
314
|
-
"position:absolute",
|
|
315
|
-
`left:${ox}px`,
|
|
316
|
-
`top:${oy}px`,
|
|
317
|
-
`width:${embed.width}px`,
|
|
318
|
-
`z-index:${layout === "behind" ? -1 : 2}`,
|
|
319
|
-
);
|
|
320
|
-
break;
|
|
321
|
-
case "inline":
|
|
322
|
-
default:
|
|
323
|
-
if (align) {
|
|
324
|
-
wrapperStyles.push(
|
|
325
|
-
"display:block",
|
|
326
|
-
`width:${embed.width}px`,
|
|
327
|
-
`margin-left:${align === "left" ? "0" : "auto"}`,
|
|
328
|
-
`margin-right:${align === "right" ? "0" : "auto"}`,
|
|
329
|
-
);
|
|
330
|
-
} else {
|
|
331
|
-
wrapperStyles.push(
|
|
332
|
-
"display:inline-block",
|
|
333
|
-
"vertical-align:text-bottom",
|
|
334
|
-
`width:${embed.width}px`,
|
|
335
|
-
);
|
|
336
|
-
}
|
|
337
|
-
break;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
const imgStyles: string[] = hasCaption
|
|
341
|
-
? [`width:${embed.width}px`, `height:${embed.height}px`, "display:block"]
|
|
342
|
-
: wrapperStyles.concat(
|
|
343
|
-
// Preserve old height when emitting bare <img>.
|
|
344
|
-
`height:${embed.height}px`,
|
|
345
|
-
);
|
|
346
|
-
|
|
347
|
-
const imgHtml = `<img src="${escapeAttr(embed.src)}" width="${embed.width}" height="${embed.height}" alt="${escapeAttr(embed.caption ?? "")}" data-embed="image" data-embed-layout="${layout}"${
|
|
348
|
-
align ? ` data-embed-align="${escapeAttr(align)}"` : ""
|
|
349
|
-
} style="${imgStyles.join(";")}">`;
|
|
350
|
-
|
|
351
|
-
if (!hasCaption) return imgHtml;
|
|
352
|
-
|
|
353
|
-
const captionAlign = embed.captionAlign ?? "center";
|
|
354
|
-
return `<figure class="ed-figure" data-embed-figure="true" data-embed-layout="${layout}" style="${wrapperStyles.join(";")}">${imgHtml}<figcaption class="ed-figcaption" data-caption-align="${captionAlign}" style="text-align:${captionAlign}">${escapeHtml(
|
|
355
|
-
embed.caption!,
|
|
356
|
-
)}</figcaption></figure>`;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// ---------- helpers ----------
|
|
360
|
-
|
|
361
|
-
function blockAttrs(baseClass: string, attrs: BlockAttrs): string {
|
|
362
|
-
const classes = [baseClass, "ed-block"];
|
|
363
|
-
const align = clampAlign(attrs.align);
|
|
364
|
-
if (align) classes.push(`ed-align-${align}`);
|
|
365
|
-
const dir = clampDir(attrs.dir);
|
|
366
|
-
const dirAttr = dir ? ` dir="${dir}"` : "";
|
|
367
|
-
return ` class="${classes.join(" ")}"${dirAttr}`;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function clampHeadingLevel(l: unknown): 1 | 2 | 3 | 4 | 5 | 6 {
|
|
371
|
-
if (typeof l !== "number") return 1;
|
|
372
|
-
const n = Math.trunc(l);
|
|
373
|
-
if (n < 1) return 1;
|
|
374
|
-
if (n > 6) return 6;
|
|
375
|
-
return n as 1 | 2 | 3 | 4 | 5 | 6;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function clampListLevel(l: unknown): number {
|
|
379
|
-
if (typeof l !== "number" || !Number.isFinite(l)) return 0;
|
|
380
|
-
return Math.max(0, Math.min(5, Math.trunc(l)));
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function clampAlign(a: unknown): AlignValue | undefined {
|
|
384
|
-
if (a === "left" || a === "center" || a === "right" || a === "justify") return a;
|
|
385
|
-
return undefined;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
function clampDir(d: unknown): DirValue | undefined {
|
|
389
|
-
if (d === "ltr" || d === "rtl") return d;
|
|
390
|
-
return undefined;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const HTML_ESCAPES: Record<string, string> = {
|
|
394
|
-
"&": "&",
|
|
395
|
-
"<": "<",
|
|
396
|
-
">": ">",
|
|
397
|
-
'"': """,
|
|
398
|
-
"'": "'",
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
function escapeHtml(s: string): string {
|
|
402
|
-
return s.replace(/[&<>"']/g, (c) => HTML_ESCAPES[c]);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function escapeAttr(s: string): string {
|
|
406
|
-
return s.replace(/[&<>"']/g, (c) => HTML_ESCAPES[c]);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/** Allow only safe characters in CSS values — defense in depth against injected `</style>`. */
|
|
410
|
-
function cssValue(v: string): string {
|
|
411
|
-
return v.replace(/[<>"';\\]/g, "");
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function baseStylesheet(page: NonNullable<DocumentToHtmlOptions["page"]>): string {
|
|
415
|
-
// Margins are owned by `@page` only. Chromium honors that block both for
|
|
416
|
-
// `window.print()` (client-side `exportPdfFromDocument`) and for
|
|
417
|
-
// `page.pdf({ preferCSSPageSize: true })` (server-side `renderPdf` in
|
|
418
|
-
// `@sofereditor/export-pdf-server`). Adding the same offsets via `body { padding }`
|
|
419
|
-
// would compound and double the margin in the output PDF.
|
|
420
|
-
return `
|
|
421
|
-
@page {
|
|
422
|
-
size: ${page.width} ${page.height};
|
|
423
|
-
margin: ${page.marginTop} ${page.marginRight} ${page.marginBottom} ${page.marginLeft};
|
|
424
|
-
}
|
|
425
|
-
html, body { margin: 0; padding: 0; background: white; color: #1a1a1f; }
|
|
426
|
-
body {
|
|
427
|
-
font-family: "Arial", "Helvetica", sans-serif;
|
|
428
|
-
font-size: 11pt;
|
|
429
|
-
line-height: 1.5;
|
|
430
|
-
}
|
|
431
|
-
.ed-paragraph { margin: 0 0 0.5em; min-height: 1.5em; }
|
|
432
|
-
.ed-heading { margin: 0.5em 0 0.25em; font-weight: 600; line-height: 1.25; }
|
|
433
|
-
.ed-heading-1 { font-size: 28pt; }
|
|
434
|
-
.ed-heading-2 { font-size: 22pt; }
|
|
435
|
-
.ed-heading-3 { font-size: 18pt; }
|
|
436
|
-
.ed-heading-4 { font-size: 14pt; }
|
|
437
|
-
.ed-heading-5 { font-size: 12pt; }
|
|
438
|
-
.ed-heading-6 { font-size: 11pt; color: #6b7280; }
|
|
439
|
-
.ed-blockquote {
|
|
440
|
-
margin: 0 0 0.5em; padding: 0.25em 12px;
|
|
441
|
-
border-inline-start: 3px solid #e5e7eb; color: #6b7280;
|
|
442
|
-
}
|
|
443
|
-
.ed-codeblock {
|
|
444
|
-
margin: 0 0 0.5em; background: #0f172a; color: #e2e8f0;
|
|
445
|
-
padding: 12px 14px; border-radius: 6px;
|
|
446
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
447
|
-
font-size: 11pt; white-space: pre-wrap;
|
|
448
|
-
}
|
|
449
|
-
.ed-codeblock code { font-family: inherit; background: transparent; color: inherit; }
|
|
450
|
-
.ed-block[dir="rtl"] { text-align: start; }
|
|
451
|
-
.ed-align-left { text-align: left; }
|
|
452
|
-
.ed-align-center { text-align: center; }
|
|
453
|
-
.ed-align-right { text-align: right; }
|
|
454
|
-
.ed-align-justify { text-align: justify; }
|
|
455
|
-
.ed-list { margin: 0 0 0.5em; padding-inline-start: 28px; }
|
|
456
|
-
.ed-list .ed-list { margin-block: 0; }
|
|
457
|
-
.ed-list-bullet { list-style-type: disc; }
|
|
458
|
-
.ed-list-bullet .ed-list-bullet { list-style-type: circle; }
|
|
459
|
-
.ed-list-bullet .ed-list-bullet .ed-list-bullet { list-style-type: square; }
|
|
460
|
-
.ed-list-ordered { list-style-type: decimal; }
|
|
461
|
-
.ed-list-ordered .ed-list-ordered { list-style-type: lower-alpha; }
|
|
462
|
-
.ed-list-ordered .ed-list-ordered .ed-list-ordered { list-style-type: lower-roman; }
|
|
463
|
-
.ed-listitem { margin: 0; min-height: 1.5em; }
|
|
464
|
-
.ed-table-wrap { margin: 0 0 0.5em; position: relative; }
|
|
465
|
-
.ed-table { width: 100%; border-collapse: collapse; table-layout: fixed; }
|
|
466
|
-
.ed-cell {
|
|
467
|
-
border: 1px solid #cbd5e1; padding: 6px 8px;
|
|
468
|
-
vertical-align: top; word-break: break-word; white-space: pre-wrap;
|
|
469
|
-
}
|
|
470
|
-
img[data-embed="image"] { max-width: 100%; }
|
|
471
|
-
.ed-block { position: relative; isolation: isolate; }
|
|
472
|
-
a { color: #2563eb; }
|
|
473
|
-
`;
|
|
474
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export {
|
|
2
|
-
documentToHtml,
|
|
3
|
-
documentToHtmlFragment,
|
|
4
|
-
documentToJson,
|
|
5
|
-
} from "./html";
|
|
6
|
-
export type { DocumentToHtmlOptions } from "./html";
|
|
7
|
-
export {
|
|
8
|
-
exportPdfFromDocument,
|
|
9
|
-
exportPdfFromElement,
|
|
10
|
-
serializePaginatedHtml,
|
|
11
|
-
} from "./pdf";
|
|
12
|
-
export type {
|
|
13
|
-
ExportPdfFromDocumentOptions,
|
|
14
|
-
ExportPdfFromElementOptions,
|
|
15
|
-
SerializePaginatedHtmlOptions,
|
|
16
|
-
} from "./pdf";
|
|
17
|
-
export { downloadBlob, downloadString } from "./download";
|