@stll/folio-core 0.25.1 → 0.25.3

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 (31) hide show
  1. package/dist/ai-edits/headless.d.ts +6 -0
  2. package/dist/ai-edits/headless.js +60 -7
  3. package/dist/ai-edits/read.d.ts +26 -1
  4. package/dist/ai-edits/read.js +26 -1
  5. package/dist/docx/rezip.d.ts +2 -0
  6. package/dist/docx/rezip.js +61 -58
  7. package/dist/docx/selectiveXmlPatch.d.ts +24 -1
  8. package/dist/docx/selectiveXmlPatch.js +186 -1
  9. package/dist/docx/xmlParser.d.ts +7 -1
  10. package/dist/docx/xmlParser.js +11 -4
  11. package/dist/layout-bridge/convert/toFlowBlocks.js +3 -4
  12. package/dist/layout-bridge/engine/clickToPosition.js +13 -28
  13. package/dist/layout-bridge/engine/hitTest.d.ts +2 -0
  14. package/dist/layout-bridge/engine/hitTest.js +28 -13
  15. package/dist/layout-bridge/engine/selectionRects.js +26 -15
  16. package/dist/layout-engine/index.js +6 -9
  17. package/dist/layout-engine/measure/hyperlinkInstance.d.ts +6 -0
  18. package/dist/layout-engine/measure/hyperlinkInstance.js +8 -0
  19. package/dist/layout-engine/measure/tableCellGrid.d.ts +15 -1
  20. package/dist/layout-engine/measure/tableCellGrid.js +29 -6
  21. package/dist/layout-engine/measure/tableInlinePlacement.d.ts +12 -0
  22. package/dist/layout-engine/measure/tableInlinePlacement.js +22 -0
  23. package/dist/layout-engine/types.d.ts +1 -1
  24. package/dist/layout-painter/renderParagraph.d.ts +3 -0
  25. package/dist/layout-painter/renderParagraph.js +13 -19
  26. package/dist/layout-painter/renderTable.js +36 -74
  27. package/dist/prosemirror/conversion/toProseDoc.js +42 -44
  28. package/dist/prosemirror/extensions/marks/CommentExtension.js +1 -0
  29. package/dist/utils/paragraphInlineLayout.d.ts +14 -0
  30. package/dist/utils/paragraphInlineLayout.js +25 -0
  31. package/package.json +1 -1
@@ -30,6 +30,11 @@ const createTextBoxGroupIdFactory = () => {
30
30
  let index = 0;
31
31
  return () => `${salt}:${index++}`;
32
32
  };
33
+ /** Keep imported hyperlink identity unique across every nested conversion scope. */
34
+ const createHyperlinkInstanceIndexAllocator = () => {
35
+ let index = 0;
36
+ return () => index++;
37
+ };
33
38
  /**
34
39
  * Convert a Document to a ProseMirror document
35
40
  *
@@ -42,6 +47,11 @@ function toProseDoc(document, options) {
42
47
  const styleResolver = createStyleEngine(options?.styles ?? document.package.styles);
43
48
  const theme = options?.theme ?? document.package.theme ?? null;
44
49
  const nextTextBoxGroupId = createTextBoxGroupIdFactory();
50
+ const conversionContext = {
51
+ theme,
52
+ nextTextBoxGroupId,
53
+ nextHyperlinkInstanceIndex: createHyperlinkInstanceIndexAllocator()
54
+ };
45
55
  const convertBodyBlocks = (blocks) => {
46
56
  const out = [];
47
57
  for (const block of blocks) if (block.type === "paragraph") {
@@ -49,10 +59,7 @@ function toProseDoc(document, options) {
49
59
  if (pbPos === "before") out.push(schema.node("pageBreak"));
50
60
  const converted = convertParagraphWithTextBoxes(block, styleResolver, {
51
61
  textBoxGroupId: nextTextBoxGroupId(),
52
- context: {
53
- theme,
54
- nextTextBoxGroupId
55
- }
62
+ context: conversionContext
56
63
  });
57
64
  const firstConverted = converted.at(0);
58
65
  if (pbPos === "before" && converted.length === 1 && firstConverted?.type.name === "paragraph" && firstConverted.content.size === 0 && document.package.settings?.splitPageBreakAndParagraphMark !== true) {
@@ -72,10 +79,7 @@ function toProseDoc(document, options) {
72
79
  }
73
80
  out.push(...converted);
74
81
  if (pbPos === "after") out.push(schema.node("pageBreak"));
75
- } else if (block.type === "table") out.push(convertTable(block, styleResolver, {
76
- theme,
77
- nextTextBoxGroupId
78
- }));
82
+ } else if (block.type === "table") out.push(convertTable(block, styleResolver, conversionContext));
79
83
  else out.push(convertBlockSdt(block, convertBodyBlocks));
80
84
  return out;
81
85
  };
@@ -122,14 +126,13 @@ function convertBlockSdt(blockSdt, convertBlocks) {
122
126
  * Resolves style-based text formatting and passes it to runs so that
123
127
  * paragraph styles (like Heading1) apply their font size, color, etc.
124
128
  */
125
- function convertParagraph(paragraph, styleResolver, activeCommentIds, extraRunFormatting, tableParagraphOverlay, textBoxAnchors) {
129
+ function convertParagraph(paragraph, styleResolver, nextHyperlinkInstanceIndex, activeCommentIds, extraRunFormatting, tableParagraphOverlay, textBoxAnchors) {
126
130
  const attrs = paragraphFormattingToAttrs(paragraph, styleResolver, tableParagraphOverlay);
127
131
  const isTocParagraph = isTocStyleId(paragraph.formatting?.styleId);
128
132
  const inlineNodes = [];
129
133
  let inlineOffset = 0;
130
134
  let bookmarksArr;
131
135
  let emptyHyperlinks;
132
- let hyperlinkIndex = 0;
133
136
  const commentIds = activeCommentIds ?? /* @__PURE__ */ new Set();
134
137
  const emitInlineNodes = (nodes) => {
135
138
  if (nodes.length === 0) return;
@@ -160,7 +163,7 @@ function convertParagraph(paragraph, styleResolver, activeCommentIds, extraRunFo
160
163
  return suppressParagraphMarkFormatting(baseRunFormatting, inheritableParagraphRunFormatting, formatting, paragraphMarkPrecedesStyle);
161
164
  };
162
165
  const emitTrackedChange = (change, markType, moveKind) => {
163
- emitInlineNodes(convertTrackedChange(change, markType, getInheritedRunFormatting, styleResolver, moveKind, textBoxAnchors));
166
+ emitInlineNodes(convertTrackedChange(change, markType, nextHyperlinkInstanceIndex, getInheritedRunFormatting, styleResolver, moveKind, textBoxAnchors));
164
167
  };
165
168
  for (const content of paragraph.content) {
166
169
  if (content.type === "commentRangeStart") commentIds.add(content.id);
@@ -168,12 +171,10 @@ function convertParagraph(paragraph, styleResolver, activeCommentIds, extraRunFo
168
171
  else if (content.type === "commentReference") anchorPointComment(inlineNodes, content.id);
169
172
  else if (content.type === "run") emitInlineNodes(convertRun(content, getInheritedRunFormatting(content.formatting), styleResolver, textBoxAnchors));
170
173
  else if (content.type === "hyperlink") {
171
- const currentHyperlinkIndex = hyperlinkIndex;
172
- hyperlinkIndex += 1;
173
174
  const linkNodes = convertHyperlink(content, {
174
175
  getInheritedRunFormatting,
175
176
  styleResolver,
176
- hyperlinkIndex: currentHyperlinkIndex,
177
+ hyperlinkIndex: nextHyperlinkInstanceIndex(),
177
178
  textBoxAnchors
178
179
  });
179
180
  if (linkNodes.length === 0) {
@@ -189,7 +190,7 @@ function convertParagraph(paragraph, styleResolver, activeCommentIds, extraRunFo
189
190
  }
190
191
  emitInlineNodes(linkNodes);
191
192
  } else if (content.type === "simpleField" || content.type === "complexField") emitInlineNode(convertField(content, getInheritedRunFormatting, styleResolver));
192
- else if (content.type === "inlineSdt") emitInlineNode(convertInlineSdt(content, getInheritedRunFormatting, styleResolver, textBoxAnchors));
193
+ else if (content.type === "inlineSdt") emitInlineNode(convertInlineSdt(content, nextHyperlinkInstanceIndex, getInheritedRunFormatting, styleResolver, textBoxAnchors));
193
194
  else if (content.type === "insertion" || content.type === "moveTo") emitTrackedChange(content, "insertion", content.type === "moveTo" ? "moveTo" : null);
194
195
  else if (content.type === "deletion" || content.type === "moveFrom") emitTrackedChange(content, "deletion", content.type === "moveFrom" ? "moveFrom" : null);
195
196
  else if (content.type === "mathEquation") emitInlineNode(convertMathEquation(content));
@@ -216,15 +217,17 @@ const resolveParagraphStyleFontFamily = (styleId, styleResolver) => {
216
217
  };
217
218
  /**
218
219
  * Apply comment marks to PM nodes within a comment range.
219
- * Only the first active comment ID is used (comments don't overlap visually).
220
220
  */
221
221
  function applyCommentMarks(nodes, commentIds) {
222
222
  if (commentIds.size === 0) return nodes;
223
- const commentId = [...commentIds][0];
224
- const commentMark = schema.marks["comment"].create({ commentId });
223
+ const commentMarkType = schema.marks["comment"];
224
+ if (!commentMarkType) return nodes;
225
+ const commentMarks = [...commentIds].toSorted((left, right) => left - right).map((commentId) => commentMarkType.create({ commentId }));
225
226
  return nodes.map((node) => {
226
- if (node.isText || node.isInline && node.type.allowsMarkType(commentMark.type)) return node.mark(commentMark.addToSet(node.marks));
227
- return node;
227
+ if (!node.isText && (!node.isInline || !node.type.allowsMarkType(commentMarkType))) return node;
228
+ let marks = node.marks;
229
+ for (const commentMark of commentMarks) marks = commentMark.addToSet(marks);
230
+ return node.mark(marks);
228
231
  });
229
232
  }
230
233
  function anchorPointComment(nodes, commentId) {
@@ -241,13 +244,11 @@ function anchorPointComment(nodes, commentId) {
241
244
  * Convert tracked change (insertion or deletion) content to PM nodes with
242
245
  * an insertion/deletion mark applied.
243
246
  */
244
- function convertTrackedChange(change, markType, getInheritedRunFormatting, styleResolver, moveKind = null, textBoxAnchors) {
247
+ function convertTrackedChange(change, markType, nextHyperlinkInstanceIndex, getInheritedRunFormatting, styleResolver, moveKind = null, textBoxAnchors) {
245
248
  const nodes = [];
246
- let hyperlinkIndex = 0;
247
249
  for (const item of change.content) if (item.type === "run") nodes.push(...convertRun(item, getInheritedRunFormatting(item.formatting), styleResolver, textBoxAnchors));
248
250
  else {
249
- const currentHyperlinkIndex = hyperlinkIndex;
250
- hyperlinkIndex += 1;
251
+ const currentHyperlinkIndex = nextHyperlinkInstanceIndex();
251
252
  nodes.push(...convertHyperlink(item, {
252
253
  getInheritedRunFormatting,
253
254
  styleResolver,
@@ -919,7 +920,8 @@ function standaloneTableCellToProseMirror(cell, nodeType) {
919
920
  styleResolver: null,
920
921
  context: {
921
922
  theme: null,
922
- nextTextBoxGroupId: createTextBoxGroupIdFactory()
923
+ nextTextBoxGroupId: createTextBoxGroupIdFactory(),
924
+ nextHyperlinkInstanceIndex: createHyperlinkInstanceIndexAllocator()
923
925
  },
924
926
  isHeader: nodeType === "tableHeader",
925
927
  gridWidthPercent: void 0,
@@ -971,20 +973,17 @@ function convertMathEquation(math) {
971
973
  /**
972
974
  * Convert an InlineSdt to a ProseMirror sdt node with inline content.
973
975
  */
974
- function convertInlineSdt(sdt, getInheritedRunFormatting, styleResolver, textBoxAnchors) {
976
+ function convertInlineSdt(sdt, nextHyperlinkInstanceIndex, getInheritedRunFormatting, styleResolver, textBoxAnchors) {
975
977
  const props = sdt.properties;
976
978
  const inlineNodes = [];
977
- let hyperlinkIndex = 0;
978
979
  for (const content of sdt.content) if (content.type === "run") {
979
980
  const runNodes = convertRun(content, getInheritedRunFormatting(content.formatting), styleResolver, textBoxAnchors);
980
981
  inlineNodes.push(...runNodes);
981
982
  } else if (content.type === "hyperlink") {
982
- const currentHyperlinkIndex = hyperlinkIndex;
983
- hyperlinkIndex += 1;
984
983
  const linkNodes = convertHyperlink(content, {
985
984
  getInheritedRunFormatting,
986
985
  styleResolver,
987
- hyperlinkIndex: currentHyperlinkIndex,
986
+ hyperlinkIndex: nextHyperlinkInstanceIndex(),
988
987
  textBoxAnchors
989
988
  });
990
989
  inlineNodes.push(...linkNodes);
@@ -992,12 +991,12 @@ function convertInlineSdt(sdt, getInheritedRunFormatting, styleResolver, textBox
992
991
  const fieldNode = convertField(content, getInheritedRunFormatting, styleResolver);
993
992
  if (fieldNode) inlineNodes.push(fieldNode);
994
993
  } else if (content.type === "inlineSdt") {
995
- const nestedSdt = convertInlineSdt(content, getInheritedRunFormatting, styleResolver, textBoxAnchors);
994
+ const nestedSdt = convertInlineSdt(content, nextHyperlinkInstanceIndex, getInheritedRunFormatting, styleResolver, textBoxAnchors);
996
995
  if (nestedSdt) inlineNodes.push(nestedSdt);
997
- } else if (content.type === "insertion") inlineNodes.push(...convertTrackedChange(content, "insertion", getInheritedRunFormatting, styleResolver, null, textBoxAnchors));
998
- else if (content.type === "deletion") inlineNodes.push(...convertTrackedChange(content, "deletion", getInheritedRunFormatting, styleResolver, null, textBoxAnchors));
999
- else if (content.type === "moveTo") inlineNodes.push(...convertTrackedChange(content, "insertion", getInheritedRunFormatting, styleResolver, "moveTo", textBoxAnchors));
1000
- else if (content.type === "moveFrom") inlineNodes.push(...convertTrackedChange(content, "deletion", getInheritedRunFormatting, styleResolver, "moveFrom", textBoxAnchors));
996
+ } else if (content.type === "insertion") inlineNodes.push(...convertTrackedChange(content, "insertion", nextHyperlinkInstanceIndex, getInheritedRunFormatting, styleResolver, null, textBoxAnchors));
997
+ else if (content.type === "deletion") inlineNodes.push(...convertTrackedChange(content, "deletion", nextHyperlinkInstanceIndex, getInheritedRunFormatting, styleResolver, null, textBoxAnchors));
998
+ else if (content.type === "moveTo") inlineNodes.push(...convertTrackedChange(content, "insertion", nextHyperlinkInstanceIndex, getInheritedRunFormatting, styleResolver, "moveTo", textBoxAnchors));
999
+ else if (content.type === "moveFrom") inlineNodes.push(...convertTrackedChange(content, "deletion", nextHyperlinkInstanceIndex, getInheritedRunFormatting, styleResolver, "moveFrom", textBoxAnchors));
1001
1000
  else {
1002
1001
  const mathNode = convertMathEquation(content);
1003
1002
  if (mathNode) inlineNodes.push(mathNode);
@@ -1378,7 +1377,7 @@ function convertShape(shape) {
1378
1377
  }
1379
1378
  function convertParagraphWithTextBoxes(block, styleResolver, { textBoxGroupId, context, extraRunFormatting, tableParagraphOverlay }) {
1380
1379
  const { textBoxes, textBoxAnchors } = extractTextBoxesFromParagraph(block, textBoxGroupId);
1381
- const pmParagraph = convertParagraph(block, styleResolver, void 0, extraRunFormatting, tableParagraphOverlay, textBoxAnchors);
1380
+ const pmParagraph = convertParagraph(block, styleResolver, context.nextHyperlinkInstanceIndex, void 0, extraRunFormatting, tableParagraphOverlay, textBoxAnchors);
1382
1381
  const nodes = [];
1383
1382
  const isEmptyAfterExtraction = textBoxes.length > 0 && !hasContentBesidesTextBoxAnchors(pmParagraph);
1384
1383
  const keepWrapperParagraph = isEmptyAfterExtraction && hasParagraphBoundaryPayload(block, pmParagraph);
@@ -1637,19 +1636,18 @@ function headerFooterToProseDoc(content, options) {
1637
1636
  const styleResolver = options?.styles ? createStyleEngine(options.styles) : null;
1638
1637
  const theme = options?.theme ?? null;
1639
1638
  const nextTextBoxGroupId = createTextBoxGroupIdFactory();
1639
+ const conversionContext = {
1640
+ theme,
1641
+ nextTextBoxGroupId,
1642
+ nextHyperlinkInstanceIndex: createHyperlinkInstanceIndexAllocator()
1643
+ };
1640
1644
  const convertBlocks = (blocks) => {
1641
1645
  const out = [];
1642
1646
  for (const block of blocks) if (block.type === "paragraph") out.push(...convertParagraphWithTextBoxes(block, styleResolver, {
1643
1647
  textBoxGroupId: nextTextBoxGroupId(),
1644
- context: {
1645
- theme,
1646
- nextTextBoxGroupId
1647
- }
1648
- }));
1649
- else if (block.type === "table") out.push(convertTable(block, styleResolver, {
1650
- theme,
1651
- nextTextBoxGroupId
1648
+ context: conversionContext
1652
1649
  }));
1650
+ else if (block.type === "table") out.push(convertTable(block, styleResolver, conversionContext));
1653
1651
  else out.push(convertBlockSdt(block, convertBlocks));
1654
1652
  return out;
1655
1653
  };
@@ -11,6 +11,7 @@ const CommentExtension = createMarkExtension({
11
11
  name: "comment",
12
12
  schemaMarkName: "comment",
13
13
  markSpec: {
14
+ excludes: "",
14
15
  attrs: {
15
16
  /** Comment ID (matches Comment.id) */
16
17
  commentId: { default: 0 } },
@@ -0,0 +1,14 @@
1
+ import { ParagraphAttrs, ParagraphBlock } from "../layout-engine/types.js";
2
+ //#region src/utils/paragraphInlineLayout.d.ts
3
+ type ParagraphAlignment = NonNullable<ParagraphAttrs["alignment"]>;
4
+ type PhysicalParagraphInlineLayout = {
5
+ alignment: ParagraphAlignment;
6
+ explicitAlignment?: ParagraphAlignment;
7
+ indentLeft: number;
8
+ indentRight: number;
9
+ isRtl: boolean;
10
+ };
11
+ /** Resolve logical OOXML paragraph sides into physical inline geometry. */
12
+ declare const resolvePhysicalParagraphInlineLayout: (block: ParagraphBlock) => PhysicalParagraphInlineLayout;
13
+ //#endregion
14
+ export { PhysicalParagraphInlineLayout, resolvePhysicalParagraphInlineLayout };
@@ -0,0 +1,25 @@
1
+ import { isRtlParagraph } from "./paragraphBaseDirection.js";
2
+ //#region src/utils/paragraphInlineLayout.ts
3
+ const mirrorHorizontalAlignment = (alignment) => {
4
+ if (alignment === "left") return "right";
5
+ if (alignment === "right") return "left";
6
+ return alignment;
7
+ };
8
+ /** Resolve logical OOXML paragraph sides into physical inline geometry. */
9
+ const resolvePhysicalParagraphInlineLayout = (block) => {
10
+ const attrs = block.attrs;
11
+ const isRtl = isRtlParagraph(block);
12
+ const mirrorsHorizontalSides = attrs?.bidi === true;
13
+ const explicitAlignment = mirrorsHorizontalSides ? mirrorHorizontalAlignment(attrs?.alignment) : attrs?.alignment;
14
+ const indentLeft = attrs?.indent?.left ?? 0;
15
+ const indentRight = attrs?.indent?.right ?? 0;
16
+ return {
17
+ alignment: explicitAlignment ?? (isRtl ? "right" : "left"),
18
+ ...explicitAlignment === void 0 ? {} : { explicitAlignment },
19
+ indentLeft: mirrorsHorizontalSides ? indentRight : indentLeft,
20
+ indentRight: mirrorsHorizontalSides ? indentLeft : indentRight,
21
+ isRtl
22
+ };
23
+ };
24
+ //#endregion
25
+ export { resolvePhysicalParagraphInlineLayout };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/folio-core",
3
- "version": "0.25.1",
3
+ "version": "0.25.3",
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",