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,208 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* plain.test.ts — unit tests for richTextToPlain()
|
|
3
|
-
*/
|
|
4
|
-
import { describe, test, expect } from 'bun:test';
|
|
5
|
-
import { richTextToPlain } from './plain.ts';
|
|
6
|
-
import type { KogumaDocument } from '../config/types.ts';
|
|
7
|
-
|
|
8
|
-
const emptyDoc: KogumaDocument = { nodes: [] };
|
|
9
|
-
|
|
10
|
-
const simpleDoc: KogumaDocument = {
|
|
11
|
-
nodes: [
|
|
12
|
-
{ type: 'paragraph', children: [{ type: 'text', text: 'Hello world' }] }
|
|
13
|
-
]
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const richDoc: KogumaDocument = {
|
|
17
|
-
nodes: [
|
|
18
|
-
{ type: 'heading', level: 1, children: [{ type: 'text', text: 'Title' }] },
|
|
19
|
-
{
|
|
20
|
-
type: 'paragraph',
|
|
21
|
-
children: [
|
|
22
|
-
{ type: 'text', text: 'Some ' },
|
|
23
|
-
{ type: 'text', text: 'bold', bold: true },
|
|
24
|
-
{ type: 'text', text: ' text' }
|
|
25
|
-
]
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
type: 'list',
|
|
29
|
-
ordered: false,
|
|
30
|
-
items: [
|
|
31
|
-
{ children: [{ type: 'text', text: 'Item one' }] },
|
|
32
|
-
{ children: [{ type: 'text', text: 'Item two' }] }
|
|
33
|
-
]
|
|
34
|
-
},
|
|
35
|
-
{ type: 'hr' },
|
|
36
|
-
{ type: 'code', text: 'const x = 1' }
|
|
37
|
-
]
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
describe('richTextToPlain', () => {
|
|
41
|
-
test('null input returns empty string', () => {
|
|
42
|
-
expect(richTextToPlain(null)).toBe('');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test('undefined input returns empty string', () => {
|
|
46
|
-
expect(richTextToPlain(undefined)).toBe('');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test('empty doc returns empty string', () => {
|
|
50
|
-
expect(richTextToPlain(emptyDoc)).toBe('');
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test('extracts plain text from paragraph', () => {
|
|
54
|
-
expect(richTextToPlain(simpleDoc)).toBe('Hello world');
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test('extracts text from heading', () => {
|
|
58
|
-
const doc: KogumaDocument = {
|
|
59
|
-
nodes: [
|
|
60
|
-
{
|
|
61
|
-
type: 'heading',
|
|
62
|
-
level: 2,
|
|
63
|
-
children: [{ type: 'text', text: 'My Heading' }]
|
|
64
|
-
}
|
|
65
|
-
]
|
|
66
|
-
};
|
|
67
|
-
expect(richTextToPlain(doc)).toBe('My Heading');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test('strips marks — just returns text content', () => {
|
|
71
|
-
const doc: KogumaDocument = {
|
|
72
|
-
nodes: [
|
|
73
|
-
{
|
|
74
|
-
type: 'paragraph',
|
|
75
|
-
children: [
|
|
76
|
-
{ type: 'text', text: 'bold', bold: true },
|
|
77
|
-
{ type: 'text', text: ' and ' },
|
|
78
|
-
{ type: 'text', text: 'italic', italic: true }
|
|
79
|
-
]
|
|
80
|
-
}
|
|
81
|
-
]
|
|
82
|
-
};
|
|
83
|
-
expect(richTextToPlain(doc)).toBe('bold and italic');
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test('extracts text from multiple block types', () => {
|
|
87
|
-
const result = richTextToPlain(richDoc);
|
|
88
|
-
expect(result).toContain('Title');
|
|
89
|
-
expect(result).toContain('Some bold text');
|
|
90
|
-
expect(result).toContain('Item one');
|
|
91
|
-
expect(result).toContain('Item two');
|
|
92
|
-
expect(result).toContain('const x = 1');
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test('link contributes its text, not its URL', () => {
|
|
96
|
-
const doc: KogumaDocument = {
|
|
97
|
-
nodes: [
|
|
98
|
-
{
|
|
99
|
-
type: 'paragraph',
|
|
100
|
-
children: [
|
|
101
|
-
{
|
|
102
|
-
type: 'link',
|
|
103
|
-
url: 'https://example.com',
|
|
104
|
-
children: [{ type: 'text', text: 'click here' }]
|
|
105
|
-
}
|
|
106
|
-
]
|
|
107
|
-
}
|
|
108
|
-
]
|
|
109
|
-
};
|
|
110
|
-
const result = richTextToPlain(doc);
|
|
111
|
-
expect(result).toBe('click here');
|
|
112
|
-
expect(result).not.toContain('https://');
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test('maxLength truncates with ellipsis', () => {
|
|
116
|
-
const result = richTextToPlain(richDoc, { maxLength: 10 });
|
|
117
|
-
expect(result.endsWith('…')).toBe(true);
|
|
118
|
-
expect(result.length).toBeLessThanOrEqual(11); // 10 + '…'
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test('maxLength does not truncate when text is shorter', () => {
|
|
122
|
-
const result = richTextToPlain(simpleDoc, { maxLength: 500 });
|
|
123
|
-
expect(result).toBe('Hello world');
|
|
124
|
-
expect(result.endsWith('…')).toBe(false);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
test('extracts text from nested list items', () => {
|
|
128
|
-
const doc: KogumaDocument = {
|
|
129
|
-
nodes: [
|
|
130
|
-
{
|
|
131
|
-
type: 'list',
|
|
132
|
-
ordered: false,
|
|
133
|
-
items: [
|
|
134
|
-
{
|
|
135
|
-
children: [{ type: 'text', text: 'Parent' }],
|
|
136
|
-
nestedList: {
|
|
137
|
-
ordered: false,
|
|
138
|
-
items: [{ children: [{ type: 'text', text: 'Child' }] }]
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
]
|
|
142
|
-
}
|
|
143
|
-
]
|
|
144
|
-
};
|
|
145
|
-
const result = richTextToPlain(doc);
|
|
146
|
-
expect(result).toContain('Parent');
|
|
147
|
-
expect(result).toContain('Child');
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
test('extracts text from table cells', () => {
|
|
151
|
-
const doc: KogumaDocument = {
|
|
152
|
-
nodes: [
|
|
153
|
-
{
|
|
154
|
-
type: 'table',
|
|
155
|
-
rows: [
|
|
156
|
-
{
|
|
157
|
-
isHeader: true,
|
|
158
|
-
cells: [{ children: [{ type: 'text', text: 'Name' }] }]
|
|
159
|
-
},
|
|
160
|
-
{ cells: [{ children: [{ type: 'text', text: 'Alice' }] }] }
|
|
161
|
-
]
|
|
162
|
-
}
|
|
163
|
-
]
|
|
164
|
-
};
|
|
165
|
-
const result = richTextToPlain(doc);
|
|
166
|
-
expect(result).toContain('Name');
|
|
167
|
-
expect(result).toContain('Alice');
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
test('inline image alt text is included', () => {
|
|
171
|
-
const doc: KogumaDocument = {
|
|
172
|
-
nodes: [
|
|
173
|
-
{
|
|
174
|
-
type: 'paragraph',
|
|
175
|
-
children: [{ type: 'inline-image', url: 'img.jpg', alt: 'a cat' }]
|
|
176
|
-
}
|
|
177
|
-
]
|
|
178
|
-
};
|
|
179
|
-
expect(richTextToPlain(doc)).toContain('a cat');
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
test('layout block columns are all extracted', () => {
|
|
183
|
-
const doc: KogumaDocument = {
|
|
184
|
-
nodes: [
|
|
185
|
-
{
|
|
186
|
-
type: 'layout',
|
|
187
|
-
columns: [
|
|
188
|
-
[
|
|
189
|
-
{
|
|
190
|
-
type: 'paragraph',
|
|
191
|
-
children: [{ type: 'text', text: 'Column 1' }]
|
|
192
|
-
}
|
|
193
|
-
],
|
|
194
|
-
[
|
|
195
|
-
{
|
|
196
|
-
type: 'paragraph',
|
|
197
|
-
children: [{ type: 'text', text: 'Column 2' }]
|
|
198
|
-
}
|
|
199
|
-
]
|
|
200
|
-
]
|
|
201
|
-
}
|
|
202
|
-
]
|
|
203
|
-
};
|
|
204
|
-
const result = richTextToPlain(doc);
|
|
205
|
-
expect(result).toContain('Column 1');
|
|
206
|
-
expect(result).toContain('Column 2');
|
|
207
|
-
});
|
|
208
|
-
});
|
package/src/rich-text/plain.ts
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* richTextToPlain.ts
|
|
3
|
-
*
|
|
4
|
-
* Extracts plain text from a KogumaDocument.
|
|
5
|
-
* Useful for meta descriptions, search indexes, og:description, card previews.
|
|
6
|
-
*/
|
|
7
|
-
import type {
|
|
8
|
-
KogumaDocument,
|
|
9
|
-
KogumaBlockNode,
|
|
10
|
-
KogumaInlineNode,
|
|
11
|
-
KogumaListItem
|
|
12
|
-
} from '../config/types.ts';
|
|
13
|
-
|
|
14
|
-
export interface PlainTextOptions {
|
|
15
|
-
/** Truncate output to this many characters, appending '…' if truncated */
|
|
16
|
-
maxLength?: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function richTextToPlain(
|
|
20
|
-
doc: KogumaDocument | null | undefined,
|
|
21
|
-
opts?: PlainTextOptions
|
|
22
|
-
): string {
|
|
23
|
-
if (!doc?.nodes?.length) return '';
|
|
24
|
-
|
|
25
|
-
const parts: string[] = [];
|
|
26
|
-
|
|
27
|
-
for (const node of doc.nodes) {
|
|
28
|
-
extractBlockText(node, parts);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
let result = parts.join(' ').replace(/\s+/g, ' ').trim();
|
|
32
|
-
|
|
33
|
-
if (opts?.maxLength && result.length > opts.maxLength) {
|
|
34
|
-
result = result.slice(0, opts.maxLength).trimEnd() + '…';
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return result;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function extractBlockText(node: KogumaBlockNode, out: string[]): void {
|
|
41
|
-
switch (node.type) {
|
|
42
|
-
case 'paragraph':
|
|
43
|
-
case 'heading':
|
|
44
|
-
case 'quote': {
|
|
45
|
-
const text = extractInlineText(node.children);
|
|
46
|
-
if (text) out.push(text);
|
|
47
|
-
break;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
case 'list':
|
|
51
|
-
for (const item of node.items) {
|
|
52
|
-
extractListItemText(item, out);
|
|
53
|
-
}
|
|
54
|
-
break;
|
|
55
|
-
|
|
56
|
-
case 'code':
|
|
57
|
-
if (node.text) out.push(node.text);
|
|
58
|
-
break;
|
|
59
|
-
|
|
60
|
-
case 'table':
|
|
61
|
-
for (const row of node.rows) {
|
|
62
|
-
for (const cell of row.cells) {
|
|
63
|
-
const text = extractInlineText(cell.children);
|
|
64
|
-
if (text) out.push(text);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
break;
|
|
68
|
-
|
|
69
|
-
case 'layout':
|
|
70
|
-
for (const column of node.columns) {
|
|
71
|
-
for (const block of column) {
|
|
72
|
-
extractBlockText(block, out);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
break;
|
|
76
|
-
|
|
77
|
-
case 'image':
|
|
78
|
-
if (node.alt) out.push(node.alt);
|
|
79
|
-
break;
|
|
80
|
-
|
|
81
|
-
case 'hr':
|
|
82
|
-
case 'custom':
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function extractListItemText(item: KogumaListItem, out: string[]): void {
|
|
88
|
-
const text = extractInlineText(item.children);
|
|
89
|
-
if (text) out.push(text);
|
|
90
|
-
if (item.nestedList) {
|
|
91
|
-
for (const nested of item.nestedList.items) {
|
|
92
|
-
extractListItemText(nested, out);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function extractInlineText(nodes: KogumaInlineNode[]): string {
|
|
98
|
-
return nodes
|
|
99
|
-
.map(node => {
|
|
100
|
-
switch (node.type) {
|
|
101
|
-
case 'text':
|
|
102
|
-
return node.text;
|
|
103
|
-
case 'link':
|
|
104
|
-
return extractInlineText(node.children);
|
|
105
|
-
case 'line-break':
|
|
106
|
-
return '\n';
|
|
107
|
-
case 'inline-image':
|
|
108
|
-
return node.alt ?? '';
|
|
109
|
-
case 'custom':
|
|
110
|
-
return '';
|
|
111
|
-
}
|
|
112
|
-
})
|
|
113
|
-
.join('');
|
|
114
|
-
}
|
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* snapshots.test.ts
|
|
3
|
-
*
|
|
4
|
-
* Golden snapshot tests — ensures lexicalToKoguma() output is stable
|
|
5
|
-
* across refactors. If output changes unexpectedly, these fail.
|
|
6
|
-
*
|
|
7
|
-
* To update snapshots after an intentional change, delete the snapshot
|
|
8
|
-
* assertions and re-run to get the new expected values.
|
|
9
|
-
*/
|
|
10
|
-
import { describe, test, expect } from 'bun:test';
|
|
11
|
-
import { lexicalToKoguma } from './lexical-to-koguma.ts';
|
|
12
|
-
|
|
13
|
-
// ── Complex multi-node fixture (simulates a real blog post) ──────────
|
|
14
|
-
|
|
15
|
-
const BLOG_POST_STATE = {
|
|
16
|
-
root: {
|
|
17
|
-
type: 'root',
|
|
18
|
-
version: 1,
|
|
19
|
-
children: [
|
|
20
|
-
{
|
|
21
|
-
type: 'heading',
|
|
22
|
-
version: 1,
|
|
23
|
-
tag: 'h1',
|
|
24
|
-
children: [
|
|
25
|
-
{
|
|
26
|
-
type: 'text',
|
|
27
|
-
text: 'Getting Started with Koguma',
|
|
28
|
-
format: 0,
|
|
29
|
-
version: 1
|
|
30
|
-
}
|
|
31
|
-
]
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
type: 'paragraph',
|
|
35
|
-
version: 1,
|
|
36
|
-
format: 0,
|
|
37
|
-
children: [
|
|
38
|
-
{ type: 'text', text: 'Koguma is a ', format: 0, version: 1 },
|
|
39
|
-
{ type: 'text', text: 'lightweight', format: 2, version: 1 }, // italic
|
|
40
|
-
{
|
|
41
|
-
type: 'text',
|
|
42
|
-
text: " CMS that runs on Cloudflare's free tier.",
|
|
43
|
-
format: 0,
|
|
44
|
-
version: 1
|
|
45
|
-
}
|
|
46
|
-
]
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
type: 'heading',
|
|
50
|
-
version: 1,
|
|
51
|
-
tag: 'h2',
|
|
52
|
-
children: [
|
|
53
|
-
{ type: 'text', text: 'Key Features', format: 0, version: 1 }
|
|
54
|
-
]
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
type: 'list',
|
|
58
|
-
version: 1,
|
|
59
|
-
listType: 'bullet',
|
|
60
|
-
start: 1,
|
|
61
|
-
tag: 'ul',
|
|
62
|
-
children: [
|
|
63
|
-
{
|
|
64
|
-
type: 'listitem',
|
|
65
|
-
version: 1,
|
|
66
|
-
value: 1,
|
|
67
|
-
children: [
|
|
68
|
-
{
|
|
69
|
-
type: 'text',
|
|
70
|
-
text: 'Schema-driven content types',
|
|
71
|
-
format: 0,
|
|
72
|
-
version: 1
|
|
73
|
-
}
|
|
74
|
-
]
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
type: 'listitem',
|
|
78
|
-
version: 1,
|
|
79
|
-
value: 2,
|
|
80
|
-
children: [
|
|
81
|
-
{ type: 'text', text: 'D1 SQLite storage', format: 0, version: 1 }
|
|
82
|
-
]
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
type: 'listitem',
|
|
86
|
-
version: 1,
|
|
87
|
-
value: 3,
|
|
88
|
-
children: [
|
|
89
|
-
{ type: 'text', text: 'R2 media storage', format: 0, version: 1 }
|
|
90
|
-
]
|
|
91
|
-
}
|
|
92
|
-
]
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
type: 'code',
|
|
96
|
-
version: 1,
|
|
97
|
-
language: 'typescript',
|
|
98
|
-
children: [
|
|
99
|
-
{
|
|
100
|
-
type: 'code-highlight',
|
|
101
|
-
text: 'import',
|
|
102
|
-
highlightType: 'keyword',
|
|
103
|
-
version: 1
|
|
104
|
-
},
|
|
105
|
-
{ type: 'text', text: ' { defineConfig } ', format: 0, version: 1 },
|
|
106
|
-
{
|
|
107
|
-
type: 'code-highlight',
|
|
108
|
-
text: 'from',
|
|
109
|
-
highlightType: 'keyword',
|
|
110
|
-
version: 1
|
|
111
|
-
},
|
|
112
|
-
{ type: 'text', text: " 'koguma'", format: 0, version: 1 }
|
|
113
|
-
]
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
type: 'quote',
|
|
117
|
-
version: 1,
|
|
118
|
-
children: [
|
|
119
|
-
{
|
|
120
|
-
type: 'paragraph',
|
|
121
|
-
version: 1,
|
|
122
|
-
format: 0,
|
|
123
|
-
children: [
|
|
124
|
-
{
|
|
125
|
-
type: 'text',
|
|
126
|
-
text: 'Build something great.',
|
|
127
|
-
format: 0,
|
|
128
|
-
version: 1
|
|
129
|
-
}
|
|
130
|
-
]
|
|
131
|
-
}
|
|
132
|
-
]
|
|
133
|
-
},
|
|
134
|
-
{ type: 'horizontalrule', version: 1, children: [] },
|
|
135
|
-
{
|
|
136
|
-
type: 'paragraph',
|
|
137
|
-
version: 1,
|
|
138
|
-
format: 0,
|
|
139
|
-
children: [
|
|
140
|
-
{ type: 'text', text: 'Read the ', format: 0, version: 1 },
|
|
141
|
-
{
|
|
142
|
-
type: 'link',
|
|
143
|
-
version: 1,
|
|
144
|
-
url: 'https://docs.koguma.dev',
|
|
145
|
-
target: '_blank',
|
|
146
|
-
children: [
|
|
147
|
-
{
|
|
148
|
-
type: 'text',
|
|
149
|
-
text: 'full documentation',
|
|
150
|
-
format: 0,
|
|
151
|
-
version: 1
|
|
152
|
-
}
|
|
153
|
-
]
|
|
154
|
-
},
|
|
155
|
-
{ type: 'text', text: ' to get started.', format: 0, version: 1 }
|
|
156
|
-
]
|
|
157
|
-
}
|
|
158
|
-
]
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
// ── Snapshot assertions ───────────────────────────────────────────────
|
|
163
|
-
|
|
164
|
-
describe('snapshots — KogumaDocument stability', () => {
|
|
165
|
-
test('blog post fixture produces stable output', () => {
|
|
166
|
-
const doc = lexicalToKoguma(BLOG_POST_STATE);
|
|
167
|
-
|
|
168
|
-
expect(doc).toEqual({
|
|
169
|
-
nodes: [
|
|
170
|
-
{
|
|
171
|
-
type: 'heading',
|
|
172
|
-
level: 1,
|
|
173
|
-
key: undefined,
|
|
174
|
-
children: [
|
|
175
|
-
{
|
|
176
|
-
type: 'text',
|
|
177
|
-
key: undefined,
|
|
178
|
-
text: 'Getting Started with Koguma'
|
|
179
|
-
}
|
|
180
|
-
]
|
|
181
|
-
},
|
|
182
|
-
{
|
|
183
|
-
type: 'paragraph',
|
|
184
|
-
align: undefined,
|
|
185
|
-
key: undefined,
|
|
186
|
-
children: [
|
|
187
|
-
{ type: 'text', key: undefined, text: 'Koguma is a ' },
|
|
188
|
-
{ type: 'text', key: undefined, text: 'lightweight', italic: true },
|
|
189
|
-
{
|
|
190
|
-
type: 'text',
|
|
191
|
-
key: undefined,
|
|
192
|
-
text: " CMS that runs on Cloudflare's free tier."
|
|
193
|
-
}
|
|
194
|
-
]
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
type: 'heading',
|
|
198
|
-
level: 2,
|
|
199
|
-
key: undefined,
|
|
200
|
-
children: [{ type: 'text', key: undefined, text: 'Key Features' }]
|
|
201
|
-
},
|
|
202
|
-
{
|
|
203
|
-
type: 'list',
|
|
204
|
-
ordered: false,
|
|
205
|
-
key: undefined,
|
|
206
|
-
items: [
|
|
207
|
-
{
|
|
208
|
-
key: undefined,
|
|
209
|
-
checked: undefined,
|
|
210
|
-
nestedList: undefined,
|
|
211
|
-
children: [
|
|
212
|
-
{
|
|
213
|
-
type: 'text',
|
|
214
|
-
key: undefined,
|
|
215
|
-
text: 'Schema-driven content types'
|
|
216
|
-
}
|
|
217
|
-
]
|
|
218
|
-
},
|
|
219
|
-
{
|
|
220
|
-
key: undefined,
|
|
221
|
-
checked: undefined,
|
|
222
|
-
nestedList: undefined,
|
|
223
|
-
children: [
|
|
224
|
-
{ type: 'text', key: undefined, text: 'D1 SQLite storage' }
|
|
225
|
-
]
|
|
226
|
-
},
|
|
227
|
-
{
|
|
228
|
-
key: undefined,
|
|
229
|
-
checked: undefined,
|
|
230
|
-
nestedList: undefined,
|
|
231
|
-
children: [
|
|
232
|
-
{ type: 'text', key: undefined, text: 'R2 media storage' }
|
|
233
|
-
]
|
|
234
|
-
}
|
|
235
|
-
]
|
|
236
|
-
},
|
|
237
|
-
{
|
|
238
|
-
type: 'code',
|
|
239
|
-
language: 'typescript',
|
|
240
|
-
key: undefined,
|
|
241
|
-
text: "import { defineConfig } from 'koguma'"
|
|
242
|
-
},
|
|
243
|
-
{
|
|
244
|
-
type: 'quote',
|
|
245
|
-
key: undefined,
|
|
246
|
-
children: [
|
|
247
|
-
{ type: 'text', key: undefined, text: 'Build something great.' }
|
|
248
|
-
]
|
|
249
|
-
},
|
|
250
|
-
{ type: 'hr', key: undefined },
|
|
251
|
-
{
|
|
252
|
-
type: 'paragraph',
|
|
253
|
-
align: undefined,
|
|
254
|
-
key: undefined,
|
|
255
|
-
children: [
|
|
256
|
-
{ type: 'text', key: undefined, text: 'Read the ' },
|
|
257
|
-
{
|
|
258
|
-
type: 'link',
|
|
259
|
-
key: undefined,
|
|
260
|
-
url: 'https://docs.koguma.dev',
|
|
261
|
-
newTab: true,
|
|
262
|
-
children: [
|
|
263
|
-
{ type: 'text', key: undefined, text: 'full documentation' }
|
|
264
|
-
]
|
|
265
|
-
},
|
|
266
|
-
{ type: 'text', key: undefined, text: ' to get started.' }
|
|
267
|
-
]
|
|
268
|
-
}
|
|
269
|
-
]
|
|
270
|
-
});
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
test('empty state is stable', () => {
|
|
274
|
-
const doc = lexicalToKoguma({
|
|
275
|
-
root: { type: 'root', version: 1, children: [] }
|
|
276
|
-
});
|
|
277
|
-
expect(doc).toEqual({ nodes: [] });
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
test('null input is stable', () => {
|
|
281
|
-
const doc = lexicalToKoguma(null);
|
|
282
|
-
expect(doc).toEqual({ nodes: [] });
|
|
283
|
-
});
|
|
284
|
-
});
|