@docen/docx 0.3.3 → 0.3.5

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 (68) hide show
  1. package/dist/blockquote-DY80QC06.d.mts +21 -0
  2. package/dist/bullet-list-DF60pnSL.d.mts +12 -0
  3. package/dist/converters/docx.d.mts +1 -1
  4. package/dist/converters/docx.mjs +124 -25
  5. package/dist/converters/html.d.mts +1 -1
  6. package/dist/converters/html.mjs +1 -1
  7. package/dist/converters/markdown.d.mts +1 -1
  8. package/dist/converters/markdown.mjs +1 -1
  9. package/dist/converters/patch.d.mts +1 -1
  10. package/dist/converters/prepare.d.mts +1 -1
  11. package/dist/converters/prepare.mjs +6 -9
  12. package/dist/{core-B8ba_FNi.mjs → core-BnF8XhVE.mjs} +180 -31
  13. package/dist/core-DC0_-WcE.d.mts +387 -0
  14. package/dist/core.d.mts +1 -1
  15. package/dist/core.mjs +1 -1
  16. package/dist/details-Dd5MqqmR.d.mts +17 -0
  17. package/dist/editor.d.mts +1 -1
  18. package/dist/editor.mjs +1 -1
  19. package/dist/extensions/blockquote.d.mts +2 -22
  20. package/dist/extensions/bullet-list.d.mts +2 -0
  21. package/dist/extensions/bullet-list.mjs +21 -0
  22. package/dist/extensions/code-block.d.mts +1 -18
  23. package/dist/extensions/column-break.d.mts +1 -30
  24. package/dist/extensions/column-break.mjs +1 -1
  25. package/dist/extensions/details.d.mts +2 -18
  26. package/dist/extensions/document.mjs +2 -1
  27. package/dist/extensions/extensions.d.mts +10 -6
  28. package/dist/extensions/extensions.mjs +9 -4
  29. package/dist/extensions/formatting-marks.d.mts +1 -37
  30. package/dist/extensions/formatting-marks.mjs +1 -1
  31. package/dist/extensions/image.d.mts +1 -1
  32. package/dist/extensions/image.mjs +20 -4
  33. package/dist/extensions/index.d.mts +15 -2
  34. package/dist/extensions/index.mjs +17 -2
  35. package/dist/extensions/link.d.mts +2 -0
  36. package/dist/extensions/link.mjs +56 -0
  37. package/dist/extensions/mention.d.mts +2 -24
  38. package/dist/extensions/ordered-list.d.mts +1 -24
  39. package/dist/extensions/ordered-list.mjs +10 -1
  40. package/dist/extensions/page-break.d.mts +1 -1
  41. package/dist/extensions/page-break.mjs +1 -1
  42. package/dist/extensions/paragraph.mjs +13 -0
  43. package/dist/extensions/passthrough.d.mts +2 -2
  44. package/dist/extensions/passthrough.mjs +2 -2
  45. package/dist/extensions/scroll.d.mts +2 -0
  46. package/dist/extensions/scroll.mjs +33 -0
  47. package/dist/extensions/section-break.d.mts +1 -43
  48. package/dist/extensions/section-break.mjs +1 -1
  49. package/dist/extensions/tab.d.mts +2 -0
  50. package/dist/extensions/tab.mjs +2 -0
  51. package/dist/extensions/task-item.d.mts +2 -26
  52. package/dist/extensions/tiptap.d.mts +1 -1
  53. package/dist/extensions/toc-field.d.mts +1 -1
  54. package/dist/extensions/toc-field.mjs +1 -1
  55. package/dist/extensions/wpg-group.d.mts +2 -2
  56. package/dist/extensions/wpg-group.mjs +2 -2
  57. package/dist/extensions/wps-shape.d.mts +1 -1
  58. package/dist/extensions/wps-shape.mjs +1 -1
  59. package/dist/index.d.mts +31 -7
  60. package/dist/index.mjs +17 -2
  61. package/dist/link-BawPjQZR.d.mts +6 -0
  62. package/dist/mention-BGLzLVYw.d.mts +23 -0
  63. package/dist/ordered-list-DFAe-YEV.d.mts +25 -0
  64. package/dist/scroll-ZNeThJsJ.d.mts +18 -0
  65. package/dist/task-item-B0ntvQ1Y.d.mts +25 -0
  66. package/dist/{tiptap-pZsNPsvV.d.mts → tiptap-BKqn41uT.d.mts} +2 -2
  67. package/package.json +3 -3
  68. package/dist/core-DRaLI8nd.d.mts +0 -203
@@ -0,0 +1,21 @@
1
+ //#region src/extensions/blockquote.d.ts
2
+ /**
3
+ * Blockquote extension — owns the DOCX expression of a blockquote.
4
+ *
5
+ * DOCX has no blockquote element; the convention is a left indent plus a left
6
+ * border on each contained paragraph. This module owns that signature; the
7
+ * DocxManager walks the blockquote's child paragraphs and applies it to each.
8
+ */
9
+ /** Left indent (twips, ~0.5 inch) marking a blockquote. */
10
+ declare const BLOCKQUOTE_INDENT_LEFT = 720;
11
+ /** Left border marking a blockquote. */
12
+ declare const BLOCKQUOTE_BORDER: {
13
+ readonly style: "single";
14
+ readonly size: 18;
15
+ readonly space: 12;
16
+ readonly color: "CCCCCC";
17
+ };
18
+ /** Apply the blockquote signature (left indent + left border) to paragraph opts. */
19
+ declare function applyBlockquoteStyle(paraObj: Record<string, unknown>): void;
20
+ //#endregion
21
+ export { BLOCKQUOTE_INDENT_LEFT as n, applyBlockquoteStyle as r, BLOCKQUOTE_BORDER as t };
@@ -0,0 +1,12 @@
1
+ //#region src/extensions/bullet-list.d.ts
2
+ /**
3
+ * BulletList — carries the source DOCX abstractNum reference (when the list came
4
+ * from parseDOCX) so the round-trip reuses the original numbering definition
5
+ * (custom bullet glyph/font/indent, e.g. a Wingdings marker) instead of
6
+ * regenerating the default. The `numbering` attr is DOCX-only (not rendered
7
+ * to HTML); lists created in the editor carry null and compile to the default
8
+ * bullet.
9
+ */
10
+ declare const BulletList: import("@tiptap/core").Node<import("@tiptap/extension-bullet-list").BulletListOptions, any>;
11
+ //#endregion
12
+ export { BulletList as t };
@@ -1,4 +1,4 @@
1
- import { a as JSONContent } from "../core-DRaLI8nd.mjs";
1
+ import { a as JSONContent } from "../core-DC0_-WcE.mjs";
2
2
  import { PrepareStep } from "./prepare.mjs";
3
3
  import { DocumentOptions, OutputByType, OutputType, PackerOptions, parseDocument } from "@office-open/docx";
4
4
 
@@ -120,13 +120,16 @@ var DocxManager = class {
120
120
  const core = docAttrs.core ?? void 0;
121
121
  const background = docAttrs.background ?? void 0;
122
122
  const documentExtras = docAttrs.documentExtras ?? void 0;
123
+ const origNumberingConfig = docAttrs.numbering?.config ?? [];
124
+ const regeneratedRefs = new Set(this.numberingConfigs.map((c) => c.reference));
125
+ const numberingConfig = [...origNumberingConfig.filter((c) => !regeneratedRefs.has(c.reference)), ...this.numberingConfigs];
123
126
  return {
124
127
  sections,
125
128
  ...styles ? { styles } : {},
126
129
  ...core,
127
130
  ...background ? { background } : {},
128
131
  ...documentExtras,
129
- ...this.numberingConfigs.length > 0 ? { numbering: { config: this.numberingConfigs } } : {}
132
+ ...numberingConfig.length > 0 ? { numbering: { config: numberingConfig } } : {}
130
133
  };
131
134
  }
132
135
  /** Assemble a SectionOptions from compiled children + optional layout/headers/footers. */
@@ -218,6 +221,7 @@ var DocxManager = class {
218
221
  const attrs = {};
219
222
  if (docOpts.styles) attrs.styles = docOpts.styles;
220
223
  if (docOpts.background) attrs.background = docOpts.background;
224
+ if (docOpts.numbering) attrs.numbering = docOpts.numbering;
221
225
  const core = extractCoreProperties(docOpts);
222
226
  if (core) attrs.core = core;
223
227
  const lastSection = sections[lastIndex];
@@ -339,6 +343,7 @@ var DocxManager = class {
339
343
  compiledCells.push({
340
344
  verticalMerge: "continue",
341
345
  columnSpan: span.colspan,
346
+ ...span.borders ? { borders: span.borders } : {},
342
347
  children: [{ paragraph: "" }]
343
348
  });
344
349
  colIdx += span.colspan;
@@ -353,7 +358,8 @@ var DocxManager = class {
353
358
  newSpans.push({
354
359
  colStart: colIdx,
355
360
  colspan: cs,
356
- remainingRows: rs - 1
361
+ remainingRows: rs - 1,
362
+ borders: cell.borders
357
363
  });
358
364
  }
359
365
  compiledCells.push(cell);
@@ -458,15 +464,20 @@ var DocxManager = class {
458
464
  const items = [];
459
465
  const isOrdered = node.type === "orderedList";
460
466
  const isTask = node.type === "taskList";
467
+ const numbering = node.attrs?.numbering;
461
468
  let ordered;
462
- if (isOrdered) ordered = this.registerOrderedNumbering(node);
469
+ if (isOrdered && !numbering) ordered = this.registerOrderedNumbering(node);
463
470
  for (const listItem of node.content ?? []) {
464
471
  if (listItem.type !== "listItem" && listItem.type !== "taskItem") continue;
465
472
  const checked = Boolean(listItem.attrs?.checked);
466
473
  for (const child of listItem.content ?? []) if (child.type === "paragraph" || child.type === "heading") {
467
474
  const para = child.type === "heading" ? this.compileHeadingNode(child) : this.compileParagraphNode(child);
468
475
  const paraObj = typeof para === "string" ? { text: para } : para;
469
- if (ordered) paraObj.numbering = {
476
+ if (numbering) paraObj.numbering = {
477
+ reference: numbering,
478
+ level
479
+ };
480
+ else if (ordered) paraObj.numbering = {
470
481
  reference: ordered.reference,
471
482
  instance: ordered.instance,
472
483
  level
@@ -559,6 +570,17 @@ var DocxManager = class {
559
570
  case "columnBreak":
560
571
  children.push({ columnBreak: true });
561
572
  break;
573
+ case "tab":
574
+ children.push({ tab: true });
575
+ break;
576
+ case "inlinePassthrough": {
577
+ const data = node.attrs?.data ?? "{}";
578
+ try {
579
+ const parsed = JSON.parse(data);
580
+ if (parsed) children.push(parsed);
581
+ } catch {}
582
+ break;
583
+ }
562
584
  case "image": {
563
585
  const imageRun = renderDocx$2(node);
564
586
  if (imageRun) children.push(imageRun);
@@ -570,8 +592,18 @@ var DocxManager = class {
570
592
  break;
571
593
  }
572
594
  case "wpsShape": {
573
- const wpsShape = node.attrs?.wpsShape;
574
- if (wpsShape) children.push({ wpsShape });
595
+ const geometry = node.attrs?.wpsShape ?? {};
596
+ const body = [];
597
+ for (const child of node.content ?? []) {
598
+ const compiled = this.compileSectionChild(child);
599
+ if (!compiled) continue;
600
+ const items = Array.isArray(compiled) ? compiled : [compiled];
601
+ for (const it of items) if (it && typeof it === "object" && "paragraph" in it) body.push(it.paragraph);
602
+ }
603
+ children.push({ wpsShape: {
604
+ ...geometry,
605
+ children: body
606
+ } });
575
607
  break;
576
608
  }
577
609
  case "mention":
@@ -858,10 +890,7 @@ var DocxManager = class {
858
890
  if (cfg && cfg.format && cfg.format !== "bullet") {
859
891
  kind = "ordered";
860
892
  start = cfg.start;
861
- } else {
862
- kind = "bullet";
863
- reference = void 0;
864
- }
893
+ } else kind = "bullet";
865
894
  } else if (bullet) {
866
895
  kind = "bullet";
867
896
  level = bullet.level ?? 0;
@@ -910,7 +939,10 @@ var DocxManager = class {
910
939
  type: listType,
911
940
  content: [newItem]
912
941
  };
913
- if (listType === "orderedList" && info.level === 0 && typeof info.start === "number" && info.start !== 1) newList.attrs = { start: info.start };
942
+ const listAttrs = {};
943
+ if (listType === "orderedList" && info.level === 0 && typeof info.start === "number" && info.start !== 1) listAttrs.start = info.start;
944
+ if (info.reference) listAttrs.numbering = info.reference;
945
+ if (Object.keys(listAttrs).length > 0) newList.attrs = listAttrs;
914
946
  if (top) top.currentItem.content.push(newList);
915
947
  else topLevel.push(newList);
916
948
  stack.push({
@@ -1136,21 +1168,55 @@ var DocxManager = class {
1136
1168
  return nodes;
1137
1169
  }
1138
1170
  resolveParagraphChild(child) {
1171
+ if ("tab" in child) return { type: "tab" };
1139
1172
  if ("text" in child || "children" in child || "break" in child) return this.resolveRun(child);
1140
1173
  if ("image" in child) return this.resolveImage(child.image);
1141
1174
  if ("wpgGroup" in child) return {
1142
1175
  type: "wpgGroup",
1143
1176
  attrs: { wpgGroup: child.wpgGroup }
1144
1177
  };
1145
- if ("wpsShape" in child) return {
1146
- type: "wpsShape",
1147
- attrs: { wpsShape: child.wpsShape }
1148
- };
1178
+ if ("wpsShape" in child) {
1179
+ const ws = child.wpsShape;
1180
+ const content = [];
1181
+ if (ws?.children) for (const para of ws.children) {
1182
+ if (typeof para !== "object" || para === null) {
1183
+ const node = this.resolveParagraph(para);
1184
+ if (node) content.push(node);
1185
+ continue;
1186
+ }
1187
+ const defRPr = para.run ?? {};
1188
+ const children = Array.isArray(para.children) ? para.children.map((c) => typeof c !== "object" || c === null ? {
1189
+ ...defRPr,
1190
+ text: c
1191
+ } : {
1192
+ ...defRPr,
1193
+ ...c
1194
+ }) : void 0;
1195
+ const node = this.resolveParagraph({
1196
+ ...para,
1197
+ run: void 0,
1198
+ ...children ? { children } : {}
1199
+ });
1200
+ if (node) content.push(node);
1201
+ }
1202
+ if (content.length === 0) content.push({ type: "paragraph" });
1203
+ const { children: _omit, ...geometry } = ws ?? {};
1204
+ const node = {
1205
+ type: "wpsShape",
1206
+ content
1207
+ };
1208
+ const cleanGeometry = cleanAttrs(geometry);
1209
+ if (Object.keys(cleanGeometry).length > 0) node.attrs = { wpsShape: cleanGeometry };
1210
+ return node;
1211
+ }
1149
1212
  if ("sdt" in child) return this.resolveInlineSdt(child);
1150
1213
  if ("hyperlink" in child) return this.resolveHyperlink(child.hyperlink);
1151
1214
  if ("pageBreak" in child) return { type: "pageBreak" };
1152
1215
  if ("columnBreak" in child) return { type: "columnBreak" };
1153
- return null;
1216
+ return {
1217
+ type: "inlinePassthrough",
1218
+ attrs: { data: JSON.stringify(child) }
1219
+ };
1154
1220
  }
1155
1221
  /** Resolve an inline SDT (mention carrier; other inline SDTs unsupported). */
1156
1222
  resolveInlineSdt(child) {
@@ -1170,15 +1236,48 @@ var DocxManager = class {
1170
1236
  if (opts.break && opts.text === void 0 && !opts.children) return { type: "hardBreak" };
1171
1237
  const text = opts.text;
1172
1238
  if (text === void 0 && !opts.children) return null;
1173
- if (opts.children && !text) {
1174
- const parts = [];
1175
- for (const c of opts.children) if (typeof c === "string") parts.push(c);
1176
- if (parts.length === 0) return null;
1177
- return {
1178
- type: "text",
1179
- text: parts.join(""),
1180
- marks: this.resolveMarks(opts)
1239
+ if (opts.children) {
1240
+ const marks = this.resolveMarks(opts);
1241
+ const nodes = [];
1242
+ let parts = [];
1243
+ const flushText = () => {
1244
+ if (parts.length > 0) {
1245
+ const node = {
1246
+ type: "text",
1247
+ text: parts.join("")
1248
+ };
1249
+ if (marks) node.marks = marks;
1250
+ nodes.push(node);
1251
+ parts = [];
1252
+ }
1181
1253
  };
1254
+ for (const c of opts.children) if (typeof c === "string") parts.push(c);
1255
+ else if (c && typeof c === "object") {
1256
+ if ("pageBreak" in c) {
1257
+ flushText();
1258
+ nodes.push({ type: "pageBreak" });
1259
+ } else if ("columnBreak" in c) {
1260
+ flushText();
1261
+ nodes.push({ type: "columnBreak" });
1262
+ } else if ("break" in c) {
1263
+ flushText();
1264
+ nodes.push({ type: "hardBreak" });
1265
+ } else if ("tab" in c) {
1266
+ flushText();
1267
+ nodes.push({ type: "tab" });
1268
+ }
1269
+ }
1270
+ flushText();
1271
+ if (text !== void 0) {
1272
+ const node = {
1273
+ type: "text",
1274
+ text,
1275
+ marks
1276
+ };
1277
+ nodes.push(node);
1278
+ }
1279
+ if (nodes.length === 0) return null;
1280
+ return nodes.length === 1 ? nodes[0] : nodes;
1182
1281
  }
1183
1282
  return {
1184
1283
  type: "text",
@@ -1236,7 +1335,7 @@ var DocxManager = class {
1236
1335
  type: "link",
1237
1336
  attrs: {
1238
1337
  href,
1239
- target: "_blank",
1338
+ target: href.startsWith("#") ? null : "_blank",
1240
1339
  rel: "noopener noreferrer nofollow",
1241
1340
  class: null,
1242
1341
  title: hyperlink.tooltip ?? null
@@ -1,4 +1,4 @@
1
- import { a as JSONContent, i as Extensions } from "../core-DRaLI8nd.mjs";
1
+ import { a as JSONContent, i as Extensions } from "../core-DC0_-WcE.mjs";
2
2
  import { ParseOptions } from "@tiptap/pm/model";
3
3
 
4
4
  //#region src/converters/html.d.ts
@@ -1,4 +1,4 @@
1
- import { s as docxExtensions } from "../core-B8ba_FNi.mjs";
1
+ import { o as docxExtensions } from "../core-BnF8XhVE.mjs";
2
2
  import { sectionLinePitchCss, sectionMarginCss } from "../extensions/utils.mjs";
3
3
  import { getSchema } from "@tiptap/core";
4
4
  import { encodeBase64 } from "@office-open/core";
@@ -1,4 +1,4 @@
1
- import { a as JSONContent } from "../core-DRaLI8nd.mjs";
1
+ import { a as JSONContent } from "../core-DC0_-WcE.mjs";
2
2
 
3
3
  //#region src/converters/markdown.d.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { s as docxExtensions } from "../core-B8ba_FNi.mjs";
1
+ import { o as docxExtensions } from "../core-BnF8XhVE.mjs";
2
2
  import { Markdown, MarkdownManager } from "@tiptap/markdown";
3
3
  //#region src/converters/markdown.ts
4
4
  const markdownManager = new MarkdownManager({ extensions: [...docxExtensions, Markdown] });
@@ -1,4 +1,4 @@
1
- import { a as JSONContent } from "../core-DRaLI8nd.mjs";
1
+ import { a as JSONContent } from "../core-DC0_-WcE.mjs";
2
2
  import { PrepareStep } from "./prepare.mjs";
3
3
  import { OutputByType, OutputType, patchDocument } from "@office-open/docx";
4
4
 
@@ -1,4 +1,4 @@
1
- import { a as JSONContent } from "../core-DRaLI8nd.mjs";
1
+ import { a as JSONContent } from "../core-DC0_-WcE.mjs";
2
2
 
3
3
  //#region src/converters/prepare.d.ts
4
4
  /**
@@ -67,15 +67,12 @@ async function prepareDocument(json, steps = DEFAULT_STEPS) {
67
67
  }
68
68
  async function toDataUrl(src, handler) {
69
69
  const data = await handler(src);
70
- return `data:${{
71
- jpg: "image/jpeg",
72
- jpeg: "image/jpeg",
73
- png: "image/png",
74
- gif: "image/gif",
75
- bmp: "image/bmp",
76
- svg: "image/svg+xml",
77
- webp: "image/webp"
78
- }[src.split(".").pop()?.toLowerCase() ?? "png"] ?? "image/png"};base64,${encodeBase64(data)}`;
70
+ let mime = "image/png";
71
+ try {
72
+ const type = imageMeta(data).type;
73
+ if (type) mime = type === "jpg" ? "image/jpeg" : `image/${type}`;
74
+ } catch {}
75
+ return `data:${mime};base64,${encodeBase64(data)}`;
79
76
  }
80
77
  async function walkImages(node, handler) {
81
78
  if (node.type === "image" && node.attrs) {
@@ -1,13 +1,15 @@
1
- import { Blockquote, Bold, BulletList, Code, CodeBlockLowlight, Details, DetailsContent, DetailsSummary, Emoji, HardBreak, Highlight, HorizontalRule, Italic, Link, ListItem, Mathematics, Mention, OrderedList, Subscript, Superscript, TaskItem, TaskList, Text, TextAlign as TextAlign$1, Underline } from "./extensions/tiptap.mjs";
1
+ import { Blockquote, Bold, Code, CodeBlockLowlight, Details, DetailsContent, DetailsSummary, Emoji, HardBreak, Highlight, HorizontalRule, Italic, ListItem, Mathematics, Mention, Subscript, Superscript, TaskItem, TaskList, Text, TextAlign, Underline } from "./extensions/tiptap.mjs";
2
2
  import "./extensions/blockquote.mjs";
3
+ import { BulletList } from "./extensions/bullet-list.mjs";
3
4
  import { CodeBlock } from "./extensions/code-block.mjs";
4
5
  import "./extensions/details.mjs";
5
6
  import { attrNative, floatAnchorScope, floatingToStyles, normalizeColorToHex, renderRunStyles } from "./extensions/utils.mjs";
6
7
  import { Document } from "./extensions/document.mjs";
7
8
  import { Heading } from "./extensions/heading.mjs";
8
9
  import { Image } from "./extensions/image.mjs";
10
+ import { Link } from "./extensions/link.mjs";
9
11
  import "./extensions/mention.mjs";
10
- import "./extensions/ordered-list.mjs";
12
+ import { OrderedList } from "./extensions/ordered-list.mjs";
11
13
  import { Paragraph } from "./extensions/paragraph.mjs";
12
14
  import { Strike } from "./extensions/strike.mjs";
13
15
  import { Table } from "./extensions/table.mjs";
@@ -18,7 +20,6 @@ import "./extensions/task-item.mjs";
18
20
  import { TextStyle } from "./extensions/text-style.mjs";
19
21
  import { Editor, Extension, Mark, Node } from "@tiptap/core";
20
22
  import { all, createLowlight } from "lowlight";
21
- import { TextAlign } from "@tiptap/extension-text-align";
22
23
  import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
23
24
  import { Decoration, DecorationSet } from "@tiptap/pm/view";
24
25
  import { convertEmuToPixels, encodeBase64 } from "@office-open/core";
@@ -277,6 +278,46 @@ const Passthrough = Node.create({
277
278
  ];
278
279
  }
279
280
  });
281
+ /**
282
+ * InlinePassthrough — inline atom carrying an opaque inline ParagraphChild that
283
+ * has no native Tiptap representation (bookmarkStart/End, comment range markers,
284
+ * proofErr, track-change markers, …). The full ParagraphChild rides in
285
+ * `attrs.data` as JSON so DOCX→JSON→DOCX round-trips byte-faithful; the atom is
286
+ * zero-width (bookmark/range markers carry no layout box), matching Word's
287
+ * non-printing metadata. Mirrors the block-level Passthrough for inline children.
288
+ */
289
+ const InlinePassthrough = Node.create({
290
+ name: "inlinePassthrough",
291
+ group: "inline",
292
+ inline: true,
293
+ atom: true,
294
+ addAttributes() {
295
+ return { data: {
296
+ default: "{}",
297
+ rendered: false,
298
+ parseHTML: (element) => element.getAttribute("data-inline-passthrough") ?? "{}"
299
+ } };
300
+ },
301
+ parseHTML() {
302
+ return [{ tag: "span[data-inline-passthrough]" }];
303
+ },
304
+ renderHTML({ node }) {
305
+ const data = node.attrs.data || "{}";
306
+ let bookmarkName;
307
+ try {
308
+ bookmarkName = JSON.parse(data).bookmarkStart?.name;
309
+ } catch {}
310
+ const attrs = {
311
+ "data-inline-passthrough": data,
312
+ contenteditable: "false"
313
+ };
314
+ if (bookmarkName) {
315
+ attrs.id = bookmarkName;
316
+ attrs.style = "display:inline-block;width:0;height:0;overflow:hidden;vertical-align:baseline";
317
+ } else attrs.style = "display:none";
318
+ return ["span", attrs];
319
+ }
320
+ });
280
321
  //#endregion
281
322
  //#region src/extensions/section-break.ts
282
323
  /**
@@ -342,6 +383,35 @@ const SectionBreak = Extension.create({
342
383
  }
343
384
  });
344
385
  //#endregion
386
+ //#region src/extensions/tab.ts
387
+ /**
388
+ * Tab — an inline atom representing a DOCX `<w:r><w:tab/></w:r>` tab character.
389
+ *
390
+ * office-open parses `<w:tab/>` as `{ tab: true }` inside a run's children. The
391
+ * resolve path turns that into this node so the tab is not lost: previously
392
+ * `<w:tab/>` was dropped, which let `mergeTextNodes` collapse a TOC entry's title
393
+ * and page number into adjacent text (no leader, no right alignment). The node is
394
+ * zero-width, non-editable, and carries no text height — measure skips it the
395
+ * same way it skips other inline atoms, so pagination is unaffected. It only
396
+ * marks where a tab leader (e.g. a TOC's dotted leader) renders. compile turns it
397
+ * back into `{ tab: true }`.
398
+ */
399
+ const Tab = Node.create({
400
+ name: "tab",
401
+ group: "inline",
402
+ inline: true,
403
+ atom: true,
404
+ parseHTML() {
405
+ return [{ tag: "span.docx-tab" }];
406
+ },
407
+ renderHTML() {
408
+ return ["span", {
409
+ class: "docx-tab",
410
+ contenteditable: "false"
411
+ }];
412
+ }
413
+ });
414
+ //#endregion
345
415
  //#region src/extensions/toc-field.ts
346
416
  /**
347
417
  * TOC field (`tocField`) — a block container representing a DOCX table of
@@ -499,6 +569,35 @@ function renderWpsText(children) {
499
569
  ];
500
570
  });
501
571
  }
572
+ /** Inner style of a wps text-box: fill + outline + textbox insets (padding).
573
+ * Lives on the contentDOM (the editable interior). Never carries rotation or
574
+ * writing-mode — those go on the outer positioning wrapper (see
575
+ * wpsRotationVert), because transform/writing-mode on an editable region
576
+ * distort the caret rect and break CJK IME composition. */
577
+ function wpsInnerStyle(data) {
578
+ const parts = [];
579
+ const fill = fillToCss(data.fill);
580
+ if (fill) parts.push(`background-color:${fill}`);
581
+ const outline = outlineToCss(data.outline);
582
+ if (outline) parts.push(outline);
583
+ const bp = data.bodyProperties;
584
+ if (bp) {
585
+ const ins = (v) => v != null ? `${(v / EMU_PER_PX).toFixed(1)}px` : "0px";
586
+ parts.push(`padding:${ins(bp.tIns)} ${ins(bp.rIns)} ${ins(bp.bIns)} ${ins(bp.lIns)}`);
587
+ }
588
+ return parts.join(";");
589
+ }
590
+ /** Rotation + writing-mode (text direction) for a wps shape. Lives on the
591
+ * positioning wrapper OUTSIDE the contentDOM. `rotationOverride` covers a
592
+ * group child whose rotation comes from the group transform, not bodyPr. */
593
+ function wpsRotationVert(data, rotationOverride) {
594
+ const parts = [];
595
+ const rotation = rotationOverride ?? data.bodyProperties?.rotation;
596
+ if (rotation) parts.push(`transform:rotate(${rotation}deg)`);
597
+ const vert = data.bodyProperties?.vert;
598
+ if (vert && vert !== "horz") parts.push(`writing-mode:${vert === "vert270" ? "vertical-lr" : "vertical-rl"}`);
599
+ return parts.join(";");
600
+ }
502
601
  /**
503
602
  * Render a wps shape's interior (fill/outline/insets/rotation/writing-mode + text
504
603
  * body) as a positioned div. Shared by the wpg group's inline wps children and
@@ -507,23 +606,18 @@ function renderWpsText(children) {
507
606
  * carries the placement — absolute group coords for a group child, the floating
508
607
  * anchor CSS for a standalone shape. `opts.rotation` overrides bodyPr rotation
509
608
  * (a group child's rotation may come from the group transform, not bodyPr).
609
+ *
610
+ * The wpg path merges placement + interior into ONE div (an atom has no
611
+ * contentDOM, so rotation/writing-mode on the element is safe — no caret inside).
612
+ * The editable wpsShape node instead splits these across two elements via
613
+ * wpsShapeStyles (outer = position+rotation+vert, inner = contentDOM).
510
614
  */
511
615
  function renderWpsInterior(data, positionStyle, opts) {
512
- const styles = [positionStyle];
513
- const fill = fillToCss(data.fill);
514
- if (fill) styles.push(`background-color:${fill}`);
515
- const outline = outlineToCss(data.outline);
516
- if (outline) styles.push(outline);
517
- const bp = data.bodyProperties;
518
- if (bp) {
519
- const ins = (v) => v != null ? `${(v / EMU_PER_PX).toFixed(1)}px` : "0px";
520
- styles.push(`padding:${ins(bp.tIns)} ${ins(bp.rIns)} ${ins(bp.bIns)} ${ins(bp.lIns)}`);
521
- }
522
- const rotation = opts?.rotation ?? bp?.rotation;
523
- if (rotation) styles.push(`transform:rotate(${rotation}deg)`);
524
- const vert = bp?.vert;
525
- if (vert && vert !== "horz") styles.push(`writing-mode:${vert === "vert270" ? "vertical-lr" : "vertical-rl"}`);
526
- const attrs = { style: styles.join(";") };
616
+ const attrs = { style: [
617
+ positionStyle,
618
+ wpsInnerStyle(data),
619
+ wpsRotationVert(data, opts?.rotation)
620
+ ].filter(Boolean).join(";") };
527
621
  if (opts?.attrs) Object.assign(attrs, opts.attrs);
528
622
  return [
529
623
  "div",
@@ -531,6 +625,37 @@ function renderWpsInterior(data, positionStyle, opts) {
531
625
  ...renderWpsText(data.children)
532
626
  ];
533
627
  }
628
+ /** Two-element style split for an editable standalone wpsShape: `outer` for the
629
+ * positioning wrapper (dom), `inner` for the contentDOM. rotation/writing-mode
630
+ * stay on `outer` so the editable interior has a clean caret/IME rect. The
631
+ * geometry (EMU extent → px, floating anchor CSS) is computed here so the
632
+ * editor's NodeView and generateHTML render identically without re-deriving
633
+ * the engine's EMU/floating math. */
634
+ function wpsShapeStyles(ws) {
635
+ const sizeStyle = `width:${ws.transformation?.width != null ? convertEmuToPixels(ws.transformation.width) : 0}px;height:${ws.transformation?.height != null ? convertEmuToPixels(ws.transformation.height) : 0}px;box-sizing:border-box;overflow:hidden`;
636
+ const rotVert = wpsRotationVert(ws);
637
+ let outer;
638
+ let paragraphAnchor = false;
639
+ if (ws.floating) {
640
+ outer = [
641
+ ...floatingToStyles(ws.floating, void 0, ws.transformation?.width),
642
+ sizeStyle,
643
+ rotVert
644
+ ].filter(Boolean).join(";");
645
+ paragraphAnchor = floatAnchorScope(ws.floating) === "paragraph";
646
+ } else outer = [
647
+ `display:inline-block;vertical-align:middle`,
648
+ sizeStyle,
649
+ rotVert
650
+ ].filter(Boolean).join(";");
651
+ const anchor = ws.bodyProperties?.anchor;
652
+ const inner = `box-sizing:border-box;display:flex;flex-direction:column;justify-content:${anchor === "ctr" ? "center" : anchor === "b" ? "flex-end" : "flex-start"};height:100%;${wpsInnerStyle(ws)}`;
653
+ return {
654
+ outer,
655
+ inner,
656
+ paragraphAnchor
657
+ };
658
+ }
534
659
  /** Render a wpg group (top-level or nested) as a positioned container.
535
660
  * actualW/H are the group's real pixel size — top-level groups read them from
536
661
  * transformation.width/height; nested groups receive the box their parent
@@ -550,7 +675,7 @@ function renderGroup(group, actualW, actualH, containerStyle, extraAttrs) {
550
675
  const style = containerStyle ?? `position:relative;display:inline-block;vertical-align:middle;width:${actualW}px;height:${actualH}px`;
551
676
  const children = (group.children ?? []).map((c) => renderChild(c, chOff, scaleX, scaleY)).filter((c) => Array.isArray(c) && c.length > 0);
552
677
  const attrs = {
553
- "data-wpg-group": "",
678
+ "data-wpg-group": JSON.stringify(group),
554
679
  style
555
680
  };
556
681
  if (extraAttrs) Object.assign(attrs, extraAttrs);
@@ -597,6 +722,16 @@ const WpgGroup = Node.create({
597
722
  });
598
723
  //#endregion
599
724
  //#region src/extensions/wps-shape.ts
725
+ /**
726
+ * wpsShape — inline node carrying a standalone DOCX text-box shape
727
+ * (wp:anchor > wps:wsp > wps:txbx; NOT inside a wpg group). The shape geometry
728
+ * + styling (transformation/floating/fill/outline/bodyProperties) ride on
729
+ * attrs.wpsShape; the editable text body is PM content (block+), one paragraph
730
+ * per office-open ParagraphOptions. Unlike a group's interior wps children
731
+ * (laid out in the group's coordinate space), this shape floats on its own
732
+ * anchor. The engine node has no NodeView (UI-free); the editor layer extends
733
+ * it with a two-element NodeView (outer placement/rotation, inner contentDOM).
734
+ */
600
735
  const attrWpsShape = () => ({
601
736
  default: null,
602
737
  rendered: false,
@@ -614,23 +749,35 @@ const WpsShape = Node.create({
614
749
  name: "wpsShape",
615
750
  group: "inline",
616
751
  inline: true,
617
- atom: true,
752
+ content: "block+",
753
+ isolating: true,
754
+ defining: true,
618
755
  addAttributes() {
619
756
  return { wpsShape: attrWpsShape() };
620
757
  },
621
758
  parseHTML() {
622
- return [{ tag: "div[data-wps-shape]" }];
759
+ return [{
760
+ tag: "div[data-wps-shape]",
761
+ contentElement: "div"
762
+ }];
623
763
  },
624
764
  renderHTML({ node }) {
625
765
  const ws = node.attrs.wpsShape ?? {};
626
- const sizeStyle = `width:${ws.transformation?.width != null ? convertEmuToPixels(ws.transformation.width) : 0}px;height:${ws.transformation?.height != null ? convertEmuToPixels(ws.transformation.height) : 0}px;box-sizing:border-box`;
627
- if (ws.floating) {
628
- const pos = [...floatingToStyles(ws.floating, void 0, ws.transformation?.width), sizeStyle].join(";");
629
- const attrs = { "data-wps-shape": "" };
630
- if (floatAnchorScope(ws.floating) === "paragraph") attrs["data-float-anchor"] = "paragraph";
631
- return renderWpsInterior(ws, pos, { attrs });
632
- }
633
- return renderWpsInterior(ws, `display:inline-block;vertical-align:middle;${sizeStyle}`, { attrs: { "data-wps-shape": "" } });
766
+ const { outer, inner, paragraphAnchor } = wpsShapeStyles(ws);
767
+ const attrs = {
768
+ "data-wps-shape": JSON.stringify(ws),
769
+ style: outer
770
+ };
771
+ if (paragraphAnchor) attrs["data-float-anchor"] = "paragraph";
772
+ return [
773
+ "div",
774
+ attrs,
775
+ [
776
+ "div",
777
+ { style: inner },
778
+ 0
779
+ ]
780
+ ];
634
781
  }
635
782
  });
636
783
  //#endregion
@@ -642,8 +789,10 @@ const tiptapNodeExtensions = [
642
789
  HardBreak,
643
790
  PageBreak,
644
791
  ColumnBreak,
792
+ Tab,
645
793
  SectionBreak,
646
794
  Passthrough,
795
+ InlinePassthrough,
647
796
  TocField,
648
797
  Blockquote,
649
798
  OrderedList,
@@ -667,7 +816,7 @@ const tiptapNodeExtensions = [
667
816
  TaskList,
668
817
  TaskItem,
669
818
  Heading,
670
- TextAlign$1.configure({ types: ["heading", "paragraph"] })
819
+ TextAlign.configure({ types: ["heading", "paragraph"] })
671
820
  ];
672
821
  const tiptapMarkExtensions = [
673
822
  Bold,
@@ -714,4 +863,4 @@ const DocxKit = Extension.create({
714
863
  }
715
864
  });
716
865
  //#endregion
717
- export { PageBreak as _, DocxKit as a, tiptapMarkExtensions as c, WpgGroup as d, renderWpsInterior as f, Passthrough as g, SectionBreak as h, Node as i, tiptapNodeExtensions as l, TocField as m, Extension as n, TextAlign as o, renderWpsText as p, Mark as r, docxExtensions as s, Editor as t, WpsShape as u, FormattingMarks as v, ColumnBreak as y };
866
+ export { ColumnBreak as C, FormattingMarks as S, Tab as _, DocxKit as a, Passthrough as b, tiptapNodeExtensions as c, renderWpsInterior as d, renderWpsText as f, TocField as g, wpsShapeStyles as h, Node as i, WpsShape as l, wpsRotationVert as m, Extension as n, docxExtensions as o, wpsInnerStyle as p, Mark as r, tiptapMarkExtensions as s, Editor as t, WpgGroup as u, SectionBreak as v, PageBreak as x, InlinePassthrough as y };