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
package/src/react/RichText.tsx
DELETED
|
@@ -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
|
-
}
|
package/src/rich-text/index.ts
DELETED
|
@@ -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
|
-
}
|