@stll/folio-core 0.39.1 → 0.41.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 (64) hide show
  1. package/dist/ai-edits/apply.js +6 -4
  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/index.d.ts +2 -2
  7. package/dist/ai-edits/snapshot.d.ts +6 -1
  8. package/dist/ai-edits/snapshot.js +15 -10
  9. package/dist/ai-edits/types.d.ts +19 -6
  10. package/dist/ai-suggestions/text-positions.js +15 -3
  11. package/dist/compare/inline-atoms.js +2 -2
  12. package/dist/compat/eigenpal.d.ts +3 -2
  13. package/dist/compat/eigenpal.js +2 -1
  14. package/dist/display-list/primitives.d.ts +2 -1
  15. package/dist/document-operations.d.ts +24 -23
  16. package/dist/document-operations.js +163 -127
  17. package/dist/docx/blockPlainText.d.ts +8 -0
  18. package/dist/docx/blockPlainText.js +40 -0
  19. package/dist/docx/commentRangeIntegrity.js +0 -1
  20. package/dist/docx/compatibility.d.ts +16 -2
  21. package/dist/docx/compatibility.js +30 -9
  22. package/dist/docx/footnoteParser.js +4 -23
  23. package/dist/docx/graphicFrameLocks.d.ts +22 -0
  24. package/dist/docx/graphicFrameLocks.js +55 -0
  25. package/dist/docx/groupDrawingParser.d.ts +7 -1
  26. package/dist/docx/groupDrawingParser.js +11 -2
  27. package/dist/docx/headerFooterParser.d.ts +5 -1
  28. package/dist/docx/headerFooterParser.js +7 -21
  29. package/dist/docx/headerFooterVerbatim.d.ts +1 -12
  30. package/dist/docx/imageParser.js +5 -0
  31. package/dist/docx/imageRawXml.d.ts +33 -1
  32. package/dist/docx/imageRawXml.js +56 -1
  33. package/dist/docx/numberingParser.d.ts +1 -3
  34. package/dist/docx/runParser.js +43 -21
  35. package/dist/docx/serializer/runSerializer.js +4 -2
  36. package/dist/docx/server/createBilingualDocument.js +31 -5
  37. package/dist/docx/settingsParser.d.ts +2 -6
  38. package/dist/docx/shapeParser.js +32 -6
  39. package/dist/docx/vmlImageParser.d.ts +11 -1
  40. package/dist/docx/vmlImageParser.js +29 -2
  41. package/dist/headless-layout.js +1 -7
  42. package/dist/index.d.ts +3 -2
  43. package/dist/index.js +2 -1
  44. package/dist/internal/compare/inline-presentation.d.ts +11 -3
  45. package/dist/internal/compare/inline-presentation.js +8 -1
  46. package/dist/prosemirror/attrs/index.js +33 -3
  47. package/dist/prosemirror/conversion/fromProseDoc.d.ts +13 -2
  48. package/dist/prosemirror/conversion/fromProseDoc.js +85 -46
  49. package/dist/prosemirror/conversion/index.d.ts +2 -2
  50. package/dist/prosemirror/conversion/toProseDoc.js +12 -21
  51. package/dist/prosemirror/extensions/features/ListExtension.js +2 -1
  52. package/dist/prosemirror/extensions/nodes/ImageExtension.js +6 -0
  53. package/dist/prosemirror/imageCommit.js +7 -3
  54. package/dist/prosemirror/listMarker.d.ts +1 -1
  55. package/dist/prosemirror/listMarker.js +1 -18
  56. package/dist/prosemirror/listRenderingAttrs.d.ts +42 -0
  57. package/dist/prosemirror/listRenderingAttrs.js +80 -0
  58. package/dist/prosemirror/runFormattingInlineCarriers.d.ts +16 -1
  59. package/dist/prosemirror/runFormattingInlineCarriers.js +20 -1
  60. package/dist/prosemirror/schema/marks.d.ts +1 -1
  61. package/dist/prosemirror/schema/nodes.d.ts +20 -0
  62. package/dist/prosemirror/styles/resolvedStyleAttrs.js +2 -14
  63. package/dist/utils/mergeDocumentContent.js +1 -1
  64. package/package.json +2 -2
@@ -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)
@@ -1,20 +1,9 @@
1
1
  import { document_d_exports } from "../types/document.js";
2
2
  //#region src/docx/headerFooterVerbatim.d.ts
3
- /**
4
- * Folio extension on {@link HeaderFooter}: original part XML captured at parse
5
- * time so unedited headers/footers re-emit byte-identically on save (VML OLE
6
- * wrappers, smart tags, and other constructs the model cannot fully represent).
7
- * Cleared on first edit.
8
- */
9
- type HeaderFooterWithVerbatim = document_d_exports.HeaderFooter & {
10
- verbatimXml?: string;
11
- /** Fingerprint of modeled fields at parse time; verbatim replay is safe only while it matches. */
12
- verbatimFingerprint?: string;
13
- };
14
3
  declare const getHeaderFooterVerbatimXml: (hf: document_d_exports.HeaderFooter) => string | undefined;
15
4
  declare const canReplayHeaderFooterVerbatim: (hf: document_d_exports.HeaderFooter) => boolean;
16
5
  declare const assignHeaderFooterVerbatimXml: (hf: document_d_exports.HeaderFooter, xml: string) => void;
17
6
  declare const refreshHeaderFooterVerbatimFingerprint: (hf: document_d_exports.HeaderFooter) => void;
18
7
  declare const clearHeaderFooterVerbatimXml: (hf: document_d_exports.HeaderFooter) => void;
19
8
  //#endregion
20
- export { HeaderFooterWithVerbatim, assignHeaderFooterVerbatimXml, canReplayHeaderFooterVerbatim, clearHeaderFooterVerbatimXml, getHeaderFooterVerbatimXml, refreshHeaderFooterVerbatimFingerprint };
9
+ export { assignHeaderFooterVerbatimXml, canReplayHeaderFooterVerbatim, clearHeaderFooterVerbatimXml, getHeaderFooterVerbatimXml, refreshHeaderFooterVerbatimFingerprint };
@@ -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 };
@@ -45,9 +45,7 @@ declare function createNumberingMap(definitions: document_d_exports.NumberingDef
45
45
  declare function computeListRendering(numPr: {
46
46
  numId?: number;
47
47
  ilvl?: number;
48
- }, numbering: NumberingMap): (document_d_exports.ListRendering & {
49
- levelStarts: number[];
50
- }) | null;
48
+ }, numbering: NumberingMap): document_d_exports.ListRendering | null;
51
49
  /**
52
50
  * Render list marker text by replacing placeholders with formatted numbers
53
51
  *
@@ -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":
@@ -1,5 +1,6 @@
1
1
  import { isValidHexColor } from "../../utils/colorResolver.js";
2
2
  import { THEME_COLOR_TO_DRAWING_SCHEME } from "../drawingUtils.js";
3
+ import { serializeGraphicFrameLocks } from "../graphicFrameLocks.js";
3
4
  import { canReplayEditableImageRawXml } from "../imageRawXml.js";
4
5
  import { requiresXmlSpacePreserve } from "../textWhitespace.js";
5
6
  import { serializeParagraph } from "./paragraphSerializer.js";
@@ -285,6 +286,7 @@ function serializeDrawingContent(content) {
285
286
  const inlineDocPr = hlinkClick ? `<wp:docPr ${inlineDocPrAttrs}>${hlinkClick}</wp:docPr>` : `<wp:docPr ${inlineDocPrAttrs}/>`;
286
287
  const anchorDocPrAttrs = `id="${docPrId}" name="${escapeXml(docPrName)}"${docPrDescription}${docPrTitle}`;
287
288
  const anchorDocPr = hlinkClick ? `<wp:docPr ${anchorDocPrAttrs}>${hlinkClick}</wp:docPr>` : `<wp:docPr ${anchorDocPrAttrs}/>`;
289
+ const graphicFramePr = serializeGraphicFrameLocks(image.frameLocks);
288
290
  const graphic = serializePicGraphic(image, docPrId);
289
291
  if (!isFloating) return [
290
292
  "<w:drawing>",
@@ -292,7 +294,7 @@ function serializeDrawingContent(content) {
292
294
  `<wp:extent cx="${intAttr(cx)}" cy="${intAttr(cy)}"/>`,
293
295
  effectExtentEl,
294
296
  inlineDocPr,
295
- "<wp:cNvGraphicFramePr><a:graphicFrameLocks xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\" noChangeAspect=\"1\"/></wp:cNvGraphicFramePr>",
297
+ graphicFramePr,
296
298
  graphic,
297
299
  "</wp:inline>",
298
300
  "</w:drawing>"
@@ -309,7 +311,7 @@ function serializeDrawingContent(content) {
309
311
  effectExtentEl,
310
312
  wrap,
311
313
  anchorDocPr,
312
- "<wp:cNvGraphicFramePr><a:graphicFrameLocks xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\" noChangeAspect=\"1\"/></wp:cNvGraphicFramePr>",
314
+ graphicFramePr,
313
315
  graphic,
314
316
  "</wp:anchor>",
315
317
  "</w:drawing>"
@@ -1,6 +1,7 @@
1
1
  import { deterministicHexId } from "../../utils/hexId.js";
2
2
  import { getParagraphText } from "../paragraphParser.js";
3
3
  import { cloneParagraphWithoutPropertySource } from "../paragraphPropertySource.js";
4
+ import { getRunText } from "../runParser.js";
4
5
  import { TaggedError } from "better-result";
5
6
  //#region src/docx/server/createBilingualDocument.ts
6
7
  /**
@@ -115,7 +116,7 @@ function createBilingualDocument(source, options) {
115
116
  }
116
117
  if (block.type === "paragraph") {
117
118
  if (isEmptyParagraph(block)) continue;
118
- if (block.paraId === void 0 || !options.editableParagraphIds.has(block.paraId)) {
119
+ if (!isTranslatableParagraph(block, options.editableParagraphIds)) {
119
120
  flushSection();
120
121
  content.push(block);
121
122
  continue;
@@ -129,7 +130,7 @@ function createBilingualDocument(source, options) {
129
130
  sectionRows.push(buildRow(block, copy, styleById, textWidth));
130
131
  continue;
131
132
  }
132
- const paragraphs = collectTableParagraphs(block).filter((paragraph) => paragraph.paraId !== void 0 && options.editableParagraphIds.has(paragraph.paraId)).map((paragraph) => ({
133
+ const paragraphs = collectTableParagraphs(block).filter((paragraph) => isTranslatableParagraph(paragraph, options.editableParagraphIds)).map((paragraph) => ({
133
134
  paraId: paragraph.paraId,
134
135
  sourceText: getParagraphText(paragraph)
135
136
  }));
@@ -208,6 +209,31 @@ const isEmptyParagraph = (paragraph) => {
208
209
  if (getParagraphText(paragraph).trim().length > 0) return false;
209
210
  return paragraph.content.every((item) => item.type === "run" && item.formatting?.hidden !== true && item.content.every((part) => part.type === "text"));
210
211
  };
212
+ /**
213
+ * A paragraph whose only text is a field result (a table of contents, a page
214
+ * reference). Word recomputes that text, so translating it would be discarded;
215
+ * the paragraph is copied through full width instead of becoming a row.
216
+ */
217
+ const isFieldOnlyParagraph = (paragraph) => {
218
+ let hasField = false;
219
+ for (const item of paragraph.content) {
220
+ if (item.type === "simpleField" || item.type === "complexField") {
221
+ hasField = true;
222
+ continue;
223
+ }
224
+ if (item.type === "run" && getRunText(item).trim().length === 0) continue;
225
+ return false;
226
+ }
227
+ return hasField;
228
+ };
229
+ /**
230
+ * A paragraph offered as a translation row.
231
+ *
232
+ * Every site that builds or reads a row reads this one predicate: creation and
233
+ * reading derive the manifest independently, so a rule added to only one of
234
+ * them would surface as a missing handle rather than as the rule it is.
235
+ */
236
+ const isTranslatableParagraph = (paragraph, editableParagraphIds) => paragraph.paraId !== void 0 && editableParagraphIds.has(paragraph.paraId) && !isFieldOnlyParagraph(paragraph);
211
237
  /** Heading style families across Word UI languages (en, cs/sk, de, fr, pl). */
212
238
  const HEADING_STYLE_PATTERN = /heading|nadpis|berschrift|titre|nag[łl]/iu;
213
239
  const classifyParagraph = (paragraph, styleById) => {
@@ -459,7 +485,7 @@ const cloneTableForTarget = ({ table, editableParagraphIds, paraIds, styleCloner
459
485
  if (item.type === "table") return cloneTable(item);
460
486
  const targetParaId = paraIds.mint(item.paraId);
461
487
  const copy = cloneParagraphForTarget(item, targetParaId, styleCloner, cloner, bookmarkIds);
462
- if (item.paraId !== void 0 && editableParagraphIds.has(item.paraId)) paragraphs.push({
488
+ if (isTranslatableParagraph(item, editableParagraphIds)) paragraphs.push({
463
489
  sourceParaId: item.paraId,
464
490
  targetParaId,
465
491
  sourceText: getParagraphText(item)
@@ -701,7 +727,7 @@ function readBilingualDocument(document, editableParagraphIds) {
701
727
  }
702
728
  const paragraphs = [];
703
729
  for (const [index, source] of sourceParagraphs.entries()) {
704
- if (source.paraId === void 0 || !editableParagraphIds.has(source.paraId)) continue;
730
+ if (!isTranslatableParagraph(source, editableParagraphIds)) continue;
705
731
  const target = targetParagraphs.at(index);
706
732
  if (target?.paraId === void 0 || target.paraId === source.paraId || !editableParagraphIds.has(target.paraId)) {
707
733
  missingHandleCount += 1;
@@ -725,7 +751,7 @@ function readBilingualDocument(document, editableParagraphIds) {
725
751
  missingHandleCount += 1;
726
752
  continue;
727
753
  }
728
- const paragraphs = collectTableParagraphs(sourceTable).filter((paragraph) => paragraph.paraId !== void 0 && editableParagraphIds.has(paragraph.paraId)).map((paragraph) => ({
754
+ const paragraphs = collectTableParagraphs(sourceTable).filter((paragraph) => isTranslatableParagraph(paragraph, editableParagraphIds)).map((paragraph) => ({
729
755
  paraId: paragraph.paraId,
730
756
  sourceText: getParagraphText(paragraph)
731
757
  }));
@@ -1,11 +1,7 @@
1
1
  import { document_d_exports } from "../types/document.js";
2
2
  //#region src/docx/settingsParser.d.ts
3
- type FolioDocumentSettings = document_d_exports.DocumentSettings & {
4
- /** Swap left/right section margins on even physical pages. */
5
- mirrorMargins?: boolean;
6
- };
7
3
  /** OOXML default per §17.6.13 when `w:defaultTabStop` is absent. */
8
4
  declare const DEFAULT_TAB_STOP_TWIPS = 720;
9
- declare function parseSettings(xml: string | null): FolioDocumentSettings;
5
+ declare function parseSettings(xml: string | null): document_d_exports.DocumentSettings;
10
6
  //#endregion
11
- export { DEFAULT_TAB_STOP_TWIPS, FolioDocumentSettings, parseSettings };
7
+ export { DEFAULT_TAB_STOP_TWIPS, parseSettings };