@sofereditor/export-docx 0.3.2 → 0.3.3
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 +121 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -4
- package/dist/index.d.ts +22 -4
- package/dist/index.js +122 -36
- package/dist/index.js.map +1 -1
- package/package.json +25 -8
- package/src/__tests__/docx.test.ts +0 -94
- package/src/docx.ts +0 -514
- package/src/index.ts +0 -2
- package/tsconfig.json +0 -8
- package/tsup.config.ts +0 -11
package/src/docx.ts
DELETED
|
@@ -1,514 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AlignValue,
|
|
3
|
-
DeltaOp,
|
|
4
|
-
ImageEmbed,
|
|
5
|
-
LegacySerializedDocument,
|
|
6
|
-
ListKind,
|
|
7
|
-
MarkAttrs,
|
|
8
|
-
SerializedBlock,
|
|
9
|
-
SerializedCell,
|
|
10
|
-
SerializedDocument,
|
|
11
|
-
} from "@sofereditor/core";
|
|
12
|
-
import {
|
|
13
|
-
DEFAULT_PAGE_SETTINGS,
|
|
14
|
-
isImageEmbed,
|
|
15
|
-
isLegacySerializedDocument,
|
|
16
|
-
pxToMm,
|
|
17
|
-
type PageSettings,
|
|
18
|
-
} from "@sofereditor/core";
|
|
19
|
-
import {
|
|
20
|
-
AlignmentType,
|
|
21
|
-
BorderStyle,
|
|
22
|
-
Document,
|
|
23
|
-
HeadingLevel,
|
|
24
|
-
ImageRun,
|
|
25
|
-
LevelFormat,
|
|
26
|
-
Packer,
|
|
27
|
-
Paragraph,
|
|
28
|
-
PageOrientation,
|
|
29
|
-
ShadingType,
|
|
30
|
-
Table,
|
|
31
|
-
TableCell,
|
|
32
|
-
TableRow,
|
|
33
|
-
TextRun,
|
|
34
|
-
UnderlineType,
|
|
35
|
-
VerticalAlign,
|
|
36
|
-
WidthType,
|
|
37
|
-
convertMillimetersToTwip,
|
|
38
|
-
} from "docx";
|
|
39
|
-
|
|
40
|
-
const BULLET_REF = "ed-bullet";
|
|
41
|
-
const ORDERED_REF = "ed-ordered";
|
|
42
|
-
|
|
43
|
-
export interface DocumentToDocxOptions {
|
|
44
|
-
/** Document title; embedded in core properties. */
|
|
45
|
-
title?: string;
|
|
46
|
-
/** Author for core properties. */
|
|
47
|
-
creator?: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Build a docx `Document` from a `SerializedDocument`. Output is a Blob you can
|
|
52
|
-
* download with `downloadBlob` from `@sofereditor/export-pdf`.
|
|
53
|
-
*
|
|
54
|
-
* Mappings:
|
|
55
|
-
* - paragraph / heading / blockquote / codeBlock → Paragraph (with `heading`,
|
|
56
|
-
* shading, monospace font for code)
|
|
57
|
-
* - listItem → Paragraph with `numbering: { reference, level }`
|
|
58
|
-
* - table → docx `Table`. `rowspan`/`colspan` map to TableCell's `rowSpan`/
|
|
59
|
-
* `columnSpan`; "covered" cells are skipped (docx infers cover from spans)
|
|
60
|
-
* - inline marks → TextRun props (bold, italics, underline, strike, color,
|
|
61
|
-
* font, size in half-points)
|
|
62
|
-
* - image embeds → ImageRun with the base64 payload decoded to Uint8Array
|
|
63
|
-
*/
|
|
64
|
-
export async function documentToDocxBlob(
|
|
65
|
-
doc: SerializedDocument | LegacySerializedDocument,
|
|
66
|
-
options: DocumentToDocxOptions = {},
|
|
67
|
-
): Promise<Blob> {
|
|
68
|
-
const built = buildDocument(normalize(doc), options);
|
|
69
|
-
return Packer.toBlob(built);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** Same as `documentToDocxBlob` but returns a Uint8Array (useful in Node/tests). */
|
|
73
|
-
export async function documentToDocxBuffer(
|
|
74
|
-
doc: SerializedDocument | LegacySerializedDocument,
|
|
75
|
-
options: DocumentToDocxOptions = {},
|
|
76
|
-
): Promise<Uint8Array> {
|
|
77
|
-
const built = buildDocument(normalize(doc), options);
|
|
78
|
-
const buf = await Packer.toBuffer(built);
|
|
79
|
-
// `Packer.toBuffer` returns a Node Buffer at runtime — coerce for consumers.
|
|
80
|
-
return buf instanceof Uint8Array ? buf : new Uint8Array(buf);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function normalize(doc: SerializedDocument | LegacySerializedDocument): SerializedDocument {
|
|
84
|
-
return isLegacySerializedDocument(doc) ? { blocks: doc } : doc;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function buildDocument(doc: SerializedDocument, options: DocumentToDocxOptions): Document {
|
|
88
|
-
const children = blocksToDocxChildren(doc);
|
|
89
|
-
const settings = doc.pageSettings ?? DEFAULT_PAGE_SETTINGS;
|
|
90
|
-
return new Document({
|
|
91
|
-
creator: options.creator,
|
|
92
|
-
title: options.title,
|
|
93
|
-
numbering: {
|
|
94
|
-
config: [
|
|
95
|
-
{
|
|
96
|
-
reference: BULLET_REF,
|
|
97
|
-
levels: buildBulletLevels(),
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
reference: ORDERED_REF,
|
|
101
|
-
levels: buildOrderedLevels(),
|
|
102
|
-
},
|
|
103
|
-
],
|
|
104
|
-
},
|
|
105
|
-
sections: [
|
|
106
|
-
{
|
|
107
|
-
properties: sectionPropertiesFor(settings),
|
|
108
|
-
children,
|
|
109
|
-
},
|
|
110
|
-
],
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function sectionPropertiesFor(settings: PageSettings) {
|
|
115
|
-
return {
|
|
116
|
-
page: {
|
|
117
|
-
size: {
|
|
118
|
-
width: convertMillimetersToTwip(pxToMm(settings.width)),
|
|
119
|
-
height: convertMillimetersToTwip(pxToMm(settings.height)),
|
|
120
|
-
orientation: PageOrientation.PORTRAIT,
|
|
121
|
-
},
|
|
122
|
-
margin: {
|
|
123
|
-
top: convertMillimetersToTwip(pxToMm(settings.marginTop)),
|
|
124
|
-
bottom: convertMillimetersToTwip(pxToMm(settings.marginBottom)),
|
|
125
|
-
left: convertMillimetersToTwip(pxToMm(settings.marginLeft)),
|
|
126
|
-
right: convertMillimetersToTwip(pxToMm(settings.marginRight)),
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function blocksToDocxChildren(doc: SerializedDocument): Array<Paragraph | Table> {
|
|
133
|
-
const out: Array<Paragraph | Table> = [];
|
|
134
|
-
const blocks = doc.blocks;
|
|
135
|
-
for (let i = 0; i < blocks.length; i++) {
|
|
136
|
-
const block = blocks[i];
|
|
137
|
-
switch (block.type) {
|
|
138
|
-
case "paragraph":
|
|
139
|
-
out.push(makeParagraph(block));
|
|
140
|
-
break;
|
|
141
|
-
case "heading":
|
|
142
|
-
out.push(makeHeading(block));
|
|
143
|
-
break;
|
|
144
|
-
case "blockquote":
|
|
145
|
-
out.push(makeBlockquote(block));
|
|
146
|
-
break;
|
|
147
|
-
case "codeBlock":
|
|
148
|
-
out.push(makeCodeBlock(block));
|
|
149
|
-
break;
|
|
150
|
-
case "listItem":
|
|
151
|
-
out.push(makeListItem(block));
|
|
152
|
-
break;
|
|
153
|
-
case "table":
|
|
154
|
-
out.push(makeTable(block));
|
|
155
|
-
break;
|
|
156
|
-
default:
|
|
157
|
-
out.push(makeParagraph(block));
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return out;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// ---------- block builders ----------
|
|
164
|
-
|
|
165
|
-
// School standard: Arial for all body text. Code blocks override with
|
|
166
|
-
// Consolas (monospace is more legible for code). Italics on blockquote is
|
|
167
|
-
// kept as a stylistic default.
|
|
168
|
-
const ARIAL: RunDefaults = { font: "Arial" };
|
|
169
|
-
|
|
170
|
-
function makeParagraph(block: SerializedBlock): Paragraph {
|
|
171
|
-
return new Paragraph({
|
|
172
|
-
alignment: alignFor(block.attrs.align),
|
|
173
|
-
bidirectional: block.attrs.dir === "rtl" ? true : undefined,
|
|
174
|
-
children: deltaToRuns(block.delta, ARIAL),
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function makeHeading(block: SerializedBlock): Paragraph {
|
|
179
|
-
const level = clampHeadingLevel(block.attrs.level);
|
|
180
|
-
return new Paragraph({
|
|
181
|
-
heading: HEADING_LEVELS[level - 1],
|
|
182
|
-
alignment: alignFor(block.attrs.align),
|
|
183
|
-
children: deltaToRuns(block.delta, ARIAL),
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function makeBlockquote(block: SerializedBlock): Paragraph {
|
|
188
|
-
return new Paragraph({
|
|
189
|
-
alignment: alignFor(block.attrs.align),
|
|
190
|
-
indent: { left: 480 }, // ~0.25"
|
|
191
|
-
border: {
|
|
192
|
-
left: { color: "CBD5E1", space: 12, style: BorderStyle.SINGLE, size: 12 },
|
|
193
|
-
},
|
|
194
|
-
children: deltaToRuns(block.delta, { ...ARIAL, italics: true }),
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function makeCodeBlock(block: SerializedBlock): Paragraph {
|
|
199
|
-
return new Paragraph({
|
|
200
|
-
spacing: { before: 100, after: 100 },
|
|
201
|
-
shading: { type: ShadingType.CLEAR, color: "auto", fill: "F1F5F9" },
|
|
202
|
-
children: deltaToRuns(block.delta, { font: "Consolas", size: 20 }),
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function makeListItem(block: SerializedBlock): Paragraph {
|
|
207
|
-
const kind: ListKind = block.attrs.listKind === "ordered" ? "ordered" : "bullet";
|
|
208
|
-
const reference = kind === "ordered" ? ORDERED_REF : BULLET_REF;
|
|
209
|
-
const level = clampListLevel(block.attrs.listLevel);
|
|
210
|
-
return new Paragraph({
|
|
211
|
-
numbering: { reference, level },
|
|
212
|
-
alignment: alignFor(block.attrs.align),
|
|
213
|
-
children: deltaToRuns(block.delta, ARIAL),
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function makeTable(block: SerializedBlock): Table {
|
|
218
|
-
const rows = typeof block.attrs.rows === "number" ? block.attrs.rows : 0;
|
|
219
|
-
const cols = typeof block.attrs.cols === "number" ? block.attrs.cols : 0;
|
|
220
|
-
const cells = block.cells ?? [];
|
|
221
|
-
|
|
222
|
-
const rowsOut: TableRow[] = [];
|
|
223
|
-
for (let r = 0; r < rows; r++) {
|
|
224
|
-
const cellsOut: TableCell[] = [];
|
|
225
|
-
for (let c = 0; c < cols; c++) {
|
|
226
|
-
const cell = cells[r * cols + c];
|
|
227
|
-
if (!cell) {
|
|
228
|
-
cellsOut.push(emptyCell());
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
if (cell.attrs?.covered) continue;
|
|
232
|
-
cellsOut.push(makeCell(cell));
|
|
233
|
-
}
|
|
234
|
-
rowsOut.push(new TableRow({ children: cellsOut }));
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return new Table({
|
|
238
|
-
rows: rowsOut,
|
|
239
|
-
width: { size: 100, type: WidthType.PERCENTAGE },
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function makeCell(cell: SerializedCell): TableCell {
|
|
244
|
-
const span = cell.attrs?.colspan && cell.attrs.colspan > 1 ? cell.attrs.colspan : 1;
|
|
245
|
-
const rowSpan = cell.attrs?.rowspan && cell.attrs.rowspan > 1 ? cell.attrs.rowspan : 1;
|
|
246
|
-
return new TableCell({
|
|
247
|
-
columnSpan: span,
|
|
248
|
-
rowSpan,
|
|
249
|
-
verticalAlign: VerticalAlign.TOP,
|
|
250
|
-
children: [new Paragraph({ children: deltaToRuns(cell.delta, ARIAL) })],
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function emptyCell(): TableCell {
|
|
255
|
-
return new TableCell({ children: [new Paragraph("")] });
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// ---------- inline ----------
|
|
259
|
-
|
|
260
|
-
interface RunDefaults {
|
|
261
|
-
bold?: boolean;
|
|
262
|
-
italics?: boolean;
|
|
263
|
-
font?: string;
|
|
264
|
-
size?: number;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function deltaToRuns(
|
|
268
|
-
delta: DeltaOp[],
|
|
269
|
-
defaults: RunDefaults = {},
|
|
270
|
-
): Array<TextRun | ImageRun> {
|
|
271
|
-
if (delta.length === 0) return [new TextRun("")];
|
|
272
|
-
const out: Array<TextRun | ImageRun> = [];
|
|
273
|
-
for (const op of delta) {
|
|
274
|
-
if (typeof op.insert === "string") {
|
|
275
|
-
if (op.insert.length === 0) continue;
|
|
276
|
-
out.push(makeTextRun(op.insert, op.attributes, defaults));
|
|
277
|
-
} else if (isImageEmbed(op.insert)) {
|
|
278
|
-
const embed = op.insert;
|
|
279
|
-
const img = makeImageRun(embed);
|
|
280
|
-
if (img) out.push(img);
|
|
281
|
-
// Captions are emitted as italic-smaller text on a new line right after
|
|
282
|
-
// the image. Word-style "Caption" paragraph would need a paragraph
|
|
283
|
-
// break, which we can't do mid-delta — visually correct but loses the
|
|
284
|
-
// semantic "Caption" style.
|
|
285
|
-
if (embed.caption && embed.caption.length > 0) {
|
|
286
|
-
out.push(
|
|
287
|
-
new TextRun({
|
|
288
|
-
text: embed.caption,
|
|
289
|
-
italics: true,
|
|
290
|
-
size: 18, // 9pt — typical caption size
|
|
291
|
-
break: 1,
|
|
292
|
-
}),
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
if (out.length === 0) out.push(new TextRun(""));
|
|
298
|
-
return out;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function makeTextRun(
|
|
302
|
-
text: string,
|
|
303
|
-
marks: MarkAttrs | undefined,
|
|
304
|
-
defaults: RunDefaults,
|
|
305
|
-
): TextRun {
|
|
306
|
-
const m = marks ?? {};
|
|
307
|
-
const fontSize = parseFontSizeToHalfPoints(m.fontSize);
|
|
308
|
-
// TextRun reads newlines via `break`; convert \n into break runs.
|
|
309
|
-
const segments = text.split("\n");
|
|
310
|
-
if (segments.length === 1) {
|
|
311
|
-
return new TextRun({
|
|
312
|
-
text,
|
|
313
|
-
bold: m.bold || defaults.bold,
|
|
314
|
-
italics: m.italic || defaults.italics,
|
|
315
|
-
underline: m.underline ? { type: UnderlineType.SINGLE } : undefined,
|
|
316
|
-
strike: m.strike,
|
|
317
|
-
color: cssColorToDocxHex(m.color),
|
|
318
|
-
font: m.fontFamily ?? defaults.font,
|
|
319
|
-
size: fontSize ?? defaults.size,
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
// Multi-line: rejoin via TextRun children — but TextRun expects a single text
|
|
323
|
-
// string. Approximate with non-printing line breaks: produce a sequence of
|
|
324
|
-
// TextRuns separated by `new TextRun({ break: 1 })` packed into the parent.
|
|
325
|
-
// Simpler: collapse to a single TextRun with the string, treating \n as a
|
|
326
|
-
// soft-break placeholder. Word renders \n inside a w:t as a literal space,
|
|
327
|
-
// so we replace with a small symbol-friendly fallback.
|
|
328
|
-
return new TextRun({
|
|
329
|
-
text: text.replace(/\n/g, " "),
|
|
330
|
-
bold: m.bold || defaults.bold,
|
|
331
|
-
italics: m.italic || defaults.italics,
|
|
332
|
-
underline: m.underline ? { type: UnderlineType.SINGLE } : undefined,
|
|
333
|
-
strike: m.strike,
|
|
334
|
-
color: cssColorToDocxHex(m.color),
|
|
335
|
-
font: m.fontFamily ?? defaults.font,
|
|
336
|
-
size: fontSize ?? defaults.size,
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function makeImageRun(embed: ImageEmbed): ImageRun | null {
|
|
341
|
-
const decoded = decodeDataUrl(embed.src);
|
|
342
|
-
if (!decoded) return null;
|
|
343
|
-
return new ImageRun({
|
|
344
|
-
data: decoded.bytes,
|
|
345
|
-
type: decoded.kind,
|
|
346
|
-
transformation: {
|
|
347
|
-
width: Math.max(1, Math.round(embed.width)),
|
|
348
|
-
height: Math.max(1, Math.round(embed.height)),
|
|
349
|
-
},
|
|
350
|
-
} as ConstructorParameters<typeof ImageRun>[0]);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
interface DecodedImage {
|
|
354
|
-
bytes: Uint8Array;
|
|
355
|
-
kind: "png" | "jpg" | "gif" | "bmp" | "svg";
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function decodeDataUrl(src: string): DecodedImage | null {
|
|
359
|
-
const m = /^data:image\/(png|jpe?g|gif|bmp|svg\+xml);base64,(.+)$/i.exec(src);
|
|
360
|
-
if (!m) return null;
|
|
361
|
-
const fmt = m[1].toLowerCase();
|
|
362
|
-
const kind: DecodedImage["kind"] =
|
|
363
|
-
fmt === "jpeg" || fmt === "jpg"
|
|
364
|
-
? "jpg"
|
|
365
|
-
: fmt === "svg+xml"
|
|
366
|
-
? "svg"
|
|
367
|
-
: (fmt as "png" | "gif" | "bmp");
|
|
368
|
-
const base64 = m[2];
|
|
369
|
-
const bytes = base64ToBytes(base64);
|
|
370
|
-
return { bytes, kind };
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function base64ToBytes(b64: string): Uint8Array {
|
|
374
|
-
if (typeof atob === "function") {
|
|
375
|
-
const bin = atob(b64);
|
|
376
|
-
const out = new Uint8Array(bin.length);
|
|
377
|
-
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
378
|
-
return out;
|
|
379
|
-
}
|
|
380
|
-
// Node fallback.
|
|
381
|
-
return new Uint8Array(Buffer.from(b64, "base64"));
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// ---------- attribute mapping ----------
|
|
385
|
-
|
|
386
|
-
const HEADING_LEVELS = [
|
|
387
|
-
HeadingLevel.HEADING_1,
|
|
388
|
-
HeadingLevel.HEADING_2,
|
|
389
|
-
HeadingLevel.HEADING_3,
|
|
390
|
-
HeadingLevel.HEADING_4,
|
|
391
|
-
HeadingLevel.HEADING_5,
|
|
392
|
-
HeadingLevel.HEADING_6,
|
|
393
|
-
];
|
|
394
|
-
|
|
395
|
-
function clampHeadingLevel(l: unknown): 1 | 2 | 3 | 4 | 5 | 6 {
|
|
396
|
-
if (typeof l !== "number") return 1;
|
|
397
|
-
const n = Math.trunc(l);
|
|
398
|
-
if (n < 1) return 1;
|
|
399
|
-
if (n > 6) return 6;
|
|
400
|
-
return n as 1 | 2 | 3 | 4 | 5 | 6;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function clampListLevel(l: unknown): number {
|
|
404
|
-
if (typeof l !== "number" || !Number.isFinite(l)) return 0;
|
|
405
|
-
return Math.max(0, Math.min(5, Math.trunc(l)));
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function alignFor(a: AlignValue | undefined): (typeof AlignmentType)[keyof typeof AlignmentType] | undefined {
|
|
409
|
-
switch (a) {
|
|
410
|
-
case "left":
|
|
411
|
-
return AlignmentType.LEFT;
|
|
412
|
-
case "center":
|
|
413
|
-
return AlignmentType.CENTER;
|
|
414
|
-
case "right":
|
|
415
|
-
return AlignmentType.RIGHT;
|
|
416
|
-
case "justify":
|
|
417
|
-
return AlignmentType.JUSTIFIED;
|
|
418
|
-
default:
|
|
419
|
-
return undefined;
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
function cssColorToDocxHex(color: string | undefined): string | undefined {
|
|
424
|
-
if (!color) return undefined;
|
|
425
|
-
// docx expects RRGGBB without leading '#'.
|
|
426
|
-
const c = color.trim();
|
|
427
|
-
if (/^#[0-9a-f]{6}$/i.test(c)) return c.slice(1).toUpperCase();
|
|
428
|
-
if (/^#[0-9a-f]{3}$/i.test(c)) {
|
|
429
|
-
// expand short hex
|
|
430
|
-
return c
|
|
431
|
-
.slice(1)
|
|
432
|
-
.split("")
|
|
433
|
-
.map((d) => d + d)
|
|
434
|
-
.join("")
|
|
435
|
-
.toUpperCase();
|
|
436
|
-
}
|
|
437
|
-
const rgb = /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i.exec(c);
|
|
438
|
-
if (rgb) {
|
|
439
|
-
const r = Number(rgb[1]);
|
|
440
|
-
const g = Number(rgb[2]);
|
|
441
|
-
const b = Number(rgb[3]);
|
|
442
|
-
return [r, g, b].map((n) => Math.max(0, Math.min(255, n)).toString(16).padStart(2, "0")).join("").toUpperCase();
|
|
443
|
-
}
|
|
444
|
-
// Named colors / other forms — let docx fall back to default.
|
|
445
|
-
return undefined;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
* docx sizes are in half-points (24 = 12pt). Parse common CSS sizes.
|
|
450
|
-
* Returns `undefined` when not parseable so callers can pick a default.
|
|
451
|
-
*/
|
|
452
|
-
function parseFontSizeToHalfPoints(size: string | undefined): number | undefined {
|
|
453
|
-
if (!size) return undefined;
|
|
454
|
-
const s = size.trim();
|
|
455
|
-
const m = /^(-?\d*\.?\d+)\s*(pt|px|em|rem)?$/i.exec(s);
|
|
456
|
-
if (!m) return undefined;
|
|
457
|
-
const n = Number(m[1]);
|
|
458
|
-
if (!Number.isFinite(n) || n <= 0) return undefined;
|
|
459
|
-
const unit = (m[2] ?? "pt").toLowerCase();
|
|
460
|
-
const pt =
|
|
461
|
-
unit === "pt"
|
|
462
|
-
? n
|
|
463
|
-
: unit === "px"
|
|
464
|
-
? n * 0.75 // CSS px → pt at 96dpi
|
|
465
|
-
: n * 12;
|
|
466
|
-
return Math.round(pt * 2);
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// ---------- list numbering definitions ----------
|
|
470
|
-
|
|
471
|
-
function buildBulletLevels(): Array<{
|
|
472
|
-
level: number;
|
|
473
|
-
format: (typeof LevelFormat)[keyof typeof LevelFormat];
|
|
474
|
-
text: string;
|
|
475
|
-
alignment: (typeof AlignmentType)[keyof typeof AlignmentType];
|
|
476
|
-
style?: { paragraph?: { indent?: { left: number; hanging: number } } };
|
|
477
|
-
}> {
|
|
478
|
-
const symbols = ["•", "◦", "▪", "•", "◦", "▪"];
|
|
479
|
-
return symbols.map((sym, i) => ({
|
|
480
|
-
level: i,
|
|
481
|
-
format: LevelFormat.BULLET,
|
|
482
|
-
text: sym,
|
|
483
|
-
alignment: AlignmentType.LEFT,
|
|
484
|
-
style: {
|
|
485
|
-
paragraph: { indent: { left: 360 + i * 360, hanging: 260 } },
|
|
486
|
-
},
|
|
487
|
-
}));
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
function buildOrderedLevels(): Array<{
|
|
491
|
-
level: number;
|
|
492
|
-
format: (typeof LevelFormat)[keyof typeof LevelFormat];
|
|
493
|
-
text: string;
|
|
494
|
-
alignment: (typeof AlignmentType)[keyof typeof AlignmentType];
|
|
495
|
-
style?: { paragraph?: { indent?: { left: number; hanging: number } } };
|
|
496
|
-
}> {
|
|
497
|
-
const formats = [
|
|
498
|
-
LevelFormat.DECIMAL,
|
|
499
|
-
LevelFormat.LOWER_LETTER,
|
|
500
|
-
LevelFormat.LOWER_ROMAN,
|
|
501
|
-
LevelFormat.DECIMAL,
|
|
502
|
-
LevelFormat.LOWER_LETTER,
|
|
503
|
-
LevelFormat.LOWER_ROMAN,
|
|
504
|
-
];
|
|
505
|
-
return formats.map((format, i) => ({
|
|
506
|
-
level: i,
|
|
507
|
-
format,
|
|
508
|
-
text: `%${i + 1}.`,
|
|
509
|
-
alignment: AlignmentType.LEFT,
|
|
510
|
-
style: {
|
|
511
|
-
paragraph: { indent: { left: 360 + i * 360, hanging: 260 } },
|
|
512
|
-
},
|
|
513
|
-
}));
|
|
514
|
-
}
|
package/src/index.ts
DELETED
package/tsconfig.json
DELETED
package/tsup.config.ts
DELETED