@abraca/convert 2.3.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/abracadabra-convert.cjs +3237 -0
- package/dist/abracadabra-convert.cjs.map +1 -0
- package/dist/abracadabra-convert.esm.js +3163 -0
- package/dist/abracadabra-convert.esm.js.map +1 -0
- package/dist/index.d.ts +356 -0
- package/package.json +41 -0
- package/src/diff.ts +302 -0
- package/src/file-blocks/manifest.ts +169 -0
- package/src/file-blocks/paths.ts +207 -0
- package/src/html-to-yjs.ts +322 -0
- package/src/index.ts +103 -0
- package/src/markdown-to-yjs.ts +1208 -0
- package/src/spec/index.ts +7 -0
- package/src/spec/marks.ts +92 -0
- package/src/spec/nodes.ts +333 -0
- package/src/spec/universal-meta.ts +147 -0
- package/src/types.ts +89 -0
- package/src/yjs-to-markdown.ts +820 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Spec registry — the executable mirror of SPEC.md. Parsers and
|
|
2
|
+
// serialisers consult these tables instead of hardcoding node/mark
|
|
3
|
+
// names. When the registry and SPEC.md disagree, the registry wins.
|
|
4
|
+
|
|
5
|
+
export * from './nodes.ts'
|
|
6
|
+
export * from './marks.ts'
|
|
7
|
+
export * from './universal-meta.ts'
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// MarkSpec registry — every inline mark supported by the converter,
|
|
2
|
+
// paired with its canonical Markdown wire form.
|
|
3
|
+
//
|
|
4
|
+
// Marks are pure inline annotations on text runs; they have no
|
|
5
|
+
// children. The parser sets these as `op.attributes` on Yjs delta
|
|
6
|
+
// inserts; the serialiser wraps text runs with the matching wire
|
|
7
|
+
// delimiters.
|
|
8
|
+
//
|
|
9
|
+
// See SPEC.md §3-4 for the rendered table and the disambiguation
|
|
10
|
+
// rules between bold (`**…**`) and underline (`__…__`).
|
|
11
|
+
|
|
12
|
+
export type MarkWireKind =
|
|
13
|
+
| 'delimited' // `**text**`, `*text*`, `~~text~~`, `` `text` ``
|
|
14
|
+
| 'link' // `[text](href)`
|
|
15
|
+
| 'mdc-span' // `:span[text]{prop=value}` for color/font/textStyle
|
|
16
|
+
|
|
17
|
+
export interface MarkAttrSpec {
|
|
18
|
+
key: string
|
|
19
|
+
type: 'string' | 'number' | 'boolean'
|
|
20
|
+
optional?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface MarkSpec {
|
|
24
|
+
name: string
|
|
25
|
+
wire: MarkWireKind
|
|
26
|
+
/** For `delimited`: the markdown delimiter (e.g. `**`, `*`, `~~`, `` ` ``, `__`, `==`). */
|
|
27
|
+
delim?: string
|
|
28
|
+
attrs?: readonly MarkAttrSpec[]
|
|
29
|
+
doc?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const MARK_SPECS: readonly MarkSpec[] = [
|
|
33
|
+
// Vanilla MD marks
|
|
34
|
+
{ name: 'bold', wire: 'delimited', delim: '**' },
|
|
35
|
+
{ name: 'italic', wire: 'delimited', delim: '*' },
|
|
36
|
+
{ name: 'strike', wire: 'delimited', delim: '~~' },
|
|
37
|
+
{ name: 'code', wire: 'delimited', delim: '`' },
|
|
38
|
+
{
|
|
39
|
+
name: 'link',
|
|
40
|
+
wire: 'link',
|
|
41
|
+
attrs: [
|
|
42
|
+
{ key: 'href', type: 'string' },
|
|
43
|
+
{ key: 'title', type: 'string', optional: true },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// Extended marks (lossless via MDC syntax)
|
|
48
|
+
{
|
|
49
|
+
name: 'underline',
|
|
50
|
+
wire: 'delimited',
|
|
51
|
+
delim: '__',
|
|
52
|
+
doc: 'Disambiguated from bold by delimiter character. Two underscores = underline; two asterisks = bold.',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'highlight',
|
|
56
|
+
wire: 'delimited',
|
|
57
|
+
delim: '==',
|
|
58
|
+
doc: 'Pandoc-style.',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'subscript',
|
|
62
|
+
wire: 'delimited',
|
|
63
|
+
delim: '~',
|
|
64
|
+
doc: 'Single tilde; double tilde is strike.',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'superscript',
|
|
68
|
+
wire: 'delimited',
|
|
69
|
+
delim: '^',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'textStyle',
|
|
73
|
+
wire: 'mdc-span',
|
|
74
|
+
attrs: [
|
|
75
|
+
{ key: 'color', type: 'string', optional: true },
|
|
76
|
+
{ key: 'backgroundColor', type: 'string', optional: true },
|
|
77
|
+
{ key: 'fontSize', type: 'string', optional: true },
|
|
78
|
+
{ key: 'fontFamily', type: 'string', optional: true },
|
|
79
|
+
],
|
|
80
|
+
doc: 'Wire form `:span[text]{color="…" font-size="…"}`. Any of the attrs may be set.',
|
|
81
|
+
},
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
export const MARK_SPEC_BY_NAME: ReadonlyMap<string, MarkSpec> = new Map(
|
|
85
|
+
MARK_SPECS.map(spec => [spec.name, spec]),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
export const MARK_SPEC_BY_DELIM: ReadonlyMap<string, MarkSpec> = new Map(
|
|
89
|
+
MARK_SPECS
|
|
90
|
+
.filter((spec): spec is MarkSpec & { delim: string } => spec.wire === 'delimited' && !!spec.delim)
|
|
91
|
+
.map(spec => [spec.delim, spec]),
|
|
92
|
+
)
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
// NodeSpec registry — every TipTap node supported by the converter,
|
|
2
|
+
// paired with its canonical Markdown / MDC wire form.
|
|
3
|
+
//
|
|
4
|
+
// This is **data**, not behaviour. The parser and serialiser
|
|
5
|
+
// (markdown-to-yjs.ts, yjs-to-markdown.ts) consult this table to
|
|
6
|
+
// decide what to do; when the spec and the runtime disagree the
|
|
7
|
+
// registry wins (per SPEC.md).
|
|
8
|
+
//
|
|
9
|
+
// Wire kinds:
|
|
10
|
+
// - vanilla Standard Markdown (paragraph, heading, list, …).
|
|
11
|
+
// - fence Fenced code block with a language tag.
|
|
12
|
+
// - mdc-container `:name{props}\n…children…\n::` block with children.
|
|
13
|
+
// - mdc-slotted `:name\n :name-item{…}\n…\n ::\n…\n::` slotted container.
|
|
14
|
+
// - mdc-atom-block `:name{props}` single-line block atom.
|
|
15
|
+
// - mdc-atom-inl `:name[label]{props}` or `:name{props}` inline atom.
|
|
16
|
+
// - special Has bespoke parsing/serialising (doc-link, doc-embed,
|
|
17
|
+
// file-block, image, mention, math).
|
|
18
|
+
|
|
19
|
+
import type { DocPageMeta } from '../types.ts'
|
|
20
|
+
|
|
21
|
+
export type NodeWireKind =
|
|
22
|
+
| 'vanilla'
|
|
23
|
+
| 'fence'
|
|
24
|
+
| 'mdc-container'
|
|
25
|
+
| 'mdc-slotted'
|
|
26
|
+
| 'mdc-atom-block'
|
|
27
|
+
| 'mdc-atom-inl'
|
|
28
|
+
| 'special'
|
|
29
|
+
|
|
30
|
+
export interface NodeAttrSpec {
|
|
31
|
+
/** Attribute name on the Y.XmlElement and on the MDC prop. */
|
|
32
|
+
key: string
|
|
33
|
+
/** Wire type — controls how the attr is parsed/serialised in props. */
|
|
34
|
+
type: 'string' | 'number' | 'integer' | 'boolean' | 'json'
|
|
35
|
+
/** Optional default value omitted from serialised output when equal. */
|
|
36
|
+
default?: unknown
|
|
37
|
+
/** Allowed values for enum-like string attrs. */
|
|
38
|
+
values?: readonly string[]
|
|
39
|
+
/** Whether the attr is allowed to be omitted entirely. */
|
|
40
|
+
optional?: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface NodeSpec {
|
|
44
|
+
/** Y.XmlElement nodeName. */
|
|
45
|
+
name: string
|
|
46
|
+
/** Block vs inline. */
|
|
47
|
+
group: 'block' | 'inline'
|
|
48
|
+
/** Wire form on Markdown. */
|
|
49
|
+
wire: NodeWireKind
|
|
50
|
+
/**
|
|
51
|
+
* For slotted containers: name of the child element used as a slot
|
|
52
|
+
* (e.g. `accordion-item` under `accordion`).
|
|
53
|
+
*/
|
|
54
|
+
slotChild?: string
|
|
55
|
+
/** Declared attributes. */
|
|
56
|
+
attrs?: readonly NodeAttrSpec[]
|
|
57
|
+
/**
|
|
58
|
+
* For `mdc-container` and `mdc-slotted`: what MDC tag name to emit.
|
|
59
|
+
* Defaults to `name` (kebab-cased — `accordionItem` → `accordion-item`).
|
|
60
|
+
*/
|
|
61
|
+
mdcTag?: string
|
|
62
|
+
/**
|
|
63
|
+
* Whether the node is content-bearing (paragraph, listItem, etc.)
|
|
64
|
+
* or a child wrapper (tableRow, tableHeader).
|
|
65
|
+
*/
|
|
66
|
+
contentBearing?: boolean
|
|
67
|
+
doc?: string
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
const BOOL_FALSE_DEFAULT: NodeAttrSpec = {
|
|
73
|
+
key: '',
|
|
74
|
+
type: 'boolean',
|
|
75
|
+
default: false,
|
|
76
|
+
optional: true,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const bool = (key: string): NodeAttrSpec => ({ ...BOOL_FALSE_DEFAULT, key })
|
|
80
|
+
const str = (key: string, def?: string): NodeAttrSpec => ({
|
|
81
|
+
key,
|
|
82
|
+
type: 'string',
|
|
83
|
+
default: def,
|
|
84
|
+
optional: true,
|
|
85
|
+
})
|
|
86
|
+
const num = (key: string): NodeAttrSpec => ({ key, type: 'number', optional: true })
|
|
87
|
+
const int = (key: string): NodeAttrSpec => ({ key, type: 'integer', optional: true })
|
|
88
|
+
|
|
89
|
+
// ── Vanilla block nodes ─────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
const VANILLA_BLOCKS: readonly NodeSpec[] = [
|
|
92
|
+
{ name: 'documentHeader', group: 'block', wire: 'special', doc: 'Holds the title; hoisted to frontmatter on serialise.' },
|
|
93
|
+
{ name: 'documentMeta', group: 'block', wire: 'special', doc: 'Holds page-level meta; serialised into frontmatter.' },
|
|
94
|
+
{ name: 'paragraph', group: 'block', wire: 'vanilla', contentBearing: true },
|
|
95
|
+
{ name: 'heading', group: 'block', wire: 'vanilla', attrs: [int('level')], contentBearing: true },
|
|
96
|
+
{ name: 'blockquote', group: 'block', wire: 'vanilla', contentBearing: true },
|
|
97
|
+
{ name: 'codeBlock', group: 'block', wire: 'fence', attrs: [str('language', '')] },
|
|
98
|
+
{ name: 'bulletList', group: 'block', wire: 'vanilla' },
|
|
99
|
+
{ name: 'orderedList', group: 'block', wire: 'vanilla' },
|
|
100
|
+
{ name: 'listItem', group: 'block', wire: 'vanilla', contentBearing: true },
|
|
101
|
+
{ name: 'taskList', group: 'block', wire: 'vanilla' },
|
|
102
|
+
{ name: 'taskItem', group: 'block', wire: 'vanilla', attrs: [bool('checked')], contentBearing: true },
|
|
103
|
+
{ name: 'table', group: 'block', wire: 'vanilla' },
|
|
104
|
+
{ name: 'tableRow', group: 'block', wire: 'vanilla' },
|
|
105
|
+
{ name: 'tableHeader', group: 'block', wire: 'vanilla', contentBearing: true },
|
|
106
|
+
{ name: 'tableCell', group: 'block', wire: 'vanilla', contentBearing: true },
|
|
107
|
+
{ name: 'horizontalRule', group: 'block', wire: 'vanilla' },
|
|
108
|
+
{
|
|
109
|
+
name: 'image',
|
|
110
|
+
group: 'block',
|
|
111
|
+
wire: 'special',
|
|
112
|
+
attrs: [str('src'), str('alt', ''), int('width'), int('height')],
|
|
113
|
+
},
|
|
114
|
+
{ name: 'hardBreak', group: 'inline', wire: 'vanilla' },
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
// ── MDC custom containers ───────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
const MDC_CONTAINERS: readonly NodeSpec[] = [
|
|
120
|
+
{
|
|
121
|
+
name: 'callout',
|
|
122
|
+
group: 'block',
|
|
123
|
+
wire: 'mdc-container',
|
|
124
|
+
attrs: [
|
|
125
|
+
{ key: 'type', type: 'string', default: 'note', optional: true,
|
|
126
|
+
values: ['note', 'tip', 'warning', 'danger', 'info', 'caution', 'alert', 'success', 'error'] },
|
|
127
|
+
str('title'),
|
|
128
|
+
str('icon'),
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'collapsible',
|
|
133
|
+
group: 'block',
|
|
134
|
+
wire: 'mdc-container',
|
|
135
|
+
attrs: [str('label', 'Details'), bool('open')],
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'accordion',
|
|
139
|
+
group: 'block',
|
|
140
|
+
wire: 'mdc-slotted',
|
|
141
|
+
slotChild: 'accordionItem',
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'accordionItem',
|
|
145
|
+
group: 'block',
|
|
146
|
+
wire: 'mdc-container',
|
|
147
|
+
mdcTag: 'accordion-item',
|
|
148
|
+
attrs: [str('label', 'Item'), str('icon')],
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: 'tabs',
|
|
152
|
+
group: 'block',
|
|
153
|
+
wire: 'mdc-slotted',
|
|
154
|
+
slotChild: 'tabsItem',
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: 'tabsItem',
|
|
158
|
+
group: 'block',
|
|
159
|
+
wire: 'mdc-container',
|
|
160
|
+
mdcTag: 'tabs-item',
|
|
161
|
+
attrs: [str('label'), str('icon')],
|
|
162
|
+
},
|
|
163
|
+
{ name: 'steps', group: 'block', wire: 'mdc-container' },
|
|
164
|
+
{
|
|
165
|
+
name: 'card',
|
|
166
|
+
group: 'block',
|
|
167
|
+
wire: 'mdc-container',
|
|
168
|
+
attrs: [str('title'), str('icon'), str('to')],
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: 'cardGroup',
|
|
172
|
+
group: 'block',
|
|
173
|
+
wire: 'mdc-slotted',
|
|
174
|
+
mdcTag: 'card-group',
|
|
175
|
+
slotChild: 'card',
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: 'field',
|
|
179
|
+
group: 'block',
|
|
180
|
+
wire: 'mdc-container',
|
|
181
|
+
attrs: [str('name'), str('type', 'string'), bool('required')],
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: 'fieldGroup',
|
|
185
|
+
group: 'block',
|
|
186
|
+
wire: 'mdc-slotted',
|
|
187
|
+
mdcTag: 'field-group',
|
|
188
|
+
slotChild: 'field',
|
|
189
|
+
},
|
|
190
|
+
{ name: 'codeGroup', group: 'block', wire: 'mdc-slotted', mdcTag: 'code-group', slotChild: 'codeBlock' },
|
|
191
|
+
{ name: 'codeCollapse', group: 'block', wire: 'mdc-container', mdcTag: 'code-collapse' },
|
|
192
|
+
{ name: 'codePreview', group: 'block', wire: 'mdc-container', mdcTag: 'code-preview' },
|
|
193
|
+
{
|
|
194
|
+
name: 'codeTree',
|
|
195
|
+
group: 'block',
|
|
196
|
+
wire: 'mdc-atom-block',
|
|
197
|
+
mdcTag: 'code-tree',
|
|
198
|
+
attrs: [{ key: 'files', type: 'json' }],
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: 'figure',
|
|
202
|
+
group: 'block',
|
|
203
|
+
wire: 'mdc-container',
|
|
204
|
+
attrs: [str('src'), str('alt', ''), str('caption')],
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: 'video',
|
|
208
|
+
group: 'block',
|
|
209
|
+
wire: 'mdc-atom-block',
|
|
210
|
+
attrs: [str('src'), str('poster'), bool('autoplay'), bool('loop'), bool('controls')],
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: 'embed',
|
|
214
|
+
group: 'block',
|
|
215
|
+
wire: 'mdc-atom-block',
|
|
216
|
+
attrs: [str('src'), str('title')],
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: 'svgEmbed',
|
|
220
|
+
group: 'block',
|
|
221
|
+
wire: 'fence',
|
|
222
|
+
attrs: [str('title')],
|
|
223
|
+
mdcTag: 'svg',
|
|
224
|
+
doc: 'Serialised as a ```svg fenced block; the SVG markup is the body.',
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: 'divider',
|
|
228
|
+
group: 'block',
|
|
229
|
+
wire: 'mdc-atom-block',
|
|
230
|
+
attrs: [str('label'), str('icon')],
|
|
231
|
+
},
|
|
232
|
+
{ name: 'quote', group: 'block', wire: 'mdc-container', attrs: [str('cite')] },
|
|
233
|
+
{ name: 'progress', group: 'block', wire: 'mdc-atom-block', attrs: [num('value'), num('max'), str('label')] },
|
|
234
|
+
{ name: 'spoiler', group: 'block', wire: 'mdc-container', attrs: [str('label')] },
|
|
235
|
+
{ name: 'colorSwatch', group: 'block', wire: 'mdc-atom-block', mdcTag: 'color-swatch', attrs: [str('color'), str('label')] },
|
|
236
|
+
{ name: 'stat', group: 'block', wire: 'mdc-container', attrs: [str('label'), str('value'), str('icon')] },
|
|
237
|
+
{ name: 'statGroup', group: 'block', wire: 'mdc-slotted', mdcTag: 'stat-group', slotChild: 'stat' },
|
|
238
|
+
{ name: 'button', group: 'block', wire: 'mdc-atom-block', attrs: [str('label'), str('to'), str('icon'), str('variant')] },
|
|
239
|
+
{ name: 'buttonGroup', group: 'block', wire: 'mdc-slotted', mdcTag: 'button-group', slotChild: 'button' },
|
|
240
|
+
{ name: 'timeline', group: 'block', wire: 'mdc-slotted', slotChild: 'timelineItem' },
|
|
241
|
+
{ name: 'timelineItem', group: 'block', wire: 'mdc-container', mdcTag: 'timeline-item', attrs: [str('label'), str('icon'), str('date')] },
|
|
242
|
+
{ name: 'diff', group: 'block', wire: 'mdc-atom-block', attrs: [str('language', ''), { key: 'value', type: 'string' }] },
|
|
243
|
+
]
|
|
244
|
+
|
|
245
|
+
// ── Inline atoms / special nodes ────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
const INLINE_AND_SPECIAL: readonly NodeSpec[] = [
|
|
248
|
+
{
|
|
249
|
+
name: 'docLink',
|
|
250
|
+
group: 'inline',
|
|
251
|
+
wire: 'special',
|
|
252
|
+
attrs: [str('docId')],
|
|
253
|
+
doc: 'Wire form `[[uuid|label]]`; label regenerated on export.',
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: 'docEmbed',
|
|
257
|
+
group: 'block',
|
|
258
|
+
wire: 'special',
|
|
259
|
+
attrs: [str('docId'), bool('collapsed'), bool('tall'), bool('seamless')],
|
|
260
|
+
doc: 'Wire form `![[uuid|label]]{collapsed tall seamless}`.',
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
name: 'mention',
|
|
264
|
+
group: 'inline',
|
|
265
|
+
wire: 'special',
|
|
266
|
+
attrs: [str('userId')],
|
|
267
|
+
doc: 'Wire form `@[label](user:uuid)`; label regenerated on export.',
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: 'mathInline',
|
|
271
|
+
group: 'inline',
|
|
272
|
+
wire: 'special',
|
|
273
|
+
attrs: [{ key: 'expression', type: 'string' }],
|
|
274
|
+
doc: 'Wire form `$expression$`.',
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: 'mathBlock',
|
|
278
|
+
group: 'block',
|
|
279
|
+
wire: 'fence',
|
|
280
|
+
attrs: [{ key: 'expression', type: 'string' }],
|
|
281
|
+
mdcTag: 'math',
|
|
282
|
+
doc: 'Wire form ``` ```math\\nexpression\\n``` ```.',
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
name: 'fileBlock',
|
|
286
|
+
group: 'block',
|
|
287
|
+
wire: 'mdc-atom-block',
|
|
288
|
+
mdcTag: 'file',
|
|
289
|
+
attrs: [str('src'), str('mime'), str('uploadId'), str('filename')],
|
|
290
|
+
doc: 'Wire form `:file{src=… mime=… upload-id=… filename=…}`; binary in sidecar.',
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
name: 'badge',
|
|
294
|
+
group: 'inline',
|
|
295
|
+
wire: 'mdc-atom-inl',
|
|
296
|
+
attrs: [str('label'), str('color'), str('variant', 'subtle')],
|
|
297
|
+
doc: 'Wire form `:badge[Label]{color=… variant=…}`.',
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: 'proseIcon',
|
|
301
|
+
group: 'inline',
|
|
302
|
+
wire: 'mdc-atom-inl',
|
|
303
|
+
mdcTag: 'icon',
|
|
304
|
+
attrs: [str('name')],
|
|
305
|
+
doc: 'Wire form `:icon{name=…}`.',
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
name: 'kbd',
|
|
309
|
+
group: 'inline',
|
|
310
|
+
wire: 'mdc-atom-inl',
|
|
311
|
+
attrs: [str('value')],
|
|
312
|
+
doc: 'Wire form `:kbd{value=…}`.',
|
|
313
|
+
},
|
|
314
|
+
]
|
|
315
|
+
|
|
316
|
+
export const NODE_SPECS: readonly NodeSpec[] = [
|
|
317
|
+
...VANILLA_BLOCKS,
|
|
318
|
+
...MDC_CONTAINERS,
|
|
319
|
+
...INLINE_AND_SPECIAL,
|
|
320
|
+
]
|
|
321
|
+
|
|
322
|
+
export const NODE_SPEC_BY_NAME: ReadonlyMap<string, NodeSpec> = new Map(
|
|
323
|
+
NODE_SPECS.map(spec => [spec.name, spec]),
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
/** Derive the MDC tag for a node — `mdcTag` override or kebab-case of `name`. */
|
|
327
|
+
export function mdcTagOf(spec: NodeSpec): string {
|
|
328
|
+
if (spec.mdcTag) return spec.mdcTag
|
|
329
|
+
return spec.name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Type-only re-export so consumers can build typed meta without a second import. */
|
|
333
|
+
export type { DocPageMeta }
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Universal-meta registry.
|
|
2
|
+
//
|
|
3
|
+
// Mirrors `@abraca/schema/src/types/universal.ts` — the canonical
|
|
4
|
+
// source. We don't import @abraca/schema here to keep this package
|
|
5
|
+
// dependency-free, but the keys MUST stay in sync. A future codegen
|
|
6
|
+
// step in @abraca/schema can verify this file is up to date.
|
|
7
|
+
//
|
|
8
|
+
// Order is significant — it's the canonical key order in serialised
|
|
9
|
+
// YAML frontmatter (see SPEC.md §2). Adding new keys appends to the
|
|
10
|
+
// end so existing fixtures don't churn.
|
|
11
|
+
|
|
12
|
+
export type MetaValueType =
|
|
13
|
+
| 'string'
|
|
14
|
+
| 'number'
|
|
15
|
+
| 'integer'
|
|
16
|
+
| 'boolean'
|
|
17
|
+
| 'string[]'
|
|
18
|
+
| 'string-enum'
|
|
19
|
+
| 'iso-date'
|
|
20
|
+
| 'iso-datetime'
|
|
21
|
+
| 'hh-mm'
|
|
22
|
+
| 'members'
|
|
23
|
+
| 'json'
|
|
24
|
+
|
|
25
|
+
export interface UniversalMetaKey {
|
|
26
|
+
key: string
|
|
27
|
+
type: MetaValueType
|
|
28
|
+
/** Allowed values for `string-enum`. */
|
|
29
|
+
values?: readonly string[]
|
|
30
|
+
/** Inclusive minimum for `number` / `integer`. */
|
|
31
|
+
min?: number
|
|
32
|
+
/** Inclusive maximum for `number` / `integer`. */
|
|
33
|
+
max?: number
|
|
34
|
+
/**
|
|
35
|
+
* Alternate YAML key names recognised on parse — for backwards
|
|
36
|
+
* compatibility with hand-written frontmatter (e.g. `date` → `dateStart`).
|
|
37
|
+
* Serialise always uses the canonical `key`.
|
|
38
|
+
*/
|
|
39
|
+
parseAliases?: readonly string[]
|
|
40
|
+
/** Human-readable hint, surfaced in documentation. */
|
|
41
|
+
doc?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const UNIVERSAL_META_KEYS: readonly UniversalMetaKey[] = [
|
|
45
|
+
// Identity / display
|
|
46
|
+
{ key: 'title', type: 'string', doc: 'Display title; the first H1 is hoisted into this field on import.' },
|
|
47
|
+
{ key: 'type', type: 'string', doc: 'Page type (doc, kanban, table, …). Omitted on serialise when "doc".' },
|
|
48
|
+
{ key: 'color', type: 'string', doc: 'Hex or CSS color name.' },
|
|
49
|
+
{ key: 'icon', type: 'string', doc: 'Lucide icon name in kebab-case.' },
|
|
50
|
+
|
|
51
|
+
// Datetime
|
|
52
|
+
{ key: 'datetimeStart', type: 'iso-datetime' },
|
|
53
|
+
{ key: 'datetimeEnd', type: 'iso-datetime' },
|
|
54
|
+
{ key: 'allDay', type: 'boolean' },
|
|
55
|
+
{ key: 'dateTaken', type: 'iso-datetime' },
|
|
56
|
+
{ key: 'dateStart', type: 'iso-date', parseAliases: ['date', 'created'] },
|
|
57
|
+
{ key: 'dateEnd', type: 'iso-date', parseAliases: ['due'] },
|
|
58
|
+
{ key: 'timeStart', type: 'hh-mm' },
|
|
59
|
+
{ key: 'timeEnd', type: 'hh-mm' },
|
|
60
|
+
|
|
61
|
+
// Generic
|
|
62
|
+
{ key: 'tags', type: 'string[]' },
|
|
63
|
+
{ key: 'checked', type: 'boolean', parseAliases: ['done'] },
|
|
64
|
+
{
|
|
65
|
+
key: 'priority',
|
|
66
|
+
type: 'integer',
|
|
67
|
+
min: 0,
|
|
68
|
+
max: 4,
|
|
69
|
+
doc: 'Numeric or named (low/medium/high/urgent → 1/2/3/4).',
|
|
70
|
+
},
|
|
71
|
+
{ key: 'status', type: 'string' },
|
|
72
|
+
{ key: 'rating', type: 'number', min: 0, max: 5 },
|
|
73
|
+
{ key: 'url', type: 'string' },
|
|
74
|
+
{ key: 'email', type: 'string' },
|
|
75
|
+
{ key: 'phone', type: 'string' },
|
|
76
|
+
{ key: 'number', type: 'number' },
|
|
77
|
+
{ key: 'unit', type: 'string' },
|
|
78
|
+
{ key: 'subtitle', type: 'string', parseAliases: ['description'] },
|
|
79
|
+
{ key: 'note', type: 'string' },
|
|
80
|
+
{ key: 'taskProgress', type: 'integer', min: 0, max: 100 },
|
|
81
|
+
{ key: 'members', type: 'members' },
|
|
82
|
+
|
|
83
|
+
// Cover
|
|
84
|
+
{ key: 'coverUploadId', type: 'string' },
|
|
85
|
+
{ key: 'coverDocId', type: 'string' },
|
|
86
|
+
{ key: 'coverMimeType', type: 'string' },
|
|
87
|
+
|
|
88
|
+
// Geo / map
|
|
89
|
+
{ key: 'geoType', type: 'string-enum', values: ['marker', 'line', 'measure'] },
|
|
90
|
+
{ key: 'geoLat', type: 'number' },
|
|
91
|
+
{ key: 'geoLng', type: 'number' },
|
|
92
|
+
{ key: 'geoDescription', type: 'string' },
|
|
93
|
+
|
|
94
|
+
// Dashboard / mindmap / graph layout
|
|
95
|
+
{ key: 'deskX', type: 'number' },
|
|
96
|
+
{ key: 'deskY', type: 'number' },
|
|
97
|
+
{ key: 'deskZ', type: 'number' },
|
|
98
|
+
{ key: 'deskMode', type: 'string-enum', values: ['icon', 'widget-sm', 'widget-lg'] },
|
|
99
|
+
{ key: 'mmX', type: 'number' },
|
|
100
|
+
{ key: 'mmY', type: 'number' },
|
|
101
|
+
{ key: 'graphX', type: 'number' },
|
|
102
|
+
{ key: 'graphY', type: 'number' },
|
|
103
|
+
{ key: 'graphPinned', type: 'boolean' },
|
|
104
|
+
|
|
105
|
+
// Spatial
|
|
106
|
+
{ key: 'spX', type: 'number' },
|
|
107
|
+
{ key: 'spY', type: 'number' },
|
|
108
|
+
{ key: 'spZ', type: 'number' },
|
|
109
|
+
{ key: 'spRX', type: 'number' },
|
|
110
|
+
{ key: 'spRY', type: 'number' },
|
|
111
|
+
{ key: 'spRZ', type: 'number' },
|
|
112
|
+
{ key: 'spSX', type: 'number' },
|
|
113
|
+
{ key: 'spSY', type: 'number' },
|
|
114
|
+
{ key: 'spSZ', type: 'number' },
|
|
115
|
+
{
|
|
116
|
+
key: 'spShape',
|
|
117
|
+
type: 'string-enum',
|
|
118
|
+
values: ['box', 'sphere', 'cylinder', 'cone', 'plane', 'torus', 'glb'],
|
|
119
|
+
},
|
|
120
|
+
{ key: 'spOpacity', type: 'integer', min: 0, max: 100 },
|
|
121
|
+
{ key: 'spModelUploadId', type: 'string' },
|
|
122
|
+
{ key: 'spModelDocId', type: 'string' },
|
|
123
|
+
|
|
124
|
+
// Slides
|
|
125
|
+
{ key: 'slidesTransition', type: 'string-enum', values: ['none', 'fade', 'slide'] },
|
|
126
|
+
{ key: 'slidesTheme', type: 'string-enum', values: ['dark', 'light'] },
|
|
127
|
+
|
|
128
|
+
// Migration version (last so unknown keys ahead of it surface clearly)
|
|
129
|
+
{ key: '__schemaVersion', type: 'integer', min: 0 },
|
|
130
|
+
] as const
|
|
131
|
+
|
|
132
|
+
export const UNIVERSAL_META_KEY_NAMES: ReadonlySet<string> = new Set(
|
|
133
|
+
UNIVERSAL_META_KEYS.map(k => k.key),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Build a map of every recognised input key (canonical + aliases) to
|
|
138
|
+
* its canonical key. Used by the frontmatter parser.
|
|
139
|
+
*/
|
|
140
|
+
export function buildAliasMap(): ReadonlyMap<string, string> {
|
|
141
|
+
const map = new Map<string, string>()
|
|
142
|
+
for (const entry of UNIVERSAL_META_KEYS) {
|
|
143
|
+
map.set(entry.key, entry.key)
|
|
144
|
+
for (const alias of entry.parseAliases ?? []) map.set(alias, entry.key)
|
|
145
|
+
}
|
|
146
|
+
return map
|
|
147
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Shared types for the markdown ↔ Y.js converter.
|
|
2
|
+
//
|
|
3
|
+
// `DocPageMeta` is the metadata bag this package reads/writes in YAML
|
|
4
|
+
// frontmatter. It mirrors the universal-meta surface in
|
|
5
|
+
// `@abraca/schema/types/universal.ts` — the schema package is the
|
|
6
|
+
// source of truth; this declaration stays minimal and additive so the
|
|
7
|
+
// converter can be used standalone without pulling in schema.
|
|
8
|
+
//
|
|
9
|
+
// Universal fields cover every page-type; renderer-specific fields are
|
|
10
|
+
// included so round-trip works for kanban cards, calendar events,
|
|
11
|
+
// timeline tasks, spatial nodes, etc.
|
|
12
|
+
|
|
13
|
+
export interface DocPageMeta extends Record<string, unknown> {
|
|
14
|
+
// Universal color + icon
|
|
15
|
+
color?: string
|
|
16
|
+
icon?: string
|
|
17
|
+
|
|
18
|
+
// Datetime / date / time fields
|
|
19
|
+
datetimeStart?: string
|
|
20
|
+
datetimeEnd?: string
|
|
21
|
+
allDay?: boolean
|
|
22
|
+
dateTaken?: string
|
|
23
|
+
dateStart?: string
|
|
24
|
+
dateEnd?: string
|
|
25
|
+
timeStart?: string
|
|
26
|
+
timeEnd?: string
|
|
27
|
+
|
|
28
|
+
// Progress / member references
|
|
29
|
+
taskProgress?: number
|
|
30
|
+
members?: { id: string, label: string }[]
|
|
31
|
+
|
|
32
|
+
// Generic fields
|
|
33
|
+
tags?: string[]
|
|
34
|
+
checked?: boolean
|
|
35
|
+
priority?: number
|
|
36
|
+
status?: string
|
|
37
|
+
rating?: number
|
|
38
|
+
url?: string
|
|
39
|
+
email?: string
|
|
40
|
+
phone?: string
|
|
41
|
+
number?: number
|
|
42
|
+
unit?: string
|
|
43
|
+
subtitle?: string
|
|
44
|
+
note?: string
|
|
45
|
+
|
|
46
|
+
// Gallery / cover
|
|
47
|
+
coverUploadId?: string
|
|
48
|
+
coverDocId?: string
|
|
49
|
+
coverMimeType?: string
|
|
50
|
+
|
|
51
|
+
// Map / geo
|
|
52
|
+
geoType?: 'marker' | 'line' | 'measure'
|
|
53
|
+
geoLat?: number
|
|
54
|
+
geoLng?: number
|
|
55
|
+
geoDescription?: string
|
|
56
|
+
|
|
57
|
+
// Dashboard / mindmap / graph
|
|
58
|
+
deskX?: number
|
|
59
|
+
deskY?: number
|
|
60
|
+
deskZ?: number
|
|
61
|
+
deskMode?: string
|
|
62
|
+
mmX?: number
|
|
63
|
+
mmY?: number
|
|
64
|
+
graphX?: number
|
|
65
|
+
graphY?: number
|
|
66
|
+
graphPinned?: boolean
|
|
67
|
+
|
|
68
|
+
// Spatial
|
|
69
|
+
spX?: number
|
|
70
|
+
spY?: number
|
|
71
|
+
spZ?: number
|
|
72
|
+
spRX?: number
|
|
73
|
+
spRY?: number
|
|
74
|
+
spRZ?: number
|
|
75
|
+
spSX?: number
|
|
76
|
+
spSY?: number
|
|
77
|
+
spSZ?: number
|
|
78
|
+
spShape?: string
|
|
79
|
+
spOpacity?: number
|
|
80
|
+
spModelUploadId?: string
|
|
81
|
+
spModelDocId?: string
|
|
82
|
+
|
|
83
|
+
// Slides
|
|
84
|
+
slidesTransition?: 'none' | 'fade' | 'slide'
|
|
85
|
+
slidesTheme?: 'dark' | 'light'
|
|
86
|
+
|
|
87
|
+
// Schema migration version
|
|
88
|
+
__schemaVersion?: number
|
|
89
|
+
}
|