@stll/folio-core 0.15.9 → 0.15.11

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.
@@ -11,7 +11,7 @@ import { ensureThreadedCommentParaIds, serializeComments, serializeCommentsExten
11
11
  import { serializeDocument } from "./serializer/documentSerializer.js";
12
12
  import { serializeFontTableXml } from "./serializer/fontTableSerializer.js";
13
13
  import { serializeHeaderFooter } from "./serializer/headerFooterSerializer.js";
14
- import { serializeEndnotes, serializeFootnotes } from "./serializer/noteSerializer.js";
14
+ import { serializeEndnotes, serializeFootnotes, serializeNewEndnotesPart, serializeNewFootnotesPart } from "./serializer/noteSerializer.js";
15
15
  import { serializeNumberingXml } from "./serializer/numberingSerializer.js";
16
16
  import { serializeSettingsXml } from "./serializer/settingsSerializer.js";
17
17
  import { serializeStylesXml } from "./serializer/stylesSerializer.js";
@@ -978,9 +978,50 @@ function serializeHeadersFootersToZip(doc, zip, compressionLevel) {
978
978
  */
979
979
  async function serializeNotesToZip(doc, originalZip, newZip, compressionLevel) {
980
980
  const footnotes = doc.package.footnotes ?? [];
981
- if (footnotes.length > 0) await patchNotePartIntoZip("word/footnotes.xml", serializeFootnotes(footnotes), (xml) => serializeFootnotes(parseFootnotes(xml).getNormalFootnotes()), originalZip, newZip, compressionLevel);
981
+ if (footnotes.length > 0) if (findNotePartEntry(originalZip, "word/footnotes.xml")) await patchNotePartIntoZip("word/footnotes.xml", serializeFootnotes(footnotes), (xml) => serializeFootnotes(parseFootnotes(xml).getNormalFootnotes()), originalZip, newZip, compressionLevel);
982
+ else await materializeNewNotePart({
983
+ contentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml",
984
+ newZip,
985
+ partPath: "word/footnotes.xml",
986
+ relationshipType: RELATIONSHIP_TYPES.footnotes,
987
+ serializedPart: serializeNewFootnotesPart(footnotes),
988
+ compressionLevel
989
+ });
982
990
  const endnotes = doc.package.endnotes ?? [];
983
- if (endnotes.length > 0) await patchNotePartIntoZip("word/endnotes.xml", serializeEndnotes(endnotes), (xml) => serializeEndnotes(parseEndnotes(xml).getNormalEndnotes()), originalZip, newZip, compressionLevel);
991
+ if (endnotes.length > 0) if (findNotePartEntry(originalZip, "word/endnotes.xml")) await patchNotePartIntoZip("word/endnotes.xml", serializeEndnotes(endnotes), (xml) => serializeEndnotes(parseEndnotes(xml).getNormalEndnotes()), originalZip, newZip, compressionLevel);
992
+ else await materializeNewNotePart({
993
+ contentType: "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml",
994
+ newZip,
995
+ partPath: "word/endnotes.xml",
996
+ relationshipType: RELATIONSHIP_TYPES.endnotes,
997
+ serializedPart: serializeNewEndnotesPart(endnotes),
998
+ compressionLevel
999
+ });
1000
+ }
1001
+ async function materializeNewNotePart({ contentType, newZip, partPath, relationshipType, serializedPart, compressionLevel }) {
1002
+ const contentTypesFile = findNotePartEntry(newZip, "[content_types].xml");
1003
+ const relationshipsFile = findNotePartEntry(newZip, "word/_rels/document.xml.rels");
1004
+ if (!contentTypesFile || !relationshipsFile) throw new DocxPackageFidelityError(`Cannot create ${partPath}: the DOCX package is missing content types or document relationships`);
1005
+ const compressionOptions = { level: compressionLevel };
1006
+ const contentTypesXml = await contentTypesFile.async("text");
1007
+ const partName = `/${partPath}`;
1008
+ if (!contentTypesXml.toLowerCase().includes(partName.toLowerCase())) newZip.file(contentTypesFile.name, contentTypesXml.replace("</Types>", () => `<Override PartName="${partName}" ContentType="${contentType}"/></Types>`), {
1009
+ compression: "DEFLATE",
1010
+ compressionOptions
1011
+ });
1012
+ const relationshipsXml = await relationshipsFile.async("text");
1013
+ if (!relationshipsXml.includes(relationshipType)) {
1014
+ const relationshipId = `rId${findMaxRId(relationshipsXml) + 1}`;
1015
+ const target = partPath.slice(5);
1016
+ newZip.file(relationshipsFile.name, relationshipsXml.replace("</Relationships>", () => `<Relationship Id="${relationshipId}" Type="${relationshipType}" Target="${target}"/></Relationships>`), {
1017
+ compression: "DEFLATE",
1018
+ compressionOptions
1019
+ });
1020
+ }
1021
+ newZip.file(partPath, serializedPart, {
1022
+ compression: "DEFLATE",
1023
+ compressionOptions
1024
+ });
984
1025
  }
985
1026
  const numberingBaselines = /* @__PURE__ */ new WeakMap();
986
1027
  async function serializeNumberingIntoZip(doc, originalZip, newZip, compressionLevel) {
@@ -14,5 +14,11 @@ declare function serializeFootnotes(footnotes: readonly document_d_exports.Footn
14
14
  * @returns Complete endnotes.xml string.
15
15
  */
16
16
  declare function serializeEndnotes(endnotes: readonly document_d_exports.Endnote[]): string;
17
+ /** Serialize a brand-new footnote part, including Word's required separators
18
+ * and the automatic reference mark at the start of every normal note. */
19
+ declare function serializeNewFootnotesPart(footnotes: readonly document_d_exports.Footnote[]): string;
20
+ /** Serialize a brand-new endnote part, including Word's required separators
21
+ * and the automatic reference mark at the start of every normal note. */
22
+ declare function serializeNewEndnotesPart(endnotes: readonly document_d_exports.Endnote[]): string;
17
23
  //#endregion
18
- export { serializeEndnotes, serializeFootnotes };
24
+ export { serializeEndnotes, serializeFootnotes, serializeNewEndnotesPart, serializeNewFootnotesPart };
@@ -48,6 +48,25 @@ function serializeNote(elementName, note) {
48
48
  const body = note.content.map((block) => serializeBlock(block)).join("");
49
49
  return `<w:${elementName} ${attrs.join(" ")}>${body}</w:${elementName}>`;
50
50
  }
51
+ const NOTE_SEPARATOR_BODY = "<w:p><w:pPr><w:spacing w:after=\"0\" w:line=\"240\" w:lineRule=\"auto\"/></w:pPr>";
52
+ function serializeRequiredNoteSeparators(elementName) {
53
+ return `<w:${elementName} w:type="separator" w:id="-1">${NOTE_SEPARATOR_BODY}<w:r><w:separator/></w:r></w:p></w:${elementName}><w:${elementName} w:type="continuationSeparator" w:id="0">${NOTE_SEPARATOR_BODY}<w:r><w:continuationSeparator/></w:r></w:p></w:${elementName}>`;
54
+ }
55
+ function insertNoteReferenceMark(xml, elementName) {
56
+ const paragraphOpen = /<w:p(?=[\s>])[^>]*>/u.exec(xml);
57
+ if (!paragraphOpen) return `${`<w:p><w:r><w:rPr><w:rStyle w:val="${elementName === "footnote" ? "FootnoteReference" : "EndnoteReference"}"/></w:rPr><w:${elementName}Ref/></w:r></w:p>`}${xml}`;
58
+ const paragraphStart = paragraphOpen.index + paragraphOpen[0].length;
59
+ const paragraphEnd = xml.indexOf("</w:p>", paragraphStart);
60
+ const propertiesEnd = xml.indexOf("</w:pPr>", paragraphStart);
61
+ const insertAt = propertiesEnd !== -1 && propertiesEnd < paragraphEnd ? propertiesEnd + 8 : paragraphStart;
62
+ const referenceRun = `<w:r><w:rPr><w:rStyle w:val="${elementName === "footnote" ? "FootnoteReference" : "EndnoteReference"}"/></w:rPr><w:${elementName}Ref/></w:r>`;
63
+ return `${xml.slice(0, insertAt)}${referenceRun}${xml.slice(insertAt)}`;
64
+ }
65
+ function serializeNewNotePart(elementName, notes) {
66
+ const nsDecl = buildNamespaceDeclarations();
67
+ const serializedNotes = notes.map((note) => insertNoteReferenceMark(serializeNote(elementName, note), elementName)).join("");
68
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<w:${elementName}s ${nsDecl} mc:Ignorable="w14 w15 wp14">` + serializeRequiredNoteSeparators(elementName) + serializedNotes + `</w:${elementName}s>`;
69
+ }
51
70
  /**
52
71
  * Serialize footnotes to a complete word/footnotes.xml string.
53
72
  *
@@ -66,5 +85,15 @@ function serializeFootnotes(footnotes) {
66
85
  function serializeEndnotes(endnotes) {
67
86
  return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<w:endnotes ${buildNamespaceDeclarations()} mc:Ignorable="w14 w15 wp14">${endnotes.map((en) => serializeNote("endnote", en)).join("")}</w:endnotes>`;
68
87
  }
88
+ /** Serialize a brand-new footnote part, including Word's required separators
89
+ * and the automatic reference mark at the start of every normal note. */
90
+ function serializeNewFootnotesPart(footnotes) {
91
+ return serializeNewNotePart("footnote", footnotes);
92
+ }
93
+ /** Serialize a brand-new endnote part, including Word's required separators
94
+ * and the automatic reference mark at the start of every normal note. */
95
+ function serializeNewEndnotesPart(endnotes) {
96
+ return serializeNewNotePart("endnote", endnotes);
97
+ }
69
98
  //#endregion
70
- export { serializeEndnotes, serializeFootnotes };
99
+ export { serializeEndnotes, serializeFootnotes, serializeNewEndnotesPart, serializeNewFootnotesPart };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/folio-core",
3
- "version": "0.15.9",
3
+ "version": "0.15.11",
4
4
  "description": "Headless, framework-neutral core of folio: the OOXML (.docx) parser, document model, ProseMirror integration, and page-layout engine. No React.",
5
5
  "keywords": [
6
6
  "document-model",
@@ -107,7 +107,7 @@
107
107
  "perf": "bun scripts/profile-editor.ts"
108
108
  },
109
109
  "dependencies": {
110
- "@stll/docx-core": "^0.12.0",
110
+ "@stll/docx-core": "^0.13.0",
111
111
  "@stll/docx-utils": "^0.1.0",
112
112
  "@stll/template-conditions": "^0.1.0",
113
113
  "better-result": "2.10.0",