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,350 +0,0 @@
1
- /**
2
- * <RichText> — renders a KogumaDocument as semantic HTML.
3
- *
4
- * Simple usage:
5
- * <RichText doc={data.body} className="prose" />
6
- *
7
- * Advanced usage with element overrides:
8
- * <RichText doc={data.body} components={{ heading: ..., listItem: ... }} />
9
- */
10
- import type { ReactNode, CSSProperties } from 'react';
11
- import type {
12
- KogumaDocument,
13
- KogumaBlockNode,
14
- KogumaInlineNode,
15
- KogumaListItem
16
- } from '../config/types.ts';
17
- import type { RichTextComponents } from './types.ts';
18
-
19
- export interface RichTextProps {
20
- doc: KogumaDocument | null | undefined;
21
- /** Applied to the wrapper div */
22
- className?: string;
23
- /** Per-element component overrides */
24
- components?: Partial<RichTextComponents>;
25
- }
26
-
27
- export function RichText({
28
- doc,
29
- className,
30
- components = {}
31
- }: RichTextProps): ReactNode {
32
- if (!doc?.nodes?.length) return null;
33
-
34
- return (
35
- <div className={className}>
36
- {doc.nodes.map((node, i) => (
37
- <Block key={node.key ?? i} node={node} components={components} />
38
- ))}
39
- </div>
40
- );
41
- }
42
-
43
- // ── Block renderer ───────────────────────────────────────────────────
44
-
45
- function Block({
46
- node,
47
- components
48
- }: {
49
- node: KogumaBlockNode;
50
- components: Partial<RichTextComponents>;
51
- }): ReactNode {
52
- const inlines = (children: KogumaInlineNode[]) =>
53
- children.map((n, i) => (
54
- <Inline key={n.key ?? i} node={n} components={components} />
55
- ));
56
-
57
- switch (node.type) {
58
- case 'paragraph': {
59
- const children = inlines(node.children);
60
- if (components.paragraph) {
61
- return components.paragraph({ node, children });
62
- }
63
- return <p style={alignStyle(node.align)}>{children}</p>;
64
- }
65
-
66
- case 'heading': {
67
- const children = inlines(node.children);
68
- if (components.heading) {
69
- return components.heading({ node, children });
70
- }
71
- const H = `h${node.level}` as 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
72
- return <H>{children}</H>;
73
- }
74
-
75
- case 'quote': {
76
- const children = inlines(node.children);
77
- if (components.quote) {
78
- return components.quote({ node, children });
79
- }
80
- return <blockquote>{children}</blockquote>;
81
- }
82
-
83
- case 'list': {
84
- const renderedItems = node.items.map((item, i) => (
85
- <ListItemRenderer
86
- key={item.key ?? i}
87
- item={item}
88
- components={components}
89
- />
90
- ));
91
- if (components.list) {
92
- return components.list({ node, children: renderedItems });
93
- }
94
- const ListTag = node.ordered ? 'ol' : 'ul';
95
- return <ListTag>{renderedItems}</ListTag>;
96
- }
97
-
98
- case 'code': {
99
- if (components.code) {
100
- return components.code({ node });
101
- }
102
- return (
103
- <pre>
104
- <code
105
- className={node.language ? `language-${node.language}` : undefined}>
106
- {node.text}
107
- </code>
108
- </pre>
109
- );
110
- }
111
-
112
- case 'image': {
113
- if (components.image) {
114
- return components.image({ node });
115
- }
116
- return (
117
- <img
118
- src={node.url}
119
- alt={node.alt ?? ''}
120
- width={node.width ?? undefined}
121
- height={node.height ?? undefined}
122
- />
123
- );
124
- }
125
-
126
- case 'hr': {
127
- if (components.hr) {
128
- return components.hr({ node });
129
- }
130
- return <hr />;
131
- }
132
-
133
- case 'table': {
134
- if (components.table) {
135
- return components.table({ node, rows: node.rows });
136
- }
137
- return (
138
- <table>
139
- {node.rows.some(r => r.isHeader) && (
140
- <thead>
141
- {node.rows
142
- .filter(r => r.isHeader)
143
- .map((row, i) => (
144
- <tr key={row.key ?? i}>
145
- {row.cells.map((cell, j) => (
146
- <th key={cell.key ?? j}>
147
- {cell.children.map((n, k) => (
148
- <Inline
149
- key={n.key ?? k}
150
- node={n}
151
- components={components}
152
- />
153
- ))}
154
- </th>
155
- ))}
156
- </tr>
157
- ))}
158
- </thead>
159
- )}
160
- <tbody>
161
- {node.rows
162
- .filter(r => !r.isHeader)
163
- .map((row, i) => (
164
- <tr key={row.key ?? i}>
165
- {row.cells.map((cell, j) => (
166
- <td key={cell.key ?? j}>
167
- {cell.children.map((n, k) => (
168
- <Inline
169
- key={n.key ?? k}
170
- node={n}
171
- components={components}
172
- />
173
- ))}
174
- </td>
175
- ))}
176
- </tr>
177
- ))}
178
- </tbody>
179
- </table>
180
- );
181
- }
182
-
183
- case 'layout': {
184
- const renderedColumns = node.columns.map((col, _i) =>
185
- col.map((block, j) => (
186
- <Block key={block.key ?? j} node={block} components={components} />
187
- ))
188
- );
189
- if (components.layout) {
190
- return components.layout({
191
- node,
192
- columns: renderedColumns.map((col, _ci) => <div key={_ci}>{col}</div>)
193
- });
194
- }
195
- return (
196
- <div style={{ display: 'flex', gap: '1rem' }}>
197
- {renderedColumns.map((col, i) => (
198
- <div key={i} style={{ flex: 1 }}>
199
- {col}
200
- </div>
201
- ))}
202
- </div>
203
- );
204
- }
205
-
206
- case 'custom': {
207
- if (components.customBlock) {
208
- return components.customBlock({
209
- node,
210
- name: node.name,
211
- data: node.data
212
- });
213
- }
214
- return null;
215
- }
216
- }
217
- }
218
-
219
- // ── List item renderer ───────────────────────────────────────────────
220
-
221
- function ListItemRenderer({
222
- item,
223
- components
224
- }: {
225
- item: KogumaListItem;
226
- components: Partial<RichTextComponents>;
227
- }): ReactNode {
228
- const inlineChildren = item.children.map((n, i) => (
229
- <Inline key={n.key ?? i} node={n} components={components} />
230
- ));
231
-
232
- const nestedList = item.nestedList ? (
233
- <NestedList list={item.nestedList} components={components} />
234
- ) : null;
235
-
236
- if (components.listItem) {
237
- return components.listItem({ item, children: inlineChildren, nestedList });
238
- }
239
-
240
- return (
241
- <li>
242
- {item.checked !== undefined && (
243
- <input
244
- type='checkbox'
245
- checked={item.checked}
246
- readOnly
247
- style={{ marginRight: '0.4em' }}
248
- />
249
- )}
250
- {inlineChildren}
251
- {nestedList}
252
- </li>
253
- );
254
- }
255
-
256
- function NestedList({
257
- list,
258
- components
259
- }: {
260
- list: { ordered: boolean; items: KogumaListItem[] };
261
- components: Partial<RichTextComponents>;
262
- }): ReactNode {
263
- const Tag = list.ordered ? 'ol' : 'ul';
264
- return (
265
- <Tag>
266
- {list.items.map((item, i) => (
267
- <ListItemRenderer
268
- key={item.key ?? i}
269
- item={item}
270
- components={components}
271
- />
272
- ))}
273
- </Tag>
274
- );
275
- }
276
-
277
- // ── Inline renderer ──────────────────────────────────────────────────
278
-
279
- function Inline({
280
- node,
281
- components
282
- }: {
283
- node: KogumaInlineNode;
284
- components: Partial<RichTextComponents>;
285
- }): ReactNode {
286
- switch (node.type) {
287
- case 'text': {
288
- let content: ReactNode = node.text;
289
- if (node.bold) content = <strong>{content}</strong>;
290
- if (node.italic) content = <em>{content}</em>;
291
- if (node.underline) content = <u>{content}</u>;
292
- if (node.strikethrough) content = <s>{content}</s>;
293
- if (node.code) content = <code>{content}</code>;
294
- if (node.superscript) content = <sup>{content}</sup>;
295
- if (node.subscript) content = <sub>{content}</sub>;
296
- return content;
297
- }
298
-
299
- case 'link': {
300
- const children = node.children.map((n, i) => (
301
- <Inline key={n.key ?? i} node={n} components={components} />
302
- ));
303
- if (components.link) {
304
- return components.link({ node, children });
305
- }
306
- return (
307
- <a
308
- href={node.url}
309
- target={node.newTab ? '_blank' : undefined}
310
- rel={node.newTab ? 'noopener noreferrer' : undefined}>
311
- {children}
312
- </a>
313
- );
314
- }
315
-
316
- case 'line-break':
317
- return <br />;
318
-
319
- case 'inline-image': {
320
- if (components.inlineImage) {
321
- return components.inlineImage({ node });
322
- }
323
- return (
324
- <img
325
- src={node.url}
326
- alt={node.alt ?? ''}
327
- style={{ display: 'inline' }}
328
- />
329
- );
330
- }
331
-
332
- case 'custom': {
333
- if (components.customInline) {
334
- return components.customInline({
335
- node,
336
- name: node.name,
337
- data: node.data
338
- });
339
- }
340
- return null;
341
- }
342
- }
343
- }
344
-
345
- // ── Helpers ──────────────────────────────────────────────────────────
346
-
347
- function alignStyle(align?: string): CSSProperties | undefined {
348
- if (!align) return undefined;
349
- return { textAlign: align as CSSProperties['textAlign'] };
350
- }
@@ -1,4 +0,0 @@
1
- export { lexicalToKoguma } from './lexical-to-koguma.ts';
2
- export { kogumaToLexical } from './koguma-to-lexical.ts';
3
- export { richTextToPlain } from './plain.ts';
4
- export type { PlainTextOptions } from './plain.ts';
@@ -1,340 +0,0 @@
1
- /**
2
- * koguma-to-lexical.ts — Converts KogumaDocument back to Lexical's SerializedEditorState.
3
- *
4
- * The inverse of lexicalToKoguma(). Used by the seed pipeline to store
5
- * content in the format D1/admin expects (Lexical JSON), rather than
6
- * the public-API KogumaDocument format.
7
- */
8
- import type {
9
- KogumaDocument,
10
- KogumaBlockNode,
11
- KogumaInlineNode,
12
- KogumaListItem
13
- } from '../config/types.ts';
14
-
15
- // ── Lexical format bitmask constants (mirror of lexical-to-koguma.ts) ──
16
- const IS_BOLD = 1;
17
- const IS_ITALIC = 2;
18
- const IS_STRIKETHROUGH = 4;
19
- const IS_UNDERLINE = 8;
20
- const IS_CODE = 16;
21
- const IS_SUBSCRIPT = 32;
22
- const IS_SUPERSCRIPT = 64;
23
-
24
- // ── Output types (Lexical serialized JSON) ──────────────────────────
25
-
26
- interface LexicalNode {
27
- type: string;
28
- version: number;
29
- [k: string]: unknown;
30
- }
31
-
32
- // ── Main entry point ────────────────────────────────────────────────
33
-
34
- export function kogumaToLexical(doc: KogumaDocument): Record<string, unknown> {
35
- return {
36
- root: {
37
- type: 'root',
38
- children: doc.nodes.map(convertBlockNode),
39
- direction: 'ltr',
40
- format: '',
41
- indent: 0,
42
- version: 1
43
- }
44
- };
45
- }
46
-
47
- // ── Block node conversion ────────────────────────────────────────────
48
-
49
- function convertBlockNode(node: KogumaBlockNode): LexicalNode {
50
- switch (node.type) {
51
- case 'paragraph':
52
- return {
53
- type: 'paragraph',
54
- version: 1,
55
- children: (node.children ?? []).map(convertInlineNode),
56
- direction: 'ltr',
57
- format: alignToFormat(node.align),
58
- indent: 0,
59
- textFormat: 0,
60
- textStyle: ''
61
- };
62
-
63
- case 'heading':
64
- return {
65
- type: 'heading',
66
- version: 1,
67
- tag: `h${node.level ?? 2}`,
68
- children: (node.children ?? []).map(convertInlineNode),
69
- direction: 'ltr',
70
- format: '',
71
- indent: 0,
72
- textFormat: 0,
73
- textStyle: ''
74
- };
75
-
76
- case 'quote':
77
- return {
78
- type: 'quote',
79
- version: 1,
80
- children: [
81
- {
82
- type: 'paragraph',
83
- version: 1,
84
- children: (node.children ?? []).map(convertInlineNode),
85
- direction: 'ltr',
86
- format: '',
87
- indent: 0,
88
- textFormat: 0,
89
- textStyle: ''
90
- }
91
- ],
92
- direction: 'ltr',
93
- format: '',
94
- indent: 0
95
- };
96
-
97
- case 'list':
98
- return convertList(node);
99
-
100
- case 'code':
101
- return {
102
- type: 'code',
103
- version: 1,
104
- language: node.language ?? null,
105
- children: codeTextToHighlightNodes(node.text ?? ''),
106
- direction: 'ltr',
107
- format: '',
108
- indent: 0
109
- };
110
-
111
- case 'image':
112
- return {
113
- type: 'image',
114
- version: 1,
115
- src: node.url ?? '',
116
- altText: node.alt ?? '',
117
- width: node.width ?? null,
118
- height: node.height ?? null
119
- };
120
-
121
- case 'hr':
122
- return {
123
- type: 'horizontalrule',
124
- version: 1
125
- };
126
-
127
- case 'table':
128
- return convertTable(node);
129
-
130
- case 'layout':
131
- return convertLayout(node);
132
-
133
- case 'custom':
134
- return {
135
- type: node.name ?? 'unknown',
136
- version: 1,
137
- ...(node.data ?? {})
138
- };
139
-
140
- default:
141
- return {
142
- type: 'paragraph',
143
- version: 1,
144
- children: [],
145
- direction: 'ltr',
146
- format: '',
147
- indent: 0,
148
- textFormat: 0,
149
- textStyle: ''
150
- };
151
- }
152
- }
153
-
154
- // ── List conversion ──────────────────────────────────────────────────
155
-
156
- function convertList(node: KogumaBlockNode): LexicalNode {
157
- const ordered = (node as any).ordered ?? false;
158
- const listType = ordered ? 'number' : 'bullet';
159
- const items = (node as any).items ?? [];
160
-
161
- return {
162
- type: 'list',
163
- version: 1,
164
- listType,
165
- start: 1,
166
- tag: ordered ? 'ol' : 'ul',
167
- children: items.map((item: KogumaListItem, i: number) =>
168
- convertListItem(item, listType, i + 1)
169
- ),
170
- direction: 'ltr',
171
- format: '',
172
- indent: 0
173
- };
174
- }
175
-
176
- function convertListItem(
177
- item: KogumaListItem,
178
- listType: string,
179
- value: number
180
- ): LexicalNode {
181
- const children: LexicalNode[] = (item.children ?? []).map(convertInlineNode);
182
-
183
- if (item.nestedList) {
184
- children.push(
185
- convertList({
186
- type: 'list',
187
- ordered: item.nestedList.ordered,
188
- items: item.nestedList.items
189
- } as any)
190
- );
191
- }
192
-
193
- return {
194
- type: 'listitem',
195
- version: 1,
196
- value,
197
- children,
198
- checked: listType === 'check' ? (item.checked ?? false) : undefined,
199
- direction: 'ltr',
200
- format: '',
201
- indent: 0
202
- };
203
- }
204
-
205
- // ── Table conversion ─────────────────────────────────────────────────
206
-
207
- function convertTable(node: KogumaBlockNode): LexicalNode {
208
- const rows = (node as any).rows ?? [];
209
- return {
210
- type: 'table',
211
- version: 1,
212
- children: rows.map((row: any) => ({
213
- type: 'tablerow',
214
- version: 1,
215
- key: row.key,
216
- children: (row.cells ?? []).map((cell: any) => ({
217
- type: 'tablecell',
218
- version: 1,
219
- key: cell.key,
220
- children: (cell.children ?? []).map(convertInlineNode)
221
- }))
222
- }))
223
- };
224
- }
225
-
226
- // ── Layout conversion ────────────────────────────────────────────────
227
-
228
- function convertLayout(node: KogumaBlockNode): LexicalNode {
229
- const columns = (node as any).columns ?? [];
230
- return {
231
- type: 'layoutcontainer',
232
- version: 1,
233
- children: columns.map((col: KogumaBlockNode[]) => ({
234
- type: 'layoutitem',
235
- version: 1,
236
- children: col.map(convertBlockNode)
237
- }))
238
- };
239
- }
240
-
241
- // ── Inline node conversion ───────────────────────────────────────────
242
-
243
- function convertInlineNode(node: KogumaInlineNode): LexicalNode {
244
- switch (node.type) {
245
- case 'text': {
246
- let format = 0;
247
- if (node.bold) format |= IS_BOLD;
248
- if (node.italic) format |= IS_ITALIC;
249
- if (node.underline) format |= IS_UNDERLINE;
250
- if (node.strikethrough) format |= IS_STRIKETHROUGH;
251
- if (node.code) format |= IS_CODE;
252
- if (node.subscript) format |= IS_SUBSCRIPT;
253
- if (node.superscript) format |= IS_SUPERSCRIPT;
254
-
255
- return {
256
- type: 'text',
257
- version: 1,
258
- text: node.text ?? '',
259
- format,
260
- detail: 0,
261
- mode: 'normal',
262
- style: ''
263
- };
264
- }
265
-
266
- case 'link':
267
- return {
268
- type: 'link',
269
- version: 1,
270
- url: node.url ?? '',
271
- target: node.newTab ? '_blank' : null,
272
- children: (node.children ?? []).map(convertInlineNode),
273
- direction: 'ltr',
274
- format: '',
275
- indent: 0,
276
- rel: null
277
- };
278
-
279
- case 'line-break':
280
- return {
281
- type: 'linebreak',
282
- version: 1
283
- };
284
-
285
- case 'custom':
286
- return {
287
- type: node.name ?? 'unknown',
288
- version: 1,
289
- ...(node.data ?? {})
290
- };
291
-
292
- default:
293
- return {
294
- type: 'text',
295
- version: 1,
296
- text: (node as any).text ?? '',
297
- format: 0,
298
- detail: 0,
299
- mode: 'normal',
300
- style: ''
301
- };
302
- }
303
- }
304
-
305
- // ── Helpers ──────────────────────────────────────────────────────────
306
-
307
- function alignToFormat(align?: string): string | number {
308
- switch (align) {
309
- case 'left':
310
- return 1;
311
- case 'center':
312
- return 2;
313
- case 'right':
314
- return 3;
315
- case 'justify':
316
- return 4;
317
- default:
318
- return '';
319
- }
320
- }
321
-
322
- /** Convert plain code text to Lexical CodeHighlightNode children */
323
- function codeTextToHighlightNodes(text: string): LexicalNode[] {
324
- const lines = text.split('\n');
325
- const nodes: LexicalNode[] = [];
326
-
327
- for (let i = 0; i < lines.length; i++) {
328
- if (i > 0) {
329
- nodes.push({ type: 'linebreak', version: 1 });
330
- }
331
- nodes.push({
332
- type: 'code-highlight',
333
- version: 1,
334
- text: lines[i]!,
335
- highlightType: null
336
- });
337
- }
338
-
339
- return nodes;
340
- }