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.
Files changed (44) hide show
  1. package/README.md +109 -139
  2. package/cli/auth.ts +101 -0
  3. package/cli/config.ts +149 -0
  4. package/cli/constants.ts +38 -0
  5. package/cli/content.ts +503 -0
  6. package/cli/dev-sync.ts +305 -0
  7. package/cli/exec.ts +61 -0
  8. package/cli/index.ts +779 -1545
  9. package/cli/log.ts +49 -0
  10. package/cli/preflight.ts +105 -0
  11. package/cli/scaffold.ts +680 -0
  12. package/cli/typegen.ts +190 -0
  13. package/cli/ui.ts +55 -0
  14. package/cli/wrangler.ts +367 -0
  15. package/package.json +7 -4
  16. package/src/admin/_bundle.ts +1 -1
  17. package/src/api/router.integration.test.ts +63 -80
  18. package/src/api/router.ts +85 -59
  19. package/src/config/define.ts +1 -1
  20. package/src/config/field.ts +10 -9
  21. package/src/config/index.ts +1 -13
  22. package/src/config/meta.ts +7 -7
  23. package/src/config/types.ts +1 -95
  24. package/src/db/init.ts +68 -0
  25. package/src/db/queries.ts +120 -211
  26. package/src/db/sql.ts +10 -25
  27. package/src/media/index.ts +105 -47
  28. package/src/react/Markdown.test.tsx +195 -0
  29. package/src/react/Markdown.tsx +40 -0
  30. package/src/react/index.ts +6 -22
  31. package/src/react/types.ts +3 -112
  32. package/src/db/migrate.ts +0 -182
  33. package/src/db/schema.ts +0 -122
  34. package/src/react/RichText.test.tsx +0 -535
  35. package/src/react/RichText.tsx +0 -350
  36. package/src/rich-text/index.ts +0 -4
  37. package/src/rich-text/koguma-to-lexical.ts +0 -340
  38. package/src/rich-text/lexical-compat.test.ts +0 -513
  39. package/src/rich-text/lexical-to-koguma.test.ts +0 -906
  40. package/src/rich-text/lexical-to-koguma.ts +0 -400
  41. package/src/rich-text/markdown-to-koguma.ts +0 -164
  42. package/src/rich-text/plain.test.ts +0 -208
  43. package/src/rich-text/plain.ts +0 -114
  44. 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
- }