odf.js 1.0.0 → 1.2.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 +1040 -0
- package/dist/index.d.cts +186 -1
- package/dist/index.d.ts +186 -1
- package/dist/index.js +1006 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -2,6 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
let zod = require("zod");
|
|
3
3
|
let fast_xml_parser = require("fast-xml-parser");
|
|
4
4
|
let fflate = require("fflate");
|
|
5
|
+
let document_content_model = require("document-content-model");
|
|
5
6
|
//#region src/model/node.ts
|
|
6
7
|
const AttributeSchema = zod.z.object({
|
|
7
8
|
name: zod.z.string(),
|
|
@@ -329,10 +330,1024 @@ function encodePackage(pkg) {
|
|
|
329
330
|
return zod.z.encode(packageCodec, pkg);
|
|
330
331
|
}
|
|
331
332
|
//#endregion
|
|
333
|
+
//#region src/ns.ts
|
|
334
|
+
const ODF_NAMESPACES = Object.freeze({
|
|
335
|
+
office: "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
|
|
336
|
+
style: "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
|
|
337
|
+
text: "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
|
|
338
|
+
table: "urn:oasis:names:tc:opendocument:xmlns:table:1.0",
|
|
339
|
+
draw: "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",
|
|
340
|
+
fo: "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
|
|
341
|
+
svg: "urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0",
|
|
342
|
+
xlink: "http://www.w3.org/1999/xlink",
|
|
343
|
+
dc: "http://purl.org/dc/elements/1.1/",
|
|
344
|
+
meta: "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
|
|
345
|
+
number: "urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0",
|
|
346
|
+
chart: "urn:oasis:names:tc:opendocument:xmlns:chart:1.0",
|
|
347
|
+
dr3d: "urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0",
|
|
348
|
+
math: "http://www.w3.org/1998/Math/MathML",
|
|
349
|
+
form: "urn:oasis:names:tc:opendocument:xmlns:form:1.0",
|
|
350
|
+
script: "urn:oasis:names:tc:opendocument:xmlns:script:1.0",
|
|
351
|
+
config: "urn:oasis:names:tc:opendocument:xmlns:config:1.0",
|
|
352
|
+
presentation: "urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",
|
|
353
|
+
smil: "urn:oasis:names:tc:opendocument:xmlns:smil-compatible:1.0",
|
|
354
|
+
anim: "urn:oasis:names:tc:opendocument:xmlns:animation:1.0",
|
|
355
|
+
xforms: "http://www.w3.org/2002/xforms",
|
|
356
|
+
xsd: "http://www.w3.org/2001/XMLSchema",
|
|
357
|
+
xsi: "http://www.w3.org/2001/XMLSchema-instance",
|
|
358
|
+
manifest: "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"
|
|
359
|
+
});
|
|
360
|
+
function xmlnsAttributes(prefixes) {
|
|
361
|
+
const attrs = {};
|
|
362
|
+
for (const prefix of prefixes) attrs[`xmlns:${prefix}`] = ODF_NAMESPACES[prefix];
|
|
363
|
+
return attrs;
|
|
364
|
+
}
|
|
365
|
+
//#endregion
|
|
366
|
+
//#region src/media-type.ts
|
|
367
|
+
const ODF_MEDIA_TYPES = Object.freeze({
|
|
368
|
+
odt: "application/vnd.oasis.opendocument.text",
|
|
369
|
+
ott: "application/vnd.oasis.opendocument.text-template",
|
|
370
|
+
ods: "application/vnd.oasis.opendocument.spreadsheet",
|
|
371
|
+
ots: "application/vnd.oasis.opendocument.spreadsheet-template",
|
|
372
|
+
odp: "application/vnd.oasis.opendocument.presentation",
|
|
373
|
+
otp: "application/vnd.oasis.opendocument.presentation-template",
|
|
374
|
+
odg: "application/vnd.oasis.opendocument.graphics",
|
|
375
|
+
otg: "application/vnd.oasis.opendocument.graphics-template",
|
|
376
|
+
odf: "application/vnd.oasis.opendocument.formula",
|
|
377
|
+
otf: "application/vnd.oasis.opendocument.formula-template",
|
|
378
|
+
odm: "application/vnd.oasis.opendocument.text-master",
|
|
379
|
+
otm: "application/vnd.oasis.opendocument.text-master-template",
|
|
380
|
+
odb: "application/vnd.oasis.opendocument.base"
|
|
381
|
+
});
|
|
382
|
+
function isOdfExtension(extension) {
|
|
383
|
+
return Object.hasOwn(ODF_MEDIA_TYPES, extension);
|
|
384
|
+
}
|
|
385
|
+
function mediaTypeForExtension(extension) {
|
|
386
|
+
const lower = extension.toLowerCase();
|
|
387
|
+
return isOdfExtension(lower) ? ODF_MEDIA_TYPES[lower] : void 0;
|
|
388
|
+
}
|
|
389
|
+
//#endregion
|
|
390
|
+
//#region src/image/sniff.ts
|
|
391
|
+
const PNG_SIGNATURE = [
|
|
392
|
+
137,
|
|
393
|
+
80,
|
|
394
|
+
78,
|
|
395
|
+
71,
|
|
396
|
+
13,
|
|
397
|
+
10,
|
|
398
|
+
26,
|
|
399
|
+
10
|
|
400
|
+
];
|
|
401
|
+
const JPEG_SIGNATURE = [
|
|
402
|
+
255,
|
|
403
|
+
216,
|
|
404
|
+
255
|
|
405
|
+
];
|
|
406
|
+
function startsWith(bytes, signature) {
|
|
407
|
+
if (bytes.length < signature.length) return false;
|
|
408
|
+
for (let i = 0; i < signature.length; i++) if (bytes[i] !== signature[i]) return false;
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
function sniffImageFormat(bytes) {
|
|
412
|
+
if (startsWith(bytes, PNG_SIGNATURE)) return "png";
|
|
413
|
+
if (startsWith(bytes, JPEG_SIGNATURE)) return "jpeg";
|
|
414
|
+
}
|
|
415
|
+
//#endregion
|
|
416
|
+
//#region src/mimetype.ts
|
|
417
|
+
function readMimetype(pkg) {
|
|
418
|
+
const part = pkg.parts[MIMETYPE_PART];
|
|
419
|
+
if (part?.kind !== "binary") return;
|
|
420
|
+
return new TextDecoder("utf-8").decode(base64ToBytes(part.base64));
|
|
421
|
+
}
|
|
422
|
+
function writeMimetype(pkg, mediaType) {
|
|
423
|
+
pkg.parts[MIMETYPE_PART] = {
|
|
424
|
+
kind: "binary",
|
|
425
|
+
base64: bytesToBase64(new TextEncoder().encode(mediaType))
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
//#endregion
|
|
429
|
+
//#region src/xml/fragment.ts
|
|
430
|
+
function el(tag, attrs = {}, children = []) {
|
|
431
|
+
return {
|
|
432
|
+
type: "element",
|
|
433
|
+
tag,
|
|
434
|
+
attributes: Object.entries(attrs).map(([name, value]) => ({
|
|
435
|
+
name,
|
|
436
|
+
value
|
|
437
|
+
})),
|
|
438
|
+
children
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function txt(value) {
|
|
442
|
+
return {
|
|
443
|
+
type: "text",
|
|
444
|
+
value
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
//#endregion
|
|
448
|
+
//#region src/xml/entities.ts
|
|
449
|
+
function encodeXmlText(value) {
|
|
450
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
451
|
+
}
|
|
452
|
+
//#endregion
|
|
453
|
+
//#region src/manifest.ts
|
|
454
|
+
const ManifestEntrySchema = zod.z.object({
|
|
455
|
+
fullPath: zod.z.string(),
|
|
456
|
+
mediaType: zod.z.string(),
|
|
457
|
+
version: zod.z.string().optional()
|
|
458
|
+
});
|
|
459
|
+
const ManifestSchema = zod.z.object({
|
|
460
|
+
version: zod.z.string(),
|
|
461
|
+
entries: zod.z.array(ManifestEntrySchema)
|
|
462
|
+
});
|
|
463
|
+
const ManifestProblemSchema = zod.z.object({
|
|
464
|
+
severity: zod.z.enum(["error", "warning"]),
|
|
465
|
+
message: zod.z.string(),
|
|
466
|
+
path: zod.z.string().optional()
|
|
467
|
+
});
|
|
468
|
+
const DEFAULT_MANIFEST_VERSION = "1.3";
|
|
469
|
+
const STANDARD_XML_PART_NAMES = /* @__PURE__ */ new Set([
|
|
470
|
+
"content.xml",
|
|
471
|
+
"styles.xml",
|
|
472
|
+
"meta.xml",
|
|
473
|
+
"settings.xml"
|
|
474
|
+
]);
|
|
475
|
+
function findChildElement(nodes, tag) {
|
|
476
|
+
for (const node of nodes) if (node.type === "element" && node.tag === tag) return node;
|
|
477
|
+
}
|
|
478
|
+
function attrValue$1(element, name) {
|
|
479
|
+
return element.attributes.find((attribute) => attribute.name === name)?.value;
|
|
480
|
+
}
|
|
481
|
+
function readManifest(pkg) {
|
|
482
|
+
const part = pkg.parts[MANIFEST_PART];
|
|
483
|
+
if (part?.kind !== "xml") throw new Error(`package has no ${MANIFEST_PART} XML part to read`);
|
|
484
|
+
const root = findChildElement(part.nodes, "manifest:manifest");
|
|
485
|
+
if (root === void 0) throw new Error(`${MANIFEST_PART} has no manifest:manifest root element`);
|
|
486
|
+
const version = attrValue$1(root, "manifest:version");
|
|
487
|
+
if (version === void 0) throw new Error(`${MANIFEST_PART}'s manifest:manifest root is missing the required manifest:version attribute`);
|
|
488
|
+
const entries = [];
|
|
489
|
+
for (const child of root.children) {
|
|
490
|
+
if (child.type !== "element" || child.tag !== "manifest:file-entry") continue;
|
|
491
|
+
const fullPath = attrValue$1(child, "manifest:full-path");
|
|
492
|
+
const mediaType = attrValue$1(child, "manifest:media-type");
|
|
493
|
+
if (fullPath === void 0 || mediaType === void 0) throw new Error(`${MANIFEST_PART} has a manifest:file-entry missing manifest:full-path or manifest:media-type`);
|
|
494
|
+
const entryVersion = attrValue$1(child, "manifest:version");
|
|
495
|
+
entries.push(entryVersion === void 0 ? {
|
|
496
|
+
fullPath,
|
|
497
|
+
mediaType
|
|
498
|
+
} : {
|
|
499
|
+
fullPath,
|
|
500
|
+
mediaType,
|
|
501
|
+
version: entryVersion
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
return {
|
|
505
|
+
version,
|
|
506
|
+
entries
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
function subdocumentDirectories(partPaths) {
|
|
510
|
+
const dirs = [];
|
|
511
|
+
for (const path of partPaths) if (path.endsWith("/content.xml")) dirs.push(path.slice(0, path.length - 11));
|
|
512
|
+
return dirs;
|
|
513
|
+
}
|
|
514
|
+
function resolvePartMediaType(path, bytes, overrides) {
|
|
515
|
+
const override = overrides?.[path];
|
|
516
|
+
if (override !== void 0) return override;
|
|
517
|
+
const baseName = path.slice(path.lastIndexOf("/") + 1);
|
|
518
|
+
if (STANDARD_XML_PART_NAMES.has(baseName)) return "text/xml";
|
|
519
|
+
const dotIndex = baseName.lastIndexOf(".");
|
|
520
|
+
const extension = dotIndex === -1 ? "" : baseName.slice(dotIndex + 1);
|
|
521
|
+
const byExtension = extension === "" ? void 0 : mediaTypeForExtension(extension);
|
|
522
|
+
if (byExtension !== void 0) return byExtension;
|
|
523
|
+
if (bytes !== void 0) {
|
|
524
|
+
const sniffed = sniffImageFormat(bytes);
|
|
525
|
+
if (sniffed === "png") return "image/png";
|
|
526
|
+
if (sniffed === "jpeg") return "image/jpeg";
|
|
527
|
+
}
|
|
528
|
+
return "";
|
|
529
|
+
}
|
|
530
|
+
function buildManifest(pkg, options = {}) {
|
|
531
|
+
const version = options.version ?? DEFAULT_MANIFEST_VERSION;
|
|
532
|
+
const documentMediaType = options.documentMediaType ?? readMimetype(pkg);
|
|
533
|
+
if (documentMediaType === void 0) throw new Error("buildManifest: package has no \"mimetype\" part and no documentMediaType override was supplied -- the manifest root entry requires a known document media type");
|
|
534
|
+
const entries = [{
|
|
535
|
+
fullPath: "/",
|
|
536
|
+
mediaType: documentMediaType,
|
|
537
|
+
version
|
|
538
|
+
}];
|
|
539
|
+
const partPaths = Object.keys(pkg.parts);
|
|
540
|
+
for (const dir of new Set(subdocumentDirectories(partPaths))) entries.push({
|
|
541
|
+
fullPath: dir,
|
|
542
|
+
mediaType: resolvePartMediaType(dir, void 0, options.mediaTypeOverrides)
|
|
543
|
+
});
|
|
544
|
+
for (const [path, part] of Object.entries(pkg.parts)) {
|
|
545
|
+
if (path === "mimetype" || path === "META-INF/manifest.xml") continue;
|
|
546
|
+
const bytes = part.kind === "binary" ? base64ToBytes(part.base64) : void 0;
|
|
547
|
+
entries.push({
|
|
548
|
+
fullPath: path,
|
|
549
|
+
mediaType: resolvePartMediaType(path, bytes, options.mediaTypeOverrides)
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
return {
|
|
553
|
+
version,
|
|
554
|
+
entries
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
function buildManifestNodes(manifest) {
|
|
558
|
+
const fileEntries = manifest.entries.map((entry) => {
|
|
559
|
+
const attrs = { "manifest:full-path": encodeXmlText(entry.fullPath) };
|
|
560
|
+
if (entry.version !== void 0) attrs["manifest:version"] = encodeXmlText(entry.version);
|
|
561
|
+
attrs["manifest:media-type"] = encodeXmlText(entry.mediaType);
|
|
562
|
+
return el("manifest:file-entry", attrs);
|
|
563
|
+
});
|
|
564
|
+
return [{
|
|
565
|
+
type: "declaration",
|
|
566
|
+
attributes: [{
|
|
567
|
+
name: "version",
|
|
568
|
+
value: "1.0"
|
|
569
|
+
}, {
|
|
570
|
+
name: "encoding",
|
|
571
|
+
value: "UTF-8"
|
|
572
|
+
}]
|
|
573
|
+
}, el("manifest:manifest", {
|
|
574
|
+
...xmlnsAttributes(["manifest"]),
|
|
575
|
+
"manifest:version": encodeXmlText(manifest.version)
|
|
576
|
+
}, fileEntries)];
|
|
577
|
+
}
|
|
578
|
+
function writeManifest(pkg, manifest) {
|
|
579
|
+
pkg.parts[MANIFEST_PART] = {
|
|
580
|
+
kind: "xml",
|
|
581
|
+
nodes: buildManifestNodes(manifest)
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
function syncManifest(pkg, options) {
|
|
585
|
+
writeManifest(pkg, buildManifest(pkg, options));
|
|
586
|
+
}
|
|
587
|
+
function validateManifest(pkg) {
|
|
588
|
+
const problems = [];
|
|
589
|
+
const manifestPart = pkg.parts[MANIFEST_PART];
|
|
590
|
+
if (manifestPart === void 0) {
|
|
591
|
+
problems.push({
|
|
592
|
+
severity: "error",
|
|
593
|
+
message: `package has no ${MANIFEST_PART} part`
|
|
594
|
+
});
|
|
595
|
+
return problems;
|
|
596
|
+
}
|
|
597
|
+
if (manifestPart.kind !== "xml") {
|
|
598
|
+
problems.push({
|
|
599
|
+
severity: "error",
|
|
600
|
+
message: `${MANIFEST_PART} part is not XML`
|
|
601
|
+
});
|
|
602
|
+
return problems;
|
|
603
|
+
}
|
|
604
|
+
const root = findChildElement(manifestPart.nodes, "manifest:manifest");
|
|
605
|
+
if (root === void 0) {
|
|
606
|
+
problems.push({
|
|
607
|
+
severity: "error",
|
|
608
|
+
message: `${MANIFEST_PART} has no manifest:manifest root element`
|
|
609
|
+
});
|
|
610
|
+
return problems;
|
|
611
|
+
}
|
|
612
|
+
let manifest;
|
|
613
|
+
try {
|
|
614
|
+
manifest = readManifest(pkg);
|
|
615
|
+
} catch (error) {
|
|
616
|
+
problems.push({
|
|
617
|
+
severity: "error",
|
|
618
|
+
message: `failed to parse ${MANIFEST_PART}: ${error instanceof Error ? error.message : String(error)}`
|
|
619
|
+
});
|
|
620
|
+
return problems;
|
|
621
|
+
}
|
|
622
|
+
const rootEntry = manifest.entries.find((entry) => entry.fullPath === "/");
|
|
623
|
+
if (rootEntry === void 0) problems.push({
|
|
624
|
+
severity: "error",
|
|
625
|
+
message: "manifest has no root (\"/\") entry"
|
|
626
|
+
});
|
|
627
|
+
else {
|
|
628
|
+
const documentMediaType = readMimetype(pkg);
|
|
629
|
+
if (documentMediaType !== void 0 && documentMediaType !== rootEntry.mediaType) problems.push({
|
|
630
|
+
severity: "error",
|
|
631
|
+
message: `manifest root entry media type "${rootEntry.mediaType}" does not match the mimetype part's media type "${documentMediaType}"`,
|
|
632
|
+
path: "/"
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
const partPaths = new Set(Object.keys(pkg.parts).filter((path) => path !== "mimetype" && path !== "META-INF/manifest.xml"));
|
|
636
|
+
const manifestPaths = new Set(manifest.entries.map((entry) => entry.fullPath));
|
|
637
|
+
for (const entry of manifest.entries) {
|
|
638
|
+
if (entry.fullPath === "/" || entry.fullPath.endsWith("/")) continue;
|
|
639
|
+
if (!partPaths.has(entry.fullPath)) problems.push({
|
|
640
|
+
severity: "warning",
|
|
641
|
+
message: `manifest lists "${entry.fullPath}" but the package has no such part`,
|
|
642
|
+
path: entry.fullPath
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
for (const path of partPaths) if (!manifestPaths.has(path)) problems.push({
|
|
646
|
+
severity: "warning",
|
|
647
|
+
message: `package has part "${path}" not listed in the manifest`,
|
|
648
|
+
path
|
|
649
|
+
});
|
|
650
|
+
for (const child of root.children) {
|
|
651
|
+
if (child.type !== "element" || child.tag !== "manifest:file-entry") continue;
|
|
652
|
+
if (!child.children.some((grandchild) => grandchild.type === "element" && grandchild.tag === "manifest:encryption-data")) continue;
|
|
653
|
+
const fullPath = attrValue$1(child, "manifest:full-path");
|
|
654
|
+
if (fullPath === void 0) continue;
|
|
655
|
+
problems.push({
|
|
656
|
+
severity: "warning",
|
|
657
|
+
message: `entry "${fullPath}" carries manifest:encryption-data -- odf.js does not implement ODF encryption/decryption`,
|
|
658
|
+
path: fullPath
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
return problems;
|
|
662
|
+
}
|
|
663
|
+
function setDocumentMediaType(pkg, mediaType, version = DEFAULT_MANIFEST_VERSION) {
|
|
664
|
+
writeMimetype(pkg, mediaType);
|
|
665
|
+
const rootEntry = {
|
|
666
|
+
fullPath: "/",
|
|
667
|
+
mediaType,
|
|
668
|
+
version
|
|
669
|
+
};
|
|
670
|
+
if (pkg.parts["META-INF/manifest.xml"] === void 0) {
|
|
671
|
+
writeManifest(pkg, {
|
|
672
|
+
version,
|
|
673
|
+
entries: [rootEntry]
|
|
674
|
+
});
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const existing = readManifest(pkg);
|
|
678
|
+
const rootIndex = existing.entries.findIndex((entry) => entry.fullPath === "/");
|
|
679
|
+
writeManifest(pkg, {
|
|
680
|
+
version,
|
|
681
|
+
entries: rootIndex === -1 ? [rootEntry, ...existing.entries] : existing.entries.map((entry, index) => index === rootIndex ? rootEntry : entry)
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
//#endregion
|
|
685
|
+
//#region src/styles/properties.ts
|
|
686
|
+
const StylePropertiesSchema = zod.z.object({
|
|
687
|
+
bold: zod.z.boolean().optional(),
|
|
688
|
+
italic: zod.z.boolean().optional(),
|
|
689
|
+
underline: zod.z.boolean().optional(),
|
|
690
|
+
strike: zod.z.boolean().optional(),
|
|
691
|
+
fontFamily: zod.z.string().optional(),
|
|
692
|
+
sizePt: zod.z.number().optional(),
|
|
693
|
+
color: document_content_model.ColorSchema.optional(),
|
|
694
|
+
alignment: document_content_model.AlignmentSchema.optional(),
|
|
695
|
+
spacingBeforePt: zod.z.number().optional(),
|
|
696
|
+
spacingAfterPt: zod.z.number().optional(),
|
|
697
|
+
lineSpacing: zod.z.number().optional(),
|
|
698
|
+
indentLeftPt: zod.z.number().optional(),
|
|
699
|
+
indentFirstLinePt: zod.z.number().optional()
|
|
700
|
+
});
|
|
701
|
+
const ATTR = {
|
|
702
|
+
fontWeight: "fo:font-weight",
|
|
703
|
+
fontStyle: "fo:font-style",
|
|
704
|
+
underlineStyle: "style:text-underline-style",
|
|
705
|
+
underlineWidth: "style:text-underline-width",
|
|
706
|
+
underlineColor: "style:text-underline-color",
|
|
707
|
+
lineThroughStyle: "style:text-line-through-style",
|
|
708
|
+
lineThroughType: "style:text-line-through-type",
|
|
709
|
+
fontFamily: "fo:font-family",
|
|
710
|
+
fontSize: "fo:font-size",
|
|
711
|
+
color: "fo:color",
|
|
712
|
+
textAlign: "fo:text-align",
|
|
713
|
+
marginTop: "fo:margin-top",
|
|
714
|
+
marginBottom: "fo:margin-bottom",
|
|
715
|
+
lineHeight: "fo:line-height",
|
|
716
|
+
marginLeft: "fo:margin-left",
|
|
717
|
+
textIndent: "fo:text-indent"
|
|
718
|
+
};
|
|
719
|
+
const TEXT_ATTR_NAMES = /* @__PURE__ */ new Set([
|
|
720
|
+
ATTR.fontWeight,
|
|
721
|
+
ATTR.fontStyle,
|
|
722
|
+
ATTR.underlineStyle,
|
|
723
|
+
ATTR.underlineWidth,
|
|
724
|
+
ATTR.underlineColor,
|
|
725
|
+
ATTR.lineThroughStyle,
|
|
726
|
+
ATTR.lineThroughType,
|
|
727
|
+
ATTR.fontFamily,
|
|
728
|
+
ATTR.fontSize,
|
|
729
|
+
ATTR.color
|
|
730
|
+
]);
|
|
731
|
+
const PARAGRAPH_ATTR_NAMES = /* @__PURE__ */ new Set([
|
|
732
|
+
ATTR.textAlign,
|
|
733
|
+
ATTR.marginTop,
|
|
734
|
+
ATTR.marginBottom,
|
|
735
|
+
ATTR.lineHeight,
|
|
736
|
+
ATTR.marginLeft,
|
|
737
|
+
ATTR.textIndent
|
|
738
|
+
]);
|
|
739
|
+
function attributeMap(element) {
|
|
740
|
+
const map = /* @__PURE__ */ new Map();
|
|
741
|
+
for (const attribute of element.attributes) map.set(attribute.name, attribute.value);
|
|
742
|
+
return map;
|
|
743
|
+
}
|
|
744
|
+
const LENGTH_PATTERN = /^(-?(?:\d+(?:\.\d+)?|\.\d+))(cm|mm|in|pt|pc|px)$/;
|
|
745
|
+
function unitToPtFactor(unit) {
|
|
746
|
+
switch (unit) {
|
|
747
|
+
case "pt": return 1;
|
|
748
|
+
case "in": return 72;
|
|
749
|
+
case "cm": return 72 / 2.54;
|
|
750
|
+
case "mm": return 72 / 25.4;
|
|
751
|
+
case "pc": return 12;
|
|
752
|
+
case "px": return .75;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
function isLengthUnit(value) {
|
|
756
|
+
return value === "cm" || value === "mm" || value === "in" || value === "pt" || value === "pc" || value === "px";
|
|
757
|
+
}
|
|
758
|
+
function parseLength(value) {
|
|
759
|
+
const match = LENGTH_PATTERN.exec(value);
|
|
760
|
+
if (match === null) return;
|
|
761
|
+
const numeric = match[1];
|
|
762
|
+
const unit = match[2];
|
|
763
|
+
if (numeric === void 0 || unit === void 0 || !isLengthUnit(unit)) return;
|
|
764
|
+
return Number(numeric) * unitToPtFactor(unit);
|
|
765
|
+
}
|
|
766
|
+
function formatPt(valuePt) {
|
|
767
|
+
return `${valuePt}pt`;
|
|
768
|
+
}
|
|
769
|
+
const PERCENTAGE_PATTERN = /^(-?(?:\d+(?:\.\d+)?|\.\d+))%$/;
|
|
770
|
+
function parsePercentageMultiplier(value) {
|
|
771
|
+
const match = PERCENTAGE_PATTERN.exec(value);
|
|
772
|
+
if (match === null) return;
|
|
773
|
+
const numeric = match[1];
|
|
774
|
+
if (numeric === void 0) return;
|
|
775
|
+
return Number(numeric) / 100;
|
|
776
|
+
}
|
|
777
|
+
function formatPercentageMultiplier(multiplier) {
|
|
778
|
+
return `${multiplier * 100}%`;
|
|
779
|
+
}
|
|
780
|
+
const COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
|
|
781
|
+
function parseColor(value) {
|
|
782
|
+
if (!COLOR_PATTERN.test(value)) return;
|
|
783
|
+
return (0, document_content_model.rgbHexToColor)(value);
|
|
784
|
+
}
|
|
785
|
+
function formatColor(color) {
|
|
786
|
+
return `#${(0, document_content_model.colorToRgbHex)(color)}`;
|
|
787
|
+
}
|
|
788
|
+
function parseLineDecoration(style, companionA, companionAOnValue, companionB, companionBOnValue) {
|
|
789
|
+
if (style === void 0 && companionA === void 0 && companionB === void 0) return;
|
|
790
|
+
if (style === "solid" && (companionA === void 0 || companionA === companionAOnValue) && (companionB === void 0 || companionB === companionBOnValue)) return true;
|
|
791
|
+
if (style === "none" && companionA === void 0 && companionB === void 0) return false;
|
|
792
|
+
return "unknown";
|
|
793
|
+
}
|
|
794
|
+
const RISKY_STYLE_ELEMENT_ATTRS = /* @__PURE__ */ new Set(["style:master-page-name", "style:next-style-name"]);
|
|
795
|
+
function parseTextProperties(element) {
|
|
796
|
+
const attrs = attributeMap(element);
|
|
797
|
+
const properties = {};
|
|
798
|
+
let hasUnknown = false;
|
|
799
|
+
for (const name of attrs.keys()) if (!TEXT_ATTR_NAMES.has(name)) hasUnknown = true;
|
|
800
|
+
const fontWeight = attrs.get(ATTR.fontWeight);
|
|
801
|
+
if (fontWeight === "bold") properties.bold = true;
|
|
802
|
+
else if (fontWeight === "normal") properties.bold = false;
|
|
803
|
+
else if (fontWeight !== void 0) hasUnknown = true;
|
|
804
|
+
const fontStyle = attrs.get(ATTR.fontStyle);
|
|
805
|
+
if (fontStyle === "italic") properties.italic = true;
|
|
806
|
+
else if (fontStyle === "normal") properties.italic = false;
|
|
807
|
+
else if (fontStyle !== void 0) hasUnknown = true;
|
|
808
|
+
const underline = parseLineDecoration(attrs.get(ATTR.underlineStyle), attrs.get(ATTR.underlineWidth), "auto", attrs.get(ATTR.underlineColor), "font-color");
|
|
809
|
+
if (underline === "unknown") hasUnknown = true;
|
|
810
|
+
else if (underline !== void 0) properties.underline = underline;
|
|
811
|
+
const strike = parseLineDecoration(attrs.get(ATTR.lineThroughStyle), attrs.get(ATTR.lineThroughType), "single", void 0, "");
|
|
812
|
+
if (strike === "unknown") hasUnknown = true;
|
|
813
|
+
else if (strike !== void 0) properties.strike = strike;
|
|
814
|
+
const fontFamily = attrs.get(ATTR.fontFamily);
|
|
815
|
+
if (fontFamily !== void 0) properties.fontFamily = fontFamily;
|
|
816
|
+
const fontSize = attrs.get(ATTR.fontSize);
|
|
817
|
+
if (fontSize !== void 0) {
|
|
818
|
+
const pt = parseLength(fontSize);
|
|
819
|
+
if (pt === void 0) hasUnknown = true;
|
|
820
|
+
else properties.sizePt = pt;
|
|
821
|
+
}
|
|
822
|
+
const color = attrs.get(ATTR.color);
|
|
823
|
+
if (color !== void 0) {
|
|
824
|
+
const parsed = parseColor(color);
|
|
825
|
+
if (parsed === void 0) hasUnknown = true;
|
|
826
|
+
else properties.color = parsed;
|
|
827
|
+
}
|
|
828
|
+
return {
|
|
829
|
+
properties,
|
|
830
|
+
hasUnknown
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
function parseParagraphProperties(element) {
|
|
834
|
+
const attrs = attributeMap(element);
|
|
835
|
+
const properties = {};
|
|
836
|
+
let hasUnknown = false;
|
|
837
|
+
for (const name of attrs.keys()) if (!PARAGRAPH_ATTR_NAMES.has(name)) hasUnknown = true;
|
|
838
|
+
const textAlign = attrs.get(ATTR.textAlign);
|
|
839
|
+
if (textAlign === "left" || textAlign === "center" || textAlign === "right" || textAlign === "justify") properties.alignment = textAlign;
|
|
840
|
+
else if (textAlign !== void 0) hasUnknown = true;
|
|
841
|
+
const marginTop = attrs.get(ATTR.marginTop);
|
|
842
|
+
if (marginTop !== void 0) {
|
|
843
|
+
const pt = parseLength(marginTop);
|
|
844
|
+
if (pt === void 0) hasUnknown = true;
|
|
845
|
+
else properties.spacingBeforePt = pt;
|
|
846
|
+
}
|
|
847
|
+
const marginBottom = attrs.get(ATTR.marginBottom);
|
|
848
|
+
if (marginBottom !== void 0) {
|
|
849
|
+
const pt = parseLength(marginBottom);
|
|
850
|
+
if (pt === void 0) hasUnknown = true;
|
|
851
|
+
else properties.spacingAfterPt = pt;
|
|
852
|
+
}
|
|
853
|
+
const marginLeft = attrs.get(ATTR.marginLeft);
|
|
854
|
+
if (marginLeft !== void 0) {
|
|
855
|
+
const pt = parseLength(marginLeft);
|
|
856
|
+
if (pt === void 0) hasUnknown = true;
|
|
857
|
+
else properties.indentLeftPt = pt;
|
|
858
|
+
}
|
|
859
|
+
const textIndent = attrs.get(ATTR.textIndent);
|
|
860
|
+
if (textIndent !== void 0) {
|
|
861
|
+
const pt = parseLength(textIndent);
|
|
862
|
+
if (pt === void 0) hasUnknown = true;
|
|
863
|
+
else properties.indentFirstLinePt = pt;
|
|
864
|
+
}
|
|
865
|
+
const lineHeight = attrs.get(ATTR.lineHeight);
|
|
866
|
+
if (lineHeight !== void 0) {
|
|
867
|
+
const multiplier = parsePercentageMultiplier(lineHeight);
|
|
868
|
+
if (multiplier === void 0) hasUnknown = true;
|
|
869
|
+
else properties.lineSpacing = multiplier;
|
|
870
|
+
}
|
|
871
|
+
return {
|
|
872
|
+
properties,
|
|
873
|
+
hasUnknown
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
function parseStyleElementProperties(styleElement) {
|
|
877
|
+
let properties = {};
|
|
878
|
+
let hasUnknown = false;
|
|
879
|
+
for (const attribute of styleElement.attributes) if (RISKY_STYLE_ELEMENT_ATTRS.has(attribute.name)) hasUnknown = true;
|
|
880
|
+
for (const child of styleElement.children) {
|
|
881
|
+
if (child.type !== "element") continue;
|
|
882
|
+
if (child.tag === "style:text-properties") {
|
|
883
|
+
const result = parseTextProperties(child);
|
|
884
|
+
properties = {
|
|
885
|
+
...properties,
|
|
886
|
+
...result.properties
|
|
887
|
+
};
|
|
888
|
+
if (result.hasUnknown) hasUnknown = true;
|
|
889
|
+
} else if (child.tag === "style:paragraph-properties") {
|
|
890
|
+
const result = parseParagraphProperties(child);
|
|
891
|
+
properties = {
|
|
892
|
+
...properties,
|
|
893
|
+
...result.properties
|
|
894
|
+
};
|
|
895
|
+
if (result.hasUnknown) hasUnknown = true;
|
|
896
|
+
} else hasUnknown = true;
|
|
897
|
+
}
|
|
898
|
+
return {
|
|
899
|
+
properties,
|
|
900
|
+
hasUnknown
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
function textPropertiesToAttributes(properties) {
|
|
904
|
+
const attributes = [];
|
|
905
|
+
if (properties.bold !== void 0) attributes.push({
|
|
906
|
+
name: ATTR.fontWeight,
|
|
907
|
+
value: properties.bold ? "bold" : "normal"
|
|
908
|
+
});
|
|
909
|
+
if (properties.italic !== void 0) attributes.push({
|
|
910
|
+
name: ATTR.fontStyle,
|
|
911
|
+
value: properties.italic ? "italic" : "normal"
|
|
912
|
+
});
|
|
913
|
+
if (properties.underline !== void 0) if (properties.underline) {
|
|
914
|
+
attributes.push({
|
|
915
|
+
name: ATTR.underlineStyle,
|
|
916
|
+
value: "solid"
|
|
917
|
+
});
|
|
918
|
+
attributes.push({
|
|
919
|
+
name: ATTR.underlineWidth,
|
|
920
|
+
value: "auto"
|
|
921
|
+
});
|
|
922
|
+
attributes.push({
|
|
923
|
+
name: ATTR.underlineColor,
|
|
924
|
+
value: "font-color"
|
|
925
|
+
});
|
|
926
|
+
} else attributes.push({
|
|
927
|
+
name: ATTR.underlineStyle,
|
|
928
|
+
value: "none"
|
|
929
|
+
});
|
|
930
|
+
if (properties.strike !== void 0) if (properties.strike) {
|
|
931
|
+
attributes.push({
|
|
932
|
+
name: ATTR.lineThroughStyle,
|
|
933
|
+
value: "solid"
|
|
934
|
+
});
|
|
935
|
+
attributes.push({
|
|
936
|
+
name: ATTR.lineThroughType,
|
|
937
|
+
value: "single"
|
|
938
|
+
});
|
|
939
|
+
} else attributes.push({
|
|
940
|
+
name: ATTR.lineThroughStyle,
|
|
941
|
+
value: "none"
|
|
942
|
+
});
|
|
943
|
+
if (properties.fontFamily !== void 0) attributes.push({
|
|
944
|
+
name: ATTR.fontFamily,
|
|
945
|
+
value: encodeXmlText(properties.fontFamily)
|
|
946
|
+
});
|
|
947
|
+
if (properties.sizePt !== void 0) attributes.push({
|
|
948
|
+
name: ATTR.fontSize,
|
|
949
|
+
value: formatPt(properties.sizePt)
|
|
950
|
+
});
|
|
951
|
+
if (properties.color !== void 0) attributes.push({
|
|
952
|
+
name: ATTR.color,
|
|
953
|
+
value: formatColor(properties.color)
|
|
954
|
+
});
|
|
955
|
+
return attributes;
|
|
956
|
+
}
|
|
957
|
+
function paragraphPropertiesToAttributes(properties) {
|
|
958
|
+
const attributes = [];
|
|
959
|
+
if (properties.alignment !== void 0) attributes.push({
|
|
960
|
+
name: ATTR.textAlign,
|
|
961
|
+
value: properties.alignment
|
|
962
|
+
});
|
|
963
|
+
if (properties.spacingBeforePt !== void 0) attributes.push({
|
|
964
|
+
name: ATTR.marginTop,
|
|
965
|
+
value: formatPt(properties.spacingBeforePt)
|
|
966
|
+
});
|
|
967
|
+
if (properties.spacingAfterPt !== void 0) attributes.push({
|
|
968
|
+
name: ATTR.marginBottom,
|
|
969
|
+
value: formatPt(properties.spacingAfterPt)
|
|
970
|
+
});
|
|
971
|
+
if (properties.lineSpacing !== void 0) attributes.push({
|
|
972
|
+
name: ATTR.lineHeight,
|
|
973
|
+
value: formatPercentageMultiplier(properties.lineSpacing)
|
|
974
|
+
});
|
|
975
|
+
if (properties.indentLeftPt !== void 0) attributes.push({
|
|
976
|
+
name: ATTR.marginLeft,
|
|
977
|
+
value: formatPt(properties.indentLeftPt)
|
|
978
|
+
});
|
|
979
|
+
if (properties.indentFirstLinePt !== void 0) attributes.push({
|
|
980
|
+
name: ATTR.textIndent,
|
|
981
|
+
value: formatPt(properties.indentFirstLinePt)
|
|
982
|
+
});
|
|
983
|
+
return attributes;
|
|
984
|
+
}
|
|
985
|
+
//#endregion
|
|
986
|
+
//#region src/styles/serialize.ts
|
|
987
|
+
function attributesToRecord(attributes) {
|
|
988
|
+
const record = {};
|
|
989
|
+
for (const attribute of attributes) record[attribute.name] = attribute.value;
|
|
990
|
+
return record;
|
|
991
|
+
}
|
|
992
|
+
function buildStylePropertyElements(properties) {
|
|
993
|
+
const elements = [];
|
|
994
|
+
const paragraphAttributes = paragraphPropertiesToAttributes(properties);
|
|
995
|
+
if (paragraphAttributes.length > 0) elements.push(el("style:paragraph-properties", attributesToRecord(paragraphAttributes)));
|
|
996
|
+
const textAttributes = textPropertiesToAttributes(properties);
|
|
997
|
+
if (textAttributes.length > 0) elements.push(el("style:text-properties", attributesToRecord(textAttributes)));
|
|
998
|
+
return elements;
|
|
999
|
+
}
|
|
1000
|
+
function canonicalPropertiesString(properties) {
|
|
1001
|
+
return [...paragraphPropertiesToAttributes(properties), ...textPropertiesToAttributes(properties)].map((attribute) => `${attribute.name}=${attribute.value}`).join("|");
|
|
1002
|
+
}
|
|
1003
|
+
//#endregion
|
|
1004
|
+
//#region src/styles/registry.ts
|
|
1005
|
+
const STYLE_FAMILIES = [
|
|
1006
|
+
"paragraph",
|
|
1007
|
+
"text",
|
|
1008
|
+
"table",
|
|
1009
|
+
"table-column",
|
|
1010
|
+
"table-row",
|
|
1011
|
+
"table-cell",
|
|
1012
|
+
"graphic"
|
|
1013
|
+
];
|
|
1014
|
+
function isStyleFamily(value) {
|
|
1015
|
+
return value === "paragraph" || value === "text" || value === "table" || value === "table-column" || value === "table-row" || value === "table-cell" || value === "graphic";
|
|
1016
|
+
}
|
|
1017
|
+
const CONTENT_PREFIXES = {
|
|
1018
|
+
paragraph: "P",
|
|
1019
|
+
text: "T",
|
|
1020
|
+
table: "ta",
|
|
1021
|
+
"table-column": "co",
|
|
1022
|
+
"table-row": "ro",
|
|
1023
|
+
"table-cell": "ce",
|
|
1024
|
+
graphic: "fr"
|
|
1025
|
+
};
|
|
1026
|
+
const STYLES_PREFIXES = {
|
|
1027
|
+
paragraph: "PS",
|
|
1028
|
+
text: "TS",
|
|
1029
|
+
table: "taS",
|
|
1030
|
+
"table-column": "coS",
|
|
1031
|
+
"table-row": "roS",
|
|
1032
|
+
"table-cell": "ceS",
|
|
1033
|
+
graphic: "frS"
|
|
1034
|
+
};
|
|
1035
|
+
function prefixesForPart(partPath) {
|
|
1036
|
+
const baseName = partPath.slice(partPath.lastIndexOf("/") + 1);
|
|
1037
|
+
if (baseName === "content.xml") return CONTENT_PREFIXES;
|
|
1038
|
+
if (baseName === "styles.xml") return STYLES_PREFIXES;
|
|
1039
|
+
throw new Error(`StyleRegistry.forPart: expected a part named "content.xml" or "styles.xml" (by base name), got "${partPath}"`);
|
|
1040
|
+
}
|
|
1041
|
+
function emptyFamilySets() {
|
|
1042
|
+
return {
|
|
1043
|
+
paragraph: /* @__PURE__ */ new Set(),
|
|
1044
|
+
text: /* @__PURE__ */ new Set(),
|
|
1045
|
+
table: /* @__PURE__ */ new Set(),
|
|
1046
|
+
"table-column": /* @__PURE__ */ new Set(),
|
|
1047
|
+
"table-row": /* @__PURE__ */ new Set(),
|
|
1048
|
+
"table-cell": /* @__PURE__ */ new Set(),
|
|
1049
|
+
graphic: /* @__PURE__ */ new Set()
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
function attrValue(element, name) {
|
|
1053
|
+
return element.attributes.find((attribute) => attribute.name === name)?.value;
|
|
1054
|
+
}
|
|
1055
|
+
function findDirectChild(nodes, tag) {
|
|
1056
|
+
for (const node of nodes) if (node.type === "element" && node.tag === tag) return node;
|
|
1057
|
+
}
|
|
1058
|
+
function findRootElement(nodes) {
|
|
1059
|
+
const root = nodes.find((node) => node.type === "element");
|
|
1060
|
+
if (root === void 0) throw new Error("StyleRegistry: part has no root XML element -- construct the part's minimal root (office:document-content/office:document-styles) before building a StyleRegistry for it");
|
|
1061
|
+
return root;
|
|
1062
|
+
}
|
|
1063
|
+
function ensureAutomaticStyles(root) {
|
|
1064
|
+
const existing = findDirectChild(root.children, "office:automatic-styles");
|
|
1065
|
+
if (existing !== void 0) return existing;
|
|
1066
|
+
const created = el("office:automatic-styles");
|
|
1067
|
+
const insertBeforeTags = /* @__PURE__ */ new Set([
|
|
1068
|
+
"office:body",
|
|
1069
|
+
"office:master-styles",
|
|
1070
|
+
"office:settings"
|
|
1071
|
+
]);
|
|
1072
|
+
const insertIndex = root.children.findIndex((node) => node.type === "element" && insertBeforeTags.has(node.tag));
|
|
1073
|
+
if (insertIndex === -1) root.children.push(created);
|
|
1074
|
+
else root.children.splice(insertIndex, 0, created);
|
|
1075
|
+
return created;
|
|
1076
|
+
}
|
|
1077
|
+
function reserveStyleNames(container, reserved) {
|
|
1078
|
+
for (const child of container.children) {
|
|
1079
|
+
if (child.type !== "element" || child.tag !== "style:style") continue;
|
|
1080
|
+
const name = attrValue(child, "style:name");
|
|
1081
|
+
const family = attrValue(child, "style:family");
|
|
1082
|
+
if (name === void 0 || family === void 0 || !isStyleFamily(family)) continue;
|
|
1083
|
+
reserved[family].add(name);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
const FINGERPRINT_SEPARATOR = "\0";
|
|
1087
|
+
const NO_PARENT_SENTINEL = "";
|
|
1088
|
+
function computeFingerprint(family, properties, parentStyleName) {
|
|
1089
|
+
const parentComponent = parentStyleName ?? NO_PARENT_SENTINEL;
|
|
1090
|
+
return [
|
|
1091
|
+
family,
|
|
1092
|
+
canonicalPropertiesString(properties),
|
|
1093
|
+
parentComponent
|
|
1094
|
+
].join(FINGERPRINT_SEPARATOR);
|
|
1095
|
+
}
|
|
1096
|
+
var StyleRegistry = class StyleRegistry {
|
|
1097
|
+
automaticStyles;
|
|
1098
|
+
prefixes;
|
|
1099
|
+
reservedByFamily;
|
|
1100
|
+
knownStyles = /* @__PURE__ */ new Map();
|
|
1101
|
+
fingerprintToName = /* @__PURE__ */ new Map();
|
|
1102
|
+
nameToFingerprint = /* @__PURE__ */ new Map();
|
|
1103
|
+
familyCounters = {
|
|
1104
|
+
paragraph: 1,
|
|
1105
|
+
text: 1,
|
|
1106
|
+
table: 1,
|
|
1107
|
+
"table-column": 1,
|
|
1108
|
+
"table-row": 1,
|
|
1109
|
+
"table-cell": 1,
|
|
1110
|
+
graphic: 1
|
|
1111
|
+
};
|
|
1112
|
+
constructor(automaticStyles, prefixes, reservedByFamily) {
|
|
1113
|
+
this.automaticStyles = automaticStyles;
|
|
1114
|
+
this.prefixes = prefixes;
|
|
1115
|
+
this.reservedByFamily = reservedByFamily;
|
|
1116
|
+
}
|
|
1117
|
+
static forPart(pkg, partPath, options = {}) {
|
|
1118
|
+
const part = pkg.parts[partPath];
|
|
1119
|
+
if (part?.kind !== "xml") throw new Error(`StyleRegistry.forPart: "${partPath}" is not an XML part of the given package`);
|
|
1120
|
+
const prefixes = prefixesForPart(partPath);
|
|
1121
|
+
const root = findRootElement(part.nodes);
|
|
1122
|
+
const automaticStyles = ensureAutomaticStyles(root);
|
|
1123
|
+
const reservedByFamily = emptyFamilySets();
|
|
1124
|
+
const registry = new StyleRegistry(automaticStyles, prefixes, reservedByFamily);
|
|
1125
|
+
for (const child of automaticStyles.children) {
|
|
1126
|
+
if (child.type !== "element" || child.tag !== "style:style") continue;
|
|
1127
|
+
const name = attrValue(child, "style:name");
|
|
1128
|
+
const family = attrValue(child, "style:family");
|
|
1129
|
+
if (name === void 0 || family === void 0 || !isStyleFamily(family)) continue;
|
|
1130
|
+
registry.knownStyles.set(name, child);
|
|
1131
|
+
reservedByFamily[family].add(name);
|
|
1132
|
+
const parsed = parseStyleElementProperties(child);
|
|
1133
|
+
if (!parsed.hasUnknown) {
|
|
1134
|
+
const parentStyleName = attrValue(child, "style:parent-style-name");
|
|
1135
|
+
const fingerprint = computeFingerprint(family, parsed.properties, parentStyleName);
|
|
1136
|
+
if (!registry.fingerprintToName.has(fingerprint)) {
|
|
1137
|
+
registry.fingerprintToName.set(fingerprint, name);
|
|
1138
|
+
registry.nameToFingerprint.set(name, fingerprint);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
const ownStyles = findDirectChild(root.children, "office:styles");
|
|
1143
|
+
if (ownStyles !== void 0) reserveStyleNames(ownStyles, reservedByFamily);
|
|
1144
|
+
if (options.otherPart !== void 0) {
|
|
1145
|
+
const otherPart = options.otherPart.pkg.parts[options.otherPart.partPath];
|
|
1146
|
+
if (otherPart?.kind === "xml") {
|
|
1147
|
+
const otherRoot = findRootElement(otherPart.nodes);
|
|
1148
|
+
const otherAutomatic = findDirectChild(otherRoot.children, "office:automatic-styles");
|
|
1149
|
+
if (otherAutomatic !== void 0) reserveStyleNames(otherAutomatic, reservedByFamily);
|
|
1150
|
+
const otherStyles = findDirectChild(otherRoot.children, "office:styles");
|
|
1151
|
+
if (otherStyles !== void 0) reserveStyleNames(otherStyles, reservedByFamily);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
if (options.additionalReservedNames !== void 0) for (const family of STYLE_FAMILIES) for (const name of options.additionalReservedNames) reservedByFamily[family].add(name);
|
|
1155
|
+
return registry;
|
|
1156
|
+
}
|
|
1157
|
+
fingerprint(request) {
|
|
1158
|
+
return computeFingerprint(request.family, request.properties, request.parentStyleName);
|
|
1159
|
+
}
|
|
1160
|
+
intern(request) {
|
|
1161
|
+
const fingerprint = this.fingerprint(request);
|
|
1162
|
+
const existingName = this.fingerprintToName.get(fingerprint);
|
|
1163
|
+
if (existingName !== void 0) return existingName;
|
|
1164
|
+
const name = this.mintName(request.family);
|
|
1165
|
+
const attributes = {
|
|
1166
|
+
"style:name": name,
|
|
1167
|
+
"style:family": request.family
|
|
1168
|
+
};
|
|
1169
|
+
if (request.parentStyleName !== void 0) attributes["style:parent-style-name"] = encodeXmlText(request.parentStyleName);
|
|
1170
|
+
const styleElement = el("style:style", attributes, buildStylePropertyElements(request.properties));
|
|
1171
|
+
this.automaticStyles.children.push(styleElement);
|
|
1172
|
+
this.knownStyles.set(name, styleElement);
|
|
1173
|
+
this.reservedByFamily[request.family].add(name);
|
|
1174
|
+
this.fingerprintToName.set(fingerprint, name);
|
|
1175
|
+
this.nameToFingerprint.set(name, fingerprint);
|
|
1176
|
+
return name;
|
|
1177
|
+
}
|
|
1178
|
+
mintName(family) {
|
|
1179
|
+
const prefix = this.prefixes[family];
|
|
1180
|
+
const reserved = this.reservedByFamily[family];
|
|
1181
|
+
let counter = this.familyCounters[family];
|
|
1182
|
+
while (reserved.has(`${prefix}${counter}`)) counter += 1;
|
|
1183
|
+
const name = `${prefix}${counter}`;
|
|
1184
|
+
this.familyCounters[family] = counter + 1;
|
|
1185
|
+
return name;
|
|
1186
|
+
}
|
|
1187
|
+
names() {
|
|
1188
|
+
return [...this.knownStyles.keys()];
|
|
1189
|
+
}
|
|
1190
|
+
gc(referenced) {
|
|
1191
|
+
let removed = 0;
|
|
1192
|
+
for (const [name, element] of [...this.knownStyles]) {
|
|
1193
|
+
if (referenced.has(name)) continue;
|
|
1194
|
+
const index = this.automaticStyles.children.indexOf(element);
|
|
1195
|
+
if (index !== -1) this.automaticStyles.children.splice(index, 1);
|
|
1196
|
+
this.knownStyles.delete(name);
|
|
1197
|
+
const fingerprint = this.nameToFingerprint.get(name);
|
|
1198
|
+
if (fingerprint !== void 0) {
|
|
1199
|
+
this.fingerprintToName.delete(fingerprint);
|
|
1200
|
+
this.nameToFingerprint.delete(name);
|
|
1201
|
+
}
|
|
1202
|
+
removed += 1;
|
|
1203
|
+
}
|
|
1204
|
+
return removed;
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1207
|
+
//#endregion
|
|
1208
|
+
//#region src/styles/span.ts
|
|
1209
|
+
function ensureSpan(paragraph, start, end, styleName) {
|
|
1210
|
+
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start) throw new Error(`ensureSpan: invalid range [${start}, ${end})`);
|
|
1211
|
+
const total = sumLength(paragraph.children);
|
|
1212
|
+
if (end > total) throw new Error(`ensureSpan: range end ${end} exceeds the container's total character length ${total}`);
|
|
1213
|
+
const { before, after: rest } = splitChildrenAt(paragraph.children, start);
|
|
1214
|
+
const { before: middle, after } = splitChildrenAt(rest, end - start);
|
|
1215
|
+
let span;
|
|
1216
|
+
const soleChild = middle.length === 1 ? middle[0] : void 0;
|
|
1217
|
+
if (soleChild?.type === "element" && soleChild.tag === "text:span") {
|
|
1218
|
+
span = soleChild;
|
|
1219
|
+
setStyleName(span, styleName);
|
|
1220
|
+
} else span = el("text:span", { "text:style-name": encodeXmlText(styleName) }, middle);
|
|
1221
|
+
paragraph.children = [
|
|
1222
|
+
...before,
|
|
1223
|
+
span,
|
|
1224
|
+
...after
|
|
1225
|
+
];
|
|
1226
|
+
return span;
|
|
1227
|
+
}
|
|
1228
|
+
function cloneAttributes(attributes) {
|
|
1229
|
+
return attributes.map((attribute) => ({ ...attribute }));
|
|
1230
|
+
}
|
|
1231
|
+
function setStyleName(span, styleName) {
|
|
1232
|
+
const encoded = encodeXmlText(styleName);
|
|
1233
|
+
const existing = span.attributes.find((attribute) => attribute.name === "text:style-name");
|
|
1234
|
+
if (existing !== void 0) {
|
|
1235
|
+
existing.value = encoded;
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
span.attributes.push({
|
|
1239
|
+
name: "text:style-name",
|
|
1240
|
+
value: encoded
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
function getSpaceCount(spaceElement) {
|
|
1244
|
+
const raw = spaceElement.attributes.find((attribute) => attribute.name === "text:c")?.value;
|
|
1245
|
+
if (raw === void 0) return 1;
|
|
1246
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1247
|
+
if (!Number.isInteger(parsed) || parsed < 0 || String(parsed) !== raw) throw new Error(`ensureSpan: text:s has a malformed text:c attribute: "${raw}"`);
|
|
1248
|
+
return parsed;
|
|
1249
|
+
}
|
|
1250
|
+
function buildSpaceRun(count) {
|
|
1251
|
+
return count === 1 ? el("text:s") : el("text:s", { "text:c": String(count) });
|
|
1252
|
+
}
|
|
1253
|
+
function measureLength(node) {
|
|
1254
|
+
if (node.type === "text") return node.value.length;
|
|
1255
|
+
if (node.type !== "element") return 0;
|
|
1256
|
+
if (node.tag === "text:s") return getSpaceCount(node);
|
|
1257
|
+
if (node.tag === "text:tab" || node.tag === "text:line-break") return 1;
|
|
1258
|
+
if (node.tag === "text:span") return sumLength(node.children);
|
|
1259
|
+
return 0;
|
|
1260
|
+
}
|
|
1261
|
+
function sumLength(nodes) {
|
|
1262
|
+
let total = 0;
|
|
1263
|
+
for (const node of nodes) total += measureLength(node);
|
|
1264
|
+
return total;
|
|
1265
|
+
}
|
|
1266
|
+
function splitNode(node, offset) {
|
|
1267
|
+
if (node.type === "text") return {
|
|
1268
|
+
left: {
|
|
1269
|
+
type: "text",
|
|
1270
|
+
value: node.value.slice(0, offset)
|
|
1271
|
+
},
|
|
1272
|
+
right: {
|
|
1273
|
+
type: "text",
|
|
1274
|
+
value: node.value.slice(offset)
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
if (node.type === "element" && node.tag === "text:s") {
|
|
1278
|
+
const count = getSpaceCount(node);
|
|
1279
|
+
return {
|
|
1280
|
+
left: buildSpaceRun(offset),
|
|
1281
|
+
right: buildSpaceRun(count - offset)
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
if (node.type === "element" && node.tag === "text:span") {
|
|
1285
|
+
const inner = splitChildrenAt(node.children, offset);
|
|
1286
|
+
return {
|
|
1287
|
+
left: inner.before.length === 0 ? void 0 : {
|
|
1288
|
+
...node,
|
|
1289
|
+
attributes: cloneAttributes(node.attributes),
|
|
1290
|
+
children: inner.before
|
|
1291
|
+
},
|
|
1292
|
+
right: inner.after.length === 0 ? void 0 : {
|
|
1293
|
+
...node,
|
|
1294
|
+
attributes: cloneAttributes(node.attributes),
|
|
1295
|
+
children: inner.after
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
const label = node.type === "element" ? node.tag : node.type;
|
|
1300
|
+
throw new Error(`ensureSpan: cannot split "${label}" at a fractional offset -- this indicates a character-length computation bug, since every node type with length 1 or 0 should never reach this branch`);
|
|
1301
|
+
}
|
|
1302
|
+
function splitChildrenAt(children, offset) {
|
|
1303
|
+
if (offset <= 0) return {
|
|
1304
|
+
before: [],
|
|
1305
|
+
after: [...children]
|
|
1306
|
+
};
|
|
1307
|
+
const before = [];
|
|
1308
|
+
let remaining = offset;
|
|
1309
|
+
for (let index = 0; index < children.length; index += 1) {
|
|
1310
|
+
if (remaining === 0) return {
|
|
1311
|
+
before,
|
|
1312
|
+
after: children.slice(index)
|
|
1313
|
+
};
|
|
1314
|
+
const node = children[index];
|
|
1315
|
+
const length = measureLength(node);
|
|
1316
|
+
if (remaining >= length) {
|
|
1317
|
+
before.push(node);
|
|
1318
|
+
remaining -= length;
|
|
1319
|
+
continue;
|
|
1320
|
+
}
|
|
1321
|
+
const { left, right } = splitNode(node, remaining);
|
|
1322
|
+
const after = [];
|
|
1323
|
+
if (left !== void 0) before.push(left);
|
|
1324
|
+
if (right !== void 0) after.push(right);
|
|
1325
|
+
after.push(...children.slice(index + 1));
|
|
1326
|
+
return {
|
|
1327
|
+
before,
|
|
1328
|
+
after
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
return {
|
|
1332
|
+
before,
|
|
1333
|
+
after: []
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
//#endregion
|
|
332
1337
|
exports.AttributeSchema = AttributeSchema;
|
|
333
1338
|
exports.BinaryPartSchema = BinaryPartSchema;
|
|
1339
|
+
exports.MANIFEST_PART = MANIFEST_PART;
|
|
1340
|
+
exports.MIMETYPE_PART = MIMETYPE_PART;
|
|
1341
|
+
exports.ManifestEntrySchema = ManifestEntrySchema;
|
|
1342
|
+
exports.ManifestProblemSchema = ManifestProblemSchema;
|
|
1343
|
+
exports.ManifestSchema = ManifestSchema;
|
|
1344
|
+
exports.ODF_MEDIA_TYPES = ODF_MEDIA_TYPES;
|
|
1345
|
+
exports.ODF_NAMESPACES = ODF_NAMESPACES;
|
|
334
1346
|
exports.PackageSchema = PackageSchema;
|
|
335
1347
|
exports.PartSchema = PartSchema;
|
|
1348
|
+
exports.STYLE_FAMILIES = STYLE_FAMILIES;
|
|
1349
|
+
exports.StylePropertiesSchema = StylePropertiesSchema;
|
|
1350
|
+
exports.StyleRegistry = StyleRegistry;
|
|
336
1351
|
exports.XmlCdataSchema = XmlCdataSchema;
|
|
337
1352
|
exports.XmlCommentSchema = XmlCommentSchema;
|
|
338
1353
|
exports.XmlDeclarationSchema = XmlDeclarationSchema;
|
|
@@ -342,15 +1357,40 @@ exports.XmlPartSchema = XmlPartSchema;
|
|
|
342
1357
|
exports.XmlPiSchema = XmlPiSchema;
|
|
343
1358
|
exports.XmlTextSchema = XmlTextSchema;
|
|
344
1359
|
exports.base64ToBytes = base64ToBytes;
|
|
1360
|
+
exports.buildManifest = buildManifest;
|
|
1361
|
+
exports.buildStylePropertyElements = buildStylePropertyElements;
|
|
345
1362
|
exports.buildXml = buildXml;
|
|
346
1363
|
exports.bytesToBase64 = bytesToBase64;
|
|
1364
|
+
exports.canonicalPropertiesString = canonicalPropertiesString;
|
|
347
1365
|
exports.decodePackage = decodePackage;
|
|
1366
|
+
exports.el = el;
|
|
348
1367
|
exports.encodePackage = encodePackage;
|
|
1368
|
+
exports.encodeXmlText = encodeXmlText;
|
|
1369
|
+
exports.ensureSpan = ensureSpan;
|
|
1370
|
+
exports.formatPercentageMultiplier = formatPercentageMultiplier;
|
|
1371
|
+
exports.formatPt = formatPt;
|
|
349
1372
|
exports.isXmlNode = isXmlNode;
|
|
1373
|
+
exports.mediaTypeForExtension = mediaTypeForExtension;
|
|
350
1374
|
exports.packageCodec = packageCodec;
|
|
1375
|
+
exports.paragraphPropertiesToAttributes = paragraphPropertiesToAttributes;
|
|
1376
|
+
exports.parseLength = parseLength;
|
|
351
1377
|
exports.parsePackage = parsePackage;
|
|
1378
|
+
exports.parseParagraphProperties = parseParagraphProperties;
|
|
1379
|
+
exports.parseStyleElementProperties = parseStyleElementProperties;
|
|
1380
|
+
exports.parseTextProperties = parseTextProperties;
|
|
352
1381
|
exports.parseXml = parseXml;
|
|
1382
|
+
exports.readManifest = readManifest;
|
|
1383
|
+
exports.readMimetype = readMimetype;
|
|
353
1384
|
exports.serializePackage = serializePackage;
|
|
1385
|
+
exports.setDocumentMediaType = setDocumentMediaType;
|
|
1386
|
+
exports.sniffImageFormat = sniffImageFormat;
|
|
1387
|
+
exports.syncManifest = syncManifest;
|
|
1388
|
+
exports.textPropertiesToAttributes = textPropertiesToAttributes;
|
|
1389
|
+
exports.txt = txt;
|
|
354
1390
|
exports.unzipPackage = unzipPackage;
|
|
1391
|
+
exports.validateManifest = validateManifest;
|
|
1392
|
+
exports.writeManifest = writeManifest;
|
|
1393
|
+
exports.writeMimetype = writeMimetype;
|
|
355
1394
|
exports.xmlCodec = xmlCodec;
|
|
1395
|
+
exports.xmlnsAttributes = xmlnsAttributes;
|
|
356
1396
|
exports.zipPackage = zipPackage;
|