@shadow-garden/bapbong-model 0.5.0 → 0.7.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.
package/dist/index.cjs CHANGED
@@ -20,12 +20,55 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // packages/model/src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ anchorName: () => anchorName,
24
+ bookmarkLabel: () => bookmarkLabel,
23
25
  commentSchema: () => commentSchema,
24
26
  createNumberingCounter: () => createNumberingCounter,
27
+ fieldAt: () => fieldAt,
28
+ findBookmark: () => findBookmark,
25
29
  schema: () => schema
26
30
  });
27
31
  module.exports = __toCommonJS(index_exports);
28
32
 
33
+ // packages/model/src/lib/bookmarks.ts
34
+ function anchorName(href) {
35
+ return href && href.startsWith("#") && href.length > 1 ? href.slice(1) : null;
36
+ }
37
+ function findBookmark(doc, name) {
38
+ let found = null;
39
+ doc.descendants((node, pos) => {
40
+ if (found !== null) return false;
41
+ if (node.type.name !== "paragraph") return true;
42
+ const names = node.attrs["bookmarks"];
43
+ if (names?.includes(name)) found = pos + 1;
44
+ return false;
45
+ });
46
+ return found;
47
+ }
48
+ function bookmarkLabel(doc, name, max = 60) {
49
+ const pos = findBookmark(doc, name);
50
+ if (pos === null) return null;
51
+ const para = doc.nodeAt(pos - 1);
52
+ const text = (para?.textContent ?? "").replace(/^[\s.:–—-]+/, "").trim();
53
+ if (!text) return null;
54
+ return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
55
+ }
56
+ function fieldAt(doc, pos) {
57
+ let hit = null;
58
+ let run = null;
59
+ doc.forEach((node, offset) => {
60
+ const f = node.attrs["field"];
61
+ if (f && run && run.field === f) {
62
+ run.to = offset + node.nodeSize;
63
+ } else {
64
+ if (run && hit === run) return;
65
+ run = f ? { field: f, from: offset, to: offset + node.nodeSize } : null;
66
+ }
67
+ if (run && hit === null && pos >= run.from && pos <= run.to) hit = run;
68
+ });
69
+ return hit;
70
+ }
71
+
29
72
  // packages/model/src/lib/model.ts
30
73
  var import_prosemirror_model = require("prosemirror-model");
31
74
  function dataJson(el, name) {
@@ -43,7 +86,8 @@ function pastedParagraphAttrs(el, heading) {
43
86
  return {
44
87
  heading,
45
88
  align: m ? m[1].toLowerCase() : null,
46
- borders: dataJson(el, "data-borders")
89
+ borders: dataJson(el, "data-borders"),
90
+ carry: dataJson(el, "data-carry")
47
91
  };
48
92
  }
49
93
  function pastedImageAttrs(el) {
@@ -80,7 +124,8 @@ function pastedCellAttrs(el) {
80
124
  background: bg ? bg[1].trim() : null,
81
125
  vAlign: e.getAttribute("data-valign"),
82
126
  borders: dataJson(el, "data-borders"),
83
- padding: dataJson(el, "data-padding")
127
+ padding: dataJson(el, "data-padding"),
128
+ carry: dataJson(el, "data-carry")
84
129
  };
85
130
  }
86
131
  var schema = new import_prosemirror_model.Schema({
@@ -132,11 +177,33 @@ var schema = new import_prosemirror_model.Schema({
132
177
  tabs: { default: null },
133
178
  // w:spacing — { before?, after?, line?, lineRule? }, or null.
134
179
  spacing: { default: null },
180
+ // w:bookmarkStart names anchored in this paragraph (["_Toc89595219"]),
181
+ // or null. Link hrefs of the form "#name" resolve against these —
182
+ // paragraph-level is the right altitude: Word's TOC bookmarks wrap a
183
+ // heading's text, and jumping to the heading is what a reader wants.
184
+ bookmarks: { default: null },
185
+ // The generated field this paragraph belongs to ({ kind: 'toc',
186
+ // instr } for a TOC entry), or null for ordinary content. Word paints
187
+ // such content with field shading and regenerates it on update.
188
+ field: { default: null },
135
189
  // w:pageBreakBefore — start this paragraph on a new page.
136
190
  pageBreakBefore: { default: false },
191
+ // w:keepNext — stay on the same page as the next block's first line.
192
+ keepNext: { default: false },
193
+ // w:keepLines — never split this paragraph across pages.
194
+ keepLines: { default: false },
195
+ // w:widowControl — Word's default is ON; false only when the document
196
+ // explicitly disables widow/orphan control for this paragraph.
197
+ widowControl: { default: true },
137
198
  // w:pBdr — { top?, bottom?, left?, right? } of BorderSide, or null.
138
199
  // Importer-set; painted as a box around the paragraph's lines.
139
- borders: { default: null }
200
+ borders: { default: null },
201
+ // Carry-through fidelity (docx round-trip): OOXML paragraph
202
+ // properties the model does NOT represent, preserved verbatim so a
203
+ // customer's save never drops them. { pPr?: string, markRPr?: string }
204
+ // — raw XML fragments (pPr extras / the paragraph-mark w:rPr), or
205
+ // null. Importer-set; the exporter splices them back into w:pPr.
206
+ carry: { default: null }
140
207
  },
141
208
  // HTML paste path: recover heading level from h1–h6 and alignment from
142
209
  // inline style. Other attrs (list/indent/tabs/spacing) stay importer-only
@@ -156,6 +223,8 @@ var schema = new import_prosemirror_model.Schema({
156
223
  if (attrs.styleId) dom["data-style"] = attrs.styleId;
157
224
  if (node.attrs["borders"])
158
225
  dom["data-borders"] = JSON.stringify(node.attrs["borders"]);
226
+ if (node.attrs["carry"])
227
+ dom["data-carry"] = JSON.stringify(node.attrs["carry"]);
159
228
  return [tag, dom, 0];
160
229
  }
161
230
  },
@@ -237,7 +306,12 @@ var schema = new import_prosemirror_model.Schema({
237
306
  // insideV }, or null — OOXML tables are borderless unless declared.
238
307
  borders: { default: null },
239
308
  // w:tblPr/w:jc — 'center' | 'right' table alignment, or null (left).
240
- align: { default: null }
309
+ align: { default: null },
310
+ // Carry-through fidelity: unmodelled w:tblPr children (tblStyle,
311
+ // tblLayout, tblLook, tblInd, floating tblpPr, …) as one raw XML
312
+ // string ({ tblPr: string }), or null. Importer-set; the exporter
313
+ // splices it back so a save never drops them.
314
+ carry: { default: null }
241
315
  },
242
316
  // Complex attrs round-trip as data-* JSON — ProseMirror's clipboard is
243
317
  // a toDOM → parseDOM pass, so without this an internal copy/paste
@@ -248,7 +322,8 @@ var schema = new import_prosemirror_model.Schema({
248
322
  getAttrs: (el) => ({
249
323
  borders: dataJson(el, "data-borders"),
250
324
  cellPadding: dataJson(el, "data-cell-padding"),
251
- align: el.getAttribute("data-align")
325
+ align: el.getAttribute("data-align"),
326
+ carry: dataJson(el, "data-carry")
252
327
  })
253
328
  }
254
329
  ],
@@ -259,6 +334,7 @@ var schema = new import_prosemirror_model.Schema({
259
334
  if (a["cellPadding"])
260
335
  dom["data-cell-padding"] = JSON.stringify(a["cellPadding"]);
261
336
  if (a["align"]) dom["data-align"] = String(a["align"]);
337
+ if (a["carry"]) dom["data-carry"] = JSON.stringify(a["carry"]);
262
338
  return ["table", dom, ["tbody", 0]];
263
339
  }
264
340
  },
@@ -271,7 +347,10 @@ var schema = new import_prosemirror_model.Schema({
271
347
  // (Word's default) means the paginator may split the row mid-content.
272
348
  cantSplit: { default: false },
273
349
  // w:trHeight — { value: px, exact: boolean } or null (auto).
274
- height: { default: null }
350
+ height: { default: null },
351
+ // Carry-through fidelity: unmodelled w:trPr children ({ trPr: string
352
+ // } — gridBefore/wBefore, cnfStyle, …), or null. Importer-set.
353
+ carry: { default: null }
275
354
  },
276
355
  parseDOM: [
277
356
  {
@@ -279,7 +358,8 @@ var schema = new import_prosemirror_model.Schema({
279
358
  getAttrs: (el) => ({
280
359
  header: el.getAttribute("data-header") === "true",
281
360
  cantSplit: el.getAttribute("data-cant-split") === "true",
282
- height: dataJson(el, "data-height")
361
+ height: dataJson(el, "data-height"),
362
+ carry: dataJson(el, "data-carry")
283
363
  })
284
364
  }
285
365
  ],
@@ -289,6 +369,8 @@ var schema = new import_prosemirror_model.Schema({
289
369
  if (node.attrs["cantSplit"]) dom["data-cant-split"] = "true";
290
370
  if (node.attrs["height"])
291
371
  dom["data-height"] = JSON.stringify(node.attrs["height"]);
372
+ if (node.attrs["carry"])
373
+ dom["data-carry"] = JSON.stringify(node.attrs["carry"]);
292
374
  return ["tr", dom, 0];
293
375
  }
294
376
  },
@@ -306,8 +388,11 @@ var schema = new import_prosemirror_model.Schema({
306
388
  // w:vAlign — 'center' | 'bottom' (top default)
307
389
  borders: { default: null },
308
390
  // w:tcBorders per-side visibility override
309
- padding: { default: null }
391
+ padding: { default: null },
310
392
  // w:tcMar per-side margin override (px)
393
+ // Carry-through fidelity: unmodelled w:tcPr children ({ tcPr: string
394
+ // } — textDirection, noWrap, tcFitText, …), or null. Importer-set.
395
+ carry: { default: null }
311
396
  },
312
397
  parseDOM: [
313
398
  { tag: "td", getAttrs: pastedCellAttrs },
@@ -329,6 +414,8 @@ var schema = new import_prosemirror_model.Schema({
329
414
  attrs["data-borders"] = JSON.stringify(node.attrs["borders"]);
330
415
  if (node.attrs["padding"])
331
416
  attrs["data-padding"] = JSON.stringify(node.attrs["padding"]);
417
+ if (node.attrs["carry"])
418
+ attrs["data-carry"] = JSON.stringify(node.attrs["carry"]);
332
419
  return ["td", attrs, 0];
333
420
  }
334
421
  }
@@ -354,6 +441,32 @@ var schema = new import_prosemirror_model.Schema({
354
441
  parseDOM: [{ tag: "s" }, { tag: "strike" }],
355
442
  toDOM: () => ["s", 0]
356
443
  },
444
+ // w:dstrike — double strikethrough
445
+ dstrike: {
446
+ parseDOM: [
447
+ {
448
+ style: "text-decoration-style=double",
449
+ getAttrs: (value) => value === "double" ? {} : false
450
+ }
451
+ ],
452
+ toDOM: () => [
453
+ "span",
454
+ {
455
+ style: "text-decoration: line-through; text-decoration-style: double"
456
+ },
457
+ 0
458
+ ]
459
+ },
460
+ // w:smallCaps
461
+ smallCaps: {
462
+ parseDOM: [
463
+ {
464
+ style: "font-variant-caps",
465
+ getAttrs: (value) => value === "small-caps" ? {} : false
466
+ }
467
+ ],
468
+ toDOM: () => ["span", { style: "font-variant-caps: small-caps" }, 0]
469
+ },
357
470
  // w:color — hex "#RRGGBB"
358
471
  textColor: {
359
472
  attrs: { color: {} },
@@ -475,6 +588,28 @@ var schema = new import_prosemirror_model.Schema({
475
588
  0
476
589
  ];
477
590
  }
591
+ },
592
+ // Carry-through fidelity (docx round-trip): run properties the model does
593
+ // NOT represent (w:rtl, w:kern, w:szCs, …), preserved verbatim as one raw
594
+ // XML fragment so saving a customer's file never drops them. Invisible —
595
+ // no rendering; the docx exporter splices `xml` back into the run's rPr.
596
+ // Typing inside/at the edge of a carried run extends the mark (inclusive
597
+ // default), which is the faithful behavior for properties like w:rtl.
598
+ carryRPr: {
599
+ attrs: { xml: {} },
600
+ toDOM(mark) {
601
+ return ["span", { "data-carry-rpr": String(mark.attrs["xml"]) }, 0];
602
+ },
603
+ parseDOM: [
604
+ {
605
+ tag: "span[data-carry-rpr]",
606
+ getAttrs: (el) => ({
607
+ xml: el.getAttribute(
608
+ "data-carry-rpr"
609
+ ) ?? ""
610
+ })
611
+ }
612
+ ]
478
613
  }
479
614
  // The `comment` mark (w:commentRangeStart/End) is contributed by the comment
480
615
  // plugin (@shadow-garden/bapbong-comments) via the editor's schema
@@ -614,14 +749,19 @@ function createNumberingCounter(defs) {
614
749
  return lvlDef.lvlText.replace(/%(\d)/g, (_match, digit) => {
615
750
  const lvl = Number(digit) - 1;
616
751
  const value = arr[lvl] ?? startOf(lvl);
617
- return formatCounter(value, def.levels[lvl]?.numFmt ?? "decimal");
752
+ const fmt = lvlDef.isLgl ? "decimal" : def.levels[lvl]?.numFmt ?? "decimal";
753
+ return formatCounter(value, fmt);
618
754
  });
619
755
  }
620
- return { next };
756
+ return { next, def: (numId, level) => defs?.[numId]?.levels[level] };
621
757
  }
622
758
  // Annotate the CommonJS export names for ESM import in node:
623
759
  0 && (module.exports = {
760
+ anchorName,
761
+ bookmarkLabel,
624
762
  commentSchema,
625
763
  createNumberingCounter,
764
+ fieldAt,
765
+ findBookmark,
626
766
  schema
627
767
  });
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './lib/bookmarks.js';
1
2
  export * from './lib/model.js';
2
3
  export * from './lib/numbering.js';
3
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC"}
package/dist/index.js CHANGED
@@ -1,3 +1,42 @@
1
+ // packages/model/src/lib/bookmarks.ts
2
+ function anchorName(href) {
3
+ return href && href.startsWith("#") && href.length > 1 ? href.slice(1) : null;
4
+ }
5
+ function findBookmark(doc, name) {
6
+ let found = null;
7
+ doc.descendants((node, pos) => {
8
+ if (found !== null) return false;
9
+ if (node.type.name !== "paragraph") return true;
10
+ const names = node.attrs["bookmarks"];
11
+ if (names?.includes(name)) found = pos + 1;
12
+ return false;
13
+ });
14
+ return found;
15
+ }
16
+ function bookmarkLabel(doc, name, max = 60) {
17
+ const pos = findBookmark(doc, name);
18
+ if (pos === null) return null;
19
+ const para = doc.nodeAt(pos - 1);
20
+ const text = (para?.textContent ?? "").replace(/^[\s.:–—-]+/, "").trim();
21
+ if (!text) return null;
22
+ return text.length > max ? `${text.slice(0, max - 1)}\u2026` : text;
23
+ }
24
+ function fieldAt(doc, pos) {
25
+ let hit = null;
26
+ let run = null;
27
+ doc.forEach((node, offset) => {
28
+ const f = node.attrs["field"];
29
+ if (f && run && run.field === f) {
30
+ run.to = offset + node.nodeSize;
31
+ } else {
32
+ if (run && hit === run) return;
33
+ run = f ? { field: f, from: offset, to: offset + node.nodeSize } : null;
34
+ }
35
+ if (run && hit === null && pos >= run.from && pos <= run.to) hit = run;
36
+ });
37
+ return hit;
38
+ }
39
+
1
40
  // packages/model/src/lib/model.ts
2
41
  import { Schema } from "prosemirror-model";
3
42
  function dataJson(el, name) {
@@ -15,7 +54,8 @@ function pastedParagraphAttrs(el, heading) {
15
54
  return {
16
55
  heading,
17
56
  align: m ? m[1].toLowerCase() : null,
18
- borders: dataJson(el, "data-borders")
57
+ borders: dataJson(el, "data-borders"),
58
+ carry: dataJson(el, "data-carry")
19
59
  };
20
60
  }
21
61
  function pastedImageAttrs(el) {
@@ -52,7 +92,8 @@ function pastedCellAttrs(el) {
52
92
  background: bg ? bg[1].trim() : null,
53
93
  vAlign: e.getAttribute("data-valign"),
54
94
  borders: dataJson(el, "data-borders"),
55
- padding: dataJson(el, "data-padding")
95
+ padding: dataJson(el, "data-padding"),
96
+ carry: dataJson(el, "data-carry")
56
97
  };
57
98
  }
58
99
  var schema = new Schema({
@@ -104,11 +145,33 @@ var schema = new Schema({
104
145
  tabs: { default: null },
105
146
  // w:spacing — { before?, after?, line?, lineRule? }, or null.
106
147
  spacing: { default: null },
148
+ // w:bookmarkStart names anchored in this paragraph (["_Toc89595219"]),
149
+ // or null. Link hrefs of the form "#name" resolve against these —
150
+ // paragraph-level is the right altitude: Word's TOC bookmarks wrap a
151
+ // heading's text, and jumping to the heading is what a reader wants.
152
+ bookmarks: { default: null },
153
+ // The generated field this paragraph belongs to ({ kind: 'toc',
154
+ // instr } for a TOC entry), or null for ordinary content. Word paints
155
+ // such content with field shading and regenerates it on update.
156
+ field: { default: null },
107
157
  // w:pageBreakBefore — start this paragraph on a new page.
108
158
  pageBreakBefore: { default: false },
159
+ // w:keepNext — stay on the same page as the next block's first line.
160
+ keepNext: { default: false },
161
+ // w:keepLines — never split this paragraph across pages.
162
+ keepLines: { default: false },
163
+ // w:widowControl — Word's default is ON; false only when the document
164
+ // explicitly disables widow/orphan control for this paragraph.
165
+ widowControl: { default: true },
109
166
  // w:pBdr — { top?, bottom?, left?, right? } of BorderSide, or null.
110
167
  // Importer-set; painted as a box around the paragraph's lines.
111
- borders: { default: null }
168
+ borders: { default: null },
169
+ // Carry-through fidelity (docx round-trip): OOXML paragraph
170
+ // properties the model does NOT represent, preserved verbatim so a
171
+ // customer's save never drops them. { pPr?: string, markRPr?: string }
172
+ // — raw XML fragments (pPr extras / the paragraph-mark w:rPr), or
173
+ // null. Importer-set; the exporter splices them back into w:pPr.
174
+ carry: { default: null }
112
175
  },
113
176
  // HTML paste path: recover heading level from h1–h6 and alignment from
114
177
  // inline style. Other attrs (list/indent/tabs/spacing) stay importer-only
@@ -128,6 +191,8 @@ var schema = new Schema({
128
191
  if (attrs.styleId) dom["data-style"] = attrs.styleId;
129
192
  if (node.attrs["borders"])
130
193
  dom["data-borders"] = JSON.stringify(node.attrs["borders"]);
194
+ if (node.attrs["carry"])
195
+ dom["data-carry"] = JSON.stringify(node.attrs["carry"]);
131
196
  return [tag, dom, 0];
132
197
  }
133
198
  },
@@ -209,7 +274,12 @@ var schema = new Schema({
209
274
  // insideV }, or null — OOXML tables are borderless unless declared.
210
275
  borders: { default: null },
211
276
  // w:tblPr/w:jc — 'center' | 'right' table alignment, or null (left).
212
- align: { default: null }
277
+ align: { default: null },
278
+ // Carry-through fidelity: unmodelled w:tblPr children (tblStyle,
279
+ // tblLayout, tblLook, tblInd, floating tblpPr, …) as one raw XML
280
+ // string ({ tblPr: string }), or null. Importer-set; the exporter
281
+ // splices it back so a save never drops them.
282
+ carry: { default: null }
213
283
  },
214
284
  // Complex attrs round-trip as data-* JSON — ProseMirror's clipboard is
215
285
  // a toDOM → parseDOM pass, so without this an internal copy/paste
@@ -220,7 +290,8 @@ var schema = new Schema({
220
290
  getAttrs: (el) => ({
221
291
  borders: dataJson(el, "data-borders"),
222
292
  cellPadding: dataJson(el, "data-cell-padding"),
223
- align: el.getAttribute("data-align")
293
+ align: el.getAttribute("data-align"),
294
+ carry: dataJson(el, "data-carry")
224
295
  })
225
296
  }
226
297
  ],
@@ -231,6 +302,7 @@ var schema = new Schema({
231
302
  if (a["cellPadding"])
232
303
  dom["data-cell-padding"] = JSON.stringify(a["cellPadding"]);
233
304
  if (a["align"]) dom["data-align"] = String(a["align"]);
305
+ if (a["carry"]) dom["data-carry"] = JSON.stringify(a["carry"]);
234
306
  return ["table", dom, ["tbody", 0]];
235
307
  }
236
308
  },
@@ -243,7 +315,10 @@ var schema = new Schema({
243
315
  // (Word's default) means the paginator may split the row mid-content.
244
316
  cantSplit: { default: false },
245
317
  // w:trHeight — { value: px, exact: boolean } or null (auto).
246
- height: { default: null }
318
+ height: { default: null },
319
+ // Carry-through fidelity: unmodelled w:trPr children ({ trPr: string
320
+ // } — gridBefore/wBefore, cnfStyle, …), or null. Importer-set.
321
+ carry: { default: null }
247
322
  },
248
323
  parseDOM: [
249
324
  {
@@ -251,7 +326,8 @@ var schema = new Schema({
251
326
  getAttrs: (el) => ({
252
327
  header: el.getAttribute("data-header") === "true",
253
328
  cantSplit: el.getAttribute("data-cant-split") === "true",
254
- height: dataJson(el, "data-height")
329
+ height: dataJson(el, "data-height"),
330
+ carry: dataJson(el, "data-carry")
255
331
  })
256
332
  }
257
333
  ],
@@ -261,6 +337,8 @@ var schema = new Schema({
261
337
  if (node.attrs["cantSplit"]) dom["data-cant-split"] = "true";
262
338
  if (node.attrs["height"])
263
339
  dom["data-height"] = JSON.stringify(node.attrs["height"]);
340
+ if (node.attrs["carry"])
341
+ dom["data-carry"] = JSON.stringify(node.attrs["carry"]);
264
342
  return ["tr", dom, 0];
265
343
  }
266
344
  },
@@ -278,8 +356,11 @@ var schema = new Schema({
278
356
  // w:vAlign — 'center' | 'bottom' (top default)
279
357
  borders: { default: null },
280
358
  // w:tcBorders per-side visibility override
281
- padding: { default: null }
359
+ padding: { default: null },
282
360
  // w:tcMar per-side margin override (px)
361
+ // Carry-through fidelity: unmodelled w:tcPr children ({ tcPr: string
362
+ // } — textDirection, noWrap, tcFitText, …), or null. Importer-set.
363
+ carry: { default: null }
283
364
  },
284
365
  parseDOM: [
285
366
  { tag: "td", getAttrs: pastedCellAttrs },
@@ -301,6 +382,8 @@ var schema = new Schema({
301
382
  attrs["data-borders"] = JSON.stringify(node.attrs["borders"]);
302
383
  if (node.attrs["padding"])
303
384
  attrs["data-padding"] = JSON.stringify(node.attrs["padding"]);
385
+ if (node.attrs["carry"])
386
+ attrs["data-carry"] = JSON.stringify(node.attrs["carry"]);
304
387
  return ["td", attrs, 0];
305
388
  }
306
389
  }
@@ -326,6 +409,32 @@ var schema = new Schema({
326
409
  parseDOM: [{ tag: "s" }, { tag: "strike" }],
327
410
  toDOM: () => ["s", 0]
328
411
  },
412
+ // w:dstrike — double strikethrough
413
+ dstrike: {
414
+ parseDOM: [
415
+ {
416
+ style: "text-decoration-style=double",
417
+ getAttrs: (value) => value === "double" ? {} : false
418
+ }
419
+ ],
420
+ toDOM: () => [
421
+ "span",
422
+ {
423
+ style: "text-decoration: line-through; text-decoration-style: double"
424
+ },
425
+ 0
426
+ ]
427
+ },
428
+ // w:smallCaps
429
+ smallCaps: {
430
+ parseDOM: [
431
+ {
432
+ style: "font-variant-caps",
433
+ getAttrs: (value) => value === "small-caps" ? {} : false
434
+ }
435
+ ],
436
+ toDOM: () => ["span", { style: "font-variant-caps: small-caps" }, 0]
437
+ },
329
438
  // w:color — hex "#RRGGBB"
330
439
  textColor: {
331
440
  attrs: { color: {} },
@@ -447,6 +556,28 @@ var schema = new Schema({
447
556
  0
448
557
  ];
449
558
  }
559
+ },
560
+ // Carry-through fidelity (docx round-trip): run properties the model does
561
+ // NOT represent (w:rtl, w:kern, w:szCs, …), preserved verbatim as one raw
562
+ // XML fragment so saving a customer's file never drops them. Invisible —
563
+ // no rendering; the docx exporter splices `xml` back into the run's rPr.
564
+ // Typing inside/at the edge of a carried run extends the mark (inclusive
565
+ // default), which is the faithful behavior for properties like w:rtl.
566
+ carryRPr: {
567
+ attrs: { xml: {} },
568
+ toDOM(mark) {
569
+ return ["span", { "data-carry-rpr": String(mark.attrs["xml"]) }, 0];
570
+ },
571
+ parseDOM: [
572
+ {
573
+ tag: "span[data-carry-rpr]",
574
+ getAttrs: (el) => ({
575
+ xml: el.getAttribute(
576
+ "data-carry-rpr"
577
+ ) ?? ""
578
+ })
579
+ }
580
+ ]
450
581
  }
451
582
  // The `comment` mark (w:commentRangeStart/End) is contributed by the comment
452
583
  // plugin (@shadow-garden/bapbong-comments) via the editor's schema
@@ -586,13 +717,18 @@ function createNumberingCounter(defs) {
586
717
  return lvlDef.lvlText.replace(/%(\d)/g, (_match, digit) => {
587
718
  const lvl = Number(digit) - 1;
588
719
  const value = arr[lvl] ?? startOf(lvl);
589
- return formatCounter(value, def.levels[lvl]?.numFmt ?? "decimal");
720
+ const fmt = lvlDef.isLgl ? "decimal" : def.levels[lvl]?.numFmt ?? "decimal";
721
+ return formatCounter(value, fmt);
590
722
  });
591
723
  }
592
- return { next };
724
+ return { next, def: (numId, level) => defs?.[numId]?.levels[level] };
593
725
  }
594
726
  export {
727
+ anchorName,
728
+ bookmarkLabel,
595
729
  commentSchema,
596
730
  createNumberingCounter,
731
+ fieldAt,
732
+ findBookmark,
597
733
  schema
598
734
  };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Bookmarks and generated fields — the two things that make a table of
3
+ * contents behave like Word's rather than like a list of ordinary links.
4
+ *
5
+ * A `w:bookmarkStart` names a place in the document; a hyperlink whose href
6
+ * is `#name` points at it. Word's TOC wires every entry this way, with
7
+ * machine-generated `_Toc…` names it never shows the reader. Bookmarks ride
8
+ * the paragraph that contains them (see model.ts `bookmarks`), so they move
9
+ * with their heading through edits instead of decaying into stale offsets.
10
+ */
11
+ import type { Node as PMNode } from 'prosemirror-model';
12
+ /** A generated field's identity, as it rides a paragraph's `field` attr. */
13
+ export interface FieldInfo {
14
+ /** Field kind — only 'toc' is modelled today. */
15
+ kind: string;
16
+ /** The field instruction, e.g. `TOC \o "1-3" \h \z \u`. */
17
+ instr: string;
18
+ }
19
+ /** An href pointing inside this document (`#name`) → the bookmark name, or
20
+ * null for external/absent links. */
21
+ export declare function anchorName(href: string | null | undefined): string | null;
22
+ /** Position of the paragraph anchoring `name`, or null when no paragraph
23
+ * claims it (a stale link, or a bookmark outside a paragraph). The position
24
+ * is the paragraph's first content slot — where a caret should land. */
25
+ export declare function findBookmark(doc: PMNode, name: string): number | null;
26
+ /** The text a reader should see for an internal link — the target paragraph's
27
+ * own text, trimmed to `max` — or null when the target is missing. Beats the
28
+ * raw `_Toc89595219`, which is machine bookkeeping. */
29
+ export declare function bookmarkLabel(doc: PMNode, name: string, max?: number): string | null;
30
+ /** The field a position sits inside, with the full paragraph range it spans,
31
+ * or null. A field covers CONSECUTIVE paragraphs carrying the same `field`
32
+ * attr object — the importer stamps one shared object per field, so identity
33
+ * distinguishes two adjacent TOCs. */
34
+ export declare function fieldAt(doc: PMNode, pos: number): {
35
+ field: FieldInfo;
36
+ from: number;
37
+ to: number;
38
+ } | null;
39
+ //# sourceMappingURL=bookmarks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bookmarks.d.ts","sourceRoot":"","sources":["../../src/lib/bookmarks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,IAAI,IAAI,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAExD,4EAA4E;AAC5E,MAAM,WAAW,SAAS;IACxB,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAC;CACf;AAED;sCACsC;AACtC,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAEzE;AAED;;yEAEyE;AACzE,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUrE;AAED;;wDAEwD;AACxD,wBAAgB,aAAa,CAC3B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,GAAG,SAAK,GACP,MAAM,GAAG,IAAI,CAUf;AAED;;;uCAGuC;AACvC,wBAAgB,OAAO,CACrB,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,GACV;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAkBvD"}
@@ -8,7 +8,7 @@ import { Schema } from 'prosemirror-model';
8
8
  * extend THIS schema so the importer, layout engine, and (canvas) painter all
9
9
  * agree on one document model.
10
10
  */
11
- export declare const schema: Schema<"doc" | "paragraph" | "text" | "hard_break" | "image" | "page_field" | "table" | "table_row" | "table_cell", "strong" | "em" | "underline" | "strike" | "textColor" | "fontSize" | "vertAlign" | "highlight" | "fontFamily" | "link" | "footnote">;
11
+ export declare const schema: Schema<"paragraph" | "doc" | "text" | "hard_break" | "image" | "page_field" | "table" | "table_row" | "table_cell", "strong" | "em" | "underline" | "strike" | "dstrike" | "smallCaps" | "textColor" | "fontSize" | "vertAlign" | "highlight" | "fontFamily" | "link" | "footnote" | "carryRPr">;
12
12
  /** Concrete schema type, handy for typing Node/Mark across packages. */
13
13
  export type BapbongSchema = typeof schema;
14
14
  /**
@@ -17,7 +17,7 @@ export type BapbongSchema = typeof schema;
17
17
  * (@user). Comment bodies are stored as this schema's JSON on the comment
18
18
  * thread, kept separate from the document schema.
19
19
  */
20
- export declare const commentSchema: Schema<"doc" | "paragraph" | "text" | "mention", never>;
20
+ export declare const commentSchema: Schema<"paragraph" | "doc" | "text" | "mention", never>;
21
21
  export type CommentSchema = typeof commentSchema;
22
22
  /** Paragraph horizontal alignment (mirrors w:jc). */
23
23
  export type Align = 'left' | 'center' | 'right' | 'justify';
@@ -1 +1 @@
1
- {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../src/lib/model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAgF3C;;;;;;;;GAQG;AACH,eAAO,MAAM,MAAM,2PAiZjB,CAAC;AAEH,wEAAwE;AACxE,MAAM,MAAM,aAAa,GAAG,OAAO,MAAM,CAAC;AAE1C;;;;;GAKG;AACH,eAAO,MAAM,aAAa,yDA0CxB,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,OAAO,aAAa,CAAC;AAEjD,qDAAqD;AACrD,MAAM,MAAM,KAAK,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,CAAC;AAE5D;+EAC+E;AAC/E,MAAM,WAAW,MAAM;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,2EAA2E;AAC3E,MAAM,WAAW,OAAO;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;CACzC;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,IAAI,CAAC;CACvC;AA0BD;;;4EAG4E;AAC5E,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
1
+ {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../src/lib/model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAkF3C;;;;;;;;GAQG;AACH,eAAO,MAAM,MAAM,kSAgfjB,CAAC;AAEH,wEAAwE;AACxE,MAAM,MAAM,aAAa,GAAG,OAAO,MAAM,CAAC;AAE1C;;;;;GAKG;AACH,eAAO,MAAM,aAAa,yDA0CxB,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,OAAO,aAAa,CAAC;AAEjD,qDAAqD;AACrD,MAAM,MAAM,KAAK,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,CAAC;AAE5D;+EAC+E;AAC/E,MAAM,WAAW,MAAM;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,2EAA2E;AAC3E,MAAM,WAAW,OAAO;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;CACzC;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,IAAI,CAAC;CACvC;AA0BD;;;4EAG4E;AAC5E,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
@@ -5,11 +5,28 @@
5
5
  * the layout engine does this every pass, so inserting/deleting/reordering
6
6
  * list items renumbers everything, like Word.
7
7
  */
8
+ /** Label (marker) run formatting from the lvl's w:rPr (plain data). */
9
+ export interface MarkerRunProps {
10
+ bold?: boolean;
11
+ italic?: boolean;
12
+ sizePt?: number;
13
+ family?: string;
14
+ color?: string;
15
+ }
8
16
  /** One w:lvl of an abstract numbering definition (plain data — attr-safe). */
9
17
  export interface NumberingLevelDef {
10
18
  numFmt: string;
11
19
  lvlText: string;
12
20
  start: number;
21
+ /** Label alignment against its anchor (w:lvlJc); 'left' when omitted. */
22
+ jc?: 'center' | 'right';
23
+ /** What separates the label from the text (w:suff); 'tab' when omitted. */
24
+ suff?: 'space' | 'nothing';
25
+ /** Legal numbering (w:isLgl): every %n placeholder renders as decimal
26
+ * regardless of the referenced level's own numFmt. */
27
+ isLgl?: boolean;
28
+ /** Label formatting (w:lvl > w:rPr) — the number/bullet's own font. */
29
+ rPr?: MarkerRunProps;
13
30
  }
14
31
  /** Definitions keyed by numId. `key` groups numIds that share one abstract
15
32
  * definition (their counters advance together, mirroring w:abstractNumId). */
@@ -22,6 +39,8 @@ export interface NumberingDefs {
22
39
  /** Stateful counter: call `next` once per list paragraph, in document order. */
23
40
  export interface NumberingCounter {
24
41
  next(numId: string, level: number): string;
42
+ /** The level's definition (label styling for the layout), if any. */
43
+ def(numId: string, level: number): NumberingLevelDef | undefined;
25
44
  }
26
45
  export declare function createNumberingCounter(defs: NumberingDefs | null | undefined): NumberingCounter;
27
46
  //# sourceMappingURL=numbering.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"numbering.d.ts","sourceRoot":"","sources":["../../src/lib/numbering.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,8EAA8E;AAC9E,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;+EAC+E;AAC/E,MAAM,WAAW,aAAa;IAC5B,CAAC,KAAK,EAAE,MAAM,GAAG;QACf,GAAG,EAAE,MAAM,CAAC;QACZ,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;KAC3C,CAAC;CACH;AAED,gFAAgF;AAChF,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;CAC5C;AA2DD,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,aAAa,GAAG,IAAI,GAAG,SAAS,GACrC,gBAAgB,CA2BlB"}
1
+ {"version":3,"file":"numbering.d.ts","sourceRoot":"","sources":["../../src/lib/numbering.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,uEAAuE;AACvE,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,8EAA8E;AAC9E,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;IACxB,2EAA2E;IAC3E,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC3B;2DACuD;IACvD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,uEAAuE;IACvE,GAAG,CAAC,EAAE,cAAc,CAAC;CACtB;AAED;+EAC+E;AAC/E,MAAM,WAAW,aAAa;IAC5B,CAAC,KAAK,EAAE,MAAM,GAAG;QACf,GAAG,EAAE,MAAM,CAAC;QACZ,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;KAC3C,CAAC;CACH;AAED,gFAAgF;AAChF,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3C,qEAAqE;IACrE,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAAC;CAClE;AA2DD,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,aAAa,GAAG,IAAI,GAAG,SAAS,GACrC,gBAAgB,CA+BlB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shadow-garden/bapbong-model",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "bapbong — ProseMirror document model / schema",
5
5
  "license": "MIT",
6
6
  "repository": {