@stll/folio-core 0.29.0 → 0.31.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 (103) hide show
  1. package/dist/controller/folioEditor.d.ts +28 -1
  2. package/dist/controller/folioEditor.js +37 -26
  3. package/dist/controller/fontReadiness.js +28 -24
  4. package/dist/controller/hiddenEditorManager.d.ts +2 -7
  5. package/dist/controller/layoutPipeline.js +33 -3
  6. package/dist/controller/noteEditorManager.d.ts +3 -6
  7. package/dist/docx/metafileSvg.d.ts +12 -0
  8. package/dist/docx/metafileSvg.js +462 -0
  9. package/dist/docx/paragraphTextBoxEnrichment.js +55 -4
  10. package/dist/docx/parser.d.ts +2 -2
  11. package/dist/docx/parser.js +26 -0
  12. package/dist/docx/runParser.js +2 -1
  13. package/dist/docx/serializer/runSerializer.js +14 -2
  14. package/dist/docx/server/createBilingualDocument.d.ts +20 -3
  15. package/dist/docx/server/createBilingualDocument.js +153 -22
  16. package/dist/docx/styleParser.js +2 -1
  17. package/dist/docx/textBoxParser.js +14 -0
  18. package/dist/docx/vmlImageParser.js +72 -152
  19. package/dist/docx/vmlPreview.d.ts +31 -0
  20. package/dist/docx/vmlPreview.js +535 -0
  21. package/dist/docx/wrapTypes.d.ts +2 -36
  22. package/dist/docx/wrapTypes.js +1 -39
  23. package/dist/fonts/fontAlternates.d.ts +8 -0
  24. package/dist/fonts/fontAlternates.js +16 -0
  25. package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
  26. package/dist/layout-bridge/convert/footnoteLayout.js +34 -3
  27. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
  28. package/dist/layout-bridge/convert/headerFooterLayout.js +5 -0
  29. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +3 -0
  30. package/dist/layout-bridge/convert/toFlowBlocks.js +78 -28
  31. package/dist/layout-bridge/dom/noteStoryDom.d.ts +1 -1
  32. package/dist/layout-bridge/engine/selectionRects.js +3 -1
  33. package/dist/layout-engine/index.js +65 -32
  34. package/dist/layout-engine/justifiedLineFit.d.ts +7 -0
  35. package/dist/layout-engine/justifiedLineFit.js +6 -0
  36. package/dist/layout-engine/keep-together.js +15 -10
  37. package/dist/layout-engine/measure/complexScriptFormatting.d.ts +6 -1
  38. package/dist/layout-engine/measure/complexScriptFormatting.js +11 -2
  39. package/dist/layout-engine/measure/floatingTablePosition.d.ts +16 -1
  40. package/dist/layout-engine/measure/floatingTablePosition.js +29 -6
  41. package/dist/layout-engine/measure/lineBreakProvider.js +67 -8
  42. package/dist/layout-engine/measure/listMarkerWidth.d.ts +1 -0
  43. package/dist/layout-engine/measure/listMarkerWidth.js +23 -10
  44. package/dist/layout-engine/measure/measureBlocks.d.ts +15 -0
  45. package/dist/layout-engine/measure/measureBlocks.js +197 -30
  46. package/dist/layout-engine/measure/measureContainer.js +20 -24
  47. package/dist/layout-engine/measure/measureHelpers.d.ts +1 -1
  48. package/dist/layout-engine/measure/measureHelpers.js +14 -8
  49. package/dist/layout-engine/measure/measureParagraph.js +111 -37
  50. package/dist/layout-engine/measure/measureTypes.d.ts +3 -0
  51. package/dist/layout-engine/measure/tabCalculator.d.ts +123 -0
  52. package/dist/layout-engine/measure/tabCalculator.js +144 -0
  53. package/dist/layout-engine/measure/tableCellFlow.d.ts +21 -1
  54. package/dist/layout-engine/measure/tableCellFlow.js +45 -8
  55. package/dist/layout-engine/measure/tableInlinePlacement.d.ts +10 -2
  56. package/dist/layout-engine/measure/tableInlinePlacement.js +17 -15
  57. package/dist/layout-engine/paginator.d.ts +26 -2
  58. package/dist/layout-engine/paginator.js +76 -14
  59. package/dist/layout-engine/paragraphSpacing.d.ts +3 -1
  60. package/dist/layout-engine/paragraphSpacing.js +4 -3
  61. package/dist/layout-engine/renderedBreakReconciliation.d.ts +14 -3
  62. package/dist/layout-engine/renderedBreakReconciliation.js +45 -14
  63. package/dist/layout-engine/section-breaks.d.ts +3 -1
  64. package/dist/layout-engine/section-breaks.js +6 -2
  65. package/dist/layout-engine/types.d.ts +18 -1
  66. package/dist/layout-engine/types.js +1 -1
  67. package/dist/layout-painter/imageLayout.d.ts +1 -1
  68. package/dist/layout-painter/renderPage.js +6 -1
  69. package/dist/layout-painter/renderParagraph.js +169 -82
  70. package/dist/layout-painter/renderTable.d.ts +1 -1
  71. package/dist/layout-painter/renderTable.js +18 -7
  72. package/dist/layout-painter/renderTextBox.d.ts +1 -1
  73. package/dist/layout-painter/renderTextBox.js +6 -1
  74. package/dist/model.d.ts +3 -3
  75. package/dist/model.js +2 -2
  76. package/dist/paged-layout/sectionBlockWidths.d.ts +14 -0
  77. package/dist/paged-layout/sectionBlockWidths.js +103 -14
  78. package/dist/prosemirror/attrs/index.js +12 -1
  79. package/dist/prosemirror/conversion/fromProseDoc.js +6 -2
  80. package/dist/prosemirror/conversion/toProseDoc.js +10 -3
  81. package/dist/prosemirror/extensions/marks/CharacterSpacingExtension.js +9 -5
  82. package/dist/prosemirror/extensions/nodes/TableExtension.js +2 -0
  83. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +6 -2
  84. package/dist/prosemirror/schema/nodes.d.ts +4 -0
  85. package/dist/prosemirror/utils/tabCalculator.d.ts +2 -123
  86. package/dist/prosemirror/utils/tabCalculator.js +1 -140
  87. package/dist/render-dom/RemoteSelectionOverlay.d.ts +4 -4
  88. package/dist/server.d.ts +2 -2
  89. package/dist/server.js +2 -2
  90. package/dist/types/documentEnumValues.d.ts +4 -1
  91. package/dist/types/documentEnumValues.js +12 -1
  92. package/dist/types/editor-story.d.ts +8 -0
  93. package/dist/types/editor-story.js +0 -0
  94. package/dist/types/remote-selection.d.ts +11 -0
  95. package/dist/types/remote-selection.js +0 -0
  96. package/dist/types/wrap.d.ts +36 -0
  97. package/dist/types/wrap.js +40 -0
  98. package/dist/utils/fontResolver.d.ts +1 -1
  99. package/dist/utils/fontResolver.js +42 -2
  100. package/dist/utils/formatToStyle.js +4 -2
  101. package/dist/utils/horizontalScale.d.ts +28 -0
  102. package/dist/utils/horizontalScale.js +42 -0
  103. package/package.json +2 -2
@@ -1,5 +1,6 @@
1
1
  import { HIGHLIGHT_COLOR_VALUES } from "../../types/documentEnumValues.js";
2
2
  import { isValidHexColor } from "../../utils/colorResolver.js";
3
+ import { roundHorizontalScalePercentForSerialization } from "../../utils/horizontalScale.js";
3
4
  import { THEME_COLOR_TO_DRAWING_SCHEME } from "../drawingUtils.js";
4
5
  import { requiresXmlSpacePreserve } from "../textWhitespace.js";
5
6
  import { serializeParagraph } from "./paragraphSerializer.js";
@@ -127,7 +128,8 @@ function serializeTextFormatting(formatting) {
127
128
  const colorXml = serializeColorElement(formatting.color);
128
129
  if (colorXml) parts.push(colorXml);
129
130
  if (formatting.spacing !== void 0) parts.push(`<w:spacing w:val="${intAttr(formatting.spacing)}"/>`);
130
- if (formatting.scale !== void 0) parts.push(`<w:w w:val="${intAttr(formatting.scale)}"/>`);
131
+ const horizontalScale = roundHorizontalScalePercentForSerialization(formatting.scale);
132
+ if (horizontalScale !== void 0) parts.push(`<w:w w:val="${intAttr(horizontalScale)}"/>`);
131
133
  if (formatting.kerning !== void 0) parts.push(`<w:kern w:val="${intAttr(formatting.kerning)}"/>`);
132
134
  if (formatting.position !== void 0) parts.push(`<w:position w:val="${intAttr(formatting.position)}"/>`);
133
135
  if (formatting.fontSize !== void 0) parts.push(`<w:sz w:val="${intAttr(formatting.fontSize)}"/>`);
@@ -448,6 +450,16 @@ function serializeDrawingContent(content) {
448
450
  function serializeShapeTextBody(blocks) {
449
451
  return blocks.map((block) => block.type === "paragraph" ? serializeParagraph(block) : serializeTable(block, serializeParagraph)).join("");
450
452
  }
453
+ const serializeTextBodyAnchor = (anchor) => {
454
+ switch (anchor) {
455
+ case "top": return "t";
456
+ case "middle": return "ctr";
457
+ case "bottom": return "b";
458
+ case "distributed": return "dist";
459
+ case "justified": return "just";
460
+ default: return anchor;
461
+ }
462
+ };
451
463
  function serializeGeometryAdjustments(shape) {
452
464
  if (!shape.geometryAdjustments || shape.geometryAdjustments.length === 0) return "<a:avLst/>";
453
465
  return `<a:avLst>${shape.geometryAdjustments.map(({ name, formula }) => `<a:gd name="${escapeXml(name)}" fmla="${escapeXml(formula)}"/>`).join("")}</a:avLst>`;
@@ -484,7 +496,7 @@ function serializeShapeContent(content) {
484
496
  const tb = shape.textBody;
485
497
  const bpAttrs = ["rot=\"0\"", "vert=\"horz\""];
486
498
  if (tb.textWrap) bpAttrs.push(`wrap="${tb.textWrap}"`);
487
- if (tb.anchor) bpAttrs.push(`anchor="${tb.anchor === "middle" ? "ctr" : tb.anchor}"`);
499
+ if (tb.anchor) bpAttrs.push(`anchor="${serializeTextBodyAnchor(tb.anchor)}"`);
488
500
  if (tb.anchorCenter) bpAttrs.push("anchorCtr=\"1\"");
489
501
  if (tb.margins) {
490
502
  if (tb.margins.left != null) bpAttrs.push(`lIns="${intAttr(tb.margins.left)}"`);
@@ -16,18 +16,28 @@ type BilingualRow = ({
16
16
  rowId: string;
17
17
  } & BilingualParagraphRef) | {
18
18
  kind: "table";
19
+ layout: "inline";
19
20
  rowId: string;
20
21
  /**
21
22
  * Every paragraph inside the table, in document order. The table is not
22
23
  * copied, so these are the paragraphs to translate in place.
23
24
  */
24
25
  paragraphs: BilingualTableParagraphRef[];
26
+ } | {
27
+ kind: "table";
28
+ layout: "stacked";
29
+ /** Equals the first target paragraph id. */
30
+ rowId: string;
31
+ /** Source/target paragraph pairs in table document order. */
32
+ paragraphs: BilingualParagraphRef[];
25
33
  };
26
34
  type BilingualTableParagraphRef = {
27
35
  paraId: string | undefined;
28
36
  sourceText: string;
29
37
  };
30
38
  type BilingualBorders = "none" | "grid";
39
+ declare const BILINGUAL_TABLE_LAYOUTS: readonly ["inline", "stacked"];
40
+ type BilingualTableLayout = (typeof BILINGUAL_TABLE_LAYOUTS)[number];
31
41
  type CreateBilingualDocumentOptions = {
32
42
  /**
33
43
  * Suffix for cloned style ids and names (for example `"en"` turns
@@ -37,6 +47,13 @@ type CreateBilingualDocumentOptions = {
37
47
  targetStyleSuffix: string;
38
48
  /** Table borders; legal practice is usually `"none"`. Default `"none"`. */
39
49
  borders?: BilingualBorders;
50
+ /**
51
+ * Layout for source tables. `"inline"` keeps one full-width table whose
52
+ * paragraphs are translated in place. `"stacked"` keeps the source table
53
+ * and adds an independently addressable target copy below it. Default
54
+ * `"inline"`.
55
+ */
56
+ tableLayout?: BilingualTableLayout;
40
57
  /**
41
58
  * Paragraph handles exposed by Folio's canonical AI-edit snapshot for the
42
59
  * source DOCX. Only these paragraphs may become translation rows.
@@ -52,7 +69,7 @@ type CreateBilingualDocumentResult = {
52
69
  declare const InvalidBilingualDocumentOptionsError_base: import("better-result").TaggedErrorClass<"InvalidBilingualDocumentOptionsError">;
53
70
  declare class InvalidBilingualDocumentOptionsError extends InvalidBilingualDocumentOptionsError_base<{
54
71
  message: string;
55
- option: "targetStyleSuffix";
72
+ option: "tableLayout" | "targetStyleSuffix";
56
73
  }> {}
57
74
  declare function createBilingualDocument(source: document_d_exports.Document, options: CreateBilingualDocumentOptions): CreateBilingualDocumentResult;
58
75
  /**
@@ -60,8 +77,8 @@ declare function createBilingualDocument(source: document_d_exports.Document, op
60
77
  * {@link createBilingualDocument}. A dedicated table-style discriminator keeps
61
78
  * ordinary two-column source tables from being mistaken for bilingual output;
62
79
  * the expected row structure is validated as a second check. Rows are returned
63
- * in document order; the right paragraph's `paraId` is the row handle.
80
+ * in document order.
64
81
  */
65
82
  declare function readBilingualDocument(document: document_d_exports.Document, editableParagraphIds: ReadonlySet<string>): BilingualRow[];
66
83
  //#endregion
67
- export { BilingualBorders, BilingualParagraphRef, BilingualRow, BilingualRowKind, BilingualTableParagraphRef, CreateBilingualDocumentOptions, CreateBilingualDocumentResult, InvalidBilingualDocumentOptionsError, createBilingualDocument, readBilingualDocument };
84
+ export { BILINGUAL_TABLE_LAYOUTS, BilingualBorders, BilingualParagraphRef, BilingualRow, BilingualRowKind, BilingualTableLayout, BilingualTableParagraphRef, CreateBilingualDocumentOptions, CreateBilingualDocumentResult, InvalidBilingualDocumentOptionsError, createBilingualDocument, readBilingualDocument };
@@ -17,13 +17,15 @@ import { TaggedError } from "better-result";
17
17
  *
18
18
  * Section breaks cannot live inside a table cell, so the body is split at
19
19
  * paragraphs carrying `sectionProperties`: each section becomes its own table
20
- * and the break paragraph stays between the tables. A source table (parties,
21
- * signature block) is kept once, in a row spanning both columns: it is signed
22
- * and read once, and its labels are translated inline rather than duplicated.
20
+ * and the break paragraph stays between the tables. Source tables (parties,
21
+ * signature blocks) can either span both columns for inline translation or be
22
+ * followed by a full-width target copy, depending on `tableLayout`.
23
23
  * Structural-only paragraphs are also kept once between tables; they have no
24
24
  * independently editable text for a translation row to address.
25
25
  */
26
+ const BILINGUAL_TABLE_LAYOUTS = ["inline", "stacked"];
26
27
  const STYLE_SUFFIX_PATTERN = /^[A-Za-z0-9-]+$/u;
28
+ const DEFAULT_TABLE_LAYOUT = BILINGUAL_TABLE_LAYOUTS[0];
27
29
  var InvalidBilingualDocumentOptionsError = class extends TaggedError("InvalidBilingualDocumentOptionsError") {};
28
30
  var UneditableBilingualManifestError = class extends TaggedError("UneditableBilingualManifestError") {};
29
31
  const FULL_WIDTH_PCT = 5e3;
@@ -56,12 +58,18 @@ const TABLE_BORDERS = {
56
58
  insideV: GRID_BORDER
57
59
  }
58
60
  };
61
+ const isBilingualTableLayout = (value) => typeof value === "string" && BILINGUAL_TABLE_LAYOUTS.some((layout) => layout === value);
59
62
  function createBilingualDocument(source, options) {
60
63
  if (!STYLE_SUFFIX_PATTERN.test(options.targetStyleSuffix)) throw new InvalidBilingualDocumentOptionsError({
61
64
  message: `targetStyleSuffix must match ${STYLE_SUFFIX_PATTERN}; received ${JSON.stringify(options.targetStyleSuffix)}`,
62
65
  option: "targetStyleSuffix"
63
66
  });
67
+ if (options.tableLayout !== void 0 && !isBilingualTableLayout(options.tableLayout)) throw new InvalidBilingualDocumentOptionsError({
68
+ message: `tableLayout must be one of ${BILINGUAL_TABLE_LAYOUTS.join(", ")}; received ${JSON.stringify(options.tableLayout)}`,
69
+ option: "tableLayout"
70
+ });
64
71
  const borders = options.borders ?? "none";
72
+ const tableLayout = options.tableLayout ?? DEFAULT_TABLE_LAYOUT;
65
73
  const warnings = [];
66
74
  const styles = source.package.styles;
67
75
  const numbering = source.package.numbering;
@@ -128,12 +136,36 @@ function createBilingualDocument(source, options) {
128
136
  content.push(block);
129
137
  continue;
130
138
  }
131
- rows.push({
132
- kind: "table",
133
- rowId: paragraphs.at(0)?.paraId ?? tableRowHandle(rows.length),
134
- paragraphs
135
- });
136
- sectionRows.push(buildSpanningRow(block));
139
+ if (tableLayout === "inline") {
140
+ rows.push({
141
+ kind: "table",
142
+ layout: "inline",
143
+ rowId: paragraphs.at(0)?.paraId ?? tableRowHandle(rows.length),
144
+ paragraphs
145
+ });
146
+ sectionRows.push(buildInlineTableRow(block));
147
+ continue;
148
+ }
149
+ if (tableLayout === "stacked") {
150
+ const target = cloneTableForTarget({
151
+ table: block,
152
+ editableParagraphIds: options.editableParagraphIds,
153
+ paraIds,
154
+ styleCloner,
155
+ cloner
156
+ });
157
+ rows.push({
158
+ kind: "table",
159
+ layout: "stacked",
160
+ rowId: target.paragraphs.at(0)?.targetParaId ?? tableRowHandle(rows.length),
161
+ paragraphs: target.paragraphs
162
+ });
163
+ sectionRows.push(buildStackedTableRow({
164
+ source: block,
165
+ target: target.table
166
+ }));
167
+ continue;
168
+ }
137
169
  }
138
170
  flushSection();
139
171
  return {
@@ -367,6 +399,33 @@ const collectTableParagraphs = (table) => {
367
399
  else out.push(...collectTableParagraphs(item));
368
400
  return out;
369
401
  };
402
+ const cloneTableForTarget = ({ table, editableParagraphIds, paraIds, styleCloner, cloner }) => {
403
+ const paragraphs = [];
404
+ const cloneTable = (source) => ({
405
+ ...structuredClone(source),
406
+ rows: source.rows.map((row) => ({
407
+ ...structuredClone(row),
408
+ cells: row.cells.map((cell) => ({
409
+ ...structuredClone(cell),
410
+ content: cell.content.map((item) => {
411
+ if (item.type === "table") return cloneTable(item);
412
+ const targetParaId = paraIds.mint(item.paraId);
413
+ const copy = cloneParagraphForTarget(item, targetParaId, styleCloner, cloner);
414
+ if (item.paraId !== void 0 && editableParagraphIds.has(item.paraId)) paragraphs.push({
415
+ sourceParaId: item.paraId,
416
+ targetParaId,
417
+ sourceText: getParagraphText(item)
418
+ });
419
+ return copy;
420
+ })
421
+ }))
422
+ }))
423
+ });
424
+ return {
425
+ table: cloneTable(table),
426
+ paragraphs
427
+ };
428
+ };
370
429
  const buildRow = (left, right, styleById, textWidth) => {
371
430
  const columnWidth = Math.floor(textWidth / 2);
372
431
  const geometry = resolveHorizontalParagraphGeometry(left, styleById);
@@ -411,16 +470,21 @@ const projectParagraphIntoColumn = (paragraph, geometry, sourceWidth, columnWidt
411
470
  const maxSideIndent = Math.max(0, columnWidth - MIN_COLUMN_TEXT_WIDTH_TWIPS);
412
471
  let indentLeft = projectSideIndent(geometry.indentLeft, scale, maxSideIndent);
413
472
  let indentRight = projectSideIndent(geometry.indentRight, scale, maxSideIndent);
414
- if (indentLeft + indentRight - maxSideIndent > 0) {
415
- const total = indentLeft + indentRight;
416
- indentLeft = Math.round(indentLeft / total * maxSideIndent);
473
+ const indentFirstLine = geometry.indentFirstLine === void 0 ? void 0 : Math.max(-maxSideIndent, Math.round(geometry.indentFirstLine * scale));
474
+ const minimumLeftIndent = Math.min(maxSideIndent, Math.max(0, -(indentFirstLine ?? 0)));
475
+ indentLeft = Math.max(indentLeft, minimumLeftIndent);
476
+ const flexibleLeft = indentLeft - minimumLeftIndent;
477
+ const flexibleTotal = flexibleLeft + indentRight;
478
+ const flexibleMaximum = maxSideIndent - minimumLeftIndent;
479
+ if (flexibleTotal > flexibleMaximum) {
480
+ indentLeft = minimumLeftIndent + Math.round(flexibleLeft / flexibleTotal * flexibleMaximum);
417
481
  indentRight = maxSideIndent - indentLeft;
418
482
  }
419
483
  const formatting = {
420
484
  ...paragraph.formatting,
421
- ...geometry.indentLeft !== void 0 && { indentLeft },
485
+ ...(geometry.indentLeft !== void 0 || minimumLeftIndent > 0) && { indentLeft },
422
486
  ...geometry.indentRight !== void 0 && { indentRight },
423
- ...geometry.indentFirstLine !== void 0 && { indentFirstLine: Math.round(geometry.indentFirstLine * scale) },
487
+ ...indentFirstLine !== void 0 && { indentFirstLine },
424
488
  ...geometry.hangingIndent !== void 0 && { hangingIndent: geometry.hangingIndent },
425
489
  ...geometry.tabs !== void 0 && { tabs: geometry.tabs.map((tab) => ({
426
490
  ...tab,
@@ -445,7 +509,7 @@ const buildCell = (paragraph) => ({
445
509
  content: [paragraph]
446
510
  });
447
511
  /** A source table kept once, across both columns. */
448
- const buildSpanningRow = (table) => ({
512
+ const buildInlineTableRow = (table) => ({
449
513
  type: "tableRow",
450
514
  formatting: { cantSplit: true },
451
515
  cells: [{
@@ -464,6 +528,33 @@ const buildSpanningRow = (table) => ({
464
528
  }]
465
529
  }]
466
530
  });
531
+ const buildStackedTableRow = ({ source, target }) => ({
532
+ type: "tableRow",
533
+ formatting: { cantSplit: false },
534
+ cells: [{
535
+ type: "tableCell",
536
+ formatting: {
537
+ width: {
538
+ value: FULL_WIDTH_PCT,
539
+ type: "pct"
540
+ },
541
+ gridSpan: 2,
542
+ verticalAlign: "top"
543
+ },
544
+ content: [
545
+ source,
546
+ {
547
+ type: "paragraph",
548
+ content: []
549
+ },
550
+ target,
551
+ {
552
+ type: "paragraph",
553
+ content: []
554
+ }
555
+ ]
556
+ }]
557
+ });
467
558
  const buildTable = (rows, borders, textWidth) => ({
468
559
  type: "table",
469
560
  formatting: {
@@ -537,7 +628,7 @@ const createParaIdMinter = (taken) => {
537
628
  * {@link createBilingualDocument}. A dedicated table-style discriminator keeps
538
629
  * ordinary two-column source tables from being mistaken for bilingual output;
539
630
  * the expected row structure is validated as a second check. Rows are returned
540
- * in document order; the right paragraph's `paraId` is the row handle.
631
+ * in document order.
541
632
  */
542
633
  function readBilingualDocument(document, editableParagraphIds) {
543
634
  const styleById = new Map((document.package.styles?.styles ?? []).map((style) => [style.styleId, style]));
@@ -549,12 +640,54 @@ function readBilingualDocument(document, editableParagraphIds) {
549
640
  const [left, right] = row.cells;
550
641
  if (!left) continue;
551
642
  if (!right) {
552
- const paragraphs = left.content.filter((item) => item.type === "table").flatMap(collectTableParagraphs).filter((paragraph) => paragraph.paraId !== void 0 && editableParagraphIds.has(paragraph.paraId)).map((paragraph) => ({
643
+ const nestedTables = left.content.filter((item) => item.type === "table");
644
+ const sourceTable = nestedTables.at(0);
645
+ if (!sourceTable) {
646
+ missingHandleCount += 1;
647
+ continue;
648
+ }
649
+ if (nestedTables.length === 2) {
650
+ const targetTable = nestedTables.at(1);
651
+ if (!targetTable) continue;
652
+ const sourceParagraphs = collectTableParagraphs(sourceTable);
653
+ const targetParagraphs = collectTableParagraphs(targetTable);
654
+ if (sourceParagraphs.length !== targetParagraphs.length) {
655
+ missingHandleCount += 1;
656
+ continue;
657
+ }
658
+ const paragraphs = [];
659
+ for (const [index, source] of sourceParagraphs.entries()) {
660
+ if (source.paraId === void 0 || !editableParagraphIds.has(source.paraId)) continue;
661
+ const target = targetParagraphs.at(index);
662
+ if (target?.paraId === void 0 || target.paraId === source.paraId || !editableParagraphIds.has(target.paraId)) {
663
+ missingHandleCount += 1;
664
+ continue;
665
+ }
666
+ paragraphs.push({
667
+ sourceParaId: source.paraId,
668
+ targetParaId: target.paraId,
669
+ sourceText: getParagraphText(source)
670
+ });
671
+ }
672
+ rows.push({
673
+ kind: "table",
674
+ layout: "stacked",
675
+ rowId: paragraphs.at(0)?.targetParaId ?? tableRowHandle(rows.length),
676
+ paragraphs
677
+ });
678
+ continue;
679
+ }
680
+ if (nestedTables.length !== 1) {
681
+ missingHandleCount += 1;
682
+ continue;
683
+ }
684
+ const paragraphs = collectTableParagraphs(sourceTable).filter((paragraph) => paragraph.paraId !== void 0 && editableParagraphIds.has(paragraph.paraId)).map((paragraph) => ({
553
685
  paraId: paragraph.paraId,
554
686
  sourceText: getParagraphText(paragraph)
555
687
  }));
556
688
  rows.push({
557
689
  kind: "table",
690
+ layout: "inline",
558
691
  rowId: paragraphs.at(0)?.paraId ?? tableRowHandle(rows.length),
559
692
  paragraphs
560
693
  });
@@ -582,26 +715,24 @@ function readBilingualDocument(document, editableParagraphIds) {
582
715
  }
583
716
  }
584
717
  if (missingHandleCount > 0) throw new UneditableBilingualManifestError({
585
- message: "Bilingual manifest contains handles absent from the Folio AI-edit snapshot.",
718
+ message: "Bilingual manifest structure or handles do not match the Folio AI-edit snapshot.",
586
719
  missingHandleCount
587
720
  });
588
721
  return rows;
589
722
  }
590
723
  const isBilingualTable = (table) => {
591
724
  if (table.formatting?.styleId !== BILINGUAL_TABLE_STYLE_ID) return false;
592
- let pairs = 0;
593
725
  for (const row of table.rows) {
594
726
  const cells = row.cells;
595
727
  if (cells.length === 2) {
596
728
  if (!cells.every((cell) => cell.content.length === 1 && cell.content[0]?.type === "paragraph")) return false;
597
- pairs += 1;
598
729
  continue;
599
730
  }
600
731
  if (cells.length === 1 && cells[0]?.formatting?.gridSpan === 2) continue;
601
732
  return false;
602
733
  }
603
- return pairs > 0;
734
+ return table.rows.length > 0;
604
735
  };
605
736
  const isParagraphWithId = (value) => "type" in value && value.type === "paragraph" && "paraId" in value && typeof value.paraId === "string";
606
737
  //#endregion
607
- export { InvalidBilingualDocumentOptionsError, createBilingualDocument, readBilingualDocument };
738
+ export { BILINGUAL_TABLE_LAYOUTS, InvalidBilingualDocumentOptionsError, createBilingualDocument, readBilingualDocument };
@@ -1,4 +1,5 @@
1
1
  import { isValidHexColor } from "../utils/colorResolver.js";
2
+ import { parseHorizontalScalePercent } from "../utils/horizontalScale.js";
2
3
  import { mergeParagraphFormatting } from "../utils/paragraphFormattingMerge.js";
3
4
  import { mergeStyleTextFormatting } from "../utils/textFormattingMerge.js";
4
5
  import { BorderStyleSchema, ConditionalStyleTypeSchema, EmphasisMarkSchema, FontHintSchema, FontThemeSchema, HighlightColorSchema, LineSpacingRuleSchema, ParagraphAlignmentSchema, ShadingPatternSchema, StyleTypeSchema, TabLeaderSchema, TabStopAlignmentSchema, TableCellTextDirectionSchema, TableRowHeightRuleSchema, TableWidthTypeSchema, TextEffectSchema, ThemeColorSlotSchema, UnderlineStyleSchema, narrowEnum } from "./parserEnums.js";
@@ -137,7 +138,7 @@ function parseRunProperties(rPr, theme) {
137
138
  }
138
139
  const w = findChild(rPr, "w", "w");
139
140
  if (w) {
140
- const val = parseNumericAttribute(w, "w", "val");
141
+ const val = parseHorizontalScalePercent(getAttribute(w, "w", "val"));
141
142
  if (val !== void 0) formatting.scale = val;
142
143
  }
143
144
  const kern = findChild(rPr, "w", "kern");
@@ -4,6 +4,16 @@ import { findByFullName, findChildByLocalName, findChildrenByLocalName, getAttri
4
4
  //#region src/docx/textBoxParser.ts
5
5
  /** Default text box margins in EMUs (0.1 inch) */
6
6
  const DEFAULT_MARGIN_EMU = 91440;
7
+ const parseTextBoxVerticalAlign = (value) => {
8
+ switch (value) {
9
+ case "t": return "top";
10
+ case "ctr": return "middle";
11
+ case "b": return "bottom";
12
+ case "dist": return "distributed";
13
+ case "just": return "justified";
14
+ default: return;
15
+ }
16
+ };
7
17
  /**
8
18
  * Parse text body properties from wps:bodyPr
9
19
  * Returns margins/insets for the text box
@@ -16,6 +26,8 @@ function parseBodyProperties(bodyPr) {
16
26
  if (findChildByLocalName(bodyPr, "spAutoFit")) result.autoFit = "shape";
17
27
  else if (findChildByLocalName(bodyPr, "normAutofit")) result.autoFit = "normal";
18
28
  else if (findChildByLocalName(bodyPr, "noAutofit")) result.autoFit = "none";
29
+ const verticalAlign = parseTextBoxVerticalAlign(getAttribute(bodyPr, null, "anchor"));
30
+ if (verticalAlign) result.verticalAlign = verticalAlign;
19
31
  const lIns = parseNumericAttribute(bodyPr, null, "lIns");
20
32
  const rIns = parseNumericAttribute(bodyPr, null, "rIns");
21
33
  const tIns = parseNumericAttribute(bodyPr, null, "tIns");
@@ -131,6 +143,7 @@ function parseTextBox(drawingEl) {
131
143
  if (bodyProps.margins) textBox.margins = bodyProps.margins;
132
144
  if (bodyProps.autoFit) textBox.autoFit = bodyProps.autoFit;
133
145
  if (bodyProps.textWrap) textBox.textWrap = bodyProps.textWrap;
146
+ if (bodyProps.verticalAlign) textBox.verticalAlign = bodyProps.verticalAlign;
134
147
  if (isAnchor) {
135
148
  const position = parseAnchorPosition(container);
136
149
  if (position) textBox.position = position;
@@ -174,6 +187,7 @@ function parseTextBoxFromShape(wsp, size, position, wrap) {
174
187
  if (bodyProps.margins) textBox.margins = bodyProps.margins;
175
188
  if (bodyProps.autoFit) textBox.autoFit = bodyProps.autoFit;
176
189
  if (bodyProps.textWrap) textBox.textWrap = bodyProps.textWrap;
190
+ if (bodyProps.verticalAlign) textBox.verticalAlign = bodyProps.verticalAlign;
177
191
  if (position) textBox.position = position;
178
192
  if (wrap) textBox.wrap = wrap;
179
193
  return textBox;