@stll/docx-core 0.15.1 → 0.15.2

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.
Binary file
package/dist/index.d.ts CHANGED
@@ -122,10 +122,19 @@ type SerializeDocumentOptions = {
122
122
  declare const serializeDocumentToDocx: (document: Document, options?: SerializeDocumentOptions) => Promise<ArrayBuffer>;
123
123
  //#endregion
124
124
  //#region src/validate/docx.d.ts
125
+ declare const DOCX_PACKAGE_ISSUE_CODES: {
126
+ readonly ArchiveBoundsExceeded: "archive_bounds_exceeded";
127
+ readonly InvalidArchive: "invalid_archive";
128
+ readonly InvalidDocumentRoot: "invalid_document_root";
129
+ readonly MissingNumberingPart: "missing_numbering_part";
130
+ readonly MissingPackagePart: "missing_package_part";
131
+ };
132
+ type DocxPackageIssueCode = (typeof DOCX_PACKAGE_ISSUE_CODES)[keyof typeof DOCX_PACKAGE_ISSUE_CODES];
125
133
  type ValidateDocxPackageResult = {
126
134
  valid: true;
127
135
  } | {
128
136
  valid: false;
137
+ code: DocxPackageIssueCode;
129
138
  error: string;
130
139
  };
131
140
  type ValidateDocumentModelIssue = {
@@ -141,4 +150,4 @@ declare const validateDocxPackage: (buffer: ArrayBuffer | Uint8Array) => Promise
141
150
  declare const validateDocumentModel: (document: Document) => ValidateDocumentModelResult;
142
151
  declare const assertValidDocumentModel: (document: Document) => void;
143
152
  //#endregion
144
- export { type Autofix, type BlockContent, type BreakContent, type CompiledLegalDocument, DOCX_CONFORMANCE_CLASSES, type Document, type DocumentBody, type DocxConformanceClass, type DocxPackage, type LegalDraft, type LegalDraftBlock, type LegalDraftDiagnostic, type LegalSourceCompileOptions, type LegalSourceCompileResult, type LegalSourceDocxCompileResult, type LegalSourceParseResult, type Paragraph, type ParagraphContent, type PositionalTab, type Run, type RunContent, type SectionProperties, type Style, type Table, type TableCell, type TableRow, type TextContent, type ValidateDocumentModelIssue, type ValidateDocumentModelResult, type ValidateDocxPackageResult, assertValidDocumentModel, compileLegalSourceToDocument, compileLegalSourceToDocx, parseLegalSource, serializeDocumentToDocx, validateDocumentModel, validateDocxPackage, validateLegalDraft };
153
+ export { type Autofix, type BlockContent, type BreakContent, type CompiledLegalDocument, DOCX_CONFORMANCE_CLASSES, DOCX_PACKAGE_ISSUE_CODES, type Document, type DocumentBody, type DocxConformanceClass, type DocxPackage, type DocxPackageIssueCode, type LegalDraft, type LegalDraftBlock, type LegalDraftDiagnostic, type LegalSourceCompileOptions, type LegalSourceCompileResult, type LegalSourceDocxCompileResult, type LegalSourceParseResult, type Paragraph, type ParagraphContent, type PositionalTab, type Run, type RunContent, type SectionProperties, type Style, type Table, type TableCell, type TableRow, type TextContent, type ValidateDocumentModelIssue, type ValidateDocumentModelResult, type ValidateDocxPackageResult, assertValidDocumentModel, compileLegalSourceToDocument, compileLegalSourceToDocx, parseLegalSource, serializeDocumentToDocx, validateDocumentModel, validateDocxPackage, validateLegalDraft };
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { r as isOoxmlSymbolCharacter, t as DOCX_CONFORMANCE_CLASSES } from "./document-BO7h-LxY.js";
2
2
  import JSZip from "jszip";
3
3
  import { panic } from "better-result";
4
+ import { XMLParser, XMLValidator } from "fast-xml-parser";
4
5
  //#region src/serialize/xml.ts
5
6
  const ILLEGAL_XML_CHARS_RE = /[^\u0009\u000A\u000D\u0020-\uD7FF\uE000-\uFFFD\u{10000}-\u{10FFFF}]/gu;
6
7
  const stripIllegalXmlChars = (value) => value.replace(ILLEGAL_XML_CHARS_RE, "");
@@ -322,6 +323,13 @@ const serializeCoreProperties = (document) => {
322
323
  };
323
324
  //#endregion
324
325
  //#region src/validate/docx.ts
326
+ const DOCX_PACKAGE_ISSUE_CODES = {
327
+ ArchiveBoundsExceeded: "archive_bounds_exceeded",
328
+ InvalidArchive: "invalid_archive",
329
+ InvalidDocumentRoot: "invalid_document_root",
330
+ MissingNumberingPart: "missing_numbering_part",
331
+ MissingPackagePart: "missing_package_part"
332
+ };
325
333
  /**
326
334
  * Local bounds on the ZIP archive `validateDocxPackage` inflates parts from.
327
335
  * `docx-core` has no dependency on `@stll/folio-core`, so these mirror the
@@ -334,6 +342,17 @@ const serializeCoreProperties = (document) => {
334
342
  const VALIDATE_DOCX_MAX_ENTRIES = 4096;
335
343
  const VALIDATE_DOCX_MAX_ENTRY_BYTES = 128 * 1024 * 1024;
336
344
  const VALIDATE_DOCX_MAX_TOTAL_BYTES = 256 * 1024 * 1024;
345
+ const VALIDATE_DOCX_MAX_DOCUMENT_XML_BYTES = 32 * 1024 * 1024;
346
+ const WORDPROCESSINGML_NAMESPACES = /* @__PURE__ */ new Set(["http://schemas.openxmlformats.org/wordprocessingml/2006/main", "http://purl.oclc.org/ooxml/wordprocessingml/main"]);
347
+ const packageXmlParser = new XMLParser({
348
+ preserveOrder: true,
349
+ ignoreAttributes: false,
350
+ attributeNamePrefix: "",
351
+ parseTagValue: false,
352
+ parseAttributeValue: false,
353
+ processEntities: false,
354
+ ignoreDeclaration: true
355
+ });
337
356
  const getDeclaredUncompressedSize = (file) => {
338
357
  const metadata = file._data;
339
358
  return typeof metadata?.uncompressedSize === "number" ? metadata.uncompressedSize : null;
@@ -358,36 +377,84 @@ const checkDocxArchiveBounds = (zip) => {
358
377
  }
359
378
  return null;
360
379
  };
380
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
381
+ const elementName = (node) => Object.keys(node).find((key) => key !== ":@" && !key.startsWith("?")) ?? null;
382
+ const localName = (name) => name.slice(name.indexOf(":") + 1);
383
+ const prefix = (name) => {
384
+ const separator = name.indexOf(":");
385
+ return separator === -1 ? "" : name.slice(0, separator);
386
+ };
387
+ const namespaceFor = (name, attributes, inherited = {}) => {
388
+ const key = prefix(name) === "" ? "xmlns" : `xmlns:${prefix(name)}`;
389
+ const value = attributes[key] ?? inherited[key];
390
+ return typeof value === "string" ? value : null;
391
+ };
392
+ const validateDocumentXml = (xml) => {
393
+ if (XMLValidator.validate(xml) !== true) return "Generated DOCX has malformed word/document.xml.";
394
+ const parsed = packageXmlParser.parse(xml);
395
+ if (!Array.isArray(parsed)) return "Generated DOCX has no word/document.xml root document.";
396
+ const root = parsed.find(isRecord);
397
+ if (!root) return "Generated DOCX has no word/document.xml root document.";
398
+ const rootName = elementName(root);
399
+ const rootAttributes = isRecord(root[":@"]) ? root[":@"] : {};
400
+ if (rootName === null || localName(rootName) !== "document" || !WORDPROCESSINGML_NAMESPACES.has(namespaceFor(rootName, rootAttributes) ?? "")) return "Generated DOCX has no WordprocessingML document root.";
401
+ const children = root[rootName];
402
+ if (!Array.isArray(children)) return "Generated DOCX document root has no WordprocessingML body.";
403
+ return children.some((child) => {
404
+ if (!isRecord(child)) return false;
405
+ const childName = elementName(child);
406
+ const childAttributes = isRecord(child[":@"]) ? child[":@"] : {};
407
+ return childName !== null && localName(childName) === "body" && WORDPROCESSINGML_NAMESPACES.has(namespaceFor(childName, childAttributes, rootAttributes) ?? "");
408
+ }) ? null : "Generated DOCX document root has no WordprocessingML body.";
409
+ };
361
410
  const validateDocxPackage = async (buffer) => {
362
411
  try {
363
412
  const zip = await JSZip.loadAsync(buffer);
364
413
  const boundsError = checkDocxArchiveBounds(zip);
365
414
  if (boundsError) return {
366
415
  valid: false,
416
+ code: DOCX_PACKAGE_ISSUE_CODES.ArchiveBoundsExceeded,
367
417
  error: boundsError
368
418
  };
369
419
  for (const requiredPath of [
370
420
  "[Content_Types].xml",
371
421
  "_rels/.rels",
372
- "word/document.xml",
373
- "word/styles.xml",
374
- "word/_rels/document.xml.rels"
422
+ "word/document.xml"
375
423
  ]) if (!zip.file(requiredPath)) return {
376
424
  valid: false,
425
+ code: DOCX_PACKAGE_ISSUE_CODES.MissingPackagePart,
377
426
  error: `Generated DOCX is missing required package part: ${requiredPath}`
378
427
  };
379
- if (!(await zip.file("word/document.xml")?.async("string"))?.includes("<w:document")) return {
428
+ const documentPart = zip.file("word/document.xml");
429
+ const declaredDocumentBytes = documentPart ? getDeclaredUncompressedSize(documentPart) : null;
430
+ if (declaredDocumentBytes !== null && declaredDocumentBytes > VALIDATE_DOCX_MAX_DOCUMENT_XML_BYTES) return {
431
+ valid: false,
432
+ code: DOCX_PACKAGE_ISSUE_CODES.ArchiveBoundsExceeded,
433
+ error: `Generated DOCX word/document.xml declares ${declaredDocumentBytes} uncompressed bytes, over the ${VALIDATE_DOCX_MAX_DOCUMENT_XML_BYTES}-byte limit.`
434
+ };
435
+ const documentBytes = await documentPart?.async("uint8array");
436
+ if (documentBytes !== void 0 && documentBytes.byteLength > VALIDATE_DOCX_MAX_DOCUMENT_XML_BYTES) return {
437
+ valid: false,
438
+ code: DOCX_PACKAGE_ISSUE_CODES.ArchiveBoundsExceeded,
439
+ error: `Generated DOCX word/document.xml exceeds the ${VALIDATE_DOCX_MAX_DOCUMENT_XML_BYTES}-byte limit.`
440
+ };
441
+ const documentXml = documentBytes ? new TextDecoder().decode(documentBytes) : void 0;
442
+ const documentError = documentXml ? validateDocumentXml(documentXml) : "Generated DOCX has no word/document.xml root document.";
443
+ if (documentError) return {
380
444
  valid: false,
381
- error: "Generated DOCX has no word/document.xml root document."
445
+ code: DOCX_PACKAGE_ISSUE_CODES.InvalidDocumentRoot,
446
+ error: documentError
382
447
  };
383
448
  if ((await zip.file("word/_rels/document.xml.rels")?.async("string"))?.includes("/relationships/numbering") && !zip.file("word/numbering.xml")) return {
384
449
  valid: false,
450
+ code: DOCX_PACKAGE_ISSUE_CODES.MissingNumberingPart,
385
451
  error: "Generated DOCX references numbering.xml but does not include it."
386
452
  };
387
453
  return { valid: true };
388
454
  } catch (error) {
389
455
  return {
390
456
  valid: false,
457
+ code: DOCX_PACKAGE_ISSUE_CODES.InvalidArchive,
391
458
  error: error instanceof Error ? error.message : "Invalid DOCX package."
392
459
  };
393
460
  }
@@ -1983,4 +2050,4 @@ const compileLegalSourceToDocx = async (source, options = {}) => {
1983
2050
  };
1984
2051
  };
1985
2052
  //#endregion
1986
- export { DOCX_CONFORMANCE_CLASSES, assertValidDocumentModel, compileLegalSourceToDocument, compileLegalSourceToDocx, parseLegalSource, serializeDocumentToDocx, validateDocumentModel, validateDocxPackage, validateLegalDraft };
2053
+ export { DOCX_CONFORMANCE_CLASSES, DOCX_PACKAGE_ISSUE_CODES, assertValidDocumentModel, compileLegalSourceToDocument, compileLegalSourceToDocx, parseLegalSource, serializeDocumentToDocx, validateDocumentModel, validateDocxPackage, validateLegalDraft };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/docx-core",
3
- "version": "0.15.1",
3
+ "version": "0.15.2",
4
4
  "description": "Typed OOXML/DOCX model, validation, serialization, legal-source compilation, and browser-native package projection.",
5
5
  "keywords": [
6
6
  "document-model",
@@ -61,6 +61,7 @@
61
61
  },
62
62
  "dependencies": {
63
63
  "better-result": "3.0.1",
64
+ "fast-xml-parser": "^5.10.1",
64
65
  "jszip": "3.10.1"
65
66
  },
66
67
  "devDependencies": {