@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.
@@ -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
+ }