@stll/folio-core 0.40.0 → 0.42.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.
Files changed (51) hide show
  1. package/README.md +16 -0
  2. package/dist/ai-edits/clean-text.d.ts +28 -6
  3. package/dist/ai-edits/clean-text.js +28 -13
  4. package/dist/ai-edits/headless.d.ts +12 -0
  5. package/dist/ai-edits/headless.js +26 -3
  6. package/dist/ai-edits/snapshot.d.ts +6 -1
  7. package/dist/ai-edits/snapshot.js +15 -10
  8. package/dist/ai-suggestions/text-positions.js +15 -3
  9. package/dist/compare/inline-atoms.js +2 -2
  10. package/dist/compat/eigenpal.d.ts +4 -3
  11. package/dist/compat/eigenpal.js +2 -1
  12. package/dist/display-list/primitives.d.ts +2 -1
  13. package/dist/document-operations.d.ts +3 -2
  14. package/dist/docx/blockPlainText.d.ts +8 -0
  15. package/dist/docx/blockPlainText.js +40 -0
  16. package/dist/docx/compatibility.d.ts +16 -2
  17. package/dist/docx/compatibility.js +30 -9
  18. package/dist/docx/footnoteParser.js +4 -23
  19. package/dist/docx/graphicFrameLocks.d.ts +22 -0
  20. package/dist/docx/graphicFrameLocks.js +55 -0
  21. package/dist/docx/groupDrawingParser.d.ts +7 -1
  22. package/dist/docx/groupDrawingParser.js +11 -2
  23. package/dist/docx/headerFooterParser.d.ts +5 -1
  24. package/dist/docx/headerFooterParser.js +7 -21
  25. package/dist/docx/imageParser.js +5 -0
  26. package/dist/docx/imageRawXml.d.ts +33 -1
  27. package/dist/docx/imageRawXml.js +56 -1
  28. package/dist/docx/rezip.d.ts +20 -3
  29. package/dist/docx/rezip.js +24 -13
  30. package/dist/docx/runParser.js +43 -21
  31. package/dist/docx/serializer/runSerializer.js +4 -2
  32. package/dist/docx/server/createBilingualDocument.js +31 -5
  33. package/dist/docx/shapeParser.js +32 -6
  34. package/dist/docx/vmlImageParser.d.ts +11 -1
  35. package/dist/docx/vmlImageParser.js +29 -2
  36. package/dist/index.d.ts +4 -3
  37. package/dist/index.js +2 -1
  38. package/dist/internal/compare/inline-presentation.d.ts +11 -3
  39. package/dist/internal/compare/inline-presentation.js +8 -1
  40. package/dist/prosemirror/attrs/index.js +33 -3
  41. package/dist/prosemirror/conversion/fromProseDoc.d.ts +13 -2
  42. package/dist/prosemirror/conversion/fromProseDoc.js +80 -27
  43. package/dist/prosemirror/conversion/index.d.ts +2 -2
  44. package/dist/prosemirror/conversion/toProseDoc.js +10 -2
  45. package/dist/prosemirror/extensions/nodes/ImageExtension.js +6 -0
  46. package/dist/prosemirror/imageCommit.js +7 -3
  47. package/dist/prosemirror/runFormattingInlineCarriers.d.ts +16 -1
  48. package/dist/prosemirror/runFormattingInlineCarriers.js +20 -1
  49. package/dist/prosemirror/schema/nodes.d.ts +20 -0
  50. package/dist/server.d.ts +2 -2
  51. package/package.json +2 -2
@@ -1,9 +1,9 @@
1
+ import { blockPlainText } from "./blockPlainText.js";
1
2
  import { parseEndnoteProperties, parseFootnoteProperties } from "./notePropertiesParser.js";
2
- import { getParagraphText, parseParagraph } from "./paragraphParser.js";
3
+ import { parseParagraph } from "./paragraphParser.js";
3
4
  import { parseSdtProperties } from "./sdtProperties.js";
4
5
  import { parseTable } from "./tableParser.js";
5
6
  import { findChild, findChildren, getAttributes, getChildElements, getLocalName, parseXml } from "./xmlParser.js";
6
- import { panic } from "better-result";
7
7
  //#region src/docx/footnoteParser.ts
8
8
  /**
9
9
  * Parse note type attribute
@@ -173,32 +173,13 @@ function createEndnoteMap(byId, endnotes) {
173
173
  * Uses the accepted tracked-change view and recurses through every note block.
174
174
  */
175
175
  function getFootnoteText(footnote) {
176
- return collectNoteBlockTexts(footnote.content).join("\n");
176
+ return blockPlainText(footnote.content);
177
177
  }
178
178
  /**
179
179
  * Get plain text content of an endnote.
180
180
  */
181
181
  function getEndnoteText(endnote) {
182
- return collectNoteBlockTexts(endnote.content).join("\n");
183
- }
184
- function collectNoteBlockTexts(blocks) {
185
- const texts = [];
186
- for (const block of blocks) switch (block.type) {
187
- case "paragraph":
188
- texts.push(getParagraphText(block));
189
- break;
190
- case "table":
191
- for (const row of block.rows) {
192
- if (row.formatting?.hidden === true) continue;
193
- texts.push(row.cells.map((cell) => collectNoteBlockTexts(cell.content).join("\n")).join(" "));
194
- }
195
- break;
196
- case "blockSdt":
197
- texts.push(...collectNoteBlockTexts(block.content));
198
- break;
199
- default: panic(`Unsupported note block in plain-text extraction: ${JSON.stringify(block)}`);
200
- }
201
- return texts;
182
+ return blockPlainText(endnote.content);
202
183
  }
203
184
  /**
204
185
  * Check if a footnote is a separator (not regular content)
@@ -0,0 +1,22 @@
1
+ import { document_d_exports } from "../types/document.js";
2
+ import { XmlElement } from "./xmlParser.js";
3
+ //#region src/docx/graphicFrameLocks.d.ts
4
+ /** Every modeled lock, in schema order: the total map above keeps it exhaustive. */
5
+ declare const GRAPHIC_FRAME_LOCK_KEYS: ("noGrp" | "noDrilldown" | "noSelect" | "noChangeAspect" | "noMove" | "noResize")[];
6
+ /**
7
+ * Read the locks off a `wp:inline` or `wp:anchor` element.
8
+ *
9
+ * Returns undefined when the element is absent or carries no recognized
10
+ * attribute: an authored `<a:graphicFrameLocks/>` with nothing on it means the
11
+ * same as no element at all, so both stay "spec defaults" on the model.
12
+ */
13
+ declare const parseGraphicFrameLocks: (parent: XmlElement) => document_d_exports.ImageFrameLocks | undefined;
14
+ /**
15
+ * Emit the whole `wp:cNvGraphicFramePr` element for regenerated DrawingML.
16
+ *
17
+ * Absent locks mean no authored frame was ever parsed (a Folio-created
18
+ * picture), which keeps the historical `noChangeAspect="1"`.
19
+ */
20
+ declare const serializeGraphicFrameLocks: (locks: document_d_exports.ImageFrameLocks | undefined) => string;
21
+ //#endregion
22
+ export { GRAPHIC_FRAME_LOCK_KEYS, parseGraphicFrameLocks, serializeGraphicFrameLocks };
@@ -0,0 +1,55 @@
1
+ import { findChild, getAttribute, parseOnOffValue } from "./xmlParser.js";
2
+ //#region src/docx/graphicFrameLocks.ts
3
+ /**
4
+ * Model key → OOXML attribute. Insertion order is the schema's attribute
5
+ * order, which is also the order we emit.
6
+ */
7
+ const GRAPHIC_FRAME_LOCK_ATTRIBUTES = {
8
+ noGrp: "noGrp",
9
+ noDrilldown: "noDrilldown",
10
+ noSelect: "noSelect",
11
+ noChangeAspect: "noChangeAspect",
12
+ noMove: "noMove",
13
+ noResize: "noResize"
14
+ };
15
+ const isGraphicFrameLockKey = (key) => key in GRAPHIC_FRAME_LOCK_ATTRIBUTES;
16
+ /** Every modeled lock, in schema order: the total map above keeps it exhaustive. */
17
+ const GRAPHIC_FRAME_LOCK_KEYS = Object.keys(GRAPHIC_FRAME_LOCK_ATTRIBUTES).filter(isGraphicFrameLockKey);
18
+ const DRAWINGML_NAMESPACE = "xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\"";
19
+ /**
20
+ * Read the locks off a `wp:inline` or `wp:anchor` element.
21
+ *
22
+ * Returns undefined when the element is absent or carries no recognized
23
+ * attribute: an authored `<a:graphicFrameLocks/>` with nothing on it means the
24
+ * same as no element at all, so both stay "spec defaults" on the model.
25
+ */
26
+ const parseGraphicFrameLocks = (parent) => {
27
+ const locksEl = findChild(findChild(parent, "wp", "cNvGraphicFramePr"), "a", "graphicFrameLocks");
28
+ if (!locksEl) return;
29
+ const locks = {};
30
+ let present = false;
31
+ for (const key of GRAPHIC_FRAME_LOCK_KEYS) {
32
+ const value = parseOnOffValue(getAttribute(locksEl, null, GRAPHIC_FRAME_LOCK_ATTRIBUTES[key]));
33
+ if (value !== void 0) {
34
+ locks[key] = value;
35
+ present = true;
36
+ }
37
+ }
38
+ return present ? locks : void 0;
39
+ };
40
+ /**
41
+ * Emit the whole `wp:cNvGraphicFramePr` element for regenerated DrawingML.
42
+ *
43
+ * Absent locks mean no authored frame was ever parsed (a Folio-created
44
+ * picture), which keeps the historical `noChangeAspect="1"`.
45
+ */
46
+ const serializeGraphicFrameLocks = (locks) => {
47
+ const attrs = locks ? GRAPHIC_FRAME_LOCK_KEYS.flatMap((key) => {
48
+ const value = locks[key];
49
+ return value === void 0 ? [] : [`${GRAPHIC_FRAME_LOCK_ATTRIBUTES[key]}="${value ? "1" : "0"}"`];
50
+ }) : ["noChangeAspect=\"1\""];
51
+ const attrList = attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
52
+ return `<wp:cNvGraphicFramePr><a:graphicFrameLocks ${DRAWINGML_NAMESPACE}${attrList}/></wp:cNvGraphicFramePr>`;
53
+ };
54
+ //#endregion
55
+ export { GRAPHIC_FRAME_LOCK_KEYS, parseGraphicFrameLocks, serializeGraphicFrameLocks };
@@ -1,7 +1,13 @@
1
1
  import { document_d_exports } from "../types/document.js";
2
2
  import { XmlElement } from "./xmlParser.js";
3
3
  //#region src/docx/groupDrawingParser.d.ts
4
+ /**
5
+ * Whether a `w:drawing` carries a WordprocessingGroup payload. A group this
6
+ * module declines to rasterize has no editable projection either — the shape
7
+ * model holds one shape, not a group — so the caller must preserve it raw.
8
+ */
9
+ declare const isGroupDrawing: (drawing: XmlElement) => boolean;
4
10
  /** Parse a WordprocessingGroup drawing into a safe SVG-backed image preview. */
5
11
  declare const parseGroupDrawing: (drawing: XmlElement, rels?: document_d_exports.RelationshipMap, media?: Map<string, document_d_exports.MediaFile>) => document_d_exports.Image | null;
6
12
  //#endregion
7
- export { parseGroupDrawing };
13
+ export { isGroupDrawing, parseGroupDrawing };
@@ -165,9 +165,18 @@ const createSvg = (group, width, height, rels, media) => {
165
165
  const viewBox = groupViewBox(group, width, height);
166
166
  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}" width="${emuToPixels(width)}" height="${emuToPixels(height)}">${content}</svg>`;
167
167
  };
168
+ const groupElement = (drawing) => {
169
+ return findChildByLocalName(findAllDeep(drawing, "a", "graphicData").at(0) ?? null, "wgp");
170
+ };
171
+ /**
172
+ * Whether a `w:drawing` carries a WordprocessingGroup payload. A group this
173
+ * module declines to rasterize has no editable projection either — the shape
174
+ * model holds one shape, not a group — so the caller must preserve it raw.
175
+ */
176
+ const isGroupDrawing = (drawing) => groupElement(drawing) !== null;
168
177
  /** Parse a WordprocessingGroup drawing into a safe SVG-backed image preview. */
169
178
  const parseGroupDrawing = (drawing, rels, media) => {
170
- const group = findChildByLocalName(findAllDeep(drawing, "a", "graphicData").at(0) ?? null, "wgp");
179
+ const group = groupElement(drawing);
171
180
  if (!group) return null;
172
181
  const image = parseImage(drawing, void 0, void 0);
173
182
  if (!image || image.size.width <= 0 || image.size.height <= 0) return null;
@@ -179,4 +188,4 @@ const parseGroupDrawing = (drawing, rels, media) => {
179
188
  return image;
180
189
  };
181
190
  //#endregion
182
- export { parseGroupDrawing };
191
+ export { isGroupDrawing, parseGroupDrawing };
@@ -77,7 +77,11 @@ declare function createEmptyHeaderFooterMap(): HeaderFooterMap;
77
77
  */
78
78
  declare function buildHeaderFooterMap(references: (document_d_exports.HeaderReference | document_d_exports.FooterReference)[], xmlContents: Map<string, string>, isHeader: boolean, styles?: StyleMap | null, theme?: document_d_exports.Theme | null, numbering?: NumberingMap | null, rels?: document_d_exports.RelationshipMap | null, media?: Map<string, document_d_exports.MediaFile> | null): HeaderFooterMap;
79
79
  /**
80
- * Get plain text content of a header/footer
80
+ * Get plain text content of a header/footer.
81
+ *
82
+ * Shares one walk with the note stories: this used to read only text runs, so a
83
+ * header's fields, hyperlinks, tabs and breaks were silently absent from its
84
+ * text while the same paragraph in a footnote read in full.
81
85
  */
82
86
  declare function getHeaderFooterText(hf: document_d_exports.HeaderFooter): string;
83
87
  /**
@@ -1,4 +1,5 @@
1
1
  import { parseBlockContent } from "./blockContentParser.js";
2
+ import { blockPlainText } from "./blockPlainText.js";
2
3
  import { parseFooterReference, parseFooterReferences, parseHeaderReference, parseHeaderReferences } from "./headerFooterRefParser.js";
3
4
  import { assignHeaderFooterVerbatimXml } from "./headerFooterVerbatim.js";
4
5
  import { cloneParagraphWithPropertySource } from "./paragraphPropertySource.js";
@@ -139,29 +140,14 @@ function buildHeaderFooterMap(references, xmlContents, isHeader, styles = null,
139
140
  return createHeaderFooterMap(byId);
140
141
  }
141
142
  /**
142
- * Get plain text content of a header/footer
143
+ * Get plain text content of a header/footer.
144
+ *
145
+ * Shares one walk with the note stories: this used to read only text runs, so a
146
+ * header's fields, hyperlinks, tabs and breaks were silently absent from its
147
+ * text while the same paragraph in a footnote read in full.
143
148
  */
144
149
  function getHeaderFooterText(hf) {
145
- const texts = [];
146
- for (const item of hf.content) if (item.type === "paragraph") {
147
- const paraTexts = [];
148
- for (const content of item.content) if (content.type === "run") {
149
- for (const runContent of content.content) if (runContent.type === "text") paraTexts.push(runContent.text);
150
- }
151
- texts.push(paraTexts.join(""));
152
- } else if (item.type === "blockSdt") texts.push(getHeaderFooterText({
153
- type: hf.type,
154
- hdrFtrType: hf.hdrFtrType,
155
- content: item.content
156
- }));
157
- else for (const row of item.rows) for (const cell of row.cells) for (const cellContent of cell.content) if (cellContent.type === "paragraph") {
158
- const paraTexts = [];
159
- for (const content of cellContent.content) if (content.type === "run") {
160
- for (const runContent of content.content) if (runContent.type === "text") paraTexts.push(runContent.text);
161
- }
162
- texts.push(paraTexts.join(""));
163
- }
164
- return texts.join("\n");
150
+ return blockPlainText(hf.content);
165
151
  }
166
152
  /**
167
153
  * Check if header/footer is empty (no content)
@@ -2,6 +2,7 @@ import { sanitizeImageSrc } from "../utils/sanitizeImageSrc.js";
2
2
  import { emuToPixels } from "../utils/units.js";
3
3
  import { sanitizeExternalUrl } from "../utils/urlSecurity.js";
4
4
  import { WRAP_ELEMENT_NAMES, parsePositionH, parsePositionV, parseWrapElement } from "./drawingUtils.js";
5
+ import { parseGraphicFrameLocks } from "./graphicFrameLocks.js";
5
6
  import { resolveTarget } from "./relsParser.js";
6
7
  import { isTextBoxDrawing } from "./textBoxParser.js";
7
8
  import { findByFullName, findChild, getAttribute, getChildElements, parseNumericAttribute, parseOnOffValue } from "./xmlParser.js";
@@ -299,6 +300,7 @@ function parseInline(inlineEl, rels, media) {
299
300
  const size = parseExtent(findByFullName(inlineEl, "wp:extent"));
300
301
  const padding = parseEffectExtent(findByFullName(inlineEl, "wp:effectExtent"));
301
302
  const props = parseDocProps(findByFullName(inlineEl, "wp:docPr"));
303
+ const frameLocks = parseGraphicFrameLocks(inlineEl);
302
304
  const blipFill = findBlipFillElement(inlineEl);
303
305
  const blip = blipFill ? findByFullName(blipFill, "a:blip") : null;
304
306
  const rId = extractBlipRId(blip);
@@ -334,6 +336,7 @@ function parseInline(inlineEl, rels, media) {
334
336
  if (transform) image.transform = transform;
335
337
  if (crop) image.crop = crop;
336
338
  if (opacity !== void 0) image.opacity = opacity;
339
+ if (frameLocks) image.frameLocks = frameLocks;
337
340
  if (props.hlinkRId && rels) {
338
341
  const safeHref = sanitizeExternalUrl(resolveTarget(rels, props.hlinkRId));
339
342
  if (safeHref) {
@@ -355,6 +358,7 @@ function parseAnchor(anchorEl, rels, media) {
355
358
  const size = parseExtent(findByFullName(anchorEl, "wp:extent"));
356
359
  const padding = parseEffectExtent(findByFullName(anchorEl, "wp:effectExtent"));
357
360
  const props = parseDocProps(findByFullName(anchorEl, "wp:docPr"));
361
+ const frameLocks = parseGraphicFrameLocks(anchorEl);
358
362
  const behindDoc = parseOnOffValue(getAttribute(anchorEl, null, "behindDoc")) === true;
359
363
  const layoutInCell = parseOnOffAttr(anchorEl, "layoutInCell");
360
364
  const allowOverlap = parseOnOffAttr(anchorEl, "allowOverlap");
@@ -405,6 +409,7 @@ function parseAnchor(anchorEl, rels, media) {
405
409
  if (transform) image.transform = transform;
406
410
  if (crop) image.crop = crop;
407
411
  if (opacity !== void 0) image.opacity = opacity;
412
+ if (frameLocks) image.frameLocks = frameLocks;
408
413
  if (layoutInCell !== void 0) image.layoutInCell = layoutInCell;
409
414
  if (allowOverlap !== void 0) image.allowOverlap = allowOverlap;
410
415
  if (props.hlinkRId && rels) {
@@ -1,8 +1,40 @@
1
1
  import { document_d_exports } from "../types/document.js";
2
2
  //#region src/docx/imageRawXml.d.ts
3
+ /** An unclassified drawing is an ordinary editable projection; a classified one is not. */
4
+ declare const allowsDirectDrawingEdit: (mode: document_d_exports.DrawingRawXmlMode | undefined) => boolean;
5
+ /** Narrow an unvalidated value (a ProseMirror attr) to a raw-XML mode. */
6
+ declare const isDrawingRawXmlMode: (value: unknown) => value is document_d_exports.DrawingRawXmlMode;
7
+ /**
8
+ * Stands in for a preview's fingerprint once the editor has changed the image
9
+ * it renders. `canonicalJson` always produces an object literal, so this can
10
+ * never collide with a real fingerprint and the drawing can never replay.
11
+ */
12
+ declare const EDITED_PREVIEW_FINGERPRINT = "editedPreview";
3
13
  /** Fingerprints modeled image fields which make raw DrawingML stale when edited. */
4
14
  declare const imageRawXmlFingerprint: (image: document_d_exports.Image) => string;
5
15
  /** Editable raw drawing XML can replay only while its modeled projection is unchanged. */
6
16
  declare const canReplayEditableImageRawXml: (drawing: document_d_exports.DrawingContent) => boolean;
17
+ /**
18
+ * What a drawing costs the document when Folio writes it back.
19
+ *
20
+ * `native` and `replayable` both survive a save intact, so neither restricts
21
+ * editing; only `opaque` loses content.
22
+ */
23
+ declare const DRAWING_SAFETY_CLASSES: {
24
+ /** No raw XML: Folio owns the whole drawing and regenerates it from the model. */
25
+ readonly NATIVE: "native";
26
+ /** Raw XML the serializer replays verbatim, so every unmodeled attribute survives. */
27
+ readonly REPLAYABLE: "replayable";
28
+ /** Raw XML the serializer can neither replay nor regenerate without losing the media. */
29
+ readonly OPAQUE: "opaque";
30
+ };
31
+ type DrawingSafetyClass = (typeof DRAWING_SAFETY_CLASSES)[keyof typeof DRAWING_SAFETY_CLASSES];
32
+ /**
33
+ * Classify a drawing by what the run serializer will do with it.
34
+ *
35
+ * Shares {@link canReplayEditableImageRawXml} with the serializer so a
36
+ * compatibility probe and a save can never disagree about the same drawing.
37
+ */
38
+ declare const classifyDrawingSafety: (drawing: document_d_exports.DrawingContent) => DrawingSafetyClass;
7
39
  //#endregion
8
- export { canReplayEditableImageRawXml, imageRawXmlFingerprint };
40
+ export { DRAWING_SAFETY_CLASSES, DrawingSafetyClass, EDITED_PREVIEW_FINGERPRINT, allowsDirectDrawingEdit, canReplayEditableImageRawXml, classifyDrawingSafety, imageRawXmlFingerprint, isDrawingRawXmlMode };
@@ -1,6 +1,28 @@
1
1
  import { canonicalJson } from "../utils/canonicalJson.js";
2
2
  import { DRAWING_RAW_XML_MODES } from "@stll/docx-core/model";
3
3
  //#region src/docx/imageRawXml.ts
4
+ /**
5
+ * Whether the editor may manipulate the modeled image of a classified drawing.
6
+ *
7
+ * A mode exists precisely because `rawXml` says more than the model does, so
8
+ * both current modes refuse: a resize would either serialize a placeholder
9
+ * (preserve-only) or one child picture in place of a group (preview-only).
10
+ * Totality is the point — a third mode cannot be added without deciding here.
11
+ */
12
+ const DRAWING_RAW_XML_MODE_ALLOWS_DIRECT_EDIT = {
13
+ [DRAWING_RAW_XML_MODES.PRESERVE_ONLY]: false,
14
+ [DRAWING_RAW_XML_MODES.PREVIEW_ONLY]: false
15
+ };
16
+ /** An unclassified drawing is an ordinary editable projection; a classified one is not. */
17
+ const allowsDirectDrawingEdit = (mode) => mode === void 0 || DRAWING_RAW_XML_MODE_ALLOWS_DIRECT_EDIT[mode];
18
+ /** Narrow an unvalidated value (a ProseMirror attr) to a raw-XML mode. */
19
+ const isDrawingRawXmlMode = (value) => typeof value === "string" && value in DRAWING_RAW_XML_MODE_ALLOWS_DIRECT_EDIT;
20
+ /**
21
+ * Stands in for a preview's fingerprint once the editor has changed the image
22
+ * it renders. `canonicalJson` always produces an object literal, so this can
23
+ * never collide with a real fingerprint and the drawing can never replay.
24
+ */
25
+ const EDITED_PREVIEW_FINGERPRINT = "editedPreview";
4
26
  const editableImageProjection = ({ id: _id, rId: _rId, src: _src, mimeType: _mimeType, filename: _filename, ...image }) => image;
5
27
  /** Fingerprints modeled image fields which make raw DrawingML stale when edited. */
6
28
  const imageRawXmlFingerprint = (image) => canonicalJson(editableImageProjection(image));
@@ -9,5 +31,38 @@ const canReplayEditableImageRawXml = (drawing) => {
9
31
  if (drawing.rawXmlMode === DRAWING_RAW_XML_MODES.PRESERVE_ONLY) return true;
10
32
  return drawing.rawImageFingerprint === void 0 || drawing.rawImageFingerprint === imageRawXmlFingerprint(drawing.image);
11
33
  };
34
+ /**
35
+ * What a drawing costs the document when Folio writes it back.
36
+ *
37
+ * `native` and `replayable` both survive a save intact, so neither restricts
38
+ * editing; only `opaque` loses content.
39
+ */
40
+ const DRAWING_SAFETY_CLASSES = {
41
+ /** No raw XML: Folio owns the whole drawing and regenerates it from the model. */
42
+ NATIVE: "native",
43
+ /** Raw XML the serializer replays verbatim, so every unmodeled attribute survives. */
44
+ REPLAYABLE: "replayable",
45
+ /** Raw XML the serializer can neither replay nor regenerate without losing the media. */
46
+ OPAQUE: "opaque"
47
+ };
48
+ /**
49
+ * Regenerated DrawingML points at `image.rId`, and `serializePicGraphic` falls
50
+ * back to `"rId1"` when that id is empty, which rebinds the picture to whichever
51
+ * relationship happens to be first. A drawing with no relationship id therefore
52
+ * has no faithful regeneration.
53
+ */
54
+ const canRegenerateDrawing = (drawing) => drawing.image.rId !== "";
55
+ /**
56
+ * Classify a drawing by what the run serializer will do with it.
57
+ *
58
+ * Shares {@link canReplayEditableImageRawXml} with the serializer so a
59
+ * compatibility probe and a save can never disagree about the same drawing.
60
+ */
61
+ const classifyDrawingSafety = (drawing) => {
62
+ if (drawing.rawXml === void 0) return DRAWING_SAFETY_CLASSES.NATIVE;
63
+ if (canReplayEditableImageRawXml(drawing)) return DRAWING_SAFETY_CLASSES.REPLAYABLE;
64
+ if (drawing.rawXmlMode === DRAWING_RAW_XML_MODES.PREVIEW_ONLY) return DRAWING_SAFETY_CLASSES.OPAQUE;
65
+ return canRegenerateDrawing(drawing) ? DRAWING_SAFETY_CLASSES.NATIVE : DRAWING_SAFETY_CLASSES.OPAQUE;
66
+ };
12
67
  //#endregion
13
- export { canReplayEditableImageRawXml, imageRawXmlFingerprint };
68
+ export { DRAWING_SAFETY_CLASSES, EDITED_PREVIEW_FINGERPRINT, allowsDirectDrawingEdit, canReplayEditableImageRawXml, classifyDrawingSafety, imageRawXmlFingerprint, isDrawingRawXmlMode };
@@ -185,18 +185,35 @@ declare const validateDocx: (buffer: ArrayBuffer) => Promise<{
185
185
  * @returns true if buffer starts with ZIP signature
186
186
  */
187
187
  declare function isDocxBuffer(buffer: ArrayBuffer): boolean;
188
+ /**
189
+ * The document properties a package built from scratch states about itself.
190
+ *
191
+ * A property the host does not name is not written at all: the package states
192
+ * an author and an application only when a caller supplies one. Both are
193
+ * ignored for a document that carries a source package, which keeps the
194
+ * properties that package already states.
195
+ */
196
+ type DocumentPropertiesOptions = {
197
+ /** `dc:creator` in `docProps/core.xml`. Omitted when absent. */
198
+ creator?: string;
199
+ /** `Application` in `docProps/app.xml`. Omitted, with `AppVersion`, when absent. */
200
+ application?: string;
201
+ };
188
202
  /**
189
203
  * Create a new empty DOCX file
190
204
  *
205
+ * @param properties - Document properties the package states about itself
191
206
  * @returns Promise resolving to minimal DOCX as ArrayBuffer
192
207
  */
193
- declare function createEmptyDocx(): Promise<ArrayBuffer>;
208
+ declare function createEmptyDocx(properties?: DocumentPropertiesOptions): Promise<ArrayBuffer>;
194
209
  /**
195
210
  * Create a new DOCX from a Document (without requiring original buffer)
196
211
  *
197
212
  * @param doc - Document to serialize
213
+ * @param properties - Document properties a package built from scratch states
214
+ * about itself; ignored when `doc` carries a source package
198
215
  * @returns Promise resolving to DOCX as ArrayBuffer
199
216
  */
200
- declare function createDocx(doc: document_d_exports.Document): Promise<ArrayBuffer>;
217
+ declare function createDocx(doc: document_d_exports.Document, properties?: DocumentPropertiesOptions): Promise<ArrayBuffer>;
201
218
  //#endregion
202
- export { COMMENTS_CONTENT_TYPE, COMMENTS_EXTENDED_CONTENT_TYPE, COMMENTS_EXTENDED_PART, COMMENTS_EXTENDED_PART_LOWER, DocxPackageFidelityError, RepackOptions, addCommentsExtendedOverride, addCommentsExtendedRelationship, addMedia, addRelationship, applyUpdatesToZip, collectHeaderFooterUpdates, collectHyperlinksWithoutRId, createDocx, createEmptyDocx, findMaxRId, hasModelDrivenPictureWatermark, hasUnmaterializedHeaderFooter, hasUnmaterializedInlineResources, isDocxBuffer, notePartRelsPath, removeCommentsExtendedOverride, removeCommentsExtendedRelationship, repackDocx, repackDocxFromRaw, updateCoreProperties, updateDocumentXml, updateMultipleFiles, updateXmlFile, validateDocx, withoutAttachedTemplate };
219
+ export { COMMENTS_CONTENT_TYPE, COMMENTS_EXTENDED_CONTENT_TYPE, COMMENTS_EXTENDED_PART, COMMENTS_EXTENDED_PART_LOWER, DocumentPropertiesOptions, DocxPackageFidelityError, RepackOptions, addCommentsExtendedOverride, addCommentsExtendedRelationship, addMedia, addRelationship, applyUpdatesToZip, collectHeaderFooterUpdates, collectHyperlinksWithoutRId, createDocx, createEmptyDocx, findMaxRId, hasModelDrivenPictureWatermark, hasUnmaterializedHeaderFooter, hasUnmaterializedInlineResources, isDocxBuffer, notePartRelsPath, removeCommentsExtendedOverride, removeCommentsExtendedRelationship, repackDocx, repackDocxFromRaw, updateCoreProperties, updateDocumentXml, updateMultipleFiles, updateXmlFile, validateDocx, withoutAttachedTemplate };
@@ -1,7 +1,7 @@
1
1
  import { consumeTrackedSectionEndpointRemoval } from "../internal/sectionEndpointResolution.js";
2
2
  import { consumeSectionReferenceResolution } from "../internal/sectionReferenceResolution.js";
3
3
  import { isAllowedExternalWatermarkImageUrl } from "../watermark/index.js";
4
- import { normalizeAppVersionInExtendedProperties } from "./appVersionNormalization.js";
4
+ import { appVersionInSchemaForm, normalizeAppVersionInExtendedProperties } from "./appVersionNormalization.js";
5
5
  import { withoutOrphanCommentRanges } from "./commentRangeIntegrity.js";
6
6
  import { applyReplyThreadMarkers } from "./commentReplyMarkers.js";
7
7
  import { normalizeDrawingIds } from "./drawingIdNormalization.js";
@@ -1524,14 +1524,24 @@ function isDocxBuffer(buffer) {
1524
1524
  return view[0] === 80 && view[1] === 75;
1525
1525
  }
1526
1526
  /**
1527
+ * The application version a newly created package states.
1528
+ *
1529
+ * `AppVersion` describes the application, so it is written only alongside one,
1530
+ * and it says nothing about the host beyond the `XX.YYYY` form the schema
1531
+ * fixes. Deriving it from the mapping every save exit already uses keeps the
1532
+ * two from drifting apart.
1533
+ */
1534
+ const CREATED_APP_VERSION = appVersionInSchemaForm("1");
1535
+ /**
1527
1536
  * Create a new empty DOCX file
1528
1537
  *
1538
+ * @param properties - Document properties the package states about itself
1529
1539
  * @returns Promise resolving to minimal DOCX as ArrayBuffer
1530
1540
  */
1531
- function createEmptyDocx() {
1532
- return generateDocxZip(createEmptyDocxZip(), 6);
1541
+ function createEmptyDocx(properties = {}) {
1542
+ return generateDocxZip(createEmptyDocxZip(properties), 6);
1533
1543
  }
1534
- const createEmptyDocxZip = () => {
1544
+ const createEmptyDocxZip = ({ creator, application }) => {
1535
1545
  const zip = new JSZip();
1536
1546
  zip.file("[Content_Types].xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1537
1547
  <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
@@ -1586,16 +1596,15 @@ const createEmptyDocxZip = () => {
1586
1596
  </w:style>
1587
1597
  </w:styles>`);
1588
1598
  const now = (/* @__PURE__ */ new Date()).toISOString();
1599
+ const creatorElement = creator === void 0 ? "" : `\n <dc:creator>${escapeXml(creator)}</dc:creator>`;
1589
1600
  zip.file("docProps/core.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1590
- <cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
1591
- <dc:creator>EigenPal DOCX Editor</dc:creator>
1601
+ <cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">${creatorElement}
1592
1602
  <dcterms:created xsi:type="dcterms:W3CDTF">${now}</dcterms:created>
1593
1603
  <dcterms:modified xsi:type="dcterms:W3CDTF">${now}</dcterms:modified>
1594
1604
  </cp:coreProperties>`);
1605
+ const applicationElements = application === void 0 ? "" : `\n <Application>${escapeXml(application)}</Application>\n <AppVersion>${CREATED_APP_VERSION}</AppVersion>`;
1595
1606
  zip.file("docProps/app.xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
1596
- <Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">
1597
- <Application>EigenPal DOCX Editor</Application>
1598
- <AppVersion>1.0000</AppVersion>
1607
+ <Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">${applicationElements}
1599
1608
  </Properties>`);
1600
1609
  return zip;
1601
1610
  };
@@ -1603,9 +1612,11 @@ const createEmptyDocxZip = () => {
1603
1612
  * Create a new DOCX from a Document (without requiring original buffer)
1604
1613
  *
1605
1614
  * @param doc - Document to serialize
1615
+ * @param properties - Document properties a package built from scratch states
1616
+ * about itself; ignored when `doc` carries a source package
1606
1617
  * @returns Promise resolving to DOCX as ArrayBuffer
1607
1618
  */
1608
- async function createDocx(doc) {
1619
+ async function createDocx(doc, properties = {}) {
1609
1620
  if (doc.originalBuffer) {
1610
1621
  const source = await loadParsedZipSource(doc, doc.originalBuffer);
1611
1622
  return finishRepack({
@@ -1618,7 +1629,7 @@ async function createDocx(doc) {
1618
1629
  updateModifiedDate: true
1619
1630
  });
1620
1631
  }
1621
- const zip = await createDocumentSeedZip(doc);
1632
+ const zip = await createDocumentSeedZip(doc, properties);
1622
1633
  return finishRepack({
1623
1634
  document: withoutOrphanCommentRanges(doc),
1624
1635
  originalZip: zip,
@@ -1629,8 +1640,8 @@ async function createDocx(doc) {
1629
1640
  updateModifiedDate: true
1630
1641
  });
1631
1642
  }
1632
- const createDocumentSeedZip = async (doc) => {
1633
- const zip = createEmptyDocxZip();
1643
+ const createDocumentSeedZip = async (doc, properties) => {
1644
+ const zip = createEmptyDocxZip(properties);
1634
1645
  const relationships = ["<Relationship Id=\"rId1\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles\" Target=\"styles.xml\"/>"];
1635
1646
  const overrides = [];
1636
1647
  let nextRelationshipId = 2;
@@ -1,7 +1,7 @@
1
1
  import { isValidHexColor } from "../utils/colorResolver.js";
2
2
  import { parseHorizontalScalePercent } from "../utils/horizontalScale.js";
3
3
  import { parseDiagramPreview } from "./diagramPreview.js";
4
- import { parseGroupDrawing } from "./groupDrawingParser.js";
4
+ import { isGroupDrawing, parseGroupDrawing } from "./groupDrawingParser.js";
5
5
  import { parseImage } from "./imageParser.js";
6
6
  import { imageRawXmlFingerprint } from "./imageRawXml.js";
7
7
  import { EmphasisMarkSchema, FontHintSchema, FontThemeSchema, HighlightColorSchema, PositionalTabAlignmentSchema, PositionalTabLeaderSchema, PositionalTabRelativeToSchema, ShadingPatternSchema, TextEffectSchema, ThemeColorSlotSchema, UnderlineStyleSchema, narrowEnum } from "./parserEnums.js";
@@ -11,7 +11,7 @@ import { requiresXmlSpacePreserve } from "./textWhitespace.js";
11
11
  import { resolveThemeFontRef } from "./themeParser.js";
12
12
  import { parsePropertyChangeInfo } from "./trackedChangeInfo.js";
13
13
  import { captureVerbatimXml } from "./verbatimCapture.js";
14
- import { parseVmlImageContent } from "./vmlImageParser.js";
14
+ import { parseVmlImageContent, shouldPreserveRawVmlPict } from "./vmlImageParser.js";
15
15
  import { cloneWithXmlnsDeclarations, findAllDeep, findChild, findChildren, getAttribute, getChildElements, getLocalName, getTextContent, mergeXmlnsDeclarations, parseBooleanElement, parseNumericAttribute, selectAlternateContentBranch } from "./xmlParser.js";
16
16
  import { DRAWING_RAW_XML_MODES } from "@stll/docx-core/model";
17
17
  //#region src/docx/runParser.ts
@@ -470,6 +470,28 @@ function parseInstrText(element) {
470
470
  };
471
471
  }
472
472
  /**
473
+ * Wrap raw XML the model cannot project at all.
474
+ *
475
+ * `DrawingContent` always carries an `Image`, so preservation-only content
476
+ * gets a placeholder one: the empty `rId` marks it as backed by no
477
+ * relationship, which keeps `classifyDrawingSafety` and the serializer on the
478
+ * replay path instead of regenerating DrawingML from the placeholder.
479
+ */
480
+ const preserveOnlyDrawing = (rawXml) => ({
481
+ type: "drawing",
482
+ image: {
483
+ type: "image",
484
+ rId: "",
485
+ size: {
486
+ width: 0,
487
+ height: 0
488
+ },
489
+ wrap: { type: "inline" }
490
+ },
491
+ rawXml,
492
+ rawXmlMode: DRAWING_RAW_XML_MODES.PRESERVE_ONLY
493
+ });
494
+ /**
473
495
  * Parse drawing content (w:drawing).
474
496
  *
475
497
  * Dispatches by graphicData payload:
@@ -480,6 +502,8 @@ function parseInstrText(element) {
480
502
  * context that is only available at the block parser level).
481
503
  * - `wps:wsp` without text body → generic shape; parsed via
482
504
  * `shapeParser.parseShapeFromDrawing` into a `ShapeContent`.
505
+ * - anything the model cannot project (a group the rasterizer declines,
506
+ * a diagram, a shape with unmodeled properties) → preservation-only raw XML.
483
507
  */
484
508
  function parseDrawingContent(element, rels, media) {
485
509
  const groupImage = parseGroupDrawing(element, rels ?? void 0, media ?? void 0);
@@ -487,8 +511,10 @@ function parseDrawingContent(element, rels, media) {
487
511
  type: "drawing",
488
512
  image: groupImage,
489
513
  rawXml: captureVerbatimXml(element),
490
- rawImageFingerprint: imageRawXmlFingerprint(groupImage)
514
+ rawImageFingerprint: imageRawXmlFingerprint(groupImage),
515
+ rawXmlMode: DRAWING_RAW_XML_MODES.PREVIEW_ONLY
491
516
  };
517
+ if (isGroupDrawing(element)) return preserveOnlyDrawing(captureVerbatimXml(element));
492
518
  const diagramImage = parseDiagramPreview(element, rels ?? void 0, media ?? void 0);
493
519
  if (diagramImage) return {
494
520
  type: "drawing",
@@ -496,20 +522,7 @@ function parseDrawingContent(element, rels, media) {
496
522
  rawXml: captureVerbatimXml(element),
497
523
  rawXmlMode: DRAWING_RAW_XML_MODES.PRESERVE_ONLY
498
524
  };
499
- if (shouldPreserveRawShapeDrawing(element)) return {
500
- type: "drawing",
501
- image: {
502
- type: "image",
503
- rId: "",
504
- size: {
505
- width: 0,
506
- height: 0
507
- },
508
- wrap: { type: "inline" }
509
- },
510
- rawXml: captureVerbatimXml(element),
511
- rawXmlMode: DRAWING_RAW_XML_MODES.PRESERVE_ONLY
512
- };
525
+ if (shouldPreserveRawShapeDrawing(element)) return preserveOnlyDrawing(captureVerbatimXml(element));
513
526
  const shape = parseShapeFromDrawing(element);
514
527
  if (shape) return {
515
528
  type: "shape",
@@ -572,7 +585,11 @@ function parseRunContents(runElement, rels, media, rootXmlns = {}) {
572
585
  }
573
586
  case "pict": {
574
587
  const vmlDrawing = parseVmlImageContent(child, rels, media, rootXmlns);
575
- if (vmlDrawing) contents.push(vmlDrawing);
588
+ if (vmlDrawing) {
589
+ contents.push(vmlDrawing);
590
+ break;
591
+ }
592
+ if (shouldPreserveRawVmlPict(child)) contents.push(preserveOnlyDrawing(captureVerbatimXml(cloneWithXmlnsDeclarations(child, rootXmlns))));
576
593
  break;
577
594
  }
578
595
  case "object": {
@@ -594,13 +611,16 @@ function parseRunContents(runElement, rels, media, rootXmlns = {}) {
594
611
  const alternateChildren = getChildElements(child);
595
612
  const choiceEl = alternateChildren.find((el) => getLocalName(el.name) === "Choice");
596
613
  const fallbackEl = alternateChildren.find((el) => getLocalName(el.name) === "Fallback");
614
+ const contentsBeforeAlternate = contents.length;
597
615
  const choiceTextBoxDrawing = choiceEl ? getChildElements(choiceEl).find((element) => getLocalName(element.name) === "drawing" && isTextBoxDrawing(element)) : void 0;
598
616
  const groupedChoiceDrawing = choiceEl ? getChildElements(choiceEl).find((element) => getLocalName(element.name) === "drawing" && findAllDeep(element, "wpg", "wgp").length > 0) : void 0;
599
617
  if (groupedChoiceDrawing) {
600
618
  const groupedDrawing = parseDrawingContent(groupedChoiceDrawing, rels, media);
601
- if (groupedDrawing?.type === "drawing" && groupedDrawing.image.src) {
602
- groupedDrawing.rawXml = captureVerbatimXml(child);
603
- contents.push(groupedDrawing);
619
+ if (groupedDrawing?.type === "drawing" && groupedDrawing.rawXmlMode === DRAWING_RAW_XML_MODES.PREVIEW_ONLY && groupedDrawing.image.src) {
620
+ contents.push({
621
+ ...groupedDrawing,
622
+ rawXml: captureVerbatimXml(child)
623
+ });
604
624
  break;
605
625
  }
606
626
  }
@@ -631,6 +651,8 @@ function parseRunContents(runElement, rels, media, rootXmlns = {}) {
631
651
  elements: [innerChild]
632
652
  }, rels, media, rootXmlns));
633
653
  }
654
+ const hasTextBoxBranch = [choiceEl, fallbackEl].some((branch) => getChildElements(branch).some((element) => getLocalName(element.name) === "drawing" && isTextBoxDrawing(element)));
655
+ if (contents.length === contentsBeforeAlternate && !hasTextBoxBranch) contents.push(preserveOnlyDrawing(captureVerbatimXml(cloneWithXmlnsDeclarations(child, rootXmlns))));
634
656
  break;
635
657
  }
636
658
  case "footnoteRef":