koguma 0.6.6 → 2.0.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/README.md +109 -139
- package/cli/auth.ts +101 -0
- package/cli/config.ts +149 -0
- package/cli/constants.ts +38 -0
- package/cli/content.ts +503 -0
- package/cli/dev-sync.ts +305 -0
- package/cli/exec.ts +61 -0
- package/cli/index.ts +779 -1545
- package/cli/log.ts +49 -0
- package/cli/preflight.ts +105 -0
- package/cli/scaffold.ts +680 -0
- package/cli/typegen.ts +190 -0
- package/cli/ui.ts +55 -0
- package/cli/wrangler.ts +367 -0
- package/package.json +7 -4
- package/src/admin/_bundle.ts +1 -1
- package/src/api/router.integration.test.ts +63 -80
- package/src/api/router.ts +85 -59
- package/src/config/define.ts +1 -1
- package/src/config/field.ts +10 -9
- package/src/config/index.ts +1 -13
- package/src/config/meta.ts +7 -7
- package/src/config/types.ts +1 -95
- package/src/db/init.ts +68 -0
- package/src/db/queries.ts +120 -211
- package/src/db/sql.ts +10 -25
- package/src/media/index.ts +105 -47
- package/src/react/Markdown.test.tsx +195 -0
- package/src/react/Markdown.tsx +40 -0
- package/src/react/index.ts +6 -22
- package/src/react/types.ts +3 -112
- package/src/db/migrate.ts +0 -182
- package/src/db/schema.ts +0 -122
- package/src/react/RichText.test.tsx +0 -535
- package/src/react/RichText.tsx +0 -350
- package/src/rich-text/index.ts +0 -4
- package/src/rich-text/koguma-to-lexical.ts +0 -340
- package/src/rich-text/lexical-compat.test.ts +0 -513
- package/src/rich-text/lexical-to-koguma.test.ts +0 -906
- package/src/rich-text/lexical-to-koguma.ts +0 -400
- package/src/rich-text/markdown-to-koguma.ts +0 -164
- package/src/rich-text/plain.test.ts +0 -208
- package/src/rich-text/plain.ts +0 -114
- package/src/rich-text/snapshots.test.ts +0 -284
|
@@ -1,400 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* lexical-to-koguma.ts
|
|
3
|
-
*
|
|
4
|
-
* Converts Lexical's SerializedEditorState into a KogumaDocument.
|
|
5
|
-
* Runs server-side at read time in queries.ts — no Lexical package dependency required.
|
|
6
|
-
* All node types registered in koguma admin's editor-x/nodes.ts are handled.
|
|
7
|
-
*/
|
|
8
|
-
import type {
|
|
9
|
-
KogumaDocument,
|
|
10
|
-
KogumaBlockNode,
|
|
11
|
-
KogumaInlineNode,
|
|
12
|
-
KogumaListItem,
|
|
13
|
-
KogumaTableRow,
|
|
14
|
-
KogumaTableCell
|
|
15
|
-
} from '../config/types.ts';
|
|
16
|
-
|
|
17
|
-
// ── Lexical format bitmask constants ────────────────────────────────
|
|
18
|
-
const IS_BOLD = 1;
|
|
19
|
-
const IS_ITALIC = 2;
|
|
20
|
-
const IS_STRIKETHROUGH = 4;
|
|
21
|
-
const IS_UNDERLINE = 8;
|
|
22
|
-
const IS_CODE = 16;
|
|
23
|
-
const IS_SUBSCRIPT = 32;
|
|
24
|
-
const IS_SUPERSCRIPT = 64;
|
|
25
|
-
|
|
26
|
-
// ── Loose types for Lexical's serialized JSON ───────────────────────
|
|
27
|
-
|
|
28
|
-
interface LexicalNode {
|
|
29
|
-
type: string;
|
|
30
|
-
version?: number;
|
|
31
|
-
children?: LexicalNode[];
|
|
32
|
-
text?: string;
|
|
33
|
-
format?: number;
|
|
34
|
-
tag?: string;
|
|
35
|
-
listType?: 'bullet' | 'number' | 'check';
|
|
36
|
-
checked?: boolean;
|
|
37
|
-
value?: number;
|
|
38
|
-
url?: string;
|
|
39
|
-
target?: string | null;
|
|
40
|
-
newTab?: boolean;
|
|
41
|
-
src?: string;
|
|
42
|
-
altText?: string;
|
|
43
|
-
width?: number | null;
|
|
44
|
-
height?: number | null;
|
|
45
|
-
language?: string;
|
|
46
|
-
hashtag?: string;
|
|
47
|
-
id?: string;
|
|
48
|
-
name?: string;
|
|
49
|
-
key?: string;
|
|
50
|
-
[k: string]: unknown;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
interface LexicalRoot {
|
|
54
|
-
root: LexicalNode;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// ── Main entry point ────────────────────────────────────────────────
|
|
58
|
-
|
|
59
|
-
export function lexicalToKoguma(input: unknown): KogumaDocument {
|
|
60
|
-
if (!input || typeof input !== 'object') return { nodes: [] };
|
|
61
|
-
|
|
62
|
-
const state = input as Partial<LexicalRoot>;
|
|
63
|
-
if (!state.root?.children) return { nodes: [] };
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
nodes: convertBlockNodes(state.root.children)
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// ── Block node conversion ────────────────────────────────────────────
|
|
71
|
-
|
|
72
|
-
function convertBlockNodes(nodes: LexicalNode[]): KogumaBlockNode[] {
|
|
73
|
-
const result: KogumaBlockNode[] = [];
|
|
74
|
-
|
|
75
|
-
for (const node of nodes) {
|
|
76
|
-
const block = convertBlockNode(node);
|
|
77
|
-
if (block) result.push(block);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return result;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function convertBlockNode(node: LexicalNode): KogumaBlockNode | null {
|
|
84
|
-
switch (node.type) {
|
|
85
|
-
case 'paragraph':
|
|
86
|
-
return {
|
|
87
|
-
type: 'paragraph',
|
|
88
|
-
key: node.key,
|
|
89
|
-
align: convertElementAlign(node.format as number | undefined),
|
|
90
|
-
children: convertInlineNodes(node.children ?? [])
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
case 'heading':
|
|
94
|
-
return {
|
|
95
|
-
type: 'heading',
|
|
96
|
-
key: node.key,
|
|
97
|
-
level: headingLevel(node.tag ?? 'h2'),
|
|
98
|
-
children: convertInlineNodes(node.children ?? [])
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
case 'quote':
|
|
102
|
-
return {
|
|
103
|
-
type: 'quote',
|
|
104
|
-
key: node.key,
|
|
105
|
-
children: convertInlineNodes(flattenQuoteChildren(node.children ?? []))
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
case 'list':
|
|
109
|
-
return convertList(node);
|
|
110
|
-
|
|
111
|
-
case 'code':
|
|
112
|
-
return {
|
|
113
|
-
type: 'code',
|
|
114
|
-
key: node.key,
|
|
115
|
-
language: node.language ?? undefined,
|
|
116
|
-
text: extractCodeText(node.children ?? [])
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
case 'image':
|
|
120
|
-
return {
|
|
121
|
-
type: 'image',
|
|
122
|
-
key: node.key,
|
|
123
|
-
url: node.src ?? '',
|
|
124
|
-
alt: node.altText ?? undefined,
|
|
125
|
-
width: node.width ?? undefined,
|
|
126
|
-
height: node.height ?? undefined
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
case 'horizontalrule':
|
|
130
|
-
return { type: 'hr', key: node.key };
|
|
131
|
-
|
|
132
|
-
case 'table':
|
|
133
|
-
return convertTable(node);
|
|
134
|
-
|
|
135
|
-
case 'layoutcontainer':
|
|
136
|
-
return convertLayout(node);
|
|
137
|
-
|
|
138
|
-
// Intentionally stripped — editor UI artefacts only, never persisted meaningfully
|
|
139
|
-
case 'overflow':
|
|
140
|
-
case 'autocomplete':
|
|
141
|
-
case 'keyword':
|
|
142
|
-
return null;
|
|
143
|
-
|
|
144
|
-
default:
|
|
145
|
-
// All other custom nodes (tweet, youtube, emoji embedded as block, etc.)
|
|
146
|
-
return {
|
|
147
|
-
type: 'custom',
|
|
148
|
-
key: node.key,
|
|
149
|
-
name: node.type,
|
|
150
|
-
data: nodeToData(node)
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// ── List conversion ──────────────────────────────────────────────────
|
|
156
|
-
|
|
157
|
-
function convertList(node: LexicalNode): KogumaBlockNode {
|
|
158
|
-
const ordered = node.listType === 'number';
|
|
159
|
-
const items: KogumaListItem[] = [];
|
|
160
|
-
|
|
161
|
-
for (const child of node.children ?? []) {
|
|
162
|
-
if (child.type !== 'listitem') continue;
|
|
163
|
-
items.push(convertListItem(child, node.listType));
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return { type: 'list', key: node.key, ordered, items };
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function convertListItem(node: LexicalNode, listType?: string): KogumaListItem {
|
|
170
|
-
const inlineChildren: KogumaInlineNode[] = [];
|
|
171
|
-
let nestedList: KogumaListItem['nestedList'] | undefined;
|
|
172
|
-
|
|
173
|
-
for (const child of node.children ?? []) {
|
|
174
|
-
if (child.type === 'list') {
|
|
175
|
-
// Nested list — recurse
|
|
176
|
-
const nested = convertList(child) as {
|
|
177
|
-
type: 'list';
|
|
178
|
-
ordered: boolean;
|
|
179
|
-
items: KogumaListItem[];
|
|
180
|
-
};
|
|
181
|
-
nestedList = { ordered: nested.ordered, items: nested.items };
|
|
182
|
-
} else {
|
|
183
|
-
inlineChildren.push(...convertInlineNode(child));
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const item: KogumaListItem = {
|
|
188
|
-
key: node.key,
|
|
189
|
-
children: inlineChildren,
|
|
190
|
-
nestedList
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
// Checklist items have `checked` defined (true or false)
|
|
194
|
-
if (listType === 'check') {
|
|
195
|
-
item.checked = node.checked ?? false;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return item;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// ── Table conversion ─────────────────────────────────────────────────
|
|
202
|
-
|
|
203
|
-
function convertTable(node: LexicalNode): KogumaBlockNode {
|
|
204
|
-
const rows: KogumaTableRow[] = [];
|
|
205
|
-
|
|
206
|
-
for (let i = 0; i < (node.children ?? []).length; i++) {
|
|
207
|
-
const rowNode = (node.children ?? [])[i];
|
|
208
|
-
if (rowNode.type !== 'tablerow') continue;
|
|
209
|
-
|
|
210
|
-
const cells: KogumaTableCell[] = [];
|
|
211
|
-
for (const cellNode of rowNode.children ?? []) {
|
|
212
|
-
if (cellNode.type !== 'tablecell') continue;
|
|
213
|
-
cells.push({
|
|
214
|
-
key: cellNode.key,
|
|
215
|
-
children: convertInlineNodes(cellNode.children ?? [])
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
rows.push({
|
|
220
|
-
key: rowNode.key,
|
|
221
|
-
isHeader: i === 0, // First row is treated as header
|
|
222
|
-
cells
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
return { type: 'table', key: node.key, rows };
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// ── Layout conversion ────────────────────────────────────────────────
|
|
230
|
-
|
|
231
|
-
function convertLayout(node: LexicalNode): KogumaBlockNode {
|
|
232
|
-
const columns: KogumaBlockNode[][] = [];
|
|
233
|
-
|
|
234
|
-
for (const itemNode of node.children ?? []) {
|
|
235
|
-
if (itemNode.type !== 'layoutitem') continue;
|
|
236
|
-
columns.push(convertBlockNodes(itemNode.children ?? []));
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
return { type: 'layout', key: node.key, columns };
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// ── Inline node conversion ───────────────────────────────────────────
|
|
243
|
-
|
|
244
|
-
function convertInlineNodes(nodes: LexicalNode[]): KogumaInlineNode[] {
|
|
245
|
-
return nodes.flatMap(convertInlineNode);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function convertInlineNode(node: LexicalNode): KogumaInlineNode[] {
|
|
249
|
-
switch (node.type) {
|
|
250
|
-
case 'text': {
|
|
251
|
-
const fmt = node.format ?? 0;
|
|
252
|
-
const textNode: KogumaInlineNode = {
|
|
253
|
-
type: 'text',
|
|
254
|
-
key: node.key,
|
|
255
|
-
text: node.text ?? ''
|
|
256
|
-
};
|
|
257
|
-
if (fmt & IS_BOLD) textNode.bold = true;
|
|
258
|
-
if (fmt & IS_ITALIC) textNode.italic = true;
|
|
259
|
-
if (fmt & IS_UNDERLINE) textNode.underline = true;
|
|
260
|
-
if (fmt & IS_STRIKETHROUGH) textNode.strikethrough = true;
|
|
261
|
-
if (fmt & IS_CODE) textNode.code = true;
|
|
262
|
-
if (fmt & IS_SUBSCRIPT) textNode.subscript = true;
|
|
263
|
-
if (fmt & IS_SUPERSCRIPT) textNode.superscript = true;
|
|
264
|
-
return [textNode];
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
case 'link':
|
|
268
|
-
case 'autolink': {
|
|
269
|
-
const linkUrl = node.url ?? '';
|
|
270
|
-
const newTab = node.newTab ?? node.target === '_blank';
|
|
271
|
-
return [
|
|
272
|
-
{
|
|
273
|
-
type: 'link',
|
|
274
|
-
key: node.key,
|
|
275
|
-
url: linkUrl,
|
|
276
|
-
newTab: newTab || undefined,
|
|
277
|
-
children: convertInlineNodes(node.children ?? [])
|
|
278
|
-
}
|
|
279
|
-
];
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
case 'linebreak':
|
|
283
|
-
return [{ type: 'line-break', key: node.key }];
|
|
284
|
-
|
|
285
|
-
case 'image':
|
|
286
|
-
return [
|
|
287
|
-
{
|
|
288
|
-
type: 'inline-image',
|
|
289
|
-
key: node.key,
|
|
290
|
-
url: node.src ?? '',
|
|
291
|
-
alt: node.altText ?? undefined
|
|
292
|
-
}
|
|
293
|
-
];
|
|
294
|
-
|
|
295
|
-
case 'hashtag':
|
|
296
|
-
return [
|
|
297
|
-
{
|
|
298
|
-
type: 'custom',
|
|
299
|
-
key: node.key,
|
|
300
|
-
name: 'hashtag',
|
|
301
|
-
data: { tag: node.hashtag ?? node.text ?? '' }
|
|
302
|
-
}
|
|
303
|
-
];
|
|
304
|
-
|
|
305
|
-
case 'mention':
|
|
306
|
-
return [
|
|
307
|
-
{
|
|
308
|
-
type: 'custom',
|
|
309
|
-
key: node.key,
|
|
310
|
-
name: 'mention',
|
|
311
|
-
data: { id: node.id ?? '', name: node.name ?? node.text ?? '' }
|
|
312
|
-
}
|
|
313
|
-
];
|
|
314
|
-
|
|
315
|
-
case 'emoji':
|
|
316
|
-
// Flatten emoji to plain text — it's just a Unicode character
|
|
317
|
-
return [{ type: 'text', key: node.key, text: node.text ?? '' }];
|
|
318
|
-
|
|
319
|
-
case 'keyword':
|
|
320
|
-
case 'autocomplete':
|
|
321
|
-
case 'tab':
|
|
322
|
-
// Editor-only UI nodes — strip from output
|
|
323
|
-
return [];
|
|
324
|
-
|
|
325
|
-
// CodeHighlightNode appears inline but should not appear outside code blocks
|
|
326
|
-
// If encountered here, treat as plain text
|
|
327
|
-
case 'code-highlight':
|
|
328
|
-
case 'codehighlight':
|
|
329
|
-
return [{ type: 'text', key: node.key, text: node.text ?? '' }];
|
|
330
|
-
|
|
331
|
-
default: {
|
|
332
|
-
// Unknown inline custom node — preserve as custom inline
|
|
333
|
-
if (node.text != null) {
|
|
334
|
-
// If it has text content and children, try inlining
|
|
335
|
-
const children = convertInlineNodes(node.children ?? []);
|
|
336
|
-
if (children.length > 0) return children;
|
|
337
|
-
return [{ type: 'text', text: node.text }];
|
|
338
|
-
}
|
|
339
|
-
return [
|
|
340
|
-
{
|
|
341
|
-
type: 'custom',
|
|
342
|
-
key: node.key,
|
|
343
|
-
name: node.type,
|
|
344
|
-
data: nodeToData(node)
|
|
345
|
-
}
|
|
346
|
-
];
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// ── Helpers ──────────────────────────────────────────────────────────
|
|
352
|
-
|
|
353
|
-
function headingLevel(tag: string): 1 | 2 | 3 | 4 | 5 | 6 {
|
|
354
|
-
const n = parseInt(tag.replace('h', ''), 10);
|
|
355
|
-
if (n >= 1 && n <= 6) return n as 1 | 2 | 3 | 4 | 5 | 6;
|
|
356
|
-
return 2;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
type Align = 'left' | 'center' | 'right' | 'justify';
|
|
360
|
-
function convertElementAlign(format?: number): Align | undefined {
|
|
361
|
-
switch (format) {
|
|
362
|
-
case 1:
|
|
363
|
-
return 'left';
|
|
364
|
-
case 2:
|
|
365
|
-
return 'center';
|
|
366
|
-
case 3:
|
|
367
|
-
return 'right';
|
|
368
|
-
case 4:
|
|
369
|
-
return 'justify';
|
|
370
|
-
default:
|
|
371
|
-
return undefined;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/** Extract plain text from CodeHighlightNode children */
|
|
376
|
-
function extractCodeText(children: LexicalNode[]): string {
|
|
377
|
-
return children
|
|
378
|
-
.filter(
|
|
379
|
-
c =>
|
|
380
|
-
c.type === 'code-highlight' ||
|
|
381
|
-
c.type === 'codehighlight' ||
|
|
382
|
-
c.type === 'text' ||
|
|
383
|
-
c.type === 'linebreak'
|
|
384
|
-
)
|
|
385
|
-
.map(c => (c.type === 'linebreak' ? '\n' : (c.text ?? '')))
|
|
386
|
-
.join('');
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
/** Quote nodes wrap a paragraph — unwrap to get inline children */
|
|
390
|
-
function flattenQuoteChildren(children: LexicalNode[]): LexicalNode[] {
|
|
391
|
-
return children.flatMap(child =>
|
|
392
|
-
child.type === 'paragraph' ? (child.children ?? []) : [child]
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
/** Strip Lexical internals, keep content data fields */
|
|
397
|
-
function nodeToData(node: LexicalNode): Record<string, unknown> {
|
|
398
|
-
const { type: _type, version: _v, children: _c, key: _k, ...rest } = node;
|
|
399
|
-
return rest as Record<string, unknown>;
|
|
400
|
-
}
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* markdown-to-koguma.ts — Lightweight markdown → KogumaDocument converter.
|
|
3
|
-
*
|
|
4
|
-
* Designed for seed content (author-controlled markdown), not arbitrary user input.
|
|
5
|
-
* Supports: headings, paragraphs, bold, italic, code, links, lists, code blocks, HR.
|
|
6
|
-
*/
|
|
7
|
-
import type {
|
|
8
|
-
KogumaDocument,
|
|
9
|
-
KogumaBlockNode,
|
|
10
|
-
KogumaInlineNode,
|
|
11
|
-
KogumaListItem
|
|
12
|
-
} from '../config/types.ts';
|
|
13
|
-
|
|
14
|
-
// ── Inline parsing ──────────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
function parseInline(text: string): KogumaInlineNode[] {
|
|
17
|
-
const nodes: KogumaInlineNode[] = [];
|
|
18
|
-
// Regex: links, bold, italic, inline code
|
|
19
|
-
const re =
|
|
20
|
-
/\[([^\]]+)\]\(([^)]+)\)|`([^`]+)`|\*\*(.+?)\*\*|\*(.+?)\*|([^[`*]+)/g;
|
|
21
|
-
let match: RegExpExecArray | null;
|
|
22
|
-
|
|
23
|
-
while ((match = re.exec(text)) !== null) {
|
|
24
|
-
if (match[1] !== undefined && match[2] !== undefined) {
|
|
25
|
-
// Link
|
|
26
|
-
nodes.push({
|
|
27
|
-
type: 'link',
|
|
28
|
-
url: match[2],
|
|
29
|
-
children: [{ type: 'text', text: match[1] }]
|
|
30
|
-
});
|
|
31
|
-
} else if (match[3] !== undefined) {
|
|
32
|
-
// Inline code
|
|
33
|
-
nodes.push({ type: 'text', text: match[3], code: true });
|
|
34
|
-
} else if (match[4] !== undefined) {
|
|
35
|
-
// Bold
|
|
36
|
-
nodes.push({ type: 'text', text: match[4], bold: true });
|
|
37
|
-
} else if (match[5] !== undefined) {
|
|
38
|
-
// Italic
|
|
39
|
-
nodes.push({ type: 'text', text: match[5], italic: true });
|
|
40
|
-
} else if (match[6] !== undefined && match[6].length > 0) {
|
|
41
|
-
// Plain text
|
|
42
|
-
nodes.push({ type: 'text', text: match[6] });
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return nodes.length > 0 ? nodes : [{ type: 'text', text }];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// ── Block parsing ───────────────────────────────────────────────────
|
|
50
|
-
|
|
51
|
-
export function markdownToKoguma(markdown: string): KogumaDocument {
|
|
52
|
-
const lines = markdown.split('\n');
|
|
53
|
-
const nodes: KogumaBlockNode[] = [];
|
|
54
|
-
let i = 0;
|
|
55
|
-
|
|
56
|
-
while (i < lines.length) {
|
|
57
|
-
const line = lines[i]!;
|
|
58
|
-
|
|
59
|
-
// Blank line — skip
|
|
60
|
-
if (line.trim() === '') {
|
|
61
|
-
i++;
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Fenced code block
|
|
66
|
-
const codeMatch = line.match(/^```(\w*)$/);
|
|
67
|
-
if (codeMatch) {
|
|
68
|
-
const language = codeMatch[1] || undefined;
|
|
69
|
-
const codeLines: string[] = [];
|
|
70
|
-
i++;
|
|
71
|
-
while (i < lines.length && !lines[i]!.startsWith('```')) {
|
|
72
|
-
codeLines.push(lines[i]!);
|
|
73
|
-
i++;
|
|
74
|
-
}
|
|
75
|
-
i++; // skip closing ```
|
|
76
|
-
nodes.push({ type: 'code', language, text: codeLines.join('\n') });
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Heading
|
|
81
|
-
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
82
|
-
if (headingMatch) {
|
|
83
|
-
const level = headingMatch[1]!.length as 1 | 2 | 3 | 4 | 5 | 6;
|
|
84
|
-
nodes.push({
|
|
85
|
-
type: 'heading',
|
|
86
|
-
level,
|
|
87
|
-
children: parseInline(headingMatch[2]!)
|
|
88
|
-
});
|
|
89
|
-
i++;
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Horizontal rule
|
|
94
|
-
if (/^(-{3,}|\*{3,}|_{3,})$/.test(line.trim())) {
|
|
95
|
-
nodes.push({ type: 'hr' });
|
|
96
|
-
i++;
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Unordered list
|
|
101
|
-
if (/^[-*+]\s/.test(line)) {
|
|
102
|
-
const items: KogumaListItem[] = [];
|
|
103
|
-
while (i < lines.length && /^[-*+]\s/.test(lines[i]!)) {
|
|
104
|
-
items.push({
|
|
105
|
-
children: parseInline(lines[i]!.replace(/^[-*+]\s/, ''))
|
|
106
|
-
});
|
|
107
|
-
i++;
|
|
108
|
-
}
|
|
109
|
-
nodes.push({ type: 'list', ordered: false, items });
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Ordered list
|
|
114
|
-
if (/^\d+\.\s/.test(line)) {
|
|
115
|
-
const items: KogumaListItem[] = [];
|
|
116
|
-
while (i < lines.length && /^\d+\.\s/.test(lines[i]!)) {
|
|
117
|
-
items.push({
|
|
118
|
-
children: parseInline(lines[i]!.replace(/^\d+\.\s/, ''))
|
|
119
|
-
});
|
|
120
|
-
i++;
|
|
121
|
-
}
|
|
122
|
-
nodes.push({ type: 'list', ordered: true, items });
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Blockquote
|
|
127
|
-
if (line.startsWith('> ')) {
|
|
128
|
-
const quoteLines: string[] = [];
|
|
129
|
-
while (i < lines.length && lines[i]!.startsWith('> ')) {
|
|
130
|
-
quoteLines.push(lines[i]!.slice(2));
|
|
131
|
-
i++;
|
|
132
|
-
}
|
|
133
|
-
nodes.push({
|
|
134
|
-
type: 'quote',
|
|
135
|
-
children: parseInline(quoteLines.join(' '))
|
|
136
|
-
});
|
|
137
|
-
continue;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Paragraph (default)
|
|
141
|
-
const paraLines: string[] = [];
|
|
142
|
-
while (
|
|
143
|
-
i < lines.length &&
|
|
144
|
-
lines[i]!.trim() !== '' &&
|
|
145
|
-
!lines[i]!.startsWith('#') &&
|
|
146
|
-
!lines[i]!.startsWith('```') &&
|
|
147
|
-
!/^[-*+]\s/.test(lines[i]!) &&
|
|
148
|
-
!/^\d+\.\s/.test(lines[i]!) &&
|
|
149
|
-
!lines[i]!.startsWith('> ') &&
|
|
150
|
-
!/^(-{3,}|\*{3,}|_{3,})$/.test(lines[i]!.trim())
|
|
151
|
-
) {
|
|
152
|
-
paraLines.push(lines[i]!);
|
|
153
|
-
i++;
|
|
154
|
-
}
|
|
155
|
-
if (paraLines.length > 0) {
|
|
156
|
-
nodes.push({
|
|
157
|
-
type: 'paragraph',
|
|
158
|
-
children: parseInline(paraLines.join(' '))
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
return { nodes };
|
|
164
|
-
}
|