@shadow-garden/bapbong-model 0.1.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/LICENSE +21 -0
- package/README.md +27 -0
- package/dist/index.cjs +441 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +412 -0
- package/dist/lib/model.d.ts +55 -0
- package/dist/lib/model.d.ts.map +1 -0
- package/dist/lib/numbering.d.ts +27 -0
- package/dist/lib/numbering.d.ts.map +1 -0
- package/package.json +59 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Le Phuoc Minh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# @shadow-garden/bapbong-model
|
|
2
|
+
|
|
3
|
+
The ProseMirror **document schema** for bapbong, plus list-numbering helpers.
|
|
4
|
+
This is the canonical in-memory document — `importDocx` produces it, the layout
|
|
5
|
+
engine consumes it, and the hidden editor edits it.
|
|
6
|
+
|
|
7
|
+
- **Scope:** `scope:model`
|
|
8
|
+
- **Depends on:** `prosemirror-model`
|
|
9
|
+
|
|
10
|
+
## What it provides
|
|
11
|
+
|
|
12
|
+
- **`schema`** — the main document `Schema`: `doc` (with undoable `attrs`
|
|
13
|
+
`numbering` / `sections` / `comments`), `paragraph`, `text`, `image`, `table`,
|
|
14
|
+
`hardBreak`, …; marks `strong`, `em`, `underline`, `strike`, `link`, `color`,
|
|
15
|
+
`highlight`, `vertAlign`, `footnote { num }`, `comment { ids }`.
|
|
16
|
+
- **`commentSchema`** — a tiny schema for comment bodies: `doc` / `paragraph` /
|
|
17
|
+
`text` + an inline **`mention`** atom (`{ id, label }`) with `leafText`/`toDOM`.
|
|
18
|
+
- **`createNumberingCounter(defs)`** — resolves live list markers (decimal,
|
|
19
|
+
bullet, multilevel) from `doc.attrs.numbering`.
|
|
20
|
+
- Types: `BapbongSchema`, `CommentSchema`, `Align`.
|
|
21
|
+
|
|
22
|
+
## Build / test
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
pnpm nx build @shadow-garden/bapbong-model
|
|
26
|
+
pnpm nx test @shadow-garden/bapbong-model
|
|
27
|
+
```
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// packages/model/src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
commentSchema: () => commentSchema,
|
|
24
|
+
createNumberingCounter: () => createNumberingCounter,
|
|
25
|
+
schema: () => schema
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
|
|
29
|
+
// packages/model/src/lib/model.ts
|
|
30
|
+
var import_prosemirror_model = require("prosemirror-model");
|
|
31
|
+
var schema = new import_prosemirror_model.Schema({
|
|
32
|
+
nodes: {
|
|
33
|
+
doc: {
|
|
34
|
+
content: "block+",
|
|
35
|
+
attrs: {
|
|
36
|
+
// NumberingDefs (numbering.ts) — list markers are recomputed from
|
|
37
|
+
// these at layout time, so edits renumber live. Importer-set.
|
|
38
|
+
numbering: { default: null },
|
|
39
|
+
// SectionConfig[] (contracts) — per-section column flow, delimited by
|
|
40
|
+
// w:sectPr breaks. null/absent → one implicit single-column section.
|
|
41
|
+
sections: { default: null },
|
|
42
|
+
// CommentNode[] (contracts) — comment threads keyed to `comment` marks.
|
|
43
|
+
// Edited via setDocAttribute so add/reply/resolve/delete undo cleanly.
|
|
44
|
+
comments: { default: null }
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
paragraph: {
|
|
48
|
+
group: "block",
|
|
49
|
+
content: "inline*",
|
|
50
|
+
// `list` is null for normal paragraphs, or ListInfo for list items.
|
|
51
|
+
// DOCX stores lists flat (each w:p carries w:numPr), so we keep them
|
|
52
|
+
// flat and let the marker be computed by the numbering counter.
|
|
53
|
+
//
|
|
54
|
+
// `align` mirrors w:jc ('left'|'center'|'right'|'justify'), null = default.
|
|
55
|
+
// `indent` mirrors w:ind, all measured in CSS px (Indent | null):
|
|
56
|
+
// { left, right, firstLine, hanging }. firstLine and hanging are
|
|
57
|
+
// mutually exclusive in OOXML; hanging wins if both appear.
|
|
58
|
+
attrs: {
|
|
59
|
+
list: { default: null },
|
|
60
|
+
align: { default: null },
|
|
61
|
+
indent: { default: null },
|
|
62
|
+
// Heading level 1–6 (maps to a Word "Heading N" style on export, and to
|
|
63
|
+
// an <h1>–<h6> in toDOM so the a11y mirror is semantic), or null for a
|
|
64
|
+
// body paragraph.
|
|
65
|
+
heading: { default: null },
|
|
66
|
+
// w:tabs — [{ pos, val: 'left'|'right'|'center'|'decimal', leader? }]
|
|
67
|
+
// in px from the paragraph's content left edge, or null. Importer-set.
|
|
68
|
+
tabs: { default: null },
|
|
69
|
+
// w:spacing — { before?, after?, line?, lineRule? }, or null.
|
|
70
|
+
spacing: { default: null },
|
|
71
|
+
// w:pageBreakBefore — start this paragraph on a new page.
|
|
72
|
+
pageBreakBefore: { default: false }
|
|
73
|
+
},
|
|
74
|
+
// No getAttrs: nothing in the pipeline parses paragraphs from the DOM
|
|
75
|
+
// yet (the importer builds nodes directly). align/indent still round-trip
|
|
76
|
+
// out through toDOM. Revisit when HTML paste lands.
|
|
77
|
+
parseDOM: [{ tag: "p" }],
|
|
78
|
+
toDOM(node) {
|
|
79
|
+
const attrs = node.attrs;
|
|
80
|
+
const style = paragraphStyle(attrs);
|
|
81
|
+
const tag = attrs.heading ? `h${attrs.heading}` : "p";
|
|
82
|
+
return [tag, style ? { style } : {}, 0];
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
text: { group: "inline" },
|
|
86
|
+
// A soft line break inside a paragraph (w:br) — forces a new line without
|
|
87
|
+
// ending the paragraph. Occupies one PM position, like an image atom.
|
|
88
|
+
hard_break: {
|
|
89
|
+
inline: true,
|
|
90
|
+
group: "inline",
|
|
91
|
+
selectable: false,
|
|
92
|
+
parseDOM: [{ tag: "br" }],
|
|
93
|
+
toDOM: () => ["br"]
|
|
94
|
+
},
|
|
95
|
+
// Inline image. `src` is typically a data URL (the importer inlines the
|
|
96
|
+
// embedded media); width/height are CSS pixels, null if unspecified.
|
|
97
|
+
image: {
|
|
98
|
+
inline: true,
|
|
99
|
+
group: "inline",
|
|
100
|
+
draggable: true,
|
|
101
|
+
attrs: {
|
|
102
|
+
src: {},
|
|
103
|
+
alt: { default: "" },
|
|
104
|
+
width: { default: null },
|
|
105
|
+
height: { default: null },
|
|
106
|
+
// wp:anchor (floating image): { wrap: 'square'|'topAndBottom'|'none',
|
|
107
|
+
// hAlign?, hOffset?, hRel?, vOffset?, vRel?, distL?, distR?, distT?,
|
|
108
|
+
// distB? } in px, or null for inline images. Importer-set.
|
|
109
|
+
float: { default: null },
|
|
110
|
+
// Drawn vector shape (wps rect / straight connector) riding this box:
|
|
111
|
+
// { kind: 'rect'|'line', stroke?, strokeWidth?, fill?, flipV? } — src
|
|
112
|
+
// is then '' and the box paints as vector. Null for real bitmaps.
|
|
113
|
+
shape: { default: null },
|
|
114
|
+
// Textbox (wps:txbx) content riding a shape: { paragraphs: <paragraph
|
|
115
|
+
// node JSON>[], inset?: {l,t,r,b} px }. The layout engine flows the
|
|
116
|
+
// paragraphs inside the shape's box (paint-only, not editable v1).
|
|
117
|
+
textbox: { default: null },
|
|
118
|
+
// Clockwise rotation in degrees around the box center (a:xfrm@rot).
|
|
119
|
+
// Paint-only: the layout box stays axis-aligned.
|
|
120
|
+
rotation: { default: 0 }
|
|
121
|
+
},
|
|
122
|
+
toDOM(node) {
|
|
123
|
+
const a = node.attrs;
|
|
124
|
+
const attrs = { src: a["src"], alt: a["alt"] };
|
|
125
|
+
if (a["width"] != null) attrs["width"] = String(a["width"]);
|
|
126
|
+
if (a["height"] != null) attrs["height"] = String(a["height"]);
|
|
127
|
+
return ["img", attrs];
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
// PAGE / NUMPAGES fields: atoms whose text is computed per page at paint
|
|
131
|
+
// time. `kind` is 'page' (current page number) or 'pages' (total count).
|
|
132
|
+
page_field: {
|
|
133
|
+
inline: true,
|
|
134
|
+
group: "inline",
|
|
135
|
+
atom: true,
|
|
136
|
+
attrs: { kind: {} },
|
|
137
|
+
parseDOM: [{ tag: "span[data-page-field]" }],
|
|
138
|
+
toDOM: (node) => [
|
|
139
|
+
"span",
|
|
140
|
+
{ "data-page-field": String(node.attrs["kind"]) },
|
|
141
|
+
node.attrs["kind"] === "pages" ? "##" : "#"
|
|
142
|
+
]
|
|
143
|
+
},
|
|
144
|
+
// Tables: kept structural (table → row → cell → block+). Horizontal spans
|
|
145
|
+
// map to colspan; vertical merges aren't collapsed yet (rowspan default 1).
|
|
146
|
+
table: {
|
|
147
|
+
group: "block",
|
|
148
|
+
content: "table_row+",
|
|
149
|
+
isolating: true,
|
|
150
|
+
attrs: {
|
|
151
|
+
// w:tblCellMar overrides (px: {left,right,top,bottom}), or null for
|
|
152
|
+
// Word defaults. Importer-set (same rationale as paragraph attrs).
|
|
153
|
+
cellPadding: { default: null },
|
|
154
|
+
// w:tblBorders visibility { top, bottom, left, right, insideH,
|
|
155
|
+
// insideV }, or null — OOXML tables are borderless unless declared.
|
|
156
|
+
borders: { default: null },
|
|
157
|
+
// w:tblPr/w:jc — 'center' | 'right' table alignment, or null (left).
|
|
158
|
+
align: { default: null }
|
|
159
|
+
},
|
|
160
|
+
parseDOM: [{ tag: "table" }],
|
|
161
|
+
toDOM: (node) => ["table", node.attrs["borders"] ? { "data-borders": "1" } : {}, ["tbody", 0]]
|
|
162
|
+
},
|
|
163
|
+
table_row: {
|
|
164
|
+
content: "table_cell+",
|
|
165
|
+
attrs: {
|
|
166
|
+
header: { default: false },
|
|
167
|
+
// w:trPr/w:tblHeader — repeat on every page
|
|
168
|
+
// w:trPr/w:cantSplit — the row must not break across pages. Absent
|
|
169
|
+
// (Word's default) means the paginator may split the row mid-content.
|
|
170
|
+
cantSplit: { default: false },
|
|
171
|
+
// w:trHeight — { value: px, exact: boolean } or null (auto).
|
|
172
|
+
height: { default: null }
|
|
173
|
+
},
|
|
174
|
+
// No getAttrs (same rationale as paragraph): the importer sets attrs
|
|
175
|
+
// directly; revisit when HTML paste lands.
|
|
176
|
+
parseDOM: [{ tag: "tr" }],
|
|
177
|
+
toDOM: (node) => node.attrs["header"] ? ["tr", { "data-header": "true" }, 0] : ["tr", 0]
|
|
178
|
+
},
|
|
179
|
+
table_cell: {
|
|
180
|
+
content: "block+",
|
|
181
|
+
isolating: true,
|
|
182
|
+
attrs: {
|
|
183
|
+
colspan: { default: 1 },
|
|
184
|
+
rowspan: { default: 1 },
|
|
185
|
+
colwidth: { default: null },
|
|
186
|
+
// px widths of the spanned columns, or null
|
|
187
|
+
background: { default: null },
|
|
188
|
+
// w:shd w:fill — cell fill "#RRGGBB"
|
|
189
|
+
vAlign: { default: null },
|
|
190
|
+
// w:vAlign — 'center' | 'bottom' (top default)
|
|
191
|
+
borders: { default: null }
|
|
192
|
+
// w:tcBorders per-side visibility override
|
|
193
|
+
},
|
|
194
|
+
parseDOM: [{ tag: "td" }, { tag: "th" }],
|
|
195
|
+
toDOM(node) {
|
|
196
|
+
const attrs = {};
|
|
197
|
+
if (node.attrs["colspan"] !== 1) attrs["colspan"] = String(node.attrs["colspan"]);
|
|
198
|
+
if (node.attrs["rowspan"] !== 1) attrs["rowspan"] = String(node.attrs["rowspan"]);
|
|
199
|
+
if (node.attrs["background"]) attrs["style"] = `background-color: ${node.attrs["background"]}`;
|
|
200
|
+
return ["td", attrs, 0];
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
marks: {
|
|
205
|
+
// w:b
|
|
206
|
+
strong: {
|
|
207
|
+
parseDOM: [{ tag: "strong" }, { tag: "b" }],
|
|
208
|
+
toDOM: () => ["strong", 0]
|
|
209
|
+
},
|
|
210
|
+
// w:i
|
|
211
|
+
em: {
|
|
212
|
+
parseDOM: [{ tag: "em" }, { tag: "i" }],
|
|
213
|
+
toDOM: () => ["em", 0]
|
|
214
|
+
},
|
|
215
|
+
// w:u
|
|
216
|
+
underline: {
|
|
217
|
+
parseDOM: [{ tag: "u" }],
|
|
218
|
+
toDOM: () => ["u", 0]
|
|
219
|
+
},
|
|
220
|
+
// w:strike
|
|
221
|
+
strike: {
|
|
222
|
+
parseDOM: [{ tag: "s" }, { tag: "strike" }],
|
|
223
|
+
toDOM: () => ["s", 0]
|
|
224
|
+
},
|
|
225
|
+
// w:color — hex "#RRGGBB"
|
|
226
|
+
textColor: {
|
|
227
|
+
attrs: { color: {} },
|
|
228
|
+
parseDOM: [{ style: "color", getAttrs: (value) => ({ color: value }) }],
|
|
229
|
+
toDOM(mark) {
|
|
230
|
+
return ["span", { style: `color: ${mark.attrs["color"]}` }, 0];
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
// w:sz — size in points
|
|
234
|
+
fontSize: {
|
|
235
|
+
attrs: { size: {} },
|
|
236
|
+
parseDOM: [
|
|
237
|
+
{
|
|
238
|
+
style: "font-size",
|
|
239
|
+
getAttrs: (value) => {
|
|
240
|
+
const pt = parseFloat(value);
|
|
241
|
+
return Number.isNaN(pt) ? false : { size: pt };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
],
|
|
245
|
+
toDOM(mark) {
|
|
246
|
+
return ["span", { style: `font-size: ${mark.attrs["size"]}pt` }, 0];
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
// w:vertAlign — superscript / subscript
|
|
250
|
+
vertAlign: {
|
|
251
|
+
attrs: { value: {} },
|
|
252
|
+
// 'super' | 'sub'
|
|
253
|
+
parseDOM: [
|
|
254
|
+
{ tag: "sup", getAttrs: () => ({ value: "super" }) },
|
|
255
|
+
{ tag: "sub", getAttrs: () => ({ value: "sub" }) }
|
|
256
|
+
],
|
|
257
|
+
toDOM: (mark) => [mark.attrs["value"] === "sub" ? "sub" : "sup", 0]
|
|
258
|
+
},
|
|
259
|
+
// w:highlight / w:shd w:fill — run background color ("#RRGGBB")
|
|
260
|
+
highlight: {
|
|
261
|
+
attrs: { color: {} },
|
|
262
|
+
parseDOM: [
|
|
263
|
+
{ style: "background-color", getAttrs: (value) => ({ color: value }) }
|
|
264
|
+
],
|
|
265
|
+
toDOM(mark) {
|
|
266
|
+
return ["span", { style: `background-color: ${mark.attrs["color"]}` }, 0];
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
// w:rFonts — font family
|
|
270
|
+
fontFamily: {
|
|
271
|
+
attrs: { family: {} },
|
|
272
|
+
parseDOM: [{ style: "font-family", getAttrs: (value) => ({ family: value }) }],
|
|
273
|
+
toDOM(mark) {
|
|
274
|
+
return ["span", { style: `font-family: ${mark.attrs["family"]}` }, 0];
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
// w:hyperlink — external URL or "#anchor"
|
|
278
|
+
link: {
|
|
279
|
+
attrs: { href: {} },
|
|
280
|
+
inclusive: false,
|
|
281
|
+
toDOM(mark) {
|
|
282
|
+
return ["a", { href: mark.attrs["href"], rel: "noopener", target: "_blank" }, 0];
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
// w:footnoteReference — the carrier text is the superscript number; `num`
|
|
286
|
+
// lets the layout engine match the reference to its page-bottom body.
|
|
287
|
+
footnote: {
|
|
288
|
+
attrs: { num: {} },
|
|
289
|
+
inclusive: false,
|
|
290
|
+
parseDOM: [
|
|
291
|
+
{
|
|
292
|
+
tag: "sup[data-footnote]",
|
|
293
|
+
// `el` is an HTMLElement at runtime; this package has no DOM lib, so
|
|
294
|
+
// narrow structurally rather than naming the type.
|
|
295
|
+
getAttrs: (el) => ({
|
|
296
|
+
num: Number(el.getAttribute("data-footnote")) || 0
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
],
|
|
300
|
+
toDOM(mark) {
|
|
301
|
+
return ["sup", { "data-footnote": String(mark.attrs["num"]) }, 0];
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// The `comment` mark (w:commentRangeStart/End) is contributed by the comment
|
|
305
|
+
// plugin (@shadow-garden/bapbong-comments) via the editor's schema
|
|
306
|
+
// composition — it is NOT part of the base document schema, so a build
|
|
307
|
+
// without comments stays free of comment marks. The `comments` doc attr
|
|
308
|
+
// above is inert storage the plugin populates when present.
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
var commentSchema = new import_prosemirror_model.Schema({
|
|
312
|
+
nodes: {
|
|
313
|
+
doc: { content: "block+" },
|
|
314
|
+
paragraph: { group: "block", content: "inline*", parseDOM: [{ tag: "p" }], toDOM: () => ["p", 0] },
|
|
315
|
+
text: { group: "inline" },
|
|
316
|
+
// @mention: an inline atom carrying the mentioned user's id + display name.
|
|
317
|
+
// `leafText` lets textContent include "@Name" (search / plain-text preview).
|
|
318
|
+
mention: {
|
|
319
|
+
group: "inline",
|
|
320
|
+
inline: true,
|
|
321
|
+
atom: true,
|
|
322
|
+
selectable: false,
|
|
323
|
+
attrs: { id: {}, label: {} },
|
|
324
|
+
leafText: (node) => `@${node.attrs["label"]}`,
|
|
325
|
+
toDOM: (node) => [
|
|
326
|
+
"span",
|
|
327
|
+
{ class: "mention", "data-id": String(node.attrs["id"]) },
|
|
328
|
+
`@${node.attrs["label"]}`
|
|
329
|
+
],
|
|
330
|
+
parseDOM: [
|
|
331
|
+
{
|
|
332
|
+
tag: "span.mention",
|
|
333
|
+
getAttrs: (el) => {
|
|
334
|
+
const e = el;
|
|
335
|
+
return { id: e.getAttribute("data-id"), label: (e.textContent ?? "").replace(/^@/, "") };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
]
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
marks: {}
|
|
342
|
+
});
|
|
343
|
+
function paragraphStyle(attrs) {
|
|
344
|
+
const parts = [];
|
|
345
|
+
if (attrs.align) parts.push(`text-align: ${attrs.align}`);
|
|
346
|
+
const ind = attrs.indent;
|
|
347
|
+
if (ind) {
|
|
348
|
+
if (ind.left) parts.push(`margin-left: ${ind.left}px`);
|
|
349
|
+
if (ind.right) parts.push(`margin-right: ${ind.right}px`);
|
|
350
|
+
if (ind.hanging) parts.push(`text-indent: ${-ind.hanging}px`);
|
|
351
|
+
else if (ind.firstLine) parts.push(`text-indent: ${ind.firstLine}px`);
|
|
352
|
+
}
|
|
353
|
+
const sp = attrs.spacing;
|
|
354
|
+
if (sp) {
|
|
355
|
+
if (sp.before) parts.push(`margin-top: ${sp.before}px`);
|
|
356
|
+
if (sp.after) parts.push(`margin-bottom: ${sp.after}px`);
|
|
357
|
+
if (sp.line && sp.lineRule === "auto") parts.push(`line-height: ${sp.line}`);
|
|
358
|
+
else if (sp.line) parts.push(`line-height: ${sp.line}px`);
|
|
359
|
+
}
|
|
360
|
+
return parts.join("; ");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// packages/model/src/lib/numbering.ts
|
|
364
|
+
function toLetters(n) {
|
|
365
|
+
let s = "";
|
|
366
|
+
let x = n;
|
|
367
|
+
while (x > 0) {
|
|
368
|
+
const rem = (x - 1) % 26;
|
|
369
|
+
s = String.fromCharCode(65 + rem) + s;
|
|
370
|
+
x = Math.floor((x - 1) / 26);
|
|
371
|
+
}
|
|
372
|
+
return s || "A";
|
|
373
|
+
}
|
|
374
|
+
function toRoman(n) {
|
|
375
|
+
if (n <= 0) return String(n);
|
|
376
|
+
const table = [
|
|
377
|
+
[1e3, "M"],
|
|
378
|
+
[900, "CM"],
|
|
379
|
+
[500, "D"],
|
|
380
|
+
[400, "CD"],
|
|
381
|
+
[100, "C"],
|
|
382
|
+
[90, "XC"],
|
|
383
|
+
[50, "L"],
|
|
384
|
+
[40, "XL"],
|
|
385
|
+
[10, "X"],
|
|
386
|
+
[9, "IX"],
|
|
387
|
+
[5, "V"],
|
|
388
|
+
[4, "IV"],
|
|
389
|
+
[1, "I"]
|
|
390
|
+
];
|
|
391
|
+
let x = n;
|
|
392
|
+
let out = "";
|
|
393
|
+
for (const [v, sym] of table) {
|
|
394
|
+
while (x >= v) {
|
|
395
|
+
out += sym;
|
|
396
|
+
x -= v;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return out;
|
|
400
|
+
}
|
|
401
|
+
function formatCounter(n, fmt) {
|
|
402
|
+
switch (fmt) {
|
|
403
|
+
case "lowerLetter":
|
|
404
|
+
return toLetters(n).toLowerCase();
|
|
405
|
+
case "upperLetter":
|
|
406
|
+
return toLetters(n);
|
|
407
|
+
case "lowerRoman":
|
|
408
|
+
return toRoman(n).toLowerCase();
|
|
409
|
+
case "upperRoman":
|
|
410
|
+
return toRoman(n);
|
|
411
|
+
default:
|
|
412
|
+
return String(n);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function createNumberingCounter(defs) {
|
|
416
|
+
const counters = /* @__PURE__ */ new Map();
|
|
417
|
+
function next(numId, level) {
|
|
418
|
+
const def = defs?.[numId];
|
|
419
|
+
if (!def) return "";
|
|
420
|
+
const startOf = (lvl) => def.levels[lvl]?.start ?? 1;
|
|
421
|
+
const arr = counters.get(def.key) ?? [];
|
|
422
|
+
arr[level] = (arr[level] ?? startOf(level) - 1) + 1;
|
|
423
|
+
for (let l = level + 1; l < arr.length; l++) arr[l] = void 0;
|
|
424
|
+
counters.set(def.key, arr);
|
|
425
|
+
const lvlDef = def.levels[level];
|
|
426
|
+
if (!lvlDef || lvlDef.numFmt === "none") return "";
|
|
427
|
+
if (lvlDef.numFmt === "bullet") return lvlDef.lvlText || "\u2022";
|
|
428
|
+
return lvlDef.lvlText.replace(/%(\d)/g, (_match, digit) => {
|
|
429
|
+
const lvl = Number(digit) - 1;
|
|
430
|
+
const value = arr[lvl] ?? startOf(lvl);
|
|
431
|
+
return formatCounter(value, def.levels[lvl]?.numFmt ?? "decimal");
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
return { next };
|
|
435
|
+
}
|
|
436
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
437
|
+
0 && (module.exports = {
|
|
438
|
+
commentSchema,
|
|
439
|
+
createNumberingCounter,
|
|
440
|
+
schema
|
|
441
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
// packages/model/src/lib/model.ts
|
|
2
|
+
import { Schema } from "prosemirror-model";
|
|
3
|
+
var schema = new Schema({
|
|
4
|
+
nodes: {
|
|
5
|
+
doc: {
|
|
6
|
+
content: "block+",
|
|
7
|
+
attrs: {
|
|
8
|
+
// NumberingDefs (numbering.ts) — list markers are recomputed from
|
|
9
|
+
// these at layout time, so edits renumber live. Importer-set.
|
|
10
|
+
numbering: { default: null },
|
|
11
|
+
// SectionConfig[] (contracts) — per-section column flow, delimited by
|
|
12
|
+
// w:sectPr breaks. null/absent → one implicit single-column section.
|
|
13
|
+
sections: { default: null },
|
|
14
|
+
// CommentNode[] (contracts) — comment threads keyed to `comment` marks.
|
|
15
|
+
// Edited via setDocAttribute so add/reply/resolve/delete undo cleanly.
|
|
16
|
+
comments: { default: null }
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
paragraph: {
|
|
20
|
+
group: "block",
|
|
21
|
+
content: "inline*",
|
|
22
|
+
// `list` is null for normal paragraphs, or ListInfo for list items.
|
|
23
|
+
// DOCX stores lists flat (each w:p carries w:numPr), so we keep them
|
|
24
|
+
// flat and let the marker be computed by the numbering counter.
|
|
25
|
+
//
|
|
26
|
+
// `align` mirrors w:jc ('left'|'center'|'right'|'justify'), null = default.
|
|
27
|
+
// `indent` mirrors w:ind, all measured in CSS px (Indent | null):
|
|
28
|
+
// { left, right, firstLine, hanging }. firstLine and hanging are
|
|
29
|
+
// mutually exclusive in OOXML; hanging wins if both appear.
|
|
30
|
+
attrs: {
|
|
31
|
+
list: { default: null },
|
|
32
|
+
align: { default: null },
|
|
33
|
+
indent: { default: null },
|
|
34
|
+
// Heading level 1–6 (maps to a Word "Heading N" style on export, and to
|
|
35
|
+
// an <h1>–<h6> in toDOM so the a11y mirror is semantic), or null for a
|
|
36
|
+
// body paragraph.
|
|
37
|
+
heading: { default: null },
|
|
38
|
+
// w:tabs — [{ pos, val: 'left'|'right'|'center'|'decimal', leader? }]
|
|
39
|
+
// in px from the paragraph's content left edge, or null. Importer-set.
|
|
40
|
+
tabs: { default: null },
|
|
41
|
+
// w:spacing — { before?, after?, line?, lineRule? }, or null.
|
|
42
|
+
spacing: { default: null },
|
|
43
|
+
// w:pageBreakBefore — start this paragraph on a new page.
|
|
44
|
+
pageBreakBefore: { default: false }
|
|
45
|
+
},
|
|
46
|
+
// No getAttrs: nothing in the pipeline parses paragraphs from the DOM
|
|
47
|
+
// yet (the importer builds nodes directly). align/indent still round-trip
|
|
48
|
+
// out through toDOM. Revisit when HTML paste lands.
|
|
49
|
+
parseDOM: [{ tag: "p" }],
|
|
50
|
+
toDOM(node) {
|
|
51
|
+
const attrs = node.attrs;
|
|
52
|
+
const style = paragraphStyle(attrs);
|
|
53
|
+
const tag = attrs.heading ? `h${attrs.heading}` : "p";
|
|
54
|
+
return [tag, style ? { style } : {}, 0];
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
text: { group: "inline" },
|
|
58
|
+
// A soft line break inside a paragraph (w:br) — forces a new line without
|
|
59
|
+
// ending the paragraph. Occupies one PM position, like an image atom.
|
|
60
|
+
hard_break: {
|
|
61
|
+
inline: true,
|
|
62
|
+
group: "inline",
|
|
63
|
+
selectable: false,
|
|
64
|
+
parseDOM: [{ tag: "br" }],
|
|
65
|
+
toDOM: () => ["br"]
|
|
66
|
+
},
|
|
67
|
+
// Inline image. `src` is typically a data URL (the importer inlines the
|
|
68
|
+
// embedded media); width/height are CSS pixels, null if unspecified.
|
|
69
|
+
image: {
|
|
70
|
+
inline: true,
|
|
71
|
+
group: "inline",
|
|
72
|
+
draggable: true,
|
|
73
|
+
attrs: {
|
|
74
|
+
src: {},
|
|
75
|
+
alt: { default: "" },
|
|
76
|
+
width: { default: null },
|
|
77
|
+
height: { default: null },
|
|
78
|
+
// wp:anchor (floating image): { wrap: 'square'|'topAndBottom'|'none',
|
|
79
|
+
// hAlign?, hOffset?, hRel?, vOffset?, vRel?, distL?, distR?, distT?,
|
|
80
|
+
// distB? } in px, or null for inline images. Importer-set.
|
|
81
|
+
float: { default: null },
|
|
82
|
+
// Drawn vector shape (wps rect / straight connector) riding this box:
|
|
83
|
+
// { kind: 'rect'|'line', stroke?, strokeWidth?, fill?, flipV? } — src
|
|
84
|
+
// is then '' and the box paints as vector. Null for real bitmaps.
|
|
85
|
+
shape: { default: null },
|
|
86
|
+
// Textbox (wps:txbx) content riding a shape: { paragraphs: <paragraph
|
|
87
|
+
// node JSON>[], inset?: {l,t,r,b} px }. The layout engine flows the
|
|
88
|
+
// paragraphs inside the shape's box (paint-only, not editable v1).
|
|
89
|
+
textbox: { default: null },
|
|
90
|
+
// Clockwise rotation in degrees around the box center (a:xfrm@rot).
|
|
91
|
+
// Paint-only: the layout box stays axis-aligned.
|
|
92
|
+
rotation: { default: 0 }
|
|
93
|
+
},
|
|
94
|
+
toDOM(node) {
|
|
95
|
+
const a = node.attrs;
|
|
96
|
+
const attrs = { src: a["src"], alt: a["alt"] };
|
|
97
|
+
if (a["width"] != null) attrs["width"] = String(a["width"]);
|
|
98
|
+
if (a["height"] != null) attrs["height"] = String(a["height"]);
|
|
99
|
+
return ["img", attrs];
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
// PAGE / NUMPAGES fields: atoms whose text is computed per page at paint
|
|
103
|
+
// time. `kind` is 'page' (current page number) or 'pages' (total count).
|
|
104
|
+
page_field: {
|
|
105
|
+
inline: true,
|
|
106
|
+
group: "inline",
|
|
107
|
+
atom: true,
|
|
108
|
+
attrs: { kind: {} },
|
|
109
|
+
parseDOM: [{ tag: "span[data-page-field]" }],
|
|
110
|
+
toDOM: (node) => [
|
|
111
|
+
"span",
|
|
112
|
+
{ "data-page-field": String(node.attrs["kind"]) },
|
|
113
|
+
node.attrs["kind"] === "pages" ? "##" : "#"
|
|
114
|
+
]
|
|
115
|
+
},
|
|
116
|
+
// Tables: kept structural (table → row → cell → block+). Horizontal spans
|
|
117
|
+
// map to colspan; vertical merges aren't collapsed yet (rowspan default 1).
|
|
118
|
+
table: {
|
|
119
|
+
group: "block",
|
|
120
|
+
content: "table_row+",
|
|
121
|
+
isolating: true,
|
|
122
|
+
attrs: {
|
|
123
|
+
// w:tblCellMar overrides (px: {left,right,top,bottom}), or null for
|
|
124
|
+
// Word defaults. Importer-set (same rationale as paragraph attrs).
|
|
125
|
+
cellPadding: { default: null },
|
|
126
|
+
// w:tblBorders visibility { top, bottom, left, right, insideH,
|
|
127
|
+
// insideV }, or null — OOXML tables are borderless unless declared.
|
|
128
|
+
borders: { default: null },
|
|
129
|
+
// w:tblPr/w:jc — 'center' | 'right' table alignment, or null (left).
|
|
130
|
+
align: { default: null }
|
|
131
|
+
},
|
|
132
|
+
parseDOM: [{ tag: "table" }],
|
|
133
|
+
toDOM: (node) => ["table", node.attrs["borders"] ? { "data-borders": "1" } : {}, ["tbody", 0]]
|
|
134
|
+
},
|
|
135
|
+
table_row: {
|
|
136
|
+
content: "table_cell+",
|
|
137
|
+
attrs: {
|
|
138
|
+
header: { default: false },
|
|
139
|
+
// w:trPr/w:tblHeader — repeat on every page
|
|
140
|
+
// w:trPr/w:cantSplit — the row must not break across pages. Absent
|
|
141
|
+
// (Word's default) means the paginator may split the row mid-content.
|
|
142
|
+
cantSplit: { default: false },
|
|
143
|
+
// w:trHeight — { value: px, exact: boolean } or null (auto).
|
|
144
|
+
height: { default: null }
|
|
145
|
+
},
|
|
146
|
+
// No getAttrs (same rationale as paragraph): the importer sets attrs
|
|
147
|
+
// directly; revisit when HTML paste lands.
|
|
148
|
+
parseDOM: [{ tag: "tr" }],
|
|
149
|
+
toDOM: (node) => node.attrs["header"] ? ["tr", { "data-header": "true" }, 0] : ["tr", 0]
|
|
150
|
+
},
|
|
151
|
+
table_cell: {
|
|
152
|
+
content: "block+",
|
|
153
|
+
isolating: true,
|
|
154
|
+
attrs: {
|
|
155
|
+
colspan: { default: 1 },
|
|
156
|
+
rowspan: { default: 1 },
|
|
157
|
+
colwidth: { default: null },
|
|
158
|
+
// px widths of the spanned columns, or null
|
|
159
|
+
background: { default: null },
|
|
160
|
+
// w:shd w:fill — cell fill "#RRGGBB"
|
|
161
|
+
vAlign: { default: null },
|
|
162
|
+
// w:vAlign — 'center' | 'bottom' (top default)
|
|
163
|
+
borders: { default: null }
|
|
164
|
+
// w:tcBorders per-side visibility override
|
|
165
|
+
},
|
|
166
|
+
parseDOM: [{ tag: "td" }, { tag: "th" }],
|
|
167
|
+
toDOM(node) {
|
|
168
|
+
const attrs = {};
|
|
169
|
+
if (node.attrs["colspan"] !== 1) attrs["colspan"] = String(node.attrs["colspan"]);
|
|
170
|
+
if (node.attrs["rowspan"] !== 1) attrs["rowspan"] = String(node.attrs["rowspan"]);
|
|
171
|
+
if (node.attrs["background"]) attrs["style"] = `background-color: ${node.attrs["background"]}`;
|
|
172
|
+
return ["td", attrs, 0];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
marks: {
|
|
177
|
+
// w:b
|
|
178
|
+
strong: {
|
|
179
|
+
parseDOM: [{ tag: "strong" }, { tag: "b" }],
|
|
180
|
+
toDOM: () => ["strong", 0]
|
|
181
|
+
},
|
|
182
|
+
// w:i
|
|
183
|
+
em: {
|
|
184
|
+
parseDOM: [{ tag: "em" }, { tag: "i" }],
|
|
185
|
+
toDOM: () => ["em", 0]
|
|
186
|
+
},
|
|
187
|
+
// w:u
|
|
188
|
+
underline: {
|
|
189
|
+
parseDOM: [{ tag: "u" }],
|
|
190
|
+
toDOM: () => ["u", 0]
|
|
191
|
+
},
|
|
192
|
+
// w:strike
|
|
193
|
+
strike: {
|
|
194
|
+
parseDOM: [{ tag: "s" }, { tag: "strike" }],
|
|
195
|
+
toDOM: () => ["s", 0]
|
|
196
|
+
},
|
|
197
|
+
// w:color — hex "#RRGGBB"
|
|
198
|
+
textColor: {
|
|
199
|
+
attrs: { color: {} },
|
|
200
|
+
parseDOM: [{ style: "color", getAttrs: (value) => ({ color: value }) }],
|
|
201
|
+
toDOM(mark) {
|
|
202
|
+
return ["span", { style: `color: ${mark.attrs["color"]}` }, 0];
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
// w:sz — size in points
|
|
206
|
+
fontSize: {
|
|
207
|
+
attrs: { size: {} },
|
|
208
|
+
parseDOM: [
|
|
209
|
+
{
|
|
210
|
+
style: "font-size",
|
|
211
|
+
getAttrs: (value) => {
|
|
212
|
+
const pt = parseFloat(value);
|
|
213
|
+
return Number.isNaN(pt) ? false : { size: pt };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
],
|
|
217
|
+
toDOM(mark) {
|
|
218
|
+
return ["span", { style: `font-size: ${mark.attrs["size"]}pt` }, 0];
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
// w:vertAlign — superscript / subscript
|
|
222
|
+
vertAlign: {
|
|
223
|
+
attrs: { value: {} },
|
|
224
|
+
// 'super' | 'sub'
|
|
225
|
+
parseDOM: [
|
|
226
|
+
{ tag: "sup", getAttrs: () => ({ value: "super" }) },
|
|
227
|
+
{ tag: "sub", getAttrs: () => ({ value: "sub" }) }
|
|
228
|
+
],
|
|
229
|
+
toDOM: (mark) => [mark.attrs["value"] === "sub" ? "sub" : "sup", 0]
|
|
230
|
+
},
|
|
231
|
+
// w:highlight / w:shd w:fill — run background color ("#RRGGBB")
|
|
232
|
+
highlight: {
|
|
233
|
+
attrs: { color: {} },
|
|
234
|
+
parseDOM: [
|
|
235
|
+
{ style: "background-color", getAttrs: (value) => ({ color: value }) }
|
|
236
|
+
],
|
|
237
|
+
toDOM(mark) {
|
|
238
|
+
return ["span", { style: `background-color: ${mark.attrs["color"]}` }, 0];
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
// w:rFonts — font family
|
|
242
|
+
fontFamily: {
|
|
243
|
+
attrs: { family: {} },
|
|
244
|
+
parseDOM: [{ style: "font-family", getAttrs: (value) => ({ family: value }) }],
|
|
245
|
+
toDOM(mark) {
|
|
246
|
+
return ["span", { style: `font-family: ${mark.attrs["family"]}` }, 0];
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
// w:hyperlink — external URL or "#anchor"
|
|
250
|
+
link: {
|
|
251
|
+
attrs: { href: {} },
|
|
252
|
+
inclusive: false,
|
|
253
|
+
toDOM(mark) {
|
|
254
|
+
return ["a", { href: mark.attrs["href"], rel: "noopener", target: "_blank" }, 0];
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
// w:footnoteReference — the carrier text is the superscript number; `num`
|
|
258
|
+
// lets the layout engine match the reference to its page-bottom body.
|
|
259
|
+
footnote: {
|
|
260
|
+
attrs: { num: {} },
|
|
261
|
+
inclusive: false,
|
|
262
|
+
parseDOM: [
|
|
263
|
+
{
|
|
264
|
+
tag: "sup[data-footnote]",
|
|
265
|
+
// `el` is an HTMLElement at runtime; this package has no DOM lib, so
|
|
266
|
+
// narrow structurally rather than naming the type.
|
|
267
|
+
getAttrs: (el) => ({
|
|
268
|
+
num: Number(el.getAttribute("data-footnote")) || 0
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
],
|
|
272
|
+
toDOM(mark) {
|
|
273
|
+
return ["sup", { "data-footnote": String(mark.attrs["num"]) }, 0];
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// The `comment` mark (w:commentRangeStart/End) is contributed by the comment
|
|
277
|
+
// plugin (@shadow-garden/bapbong-comments) via the editor's schema
|
|
278
|
+
// composition — it is NOT part of the base document schema, so a build
|
|
279
|
+
// without comments stays free of comment marks. The `comments` doc attr
|
|
280
|
+
// above is inert storage the plugin populates when present.
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
var commentSchema = new Schema({
|
|
284
|
+
nodes: {
|
|
285
|
+
doc: { content: "block+" },
|
|
286
|
+
paragraph: { group: "block", content: "inline*", parseDOM: [{ tag: "p" }], toDOM: () => ["p", 0] },
|
|
287
|
+
text: { group: "inline" },
|
|
288
|
+
// @mention: an inline atom carrying the mentioned user's id + display name.
|
|
289
|
+
// `leafText` lets textContent include "@Name" (search / plain-text preview).
|
|
290
|
+
mention: {
|
|
291
|
+
group: "inline",
|
|
292
|
+
inline: true,
|
|
293
|
+
atom: true,
|
|
294
|
+
selectable: false,
|
|
295
|
+
attrs: { id: {}, label: {} },
|
|
296
|
+
leafText: (node) => `@${node.attrs["label"]}`,
|
|
297
|
+
toDOM: (node) => [
|
|
298
|
+
"span",
|
|
299
|
+
{ class: "mention", "data-id": String(node.attrs["id"]) },
|
|
300
|
+
`@${node.attrs["label"]}`
|
|
301
|
+
],
|
|
302
|
+
parseDOM: [
|
|
303
|
+
{
|
|
304
|
+
tag: "span.mention",
|
|
305
|
+
getAttrs: (el) => {
|
|
306
|
+
const e = el;
|
|
307
|
+
return { id: e.getAttribute("data-id"), label: (e.textContent ?? "").replace(/^@/, "") };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
]
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
marks: {}
|
|
314
|
+
});
|
|
315
|
+
function paragraphStyle(attrs) {
|
|
316
|
+
const parts = [];
|
|
317
|
+
if (attrs.align) parts.push(`text-align: ${attrs.align}`);
|
|
318
|
+
const ind = attrs.indent;
|
|
319
|
+
if (ind) {
|
|
320
|
+
if (ind.left) parts.push(`margin-left: ${ind.left}px`);
|
|
321
|
+
if (ind.right) parts.push(`margin-right: ${ind.right}px`);
|
|
322
|
+
if (ind.hanging) parts.push(`text-indent: ${-ind.hanging}px`);
|
|
323
|
+
else if (ind.firstLine) parts.push(`text-indent: ${ind.firstLine}px`);
|
|
324
|
+
}
|
|
325
|
+
const sp = attrs.spacing;
|
|
326
|
+
if (sp) {
|
|
327
|
+
if (sp.before) parts.push(`margin-top: ${sp.before}px`);
|
|
328
|
+
if (sp.after) parts.push(`margin-bottom: ${sp.after}px`);
|
|
329
|
+
if (sp.line && sp.lineRule === "auto") parts.push(`line-height: ${sp.line}`);
|
|
330
|
+
else if (sp.line) parts.push(`line-height: ${sp.line}px`);
|
|
331
|
+
}
|
|
332
|
+
return parts.join("; ");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// packages/model/src/lib/numbering.ts
|
|
336
|
+
function toLetters(n) {
|
|
337
|
+
let s = "";
|
|
338
|
+
let x = n;
|
|
339
|
+
while (x > 0) {
|
|
340
|
+
const rem = (x - 1) % 26;
|
|
341
|
+
s = String.fromCharCode(65 + rem) + s;
|
|
342
|
+
x = Math.floor((x - 1) / 26);
|
|
343
|
+
}
|
|
344
|
+
return s || "A";
|
|
345
|
+
}
|
|
346
|
+
function toRoman(n) {
|
|
347
|
+
if (n <= 0) return String(n);
|
|
348
|
+
const table = [
|
|
349
|
+
[1e3, "M"],
|
|
350
|
+
[900, "CM"],
|
|
351
|
+
[500, "D"],
|
|
352
|
+
[400, "CD"],
|
|
353
|
+
[100, "C"],
|
|
354
|
+
[90, "XC"],
|
|
355
|
+
[50, "L"],
|
|
356
|
+
[40, "XL"],
|
|
357
|
+
[10, "X"],
|
|
358
|
+
[9, "IX"],
|
|
359
|
+
[5, "V"],
|
|
360
|
+
[4, "IV"],
|
|
361
|
+
[1, "I"]
|
|
362
|
+
];
|
|
363
|
+
let x = n;
|
|
364
|
+
let out = "";
|
|
365
|
+
for (const [v, sym] of table) {
|
|
366
|
+
while (x >= v) {
|
|
367
|
+
out += sym;
|
|
368
|
+
x -= v;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return out;
|
|
372
|
+
}
|
|
373
|
+
function formatCounter(n, fmt) {
|
|
374
|
+
switch (fmt) {
|
|
375
|
+
case "lowerLetter":
|
|
376
|
+
return toLetters(n).toLowerCase();
|
|
377
|
+
case "upperLetter":
|
|
378
|
+
return toLetters(n);
|
|
379
|
+
case "lowerRoman":
|
|
380
|
+
return toRoman(n).toLowerCase();
|
|
381
|
+
case "upperRoman":
|
|
382
|
+
return toRoman(n);
|
|
383
|
+
default:
|
|
384
|
+
return String(n);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
function createNumberingCounter(defs) {
|
|
388
|
+
const counters = /* @__PURE__ */ new Map();
|
|
389
|
+
function next(numId, level) {
|
|
390
|
+
const def = defs?.[numId];
|
|
391
|
+
if (!def) return "";
|
|
392
|
+
const startOf = (lvl) => def.levels[lvl]?.start ?? 1;
|
|
393
|
+
const arr = counters.get(def.key) ?? [];
|
|
394
|
+
arr[level] = (arr[level] ?? startOf(level) - 1) + 1;
|
|
395
|
+
for (let l = level + 1; l < arr.length; l++) arr[l] = void 0;
|
|
396
|
+
counters.set(def.key, arr);
|
|
397
|
+
const lvlDef = def.levels[level];
|
|
398
|
+
if (!lvlDef || lvlDef.numFmt === "none") return "";
|
|
399
|
+
if (lvlDef.numFmt === "bullet") return lvlDef.lvlText || "\u2022";
|
|
400
|
+
return lvlDef.lvlText.replace(/%(\d)/g, (_match, digit) => {
|
|
401
|
+
const lvl = Number(digit) - 1;
|
|
402
|
+
const value = arr[lvl] ?? startOf(lvl);
|
|
403
|
+
return formatCounter(value, def.levels[lvl]?.numFmt ?? "decimal");
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
return { next };
|
|
407
|
+
}
|
|
408
|
+
export {
|
|
409
|
+
commentSchema,
|
|
410
|
+
createNumberingCounter,
|
|
411
|
+
schema
|
|
412
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Schema } from 'prosemirror-model';
|
|
2
|
+
/**
|
|
3
|
+
* bapbong's ProseMirror document schema.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately minimal for M1 (DOCX import vertical slice): block paragraphs
|
|
6
|
+
* of inline text with the four common character toggles. Lists, tables,
|
|
7
|
+
* headings, images, etc. are added in later milestones — when they land they
|
|
8
|
+
* extend THIS schema so the importer, layout engine, and (canvas) painter all
|
|
9
|
+
* agree on one document model.
|
|
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">;
|
|
12
|
+
/** Concrete schema type, handy for typing Node/Mark across packages. */
|
|
13
|
+
export type BapbongSchema = typeof schema;
|
|
14
|
+
/**
|
|
15
|
+
* Minimal schema for composing a comment body (the comment sidebar's little
|
|
16
|
+
* editors). Rich enough for paragraphs of text plus an inline `mention` atom
|
|
17
|
+
* (@user). Comment bodies are stored as this schema's JSON on the comment
|
|
18
|
+
* thread, kept separate from the document schema.
|
|
19
|
+
*/
|
|
20
|
+
export declare const commentSchema: Schema<"doc" | "paragraph" | "text" | "mention", never>;
|
|
21
|
+
export type CommentSchema = typeof commentSchema;
|
|
22
|
+
/** Paragraph horizontal alignment (mirrors w:jc). */
|
|
23
|
+
export type Align = 'left' | 'center' | 'right' | 'justify';
|
|
24
|
+
/** Paragraph indentation in CSS px (mirrors w:ind). `firstLine` and `hanging`
|
|
25
|
+
* are mutually exclusive; if both are present, `hanging` takes precedence. */
|
|
26
|
+
export interface Indent {
|
|
27
|
+
left?: number;
|
|
28
|
+
right?: number;
|
|
29
|
+
firstLine?: number;
|
|
30
|
+
hanging?: number;
|
|
31
|
+
}
|
|
32
|
+
/** Shape of the paragraph node's attrs (for typed toDOM/serialization). */
|
|
33
|
+
export interface Spacing {
|
|
34
|
+
before?: number;
|
|
35
|
+
after?: number;
|
|
36
|
+
line?: number;
|
|
37
|
+
lineRule?: 'auto' | 'exact' | 'atLeast';
|
|
38
|
+
}
|
|
39
|
+
export interface ParagraphAttrs {
|
|
40
|
+
list: ListInfo | null;
|
|
41
|
+
align: Align | null;
|
|
42
|
+
indent: Indent | null;
|
|
43
|
+
spacing?: Spacing | null;
|
|
44
|
+
heading?: number | null;
|
|
45
|
+
}
|
|
46
|
+
/** Value of a list paragraph's `list` attribute. The marker string ("1.",
|
|
47
|
+
* "2.a", "•") is NOT stored — it's recomputed at layout time from the doc's
|
|
48
|
+
* numbering defs, so edits renumber live. `marker` remains only for legacy
|
|
49
|
+
* callers that pass pre-resolved markers straight to the layout engine. */
|
|
50
|
+
export interface ListInfo {
|
|
51
|
+
numId: string;
|
|
52
|
+
level: number;
|
|
53
|
+
marker?: string;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../src/lib/model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE3C;;;;;;;;GAQG;AACH,eAAO,MAAM,MAAM,2PAyRjB,CAAC;AAEH,wEAAwE;AACxE,MAAM,MAAM,aAAa,GAAG,OAAO,MAAM,CAAC;AAE1C;;;;;GAKG;AACH,eAAO,MAAM,aAAa,yDA+BxB,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;CACzB;AAyBD;;;4EAG4E;AAC5E,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live list numbering. The document stores WHAT a paragraph is (numId +
|
|
3
|
+
* level) and the numbering definitions (on the doc node's `numbering` attr);
|
|
4
|
+
* marker strings ("1.", "2.a.", "•") are recomputed by walking the document —
|
|
5
|
+
* the layout engine does this every pass, so inserting/deleting/reordering
|
|
6
|
+
* list items renumbers everything, like Word.
|
|
7
|
+
*/
|
|
8
|
+
/** One w:lvl of an abstract numbering definition (plain data — attr-safe). */
|
|
9
|
+
export interface NumberingLevelDef {
|
|
10
|
+
numFmt: string;
|
|
11
|
+
lvlText: string;
|
|
12
|
+
start: number;
|
|
13
|
+
}
|
|
14
|
+
/** Definitions keyed by numId. `key` groups numIds that share one abstract
|
|
15
|
+
* definition (their counters advance together, mirroring w:abstractNumId). */
|
|
16
|
+
export interface NumberingDefs {
|
|
17
|
+
[numId: string]: {
|
|
18
|
+
key: string;
|
|
19
|
+
levels: Record<number, NumberingLevelDef>;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** Stateful counter: call `next` once per list paragraph, in document order. */
|
|
23
|
+
export interface NumberingCounter {
|
|
24
|
+
next(numId: string, level: number): string;
|
|
25
|
+
}
|
|
26
|
+
export declare function createNumberingCounter(defs: NumberingDefs | null | undefined): NumberingCounter;
|
|
27
|
+
//# sourceMappingURL=numbering.d.ts.map
|
|
@@ -0,0 +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;AA8CD,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,GAAG,SAAS,GAAG,gBAAgB,CA2B/F"}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shadow-garden/bapbong-model",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "bapbong — ProseMirror document model / schema",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/shadowgarden-app/bapbong.git",
|
|
9
|
+
"directory": "packages/model"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./dist/index.cjs",
|
|
13
|
+
"module": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
"./package.json": "./package.json",
|
|
17
|
+
".": {
|
|
18
|
+
"@shadow-garden/source": "./src/index.ts",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js",
|
|
21
|
+
"require": "./dist/index.cjs",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"!**/*.tsbuildinfo"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"nx": {
|
|
33
|
+
"tags": [
|
|
34
|
+
"scope:model"
|
|
35
|
+
],
|
|
36
|
+
"targets": {
|
|
37
|
+
"build": {
|
|
38
|
+
"executor": "@nx/esbuild:esbuild",
|
|
39
|
+
"outputs": [
|
|
40
|
+
"{options.outputPath}"
|
|
41
|
+
],
|
|
42
|
+
"options": {
|
|
43
|
+
"outputPath": "packages/model/dist",
|
|
44
|
+
"main": "packages/model/src/index.ts",
|
|
45
|
+
"tsConfig": "packages/model/tsconfig.lib.json",
|
|
46
|
+
"format": [
|
|
47
|
+
"esm",
|
|
48
|
+
"cjs"
|
|
49
|
+
],
|
|
50
|
+
"declarationRootDir": "packages/model/src",
|
|
51
|
+
"generatePackageJson": false
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"prosemirror-model": "^1.25.7"
|
|
58
|
+
}
|
|
59
|
+
}
|