@beyondwork/docx-react-component 1.0.36 → 1.0.38

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 (107) hide show
  1. package/README.md +103 -13
  2. package/package.json +1 -1
  3. package/src/api/package-version.ts +13 -0
  4. package/src/api/public-types.ts +402 -1
  5. package/src/core/commands/index.ts +18 -1
  6. package/src/core/commands/section-layout-commands.ts +58 -0
  7. package/src/core/commands/table-grid.ts +431 -0
  8. package/src/core/commands/table-structure-commands.ts +815 -55
  9. package/src/core/selection/mapping.ts +6 -0
  10. package/src/io/docx-session.ts +24 -9
  11. package/src/io/export/build-app-properties-xml.ts +88 -0
  12. package/src/io/export/serialize-comments.ts +6 -1
  13. package/src/io/export/serialize-footnotes.ts +10 -9
  14. package/src/io/export/serialize-headers-footers.ts +11 -10
  15. package/src/io/export/serialize-main-document.ts +328 -50
  16. package/src/io/export/serialize-numbering.ts +114 -24
  17. package/src/io/export/serialize-tables.ts +87 -11
  18. package/src/io/export/table-properties-xml.ts +174 -20
  19. package/src/io/export/twip.ts +66 -0
  20. package/src/io/normalize/normalize-text.ts +20 -0
  21. package/src/io/ooxml/parse-footnotes.ts +62 -1
  22. package/src/io/ooxml/parse-headers-footers.ts +62 -1
  23. package/src/io/ooxml/parse-main-document.ts +158 -1
  24. package/src/io/ooxml/parse-tables.ts +249 -0
  25. package/src/legal/bookmarks.ts +78 -0
  26. package/src/model/canonical-document.ts +45 -0
  27. package/src/review/store/scope-tag-diff.ts +130 -0
  28. package/src/runtime/document-layout.ts +4 -2
  29. package/src/runtime/document-navigation.ts +2 -306
  30. package/src/runtime/document-runtime.ts +287 -11
  31. package/src/runtime/layout/default-page-format.ts +96 -0
  32. package/src/runtime/layout/docx-font-loader.ts +143 -0
  33. package/src/runtime/layout/index.ts +233 -0
  34. package/src/runtime/layout/inert-layout-facet.ts +59 -0
  35. package/src/runtime/layout/layout-engine-instance.ts +628 -0
  36. package/src/runtime/layout/layout-invalidation.ts +257 -0
  37. package/src/runtime/layout/layout-measurement-provider.ts +175 -0
  38. package/src/runtime/layout/margin-preset-catalog.ts +178 -0
  39. package/src/runtime/layout/measurement-backend-canvas.ts +307 -0
  40. package/src/runtime/layout/measurement-backend-empirical.ts +208 -0
  41. package/src/runtime/layout/page-format-catalog.ts +233 -0
  42. package/src/runtime/layout/page-fragment-mapper.ts +179 -0
  43. package/src/runtime/layout/page-graph.ts +452 -0
  44. package/src/runtime/layout/page-layout-snapshot-adapter.ts +70 -0
  45. package/src/runtime/layout/page-story-resolver.ts +195 -0
  46. package/src/runtime/layout/paginated-layout-engine.ts +921 -0
  47. package/src/runtime/layout/project-block-fragments.ts +91 -0
  48. package/src/runtime/layout/public-facet.ts +1398 -0
  49. package/src/runtime/layout/resolved-formatting-document.ts +317 -0
  50. package/src/runtime/layout/resolved-formatting-state.ts +430 -0
  51. package/src/runtime/layout/table-render-plan.ts +229 -0
  52. package/src/runtime/render/block-fragment-projection.ts +35 -0
  53. package/src/runtime/render/decoration-resolver.ts +189 -0
  54. package/src/runtime/render/index.ts +57 -0
  55. package/src/runtime/render/pending-op-delta-reader.ts +129 -0
  56. package/src/runtime/render/render-frame-types.ts +317 -0
  57. package/src/runtime/render/render-kernel.ts +755 -0
  58. package/src/runtime/scope-tag-registry.ts +95 -0
  59. package/src/runtime/surface-projection.ts +1 -0
  60. package/src/runtime/text-ack-range.ts +49 -0
  61. package/src/runtime/view-state.ts +67 -0
  62. package/src/runtime/workflow-markup.ts +1 -5
  63. package/src/runtime/workflow-rail-segments.ts +280 -0
  64. package/src/ui/WordReviewEditor.tsx +99 -15
  65. package/src/ui/editor-runtime-boundary.ts +10 -1
  66. package/src/ui/editor-shell-view.tsx +6 -0
  67. package/src/ui/editor-surface-controller.tsx +3 -0
  68. package/src/ui/headless/chrome-registry.ts +501 -0
  69. package/src/ui/headless/scoped-chrome-policy.ts +183 -0
  70. package/src/ui/headless/selection-tool-context.ts +2 -0
  71. package/src/ui/headless/selection-tool-resolver.ts +36 -17
  72. package/src/ui/headless/selection-tool-types.ts +10 -0
  73. package/src/ui-tailwind/chrome/chrome-preset-model.ts +23 -2
  74. package/src/ui-tailwind/chrome/role-action-sets.ts +74 -0
  75. package/src/ui-tailwind/chrome/tw-detach-handle.tsx +147 -0
  76. package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +163 -0
  77. package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +57 -92
  78. package/src/ui-tailwind/chrome/tw-selection-tool-placement.ts +149 -0
  79. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +15 -4
  80. package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +274 -138
  81. package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +90 -0
  82. package/src/ui-tailwind/chrome-overlay/index.ts +22 -0
  83. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +86 -0
  84. package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +178 -0
  85. package/src/ui-tailwind/chrome-overlay/tw-workspace-view-switcher.tsx +95 -0
  86. package/src/ui-tailwind/editor-surface/fast-text-edit-lane.ts +337 -0
  87. package/src/ui-tailwind/editor-surface/local-edit-session-state.ts +100 -0
  88. package/src/ui-tailwind/editor-surface/perf-probe.ts +27 -1
  89. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +20 -2
  90. package/src/ui-tailwind/editor-surface/pm-decorations.ts +93 -23
  91. package/src/ui-tailwind/editor-surface/predicted-position-map.ts +78 -0
  92. package/src/ui-tailwind/editor-surface/predicted-tag-preflight.ts +63 -0
  93. package/src/ui-tailwind/editor-surface/predicted-tx-gate.ts +39 -0
  94. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +176 -6
  95. package/src/ui-tailwind/index.ts +33 -0
  96. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +2 -2
  97. package/src/ui-tailwind/review/tw-rail-card.tsx +150 -0
  98. package/src/ui-tailwind/review/tw-review-rail-footer.tsx +52 -0
  99. package/src/ui-tailwind/review/tw-review-rail.tsx +166 -11
  100. package/src/ui-tailwind/review/tw-workflow-tab.tsx +108 -0
  101. package/src/ui-tailwind/theme/editor-theme.css +505 -144
  102. package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +559 -0
  103. package/src/ui-tailwind/toolbar/tw-scope-posture-menu.tsx +182 -0
  104. package/src/ui-tailwind/toolbar/tw-shell-header.tsx +162 -0
  105. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +2 -2
  106. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +304 -166
  107. package/src/ui-tailwind/tw-review-workspace.tsx +163 -2
@@ -51,6 +51,12 @@ export interface TransactionMapping {
51
51
  affectsComments?: boolean;
52
52
  affectsRevisions?: boolean;
53
53
  affectsOpaqueFragments?: boolean;
54
+ /**
55
+ * Review-tag touches performed during this transaction. Used by the
56
+ * predicted-text lane's reconciler to classify acks as `adjusted` vs
57
+ * `structural-divergence` without re-walking the document.
58
+ */
59
+ scopeTagTouches?: readonly import("../../api/public-types.ts").ScopeTagTouch[];
54
60
  [key: string]: unknown;
55
61
  };
56
62
  }
@@ -67,6 +67,7 @@ import {
67
67
  createBrokenRelationshipIssue,
68
68
  createMissingPartIssue,
69
69
  } from "./opc/corrupt-package.ts";
70
+ import { buildAppPropertiesXml } from "./export/build-app-properties-xml.ts";
70
71
  import { createExportSession } from "./export/export-session.ts";
71
72
  import { serializeMainDocument } from "./export/serialize-main-document.ts";
72
73
  import {
@@ -2909,10 +2910,31 @@ function serializeCanonicalDocumentForExport(document: CanonicalDocumentEnvelope
2909
2910
  });
2910
2911
  }
2911
2912
 
2913
+ /**
2914
+ * Read a Node-style environment variable without referencing the Node-only
2915
+ * `process` global in the production build (which excludes @types/node).
2916
+ * Returns `undefined` in browser environments or when the var is unset.
2917
+ */
2918
+ function readNodeEnvVar(name: string): string | undefined {
2919
+ const proc = (globalThis as unknown as {
2920
+ process?: { env?: Record<string, string | undefined> };
2921
+ }).process;
2922
+ return proc?.env?.[name];
2923
+ }
2924
+
2912
2925
  function canReuseSourceBytesForCurrentDocument(
2913
2926
  state: ImportedDocxState,
2914
2927
  document: CanonicalDocumentEnvelope,
2915
2928
  ): boolean {
2929
+ // The validator-loop CI harness (§1 / §7) needs to exercise the full
2930
+ // serializer pipeline — including A.1 / A.2 / A.3 / A.6 / A.7 / A.8 /
2931
+ // A.9 fixes — so it sets DOCX_VALIDATOR_FORCE_REGEN=1 to disable the
2932
+ // fast-path byte-reuse. Never set this flag in production; it forces a
2933
+ // full XML regeneration on every export and is only used by the CCEP
2934
+ // validator harness to prove our serializer's correctness.
2935
+ if (readNodeEnvVar("DOCX_VALIDATOR_FORCE_REGEN") === "1") {
2936
+ return false;
2937
+ }
2916
2938
  if (requiresHostMetadataNormalization(state.sourcePackage, state.sourceDocumentPartPath)) {
2917
2939
  return false;
2918
2940
  }
@@ -3108,15 +3130,8 @@ function buildCorePropertiesXml(document: CanonicalDocumentEnvelope): string {
3108
3130
  ].join("\n");
3109
3131
  }
3110
3132
 
3111
- function buildAppPropertiesXml(): string {
3112
- return [
3113
- `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`,
3114
- `<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">`,
3115
- ` <Application>React OOXML Office</Application>`,
3116
- ` <AppVersion>1.0</AppVersion>`,
3117
- `</Properties>`,
3118
- ].join("\n");
3119
- }
3133
+ // buildAppPropertiesXml moved to src/io/export/build-app-properties-xml.ts
3134
+ // per close-render-fidelity §2 A.5. Re-import as `buildAppPropertiesXmlFn`.
3120
3135
 
3121
3136
  function xmlNode(tagName: string, value: string | undefined): string | undefined {
3122
3137
  if (typeof value !== "string" || value.length === 0) {
@@ -0,0 +1,88 @@
1
+ /**
2
+ * build-app-properties-xml — synthesises `/docProps/app.xml` for the export.
3
+ *
4
+ * Mirrors `buildCorePropertiesXml` (in `docx-session.ts`) but targets the
5
+ * `extended-properties` namespace. Emitted once per export when the source
6
+ * package does not already carry a valid app.xml part with the correct
7
+ * content type.
8
+ *
9
+ * Required by:
10
+ * docs/plans/close-render-fidelity.md §2 A.5
11
+ *
12
+ * Schema shape (ECMA-376 Part 1, Office Extended properties):
13
+ * <Properties xmlns="…/extended-properties" xmlns:vt="…/docPropsVTypes">
14
+ * <Application>@beyondwork/docx-react-component/<pkg.version></Application>
15
+ * <AppVersion>1.0</AppVersion>
16
+ * <Pages>0</Pages>
17
+ * <Words>0</Words>
18
+ * <Characters>0</Characters>
19
+ * <Lines>0</Lines>
20
+ * <Paragraphs>0</Paragraphs>
21
+ * </Properties>
22
+ *
23
+ * Zero counts are acceptable per the ECMA schema — Word-authoritative counts
24
+ * are recomputed at open time by the hosting application. A future §4 pass
25
+ * can replace these with canonical-derived counts without changing the XML
26
+ * shape.
27
+ */
28
+
29
+ import { PACKAGE_VERSION } from "../../api/package-version.ts";
30
+
31
+ export const APP_PROPERTIES_NAMESPACE =
32
+ "http://schemas.openxmlformats.org/officeDocument/2006/extended-properties";
33
+ export const APP_PROPERTIES_VT_NAMESPACE =
34
+ "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes";
35
+
36
+ export interface AppPropertiesStats {
37
+ /** Rendered page count; zero is a valid placeholder. */
38
+ pages?: number;
39
+ /** Word count. */
40
+ words?: number;
41
+ /** Character count. */
42
+ characters?: number;
43
+ /** Line count. */
44
+ lines?: number;
45
+ /** Paragraph count. */
46
+ paragraphs?: number;
47
+ /** App-version string override; defaults to the package version. */
48
+ appVersion?: string;
49
+ /** Application identifier override; defaults to package.json name@version. */
50
+ application?: string;
51
+ }
52
+
53
+ export function buildAppPropertiesXml(
54
+ stats: AppPropertiesStats = {},
55
+ ): string {
56
+ const application = stats.application ?? defaultApplicationLabel();
57
+ const appVersion = stats.appVersion ?? PACKAGE_VERSION;
58
+ const pages = stats.pages ?? 0;
59
+ const words = stats.words ?? 0;
60
+ const characters = stats.characters ?? 0;
61
+ const lines = stats.lines ?? 0;
62
+ const paragraphs = stats.paragraphs ?? 0;
63
+
64
+ return [
65
+ `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`,
66
+ `<Properties xmlns="${APP_PROPERTIES_NAMESPACE}" xmlns:vt="${APP_PROPERTIES_VT_NAMESPACE}">`,
67
+ ` <Application>${escapeXml(application)}</Application>`,
68
+ ` <AppVersion>${escapeXml(appVersion)}</AppVersion>`,
69
+ ` <Pages>${Math.max(0, Math.round(pages))}</Pages>`,
70
+ ` <Words>${Math.max(0, Math.round(words))}</Words>`,
71
+ ` <Characters>${Math.max(0, Math.round(characters))}</Characters>`,
72
+ ` <Lines>${Math.max(0, Math.round(lines))}</Lines>`,
73
+ ` <Paragraphs>${Math.max(0, Math.round(paragraphs))}</Paragraphs>`,
74
+ `</Properties>`,
75
+ ].join("\n");
76
+ }
77
+
78
+ function defaultApplicationLabel(): string {
79
+ return `@beyondwork/docx-react-component/${PACKAGE_VERSION}`;
80
+ }
81
+
82
+ function escapeXml(value: string): string {
83
+ return value
84
+ .replace(/&/gu, "&amp;")
85
+ .replace(/</gu, "&lt;")
86
+ .replace(/>/gu, "&gt;")
87
+ .replace(/"/gu, "&quot;");
88
+ }
@@ -64,8 +64,13 @@ export interface SerializedCommentDocumentResult {
64
64
  skippedCommentIds: string[];
65
65
  }
66
66
 
67
+ // A.8: emit root attributes in deterministic order (xmlns:* alphabetised,
68
+ // then mc:Ignorable, then other attrs). The sort above happens to produce
69
+ // the same token order `mc < w < w14` + `mc:Ignorable` — keep the literal
70
+ // in sync with renderDeterministicRootAttributes so regenerated docs and
71
+ // comments.xml samples match byte-for-byte when snapshots compare them.
67
72
  const DEFAULT_COMMENTS_ROOT_TAG =
68
- `<w:comments xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="w14">`;
73
+ `<w:comments xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" mc:Ignorable="w14">`;
69
74
  const DEFAULT_COMMENTS_EXTENDED_ROOT_TAG =
70
75
  `<w15:commentsEx xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml">`;
71
76
  const DEFAULT_COMMENTS_IDS_ROOT_TAG =
@@ -16,6 +16,7 @@ import {
16
16
  serializeTablePropertiesXml,
17
17
  serializeTableRowPropertiesXml,
18
18
  } from "./table-properties-xml.ts";
19
+ import { twip } from "./twip.ts";
19
20
 
20
21
  export const WORD_FOOTNOTES_CONTENT_TYPE =
21
22
  "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml";
@@ -142,7 +143,7 @@ function serializeTable(table: TableNode): string {
142
143
  if (table.gridColumns.length > 0) {
143
144
  xml += "<w:tblGrid>";
144
145
  for (const width of table.gridColumns) {
145
- xml += `<w:gridCol w:w="${width}"/>`;
146
+ xml += `<w:gridCol w:w="${twip(width)}"/>`;
146
147
  }
147
148
  xml += "</w:tblGrid>";
148
149
  }
@@ -194,9 +195,9 @@ function buildParagraphPropertiesXml(paragraph: ParagraphNode): string {
194
195
  }
195
196
  if (paragraph.spacing) {
196
197
  const attrs: string[] = [];
197
- if (paragraph.spacing.before !== undefined) attrs.push(`w:before="${paragraph.spacing.before}"`);
198
- if (paragraph.spacing.after !== undefined) attrs.push(`w:after="${paragraph.spacing.after}"`);
199
- if (paragraph.spacing.line !== undefined) attrs.push(`w:line="${paragraph.spacing.line}"`);
198
+ if (paragraph.spacing.before !== undefined) attrs.push(`w:before="${twip(paragraph.spacing.before)}"`);
199
+ if (paragraph.spacing.after !== undefined) attrs.push(`w:after="${twip(paragraph.spacing.after)}"`);
200
+ if (paragraph.spacing.line !== undefined) attrs.push(`w:line="${twip(paragraph.spacing.line)}"`);
200
201
  if (paragraph.spacing.lineRule) attrs.push(`w:lineRule="${escapeAttribute(paragraph.spacing.lineRule)}"`);
201
202
  if (attrs.length > 0) {
202
203
  parts.push(`<w:spacing ${attrs.join(" ")}/>`);
@@ -204,10 +205,10 @@ function buildParagraphPropertiesXml(paragraph: ParagraphNode): string {
204
205
  }
205
206
  if (paragraph.indentation) {
206
207
  const attrs: string[] = [];
207
- if (paragraph.indentation.left !== undefined) attrs.push(`w:left="${paragraph.indentation.left}"`);
208
- if (paragraph.indentation.right !== undefined) attrs.push(`w:right="${paragraph.indentation.right}"`);
209
- if (paragraph.indentation.firstLine !== undefined) attrs.push(`w:firstLine="${paragraph.indentation.firstLine}"`);
210
- if (paragraph.indentation.hanging !== undefined) attrs.push(`w:hanging="${paragraph.indentation.hanging}"`);
208
+ if (paragraph.indentation.left !== undefined) attrs.push(`w:left="${twip(paragraph.indentation.left)}"`);
209
+ if (paragraph.indentation.right !== undefined) attrs.push(`w:right="${twip(paragraph.indentation.right)}"`);
210
+ if (paragraph.indentation.firstLine !== undefined) attrs.push(`w:firstLine="${twip(paragraph.indentation.firstLine)}"`);
211
+ if (paragraph.indentation.hanging !== undefined) attrs.push(`w:hanging="${twip(paragraph.indentation.hanging)}"`);
211
212
  if (attrs.length > 0) {
212
213
  parts.push(`<w:ind ${attrs.join(" ")}/>`);
213
214
  }
@@ -330,7 +331,7 @@ function buildRunPropertiesXml(marks: TextMark[] | undefined): string {
330
331
  );
331
332
  break;
332
333
  case "fontSize":
333
- parts.push(`<w:sz w:val="${mark.val}"/>`);
334
+ parts.push(`<w:sz w:val="${twip(mark.val)}"/>`);
334
335
  break;
335
336
  case "textColor":
336
337
  parts.push(`<w:color w:val="${escapeAttribute(mark.color)}"/>`);
@@ -16,6 +16,7 @@ import {
16
16
  serializeTablePropertiesXml,
17
17
  serializeTableRowPropertiesXml,
18
18
  } from "./table-properties-xml.ts";
19
+ import { twip } from "./twip.ts";
19
20
 
20
21
  export const WORD_HEADER_CONTENT_TYPE =
21
22
  "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml";
@@ -132,7 +133,7 @@ function serializeTable(table: TableNode): string {
132
133
  if (table.gridColumns.length > 0) {
133
134
  xml += "<w:tblGrid>";
134
135
  for (const width of table.gridColumns) {
135
- xml += `<w:gridCol w:w="${width}"/>`;
136
+ xml += `<w:gridCol w:w="${twip(width)}"/>`;
136
137
  }
137
138
  xml += "</w:tblGrid>";
138
139
  }
@@ -182,19 +183,19 @@ function buildParagraphPropertiesXml(paragraph: ParagraphNode): string {
182
183
  if (paragraph.spacing) {
183
184
  const s = paragraph.spacing;
184
185
  const attrs: string[] = [];
185
- if (s.before !== undefined) attrs.push(`w:before="${s.before}"`);
186
- if (s.after !== undefined) attrs.push(`w:after="${s.after}"`);
187
- if (s.line !== undefined) attrs.push(`w:line="${s.line}"`);
186
+ if (s.before !== undefined) attrs.push(`w:before="${twip(s.before)}"`);
187
+ if (s.after !== undefined) attrs.push(`w:after="${twip(s.after)}"`);
188
+ if (s.line !== undefined) attrs.push(`w:line="${twip(s.line)}"`);
188
189
  if (s.lineRule) attrs.push(`w:lineRule="${escapeAttribute(s.lineRule)}"`);
189
190
  if (attrs.length > 0) parts.push(`<w:spacing ${attrs.join(" ")}/>`);
190
191
  }
191
192
  if (paragraph.indentation) {
192
193
  const ind = paragraph.indentation;
193
194
  const attrs: string[] = [];
194
- if (ind.left !== undefined) attrs.push(`w:left="${ind.left}"`);
195
- if (ind.right !== undefined) attrs.push(`w:right="${ind.right}"`);
196
- if (ind.firstLine !== undefined) attrs.push(`w:firstLine="${ind.firstLine}"`);
197
- if (ind.hanging !== undefined) attrs.push(`w:hanging="${ind.hanging}"`);
195
+ if (ind.left !== undefined) attrs.push(`w:left="${twip(ind.left)}"`);
196
+ if (ind.right !== undefined) attrs.push(`w:right="${twip(ind.right)}"`);
197
+ if (ind.firstLine !== undefined) attrs.push(`w:firstLine="${twip(ind.firstLine)}"`);
198
+ if (ind.hanging !== undefined) attrs.push(`w:hanging="${twip(ind.hanging)}"`);
198
199
  if (attrs.length > 0) parts.push(`<w:ind ${attrs.join(" ")}/>`);
199
200
  }
200
201
  if (paragraph.alignment) {
@@ -203,7 +204,7 @@ function buildParagraphPropertiesXml(paragraph: ParagraphNode): string {
203
204
  if (paragraph.tabStops && paragraph.tabStops.length > 0) {
204
205
  const tabsXml = paragraph.tabStops.map((tab) => {
205
206
  const leaderAttr = tab.leader ? ` w:leader="${escapeAttribute(tab.leader)}"` : "";
206
- return `<w:tab w:val="${tab.align}" w:pos="${tab.position}"${leaderAttr}/>`;
207
+ return `<w:tab w:val="${tab.align}" w:pos="${twip(tab.position)}"${leaderAttr}/>`;
207
208
  }).join("");
208
209
  parts.push(`<w:tabs>${tabsXml}</w:tabs>`);
209
210
  }
@@ -303,7 +304,7 @@ function buildRunPropertiesXml(marks: TextMark[] | undefined): string {
303
304
  parts.push(`<w:rFonts w:ascii="${escapeAttribute(mark.val)}" w:hAnsi="${escapeAttribute(mark.val)}"/>`);
304
305
  break;
305
306
  case "fontSize":
306
- parts.push(`<w:sz w:val="${mark.val}"/>`);
307
+ parts.push(`<w:sz w:val="${twip(mark.val)}"/>`);
307
308
  break;
308
309
  case "textColor":
309
310
  parts.push(`<w:color w:val="${escapeAttribute(mark.color)}"/>`);