@docen/docx 0.3.3 → 0.3.4

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 +121 -24
  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];
@@ -458,15 +462,20 @@ var DocxManager = class {
458
462
  const items = [];
459
463
  const isOrdered = node.type === "orderedList";
460
464
  const isTask = node.type === "taskList";
465
+ const numbering = node.attrs?.numbering;
461
466
  let ordered;
462
- if (isOrdered) ordered = this.registerOrderedNumbering(node);
467
+ if (isOrdered && !numbering) ordered = this.registerOrderedNumbering(node);
463
468
  for (const listItem of node.content ?? []) {
464
469
  if (listItem.type !== "listItem" && listItem.type !== "taskItem") continue;
465
470
  const checked = Boolean(listItem.attrs?.checked);
466
471
  for (const child of listItem.content ?? []) if (child.type === "paragraph" || child.type === "heading") {
467
472
  const para = child.type === "heading" ? this.compileHeadingNode(child) : this.compileParagraphNode(child);
468
473
  const paraObj = typeof para === "string" ? { text: para } : para;
469
- if (ordered) paraObj.numbering = {
474
+ if (numbering) paraObj.numbering = {
475
+ reference: numbering,
476
+ level
477
+ };
478
+ else if (ordered) paraObj.numbering = {
470
479
  reference: ordered.reference,
471
480
  instance: ordered.instance,
472
481
  level
@@ -559,6 +568,17 @@ var DocxManager = class {
559
568
  case "columnBreak":
560
569
  children.push({ columnBreak: true });
561
570
  break;
571
+ case "tab":
572
+ children.push({ tab: true });
573
+ break;
574
+ case "inlinePassthrough": {
575
+ const data = node.attrs?.data ?? "{}";
576
+ try {
577
+ const parsed = JSON.parse(data);
578
+ if (parsed) children.push(parsed);
579
+ } catch {}
580
+ break;
581
+ }
562
582
  case "image": {
563
583
  const imageRun = renderDocx$2(node);
564
584
  if (imageRun) children.push(imageRun);
@@ -570,8 +590,18 @@ var DocxManager = class {
570
590
  break;
571
591
  }
572
592
  case "wpsShape": {
573
- const wpsShape = node.attrs?.wpsShape;
574
- if (wpsShape) children.push({ wpsShape });
593
+ const geometry = node.attrs?.wpsShape ?? {};
594
+ const body = [];
595
+ for (const child of node.content ?? []) {
596
+ const compiled = this.compileSectionChild(child);
597
+ if (!compiled) continue;
598
+ const items = Array.isArray(compiled) ? compiled : [compiled];
599
+ for (const it of items) if (it && typeof it === "object" && "paragraph" in it) body.push(it.paragraph);
600
+ }
601
+ children.push({ wpsShape: {
602
+ ...geometry,
603
+ children: body
604
+ } });
575
605
  break;
576
606
  }
577
607
  case "mention":
@@ -858,10 +888,7 @@ var DocxManager = class {
858
888
  if (cfg && cfg.format && cfg.format !== "bullet") {
859
889
  kind = "ordered";
860
890
  start = cfg.start;
861
- } else {
862
- kind = "bullet";
863
- reference = void 0;
864
- }
891
+ } else kind = "bullet";
865
892
  } else if (bullet) {
866
893
  kind = "bullet";
867
894
  level = bullet.level ?? 0;
@@ -910,7 +937,10 @@ var DocxManager = class {
910
937
  type: listType,
911
938
  content: [newItem]
912
939
  };
913
- if (listType === "orderedList" && info.level === 0 && typeof info.start === "number" && info.start !== 1) newList.attrs = { start: info.start };
940
+ const listAttrs = {};
941
+ if (listType === "orderedList" && info.level === 0 && typeof info.start === "number" && info.start !== 1) listAttrs.start = info.start;
942
+ if (info.reference) listAttrs.numbering = info.reference;
943
+ if (Object.keys(listAttrs).length > 0) newList.attrs = listAttrs;
914
944
  if (top) top.currentItem.content.push(newList);
915
945
  else topLevel.push(newList);
916
946
  stack.push({
@@ -1136,21 +1166,55 @@ var DocxManager = class {
1136
1166
  return nodes;
1137
1167
  }
1138
1168
  resolveParagraphChild(child) {
1169
+ if ("tab" in child) return { type: "tab" };
1139
1170
  if ("text" in child || "children" in child || "break" in child) return this.resolveRun(child);
1140
1171
  if ("image" in child) return this.resolveImage(child.image);
1141
1172
  if ("wpgGroup" in child) return {
1142
1173
  type: "wpgGroup",
1143
1174
  attrs: { wpgGroup: child.wpgGroup }
1144
1175
  };
1145
- if ("wpsShape" in child) return {
1146
- type: "wpsShape",
1147
- attrs: { wpsShape: child.wpsShape }
1148
- };
1176
+ if ("wpsShape" in child) {
1177
+ const ws = child.wpsShape;
1178
+ const content = [];
1179
+ if (ws?.children) for (const para of ws.children) {
1180
+ if (typeof para !== "object" || para === null) {
1181
+ const node = this.resolveParagraph(para);
1182
+ if (node) content.push(node);
1183
+ continue;
1184
+ }
1185
+ const defRPr = para.run ?? {};
1186
+ const children = Array.isArray(para.children) ? para.children.map((c) => typeof c !== "object" || c === null ? {
1187
+ ...defRPr,
1188
+ text: c
1189
+ } : {
1190
+ ...defRPr,
1191
+ ...c
1192
+ }) : void 0;
1193
+ const node = this.resolveParagraph({
1194
+ ...para,
1195
+ run: void 0,
1196
+ ...children ? { children } : {}
1197
+ });
1198
+ if (node) content.push(node);
1199
+ }
1200
+ if (content.length === 0) content.push({ type: "paragraph" });
1201
+ const { children: _omit, ...geometry } = ws ?? {};
1202
+ const node = {
1203
+ type: "wpsShape",
1204
+ content
1205
+ };
1206
+ const cleanGeometry = cleanAttrs(geometry);
1207
+ if (Object.keys(cleanGeometry).length > 0) node.attrs = { wpsShape: cleanGeometry };
1208
+ return node;
1209
+ }
1149
1210
  if ("sdt" in child) return this.resolveInlineSdt(child);
1150
1211
  if ("hyperlink" in child) return this.resolveHyperlink(child.hyperlink);
1151
1212
  if ("pageBreak" in child) return { type: "pageBreak" };
1152
1213
  if ("columnBreak" in child) return { type: "columnBreak" };
1153
- return null;
1214
+ return {
1215
+ type: "inlinePassthrough",
1216
+ attrs: { data: JSON.stringify(child) }
1217
+ };
1154
1218
  }
1155
1219
  /** Resolve an inline SDT (mention carrier; other inline SDTs unsupported). */
1156
1220
  resolveInlineSdt(child) {
@@ -1170,15 +1234,48 @@ var DocxManager = class {
1170
1234
  if (opts.break && opts.text === void 0 && !opts.children) return { type: "hardBreak" };
1171
1235
  const text = opts.text;
1172
1236
  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)
1237
+ if (opts.children) {
1238
+ const marks = this.resolveMarks(opts);
1239
+ const nodes = [];
1240
+ let parts = [];
1241
+ const flushText = () => {
1242
+ if (parts.length > 0) {
1243
+ const node = {
1244
+ type: "text",
1245
+ text: parts.join("")
1246
+ };
1247
+ if (marks) node.marks = marks;
1248
+ nodes.push(node);
1249
+ parts = [];
1250
+ }
1181
1251
  };
1252
+ for (const c of opts.children) if (typeof c === "string") parts.push(c);
1253
+ else if (c && typeof c === "object") {
1254
+ if ("pageBreak" in c) {
1255
+ flushText();
1256
+ nodes.push({ type: "pageBreak" });
1257
+ } else if ("columnBreak" in c) {
1258
+ flushText();
1259
+ nodes.push({ type: "columnBreak" });
1260
+ } else if ("break" in c) {
1261
+ flushText();
1262
+ nodes.push({ type: "hardBreak" });
1263
+ } else if ("tab" in c) {
1264
+ flushText();
1265
+ nodes.push({ type: "tab" });
1266
+ }
1267
+ }
1268
+ flushText();
1269
+ if (text !== void 0) {
1270
+ const node = {
1271
+ type: "text",
1272
+ text,
1273
+ marks
1274
+ };
1275
+ nodes.push(node);
1276
+ }
1277
+ if (nodes.length === 0) return null;
1278
+ return nodes.length === 1 ? nodes[0] : nodes;
1182
1279
  }
1183
1280
  return {
1184
1281
  type: "text",
@@ -1236,7 +1333,7 @@ var DocxManager = class {
1236
1333
  type: "link",
1237
1334
  attrs: {
1238
1335
  href,
1239
- target: "_blank",
1336
+ target: href.startsWith("#") ? null : "_blank",
1240
1337
  rel: "noopener noreferrer nofollow",
1241
1338
  class: null,
1242
1339
  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 };