@bendyline/squisq 0.1.1
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/Types-sh2VRxfo.d.ts +15 -0
- package/dist/chunk-7UDSRZKG.js +103 -0
- package/dist/chunk-7UDSRZKG.js.map +1 -0
- package/dist/chunk-O5HCIUAD.js +577 -0
- package/dist/chunk-O5HCIUAD.js.map +1 -0
- package/dist/chunk-PJ7AODIM.js +3040 -0
- package/dist/chunk-PJ7AODIM.js.map +1 -0
- package/dist/chunk-QWVRN6I4.js +81 -0
- package/dist/chunk-QWVRN6I4.js.map +1 -0
- package/dist/chunk-S4F2RY37.js +92 -0
- package/dist/chunk-S4F2RY37.js.map +1 -0
- package/dist/chunk-URU6QMLY.js +888 -0
- package/dist/chunk-URU6QMLY.js.map +1 -0
- package/dist/chunk-VJN7UB2Z.js +145 -0
- package/dist/chunk-VJN7UB2Z.js.map +1 -0
- package/dist/doc/index.d.ts +3 -0
- package/dist/doc/index.js +126 -0
- package/dist/doc/index.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +217 -0
- package/dist/index.js.map +1 -0
- package/dist/markdown/index.d.ts +237 -0
- package/dist/markdown/index.js +33 -0
- package/dist/markdown/index.js.map +1 -0
- package/dist/schemas/index.d.ts +70 -0
- package/dist/schemas/index.js +61 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/spatial/index.d.ts +111 -0
- package/dist/spatial/index.js +25 -0
- package/dist/spatial/index.js.map +1 -0
- package/dist/storage/index.d.ts +91 -0
- package/dist/storage/index.js +11 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/story/index.d.ts +691 -0
- package/dist/story/index.js +126 -0
- package/dist/story/index.js.map +1 -0
- package/dist/themeLibrary-DySHPcgj.d.ts +1352 -0
- package/dist/types-DHiv_Pnm.d.ts +496 -0
- package/package.json +91 -0
- package/src/__tests__/animationUtils.test.ts +77 -0
- package/src/__tests__/geohash.test.ts +109 -0
- package/src/__tests__/getLayers.test.ts +200 -0
- package/src/__tests__/haversine.test.ts +63 -0
- package/src/__tests__/localForageAdapter.test.ts +155 -0
- package/src/__tests__/markdown.test.ts +806 -0
- package/src/__tests__/markdownToDoc.test.ts +374 -0
- package/src/__tests__/storage.test.ts +60 -0
- package/src/__tests__/templates.test.ts +247 -0
- package/src/doc/docToMarkdown.ts +98 -0
- package/src/doc/getLayers.ts +152 -0
- package/src/doc/index.ts +8 -0
- package/src/doc/markdownToDoc.ts +277 -0
- package/src/doc/templates/accentImage.ts +338 -0
- package/src/doc/templates/captionUtils.ts +33 -0
- package/src/doc/templates/comparisonBar.ts +140 -0
- package/src/doc/templates/coverBlock.ts +156 -0
- package/src/doc/templates/dateEvent.ts +122 -0
- package/src/doc/templates/definitionCard.ts +129 -0
- package/src/doc/templates/factCard.ts +114 -0
- package/src/doc/templates/fullBleedQuote.ts +59 -0
- package/src/doc/templates/imageWithCaption.ts +167 -0
- package/src/doc/templates/index.ts +571 -0
- package/src/doc/templates/listBlock.ts +101 -0
- package/src/doc/templates/mapBlock.ts +132 -0
- package/src/doc/templates/persistentLayers.ts +522 -0
- package/src/doc/templates/photoGrid.ts +199 -0
- package/src/doc/templates/pullQuote.ts +119 -0
- package/src/doc/templates/quoteBlock.ts +112 -0
- package/src/doc/templates/sectionHeader.ts +105 -0
- package/src/doc/templates/statHighlight.ts +113 -0
- package/src/doc/templates/titleBlock.ts +100 -0
- package/src/doc/templates/twoColumn.ts +239 -0
- package/src/doc/templates/videoPullQuote.ts +128 -0
- package/src/doc/templates/videoWithCaption.ts +126 -0
- package/src/doc/utils/animationUtils.ts +135 -0
- package/src/doc/utils/themeUtils.ts +139 -0
- package/src/index.ts +5 -0
- package/src/markdown/convert.ts +897 -0
- package/src/markdown/htmlParse.ts +212 -0
- package/src/markdown/index.ts +110 -0
- package/src/markdown/parse.ts +103 -0
- package/src/markdown/stringify.ts +122 -0
- package/src/markdown/types.ts +605 -0
- package/src/markdown/utils.ts +165 -0
- package/src/ngeohash.d.ts +11 -0
- package/src/schemas/BlockTemplates.ts +706 -0
- package/src/schemas/Doc.ts +565 -0
- package/src/schemas/LayoutStrategy.ts +178 -0
- package/src/schemas/MediaProvider.ts +70 -0
- package/src/schemas/Theme.ts +235 -0
- package/src/schemas/Types.ts +15 -0
- package/src/schemas/Viewport.ts +91 -0
- package/src/schemas/index.ts +14 -0
- package/src/schemas/themeLibrary.ts +492 -0
- package/src/spatial/Geohash.ts +168 -0
- package/src/spatial/Haversine.ts +59 -0
- package/src/spatial/index.ts +2 -0
- package/src/storage/LocalForageAdapter.ts +103 -0
- package/src/storage/LocalStorageAdapter.ts +57 -0
- package/src/storage/MemoryStorageAdapter.ts +37 -0
- package/src/storage/Storage.ts +28 -0
- package/src/storage/index.ts +5 -0
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mdast ↔ Squisq Markdown Node Conversion
|
|
3
|
+
*
|
|
4
|
+
* Converts between the remark/mdast AST format and the squisq
|
|
5
|
+
* MarkdownDocument JSON format. This is the bridge layer that allows
|
|
6
|
+
* us to use the battle-tested unified/remark ecosystem for parsing
|
|
7
|
+
* and serialization while exposing our own clean, well-typed interfaces.
|
|
8
|
+
*
|
|
9
|
+
* The conversion is designed for perfect round-tripping:
|
|
10
|
+
* toMdast(fromMdast(tree)) should produce an equivalent mdast tree.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
MarkdownDocument,
|
|
15
|
+
MarkdownBlockNode,
|
|
16
|
+
MarkdownInlineNode,
|
|
17
|
+
MarkdownListItem,
|
|
18
|
+
MarkdownTableRow,
|
|
19
|
+
MarkdownTableCell,
|
|
20
|
+
MarkdownSourcePosition,
|
|
21
|
+
HeadingTemplateAnnotation,
|
|
22
|
+
MarkdownHeading,
|
|
23
|
+
} from './types.js';
|
|
24
|
+
import { parseHtmlToNodes } from './htmlParse.js';
|
|
25
|
+
|
|
26
|
+
// ============================================
|
|
27
|
+
// Generic mdast node shape
|
|
28
|
+
// ============================================
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Minimal interface for mdast/unist nodes.
|
|
32
|
+
* Using a generic shape avoids hard dependency on @types/mdast
|
|
33
|
+
* and handles extension nodes (GFM, math, directive) uniformly.
|
|
34
|
+
*/
|
|
35
|
+
interface MdastPosition {
|
|
36
|
+
start: { line: number; column: number; offset?: number };
|
|
37
|
+
end: { line: number; column: number; offset?: number };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface MdastNode {
|
|
41
|
+
type: string;
|
|
42
|
+
position?: MdastPosition;
|
|
43
|
+
children?: MdastNode[];
|
|
44
|
+
value?: string;
|
|
45
|
+
depth?: number;
|
|
46
|
+
ordered?: boolean | null;
|
|
47
|
+
start?: number | null;
|
|
48
|
+
spread?: boolean | null;
|
|
49
|
+
checked?: boolean | null;
|
|
50
|
+
lang?: string | null;
|
|
51
|
+
meta?: string | null;
|
|
52
|
+
url?: string;
|
|
53
|
+
title?: string | null;
|
|
54
|
+
alt?: string;
|
|
55
|
+
identifier?: string;
|
|
56
|
+
label?: string;
|
|
57
|
+
referenceType?: string;
|
|
58
|
+
align?: (string | null)[];
|
|
59
|
+
name?: string;
|
|
60
|
+
attributes?: Record<string, string>;
|
|
61
|
+
data?: Record<string, unknown>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ============================================
|
|
65
|
+
// Position conversion
|
|
66
|
+
// ============================================
|
|
67
|
+
|
|
68
|
+
function convertPosition(pos?: MdastPosition): MarkdownSourcePosition | undefined {
|
|
69
|
+
if (!pos) return undefined;
|
|
70
|
+
return {
|
|
71
|
+
start: {
|
|
72
|
+
line: pos.start.line,
|
|
73
|
+
column: pos.start.column,
|
|
74
|
+
...(pos.start.offset != null ? { offset: pos.start.offset } : {}),
|
|
75
|
+
},
|
|
76
|
+
end: {
|
|
77
|
+
line: pos.end.line,
|
|
78
|
+
column: pos.end.column,
|
|
79
|
+
...(pos.end.offset != null ? { offset: pos.end.offset } : {}),
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function toMdastPosition(pos: MarkdownSourcePosition): MdastPosition {
|
|
85
|
+
return {
|
|
86
|
+
start: {
|
|
87
|
+
line: pos.start.line,
|
|
88
|
+
column: pos.start.column,
|
|
89
|
+
...(pos.start.offset != null ? { offset: pos.start.offset } : {}),
|
|
90
|
+
},
|
|
91
|
+
end: {
|
|
92
|
+
line: pos.end.line,
|
|
93
|
+
column: pos.end.column,
|
|
94
|
+
...(pos.end.offset != null ? { offset: pos.end.offset } : {}),
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Conditionally include position in node. */
|
|
100
|
+
function posField(
|
|
101
|
+
pos?: MarkdownSourcePosition,
|
|
102
|
+
): { position: MarkdownSourcePosition } | Record<string, never> {
|
|
103
|
+
return pos ? { position: pos } : {};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function mdastPosField(
|
|
107
|
+
pos?: MarkdownSourcePosition,
|
|
108
|
+
): { position: MdastPosition } | Record<string, never> {
|
|
109
|
+
return pos ? { position: toMdastPosition(pos) } : {};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ============================================
|
|
113
|
+
// mdast → Squisq (fromMdast)
|
|
114
|
+
// ============================================
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Extract plain text content from an mdast node tree.
|
|
118
|
+
* Used for directive labels.
|
|
119
|
+
*/
|
|
120
|
+
function extractText(node: MdastNode): string {
|
|
121
|
+
if (node.value != null) return node.value;
|
|
122
|
+
if (node.children) {
|
|
123
|
+
return node.children.map(extractText).join('');
|
|
124
|
+
}
|
|
125
|
+
return '';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ============================================
|
|
129
|
+
// Template annotation helpers
|
|
130
|
+
// ============================================
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Regex matching a trailing `{[templateName key=value …]}` annotation.
|
|
134
|
+
* Captures the content between `{[` and `]}`.
|
|
135
|
+
*/
|
|
136
|
+
const TEMPLATE_ANNOTATION_RE = /\s*\{\[([^\]]+)\]\}\s*$/;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Extract a `{[templateName key=value …]}` annotation from a heading's
|
|
140
|
+
* inline children. Mutates the children array in-place: strips the
|
|
141
|
+
* annotation text from the last text node (or removes the node entirely).
|
|
142
|
+
*
|
|
143
|
+
* @returns The parsed annotation, or null if none found.
|
|
144
|
+
*/
|
|
145
|
+
function extractTemplateAnnotation(
|
|
146
|
+
children: MarkdownInlineNode[],
|
|
147
|
+
): HeadingTemplateAnnotation | null {
|
|
148
|
+
// Walk backwards to find the last text node
|
|
149
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
150
|
+
const child = children[i];
|
|
151
|
+
if (child.type === 'text') {
|
|
152
|
+
const match = child.value.match(TEMPLATE_ANNOTATION_RE);
|
|
153
|
+
if (match) {
|
|
154
|
+
const inner = match[1].trim();
|
|
155
|
+
const annotation = parseAnnotationTokens(inner);
|
|
156
|
+
|
|
157
|
+
// Strip the matched portion from the text
|
|
158
|
+
const stripped = child.value.slice(0, match.index!).replace(/\s+$/, '');
|
|
159
|
+
if (stripped) {
|
|
160
|
+
(child as { value: string }).value = stripped;
|
|
161
|
+
} else {
|
|
162
|
+
// Remove the now-empty text node
|
|
163
|
+
children.splice(i, 1);
|
|
164
|
+
}
|
|
165
|
+
return annotation;
|
|
166
|
+
}
|
|
167
|
+
// Only check the last text node (trailing position)
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
// If the last child isn't a text node, there's no annotation
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Parse the inner content of a `{[…]}` annotation into template + params.
|
|
178
|
+
*
|
|
179
|
+
* Input: `"chart colorScheme=blue size=large"`
|
|
180
|
+
* Output: `{ template: 'chart', params: { colorScheme: 'blue', size: 'large' } }`
|
|
181
|
+
*/
|
|
182
|
+
function parseAnnotationTokens(inner: string): HeadingTemplateAnnotation {
|
|
183
|
+
const tokens = inner.split(/\s+/);
|
|
184
|
+
const template = tokens[0];
|
|
185
|
+
const params: Record<string, string> = {};
|
|
186
|
+
|
|
187
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
188
|
+
const eqIdx = tokens[i].indexOf('=');
|
|
189
|
+
if (eqIdx > 0) {
|
|
190
|
+
params[tokens[i].slice(0, eqIdx)] = tokens[i].slice(eqIdx + 1);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const result: HeadingTemplateAnnotation = { template };
|
|
195
|
+
if (Object.keys(params).length > 0) {
|
|
196
|
+
result.params = params;
|
|
197
|
+
}
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Serialize a HeadingTemplateAnnotation back to `{[templateName key=value …]}` text.
|
|
203
|
+
*/
|
|
204
|
+
function serializeTemplateAnnotation(annotation: HeadingTemplateAnnotation): string {
|
|
205
|
+
let result = `{[${annotation.template}`;
|
|
206
|
+
if (annotation.params) {
|
|
207
|
+
for (const [key, value] of Object.entries(annotation.params)) {
|
|
208
|
+
result += ` ${key}=${value}`;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
result += ']}';
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Convert an mdast Root node to a MarkdownDocument.
|
|
217
|
+
*
|
|
218
|
+
* @param root - The mdast root node (from remark-parse)
|
|
219
|
+
* @param options - Conversion options
|
|
220
|
+
* @returns A MarkdownDocument
|
|
221
|
+
*/
|
|
222
|
+
export function fromMdast(root: MdastNode, options?: { parseHtml?: boolean }): MarkdownDocument {
|
|
223
|
+
const doParseHtml = options?.parseHtml !== false;
|
|
224
|
+
return {
|
|
225
|
+
type: 'document',
|
|
226
|
+
children: convertBlockChildren(root.children ?? [], doParseHtml),
|
|
227
|
+
...posField(convertPosition(root.position)),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function convertBlockChildren(children: MdastNode[], parseHtml: boolean): MarkdownBlockNode[] {
|
|
232
|
+
const result: MarkdownBlockNode[] = [];
|
|
233
|
+
for (const child of children) {
|
|
234
|
+
const node = convertBlockNode(child, parseHtml);
|
|
235
|
+
if (node) result.push(node);
|
|
236
|
+
}
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function convertInlineChildren(children: MdastNode[], parseHtml: boolean): MarkdownInlineNode[] {
|
|
241
|
+
const result: MarkdownInlineNode[] = [];
|
|
242
|
+
for (const child of children) {
|
|
243
|
+
const node = convertInlineNode(child, parseHtml);
|
|
244
|
+
if (node) result.push(node);
|
|
245
|
+
}
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function convertBlockNode(node: MdastNode, parseHtml: boolean): MarkdownBlockNode | null {
|
|
250
|
+
const pos = convertPosition(node.position);
|
|
251
|
+
|
|
252
|
+
switch (node.type) {
|
|
253
|
+
case 'heading': {
|
|
254
|
+
const headingChildren = convertInlineChildren(node.children ?? [], parseHtml);
|
|
255
|
+
const annotation = extractTemplateAnnotation(headingChildren);
|
|
256
|
+
const result: MarkdownHeading = {
|
|
257
|
+
type: 'heading',
|
|
258
|
+
depth: (node.depth ?? 1) as 1 | 2 | 3 | 4 | 5 | 6,
|
|
259
|
+
children: headingChildren,
|
|
260
|
+
...posField(pos),
|
|
261
|
+
};
|
|
262
|
+
if (annotation) {
|
|
263
|
+
result.templateAnnotation = annotation;
|
|
264
|
+
}
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
case 'paragraph':
|
|
269
|
+
return {
|
|
270
|
+
type: 'paragraph',
|
|
271
|
+
children: convertInlineChildren(node.children ?? [], parseHtml),
|
|
272
|
+
...posField(pos),
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
case 'blockquote':
|
|
276
|
+
return {
|
|
277
|
+
type: 'blockquote',
|
|
278
|
+
children: convertBlockChildren(node.children ?? [], parseHtml),
|
|
279
|
+
...posField(pos),
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
case 'list':
|
|
283
|
+
return {
|
|
284
|
+
type: 'list',
|
|
285
|
+
ordered: node.ordered ?? false,
|
|
286
|
+
...(node.start != null ? { start: node.start } : {}),
|
|
287
|
+
...(node.spread != null ? { spread: node.spread } : {}),
|
|
288
|
+
children: (node.children ?? []).map((item) => convertListItem(item, parseHtml)),
|
|
289
|
+
...posField(pos),
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
case 'code':
|
|
293
|
+
return {
|
|
294
|
+
type: 'code',
|
|
295
|
+
...(node.lang != null ? { lang: node.lang } : {}),
|
|
296
|
+
...(node.meta != null ? { meta: node.meta } : {}),
|
|
297
|
+
value: node.value ?? '',
|
|
298
|
+
...posField(pos),
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
case 'thematicBreak':
|
|
302
|
+
return {
|
|
303
|
+
type: 'thematicBreak',
|
|
304
|
+
...posField(pos),
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
case 'table':
|
|
308
|
+
return {
|
|
309
|
+
type: 'table',
|
|
310
|
+
...(node.align
|
|
311
|
+
? { align: node.align.map((a) => a as 'left' | 'right' | 'center' | null) }
|
|
312
|
+
: {}),
|
|
313
|
+
children: (node.children ?? []).map((row, i) => convertTableRow(row, i === 0, parseHtml)),
|
|
314
|
+
...posField(pos),
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
case 'html':
|
|
318
|
+
return {
|
|
319
|
+
type: 'htmlBlock',
|
|
320
|
+
rawHtml: node.value ?? '',
|
|
321
|
+
htmlChildren: parseHtml ? parseHtmlToNodes(node.value ?? '') : [],
|
|
322
|
+
...posField(pos),
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
case 'math':
|
|
326
|
+
return {
|
|
327
|
+
type: 'math',
|
|
328
|
+
value: node.value ?? '',
|
|
329
|
+
...(node.meta != null ? { meta: node.meta } : {}),
|
|
330
|
+
...posField(pos),
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
case 'definition':
|
|
334
|
+
return {
|
|
335
|
+
type: 'definition',
|
|
336
|
+
identifier: node.identifier ?? '',
|
|
337
|
+
...(node.label != null ? { label: node.label } : {}),
|
|
338
|
+
url: node.url ?? '',
|
|
339
|
+
...(node.title != null ? { title: node.title } : {}),
|
|
340
|
+
...posField(pos),
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
case 'footnoteDefinition':
|
|
344
|
+
return {
|
|
345
|
+
type: 'footnoteDefinition',
|
|
346
|
+
identifier: node.identifier ?? '',
|
|
347
|
+
...(node.label != null ? { label: node.label } : {}),
|
|
348
|
+
children: convertBlockChildren(node.children ?? [], parseHtml),
|
|
349
|
+
...posField(pos),
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
case 'containerDirective': {
|
|
353
|
+
const allChildren = node.children ?? [];
|
|
354
|
+
let label: string | undefined;
|
|
355
|
+
let contentChildren = allChildren;
|
|
356
|
+
|
|
357
|
+
// Extract directive label (first paragraph with directiveLabel flag)
|
|
358
|
+
if (
|
|
359
|
+
allChildren.length > 0 &&
|
|
360
|
+
allChildren[0].type === 'paragraph' &&
|
|
361
|
+
allChildren[0].data?.directiveLabel === true
|
|
362
|
+
) {
|
|
363
|
+
label = extractText(allChildren[0]);
|
|
364
|
+
contentChildren = allChildren.slice(1);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
type: 'containerDirective',
|
|
369
|
+
name: node.name ?? '',
|
|
370
|
+
...(label ? { label } : {}),
|
|
371
|
+
...(node.attributes && Object.keys(node.attributes).length > 0
|
|
372
|
+
? { attributes: node.attributes }
|
|
373
|
+
: {}),
|
|
374
|
+
children: convertBlockChildren(contentChildren, parseHtml),
|
|
375
|
+
...posField(pos),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
case 'leafDirective':
|
|
380
|
+
return {
|
|
381
|
+
type: 'leafDirective',
|
|
382
|
+
name: node.name ?? '',
|
|
383
|
+
...(node.attributes && Object.keys(node.attributes).length > 0
|
|
384
|
+
? { attributes: node.attributes }
|
|
385
|
+
: {}),
|
|
386
|
+
children: convertInlineChildren(node.children ?? [], parseHtml),
|
|
387
|
+
...posField(pos),
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
case 'yaml':
|
|
391
|
+
// YAML frontmatter nodes are handled separately in parse.ts — skip silently
|
|
392
|
+
return null;
|
|
393
|
+
|
|
394
|
+
default:
|
|
395
|
+
// Unknown block node — skip with a warning
|
|
396
|
+
if (typeof console !== 'undefined') {
|
|
397
|
+
console.warn(`[squisq/markdown] Unknown mdast block node type: "${node.type}"`);
|
|
398
|
+
}
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function convertListItem(node: MdastNode, parseHtml: boolean): MarkdownListItem {
|
|
404
|
+
const pos = convertPosition(node.position);
|
|
405
|
+
return {
|
|
406
|
+
type: 'listItem',
|
|
407
|
+
...(node.checked != null ? { checked: node.checked } : {}),
|
|
408
|
+
...(node.spread != null ? { spread: node.spread } : {}),
|
|
409
|
+
children: convertBlockChildren(node.children ?? [], parseHtml),
|
|
410
|
+
...posField(pos),
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function convertTableRow(node: MdastNode, isHeader: boolean, parseHtml: boolean): MarkdownTableRow {
|
|
415
|
+
const pos = convertPosition(node.position);
|
|
416
|
+
return {
|
|
417
|
+
type: 'tableRow',
|
|
418
|
+
children: (node.children ?? []).map((cell) => convertTableCell(cell, isHeader, parseHtml)),
|
|
419
|
+
...posField(pos),
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function convertTableCell(
|
|
424
|
+
node: MdastNode,
|
|
425
|
+
isHeader: boolean,
|
|
426
|
+
parseHtml: boolean,
|
|
427
|
+
): MarkdownTableCell {
|
|
428
|
+
const pos = convertPosition(node.position);
|
|
429
|
+
return {
|
|
430
|
+
type: 'tableCell',
|
|
431
|
+
...(isHeader ? { isHeader: true } : {}),
|
|
432
|
+
children: convertInlineChildren(node.children ?? [], parseHtml),
|
|
433
|
+
...posField(pos),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function convertInlineNode(node: MdastNode, parseHtml: boolean): MarkdownInlineNode | null {
|
|
438
|
+
const pos = convertPosition(node.position);
|
|
439
|
+
|
|
440
|
+
switch (node.type) {
|
|
441
|
+
case 'text':
|
|
442
|
+
return {
|
|
443
|
+
type: 'text',
|
|
444
|
+
value: node.value ?? '',
|
|
445
|
+
...posField(pos),
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
case 'emphasis':
|
|
449
|
+
return {
|
|
450
|
+
type: 'emphasis',
|
|
451
|
+
children: convertInlineChildren(node.children ?? [], parseHtml),
|
|
452
|
+
...posField(pos),
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
case 'strong':
|
|
456
|
+
return {
|
|
457
|
+
type: 'strong',
|
|
458
|
+
children: convertInlineChildren(node.children ?? [], parseHtml),
|
|
459
|
+
...posField(pos),
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
case 'delete':
|
|
463
|
+
return {
|
|
464
|
+
type: 'delete',
|
|
465
|
+
children: convertInlineChildren(node.children ?? [], parseHtml),
|
|
466
|
+
...posField(pos),
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
case 'inlineCode':
|
|
470
|
+
return {
|
|
471
|
+
type: 'inlineCode',
|
|
472
|
+
value: node.value ?? '',
|
|
473
|
+
...posField(pos),
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
case 'link':
|
|
477
|
+
return {
|
|
478
|
+
type: 'link',
|
|
479
|
+
url: node.url ?? '',
|
|
480
|
+
...(node.title != null ? { title: node.title } : {}),
|
|
481
|
+
children: convertInlineChildren(node.children ?? [], parseHtml),
|
|
482
|
+
...posField(pos),
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
case 'image':
|
|
486
|
+
return {
|
|
487
|
+
type: 'image',
|
|
488
|
+
url: node.url ?? '',
|
|
489
|
+
...(node.alt != null ? { alt: node.alt } : {}),
|
|
490
|
+
...(node.title != null ? { title: node.title } : {}),
|
|
491
|
+
...posField(pos),
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
case 'break':
|
|
495
|
+
return {
|
|
496
|
+
type: 'break',
|
|
497
|
+
...posField(pos),
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
case 'html':
|
|
501
|
+
return {
|
|
502
|
+
type: 'htmlInline',
|
|
503
|
+
rawHtml: node.value ?? '',
|
|
504
|
+
htmlChildren: parseHtml ? parseHtmlToNodes(node.value ?? '') : [],
|
|
505
|
+
...posField(pos),
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
case 'inlineMath':
|
|
509
|
+
return {
|
|
510
|
+
type: 'inlineMath',
|
|
511
|
+
value: node.value ?? '',
|
|
512
|
+
...posField(pos),
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
case 'footnoteReference':
|
|
516
|
+
return {
|
|
517
|
+
type: 'footnoteReference',
|
|
518
|
+
identifier: node.identifier ?? '',
|
|
519
|
+
...(node.label != null ? { label: node.label } : {}),
|
|
520
|
+
...posField(pos),
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
case 'linkReference':
|
|
524
|
+
return {
|
|
525
|
+
type: 'linkReference',
|
|
526
|
+
identifier: node.identifier ?? '',
|
|
527
|
+
...(node.label != null ? { label: node.label } : {}),
|
|
528
|
+
referenceType: (node.referenceType ?? 'full') as 'full' | 'collapsed' | 'shortcut',
|
|
529
|
+
children: convertInlineChildren(node.children ?? [], parseHtml),
|
|
530
|
+
...posField(pos),
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
case 'imageReference':
|
|
534
|
+
return {
|
|
535
|
+
type: 'imageReference',
|
|
536
|
+
identifier: node.identifier ?? '',
|
|
537
|
+
...(node.label != null ? { label: node.label } : {}),
|
|
538
|
+
referenceType: (node.referenceType ?? 'full') as 'full' | 'collapsed' | 'shortcut',
|
|
539
|
+
...(node.alt != null ? { alt: node.alt } : {}),
|
|
540
|
+
...posField(pos),
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
case 'textDirective':
|
|
544
|
+
return {
|
|
545
|
+
type: 'textDirective',
|
|
546
|
+
name: node.name ?? '',
|
|
547
|
+
...(node.attributes && Object.keys(node.attributes).length > 0
|
|
548
|
+
? { attributes: node.attributes }
|
|
549
|
+
: {}),
|
|
550
|
+
children: convertInlineChildren(node.children ?? [], parseHtml),
|
|
551
|
+
...posField(pos),
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
default:
|
|
555
|
+
if (typeof console !== 'undefined') {
|
|
556
|
+
console.warn(`[squisq/markdown] Unknown mdast inline node type: "${node.type}"`);
|
|
557
|
+
}
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// ============================================
|
|
563
|
+
// Squisq → mdast (toMdast)
|
|
564
|
+
// ============================================
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Convert a MarkdownDocument back to an mdast Root node.
|
|
568
|
+
*
|
|
569
|
+
* @param doc - A MarkdownDocument
|
|
570
|
+
* @returns An mdast Root node suitable for remark-stringify
|
|
571
|
+
*/
|
|
572
|
+
export function toMdast(doc: MarkdownDocument): MdastNode {
|
|
573
|
+
return {
|
|
574
|
+
type: 'root',
|
|
575
|
+
children: doc.children.map((n) => blockToMdast(n)),
|
|
576
|
+
...mdastPosField(doc.position),
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function blockToMdast(node: MarkdownBlockNode): MdastNode {
|
|
581
|
+
switch (node.type) {
|
|
582
|
+
case 'heading': {
|
|
583
|
+
const mdastChildren = node.children.map(inlineToMdast);
|
|
584
|
+
if (node.templateAnnotation) {
|
|
585
|
+
const suffix = serializeTemplateAnnotation(node.templateAnnotation);
|
|
586
|
+
// Append to last text node, or create a new one
|
|
587
|
+
const lastChild = mdastChildren[mdastChildren.length - 1];
|
|
588
|
+
if (lastChild && lastChild.type === 'text') {
|
|
589
|
+
lastChild.value = (lastChild.value ?? '') + ' ' + suffix;
|
|
590
|
+
} else {
|
|
591
|
+
mdastChildren.push({ type: 'text', value: ' ' + suffix });
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return {
|
|
595
|
+
type: 'heading',
|
|
596
|
+
depth: node.depth,
|
|
597
|
+
children: mdastChildren,
|
|
598
|
+
...mdastPosField(node.position),
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
case 'paragraph':
|
|
603
|
+
return {
|
|
604
|
+
type: 'paragraph',
|
|
605
|
+
children: node.children.map(inlineToMdast),
|
|
606
|
+
...mdastPosField(node.position),
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
case 'blockquote':
|
|
610
|
+
return {
|
|
611
|
+
type: 'blockquote',
|
|
612
|
+
children: node.children.map(blockToMdast),
|
|
613
|
+
...mdastPosField(node.position),
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
case 'list':
|
|
617
|
+
return {
|
|
618
|
+
type: 'list',
|
|
619
|
+
ordered: node.ordered,
|
|
620
|
+
...(node.start != null ? { start: node.start } : {}),
|
|
621
|
+
...(node.spread != null ? { spread: node.spread } : {}),
|
|
622
|
+
children: node.children.map(listItemToMdast),
|
|
623
|
+
...mdastPosField(node.position),
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
case 'code':
|
|
627
|
+
return {
|
|
628
|
+
type: 'code',
|
|
629
|
+
...(node.lang != null ? { lang: node.lang } : {}),
|
|
630
|
+
...(node.meta != null ? { meta: node.meta } : {}),
|
|
631
|
+
value: node.value,
|
|
632
|
+
...mdastPosField(node.position),
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
case 'thematicBreak':
|
|
636
|
+
return {
|
|
637
|
+
type: 'thematicBreak',
|
|
638
|
+
...mdastPosField(node.position),
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
case 'table':
|
|
642
|
+
return {
|
|
643
|
+
type: 'table',
|
|
644
|
+
...(node.align ? { align: node.align } : {}),
|
|
645
|
+
children: node.children.map(tableRowToMdast),
|
|
646
|
+
...mdastPosField(node.position),
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
case 'htmlBlock':
|
|
650
|
+
return {
|
|
651
|
+
type: 'html',
|
|
652
|
+
value: node.rawHtml,
|
|
653
|
+
...mdastPosField(node.position),
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
case 'math':
|
|
657
|
+
return {
|
|
658
|
+
type: 'math',
|
|
659
|
+
value: node.value,
|
|
660
|
+
...(node.meta != null ? { meta: node.meta } : {}),
|
|
661
|
+
...mdastPosField(node.position),
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
case 'definition':
|
|
665
|
+
return {
|
|
666
|
+
type: 'definition',
|
|
667
|
+
identifier: node.identifier,
|
|
668
|
+
...(node.label != null ? { label: node.label } : {}),
|
|
669
|
+
url: node.url,
|
|
670
|
+
...(node.title != null ? { title: node.title } : {}),
|
|
671
|
+
...mdastPosField(node.position),
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
case 'footnoteDefinition':
|
|
675
|
+
return {
|
|
676
|
+
type: 'footnoteDefinition',
|
|
677
|
+
identifier: node.identifier,
|
|
678
|
+
...(node.label != null ? { label: node.label } : {}),
|
|
679
|
+
children: node.children.map(blockToMdast),
|
|
680
|
+
...mdastPosField(node.position),
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
case 'containerDirective': {
|
|
684
|
+
const children: MdastNode[] = [];
|
|
685
|
+
|
|
686
|
+
// Reinject label as first paragraph with directiveLabel flag
|
|
687
|
+
if (node.label) {
|
|
688
|
+
children.push({
|
|
689
|
+
type: 'paragraph',
|
|
690
|
+
data: { directiveLabel: true },
|
|
691
|
+
children: [{ type: 'text', value: node.label }],
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
children.push(...node.children.map(blockToMdast));
|
|
696
|
+
|
|
697
|
+
return {
|
|
698
|
+
type: 'containerDirective',
|
|
699
|
+
name: node.name,
|
|
700
|
+
...(node.attributes ? { attributes: node.attributes } : {}),
|
|
701
|
+
children,
|
|
702
|
+
...mdastPosField(node.position),
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
case 'leafDirective':
|
|
707
|
+
return {
|
|
708
|
+
type: 'leafDirective',
|
|
709
|
+
name: node.name,
|
|
710
|
+
...(node.attributes ? { attributes: node.attributes } : {}),
|
|
711
|
+
children: node.children.map(inlineToMdast),
|
|
712
|
+
...mdastPosField(node.position),
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
case 'definitionList':
|
|
716
|
+
// Definition lists don't map to standard mdast.
|
|
717
|
+
// Convert to HTML as fallback.
|
|
718
|
+
return {
|
|
719
|
+
type: 'html',
|
|
720
|
+
value: definitionListToHtml(node),
|
|
721
|
+
...mdastPosField(node.position),
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
default:
|
|
725
|
+
return { type: 'html', value: '' };
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function listItemToMdast(item: MarkdownListItem): MdastNode {
|
|
730
|
+
return {
|
|
731
|
+
type: 'listItem',
|
|
732
|
+
...(item.checked != null ? { checked: item.checked } : {}),
|
|
733
|
+
...(item.spread != null ? { spread: item.spread } : {}),
|
|
734
|
+
children: item.children.map(blockToMdast),
|
|
735
|
+
...mdastPosField(item.position),
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function tableRowToMdast(row: MarkdownTableRow): MdastNode {
|
|
740
|
+
return {
|
|
741
|
+
type: 'tableRow',
|
|
742
|
+
children: row.children.map(tableCellToMdast),
|
|
743
|
+
...mdastPosField(row.position),
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function tableCellToMdast(cell: MarkdownTableCell): MdastNode {
|
|
748
|
+
return {
|
|
749
|
+
type: 'tableCell',
|
|
750
|
+
children: cell.children.map(inlineToMdast),
|
|
751
|
+
...mdastPosField(cell.position),
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function inlineToMdast(node: MarkdownInlineNode): MdastNode {
|
|
756
|
+
switch (node.type) {
|
|
757
|
+
case 'text':
|
|
758
|
+
return {
|
|
759
|
+
type: 'text',
|
|
760
|
+
value: node.value,
|
|
761
|
+
...mdastPosField(node.position),
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
case 'emphasis':
|
|
765
|
+
return {
|
|
766
|
+
type: 'emphasis',
|
|
767
|
+
children: node.children.map(inlineToMdast),
|
|
768
|
+
...mdastPosField(node.position),
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
case 'strong':
|
|
772
|
+
return {
|
|
773
|
+
type: 'strong',
|
|
774
|
+
children: node.children.map(inlineToMdast),
|
|
775
|
+
...mdastPosField(node.position),
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
case 'delete':
|
|
779
|
+
return {
|
|
780
|
+
type: 'delete',
|
|
781
|
+
children: node.children.map(inlineToMdast),
|
|
782
|
+
...mdastPosField(node.position),
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
case 'inlineCode':
|
|
786
|
+
return {
|
|
787
|
+
type: 'inlineCode',
|
|
788
|
+
value: node.value,
|
|
789
|
+
...mdastPosField(node.position),
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
case 'link':
|
|
793
|
+
return {
|
|
794
|
+
type: 'link',
|
|
795
|
+
url: node.url,
|
|
796
|
+
...(node.title != null ? { title: node.title } : {}),
|
|
797
|
+
children: node.children.map(inlineToMdast),
|
|
798
|
+
...mdastPosField(node.position),
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
case 'image':
|
|
802
|
+
return {
|
|
803
|
+
type: 'image',
|
|
804
|
+
url: node.url,
|
|
805
|
+
...(node.alt != null ? { alt: node.alt } : {}),
|
|
806
|
+
...(node.title != null ? { title: node.title } : {}),
|
|
807
|
+
...mdastPosField(node.position),
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
case 'break':
|
|
811
|
+
return {
|
|
812
|
+
type: 'break',
|
|
813
|
+
...mdastPosField(node.position),
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
case 'htmlInline':
|
|
817
|
+
return {
|
|
818
|
+
type: 'html',
|
|
819
|
+
value: node.rawHtml,
|
|
820
|
+
...mdastPosField(node.position),
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
case 'inlineMath':
|
|
824
|
+
return {
|
|
825
|
+
type: 'inlineMath',
|
|
826
|
+
value: node.value,
|
|
827
|
+
...mdastPosField(node.position),
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
case 'footnoteReference':
|
|
831
|
+
return {
|
|
832
|
+
type: 'footnoteReference',
|
|
833
|
+
identifier: node.identifier,
|
|
834
|
+
...(node.label != null ? { label: node.label } : {}),
|
|
835
|
+
...mdastPosField(node.position),
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
case 'linkReference':
|
|
839
|
+
return {
|
|
840
|
+
type: 'linkReference',
|
|
841
|
+
identifier: node.identifier,
|
|
842
|
+
...(node.label != null ? { label: node.label } : {}),
|
|
843
|
+
referenceType: node.referenceType,
|
|
844
|
+
children: node.children.map(inlineToMdast),
|
|
845
|
+
...mdastPosField(node.position),
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
case 'imageReference':
|
|
849
|
+
return {
|
|
850
|
+
type: 'imageReference',
|
|
851
|
+
identifier: node.identifier,
|
|
852
|
+
...(node.label != null ? { label: node.label } : {}),
|
|
853
|
+
referenceType: node.referenceType,
|
|
854
|
+
...(node.alt != null ? { alt: node.alt } : {}),
|
|
855
|
+
...mdastPosField(node.position),
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
case 'textDirective':
|
|
859
|
+
return {
|
|
860
|
+
type: 'textDirective',
|
|
861
|
+
name: node.name,
|
|
862
|
+
...(node.attributes ? { attributes: node.attributes } : {}),
|
|
863
|
+
children: node.children.map(inlineToMdast),
|
|
864
|
+
...mdastPosField(node.position),
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
default:
|
|
868
|
+
return { type: 'text', value: '' };
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// ============================================
|
|
873
|
+
// Helpers
|
|
874
|
+
// ============================================
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Convert a MarkdownDefinitionList to an HTML <dl> string.
|
|
878
|
+
* Fallback for serialization since mdast doesn't support definition lists.
|
|
879
|
+
*/
|
|
880
|
+
function definitionListToHtml(list: {
|
|
881
|
+
children: Array<{ type: string; children?: unknown[] }>;
|
|
882
|
+
}): string {
|
|
883
|
+
let html = '<dl>\n';
|
|
884
|
+
for (const child of list.children) {
|
|
885
|
+
if (child.type === 'definitionTerm') {
|
|
886
|
+
// Extract plain text from inline children
|
|
887
|
+
const text = (child.children as Array<{ value?: string; children?: unknown[] }>)
|
|
888
|
+
.map((c) => c.value ?? '')
|
|
889
|
+
.join('');
|
|
890
|
+
html += ` <dt>${text}</dt>\n`;
|
|
891
|
+
} else if (child.type === 'definitionDescription') {
|
|
892
|
+
html += ` <dd>...</dd>\n`;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
html += '</dl>';
|
|
896
|
+
return html;
|
|
897
|
+
}
|