koguma 0.6.5 → 0.6.6
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/cli/index.ts +4 -1
- package/package.json +1 -1
- package/src/db/sql.ts +6 -3
- package/src/rich-text/index.ts +1 -0
- package/src/rich-text/koguma-to-lexical.ts +340 -0
package/cli/index.ts
CHANGED
|
@@ -529,6 +529,8 @@ async function cmdSeed() {
|
|
|
529
529
|
// Lazy-load markdown converter
|
|
530
530
|
const { markdownToKoguma } =
|
|
531
531
|
await import('../src/rich-text/markdown-to-koguma.ts');
|
|
532
|
+
const { kogumaToLexical } =
|
|
533
|
+
await import('../src/rich-text/koguma-to-lexical.ts');
|
|
532
534
|
|
|
533
535
|
let totalEntries = 0;
|
|
534
536
|
|
|
@@ -546,7 +548,8 @@ async function cmdSeed() {
|
|
|
546
548
|
entry,
|
|
547
549
|
ct.fieldMeta,
|
|
548
550
|
assetIndex,
|
|
549
|
-
markdownToKoguma
|
|
551
|
+
markdownToKoguma,
|
|
552
|
+
kogumaToLexical
|
|
550
553
|
);
|
|
551
554
|
|
|
552
555
|
for (const r of resolutions) {
|
package/package.json
CHANGED
package/src/db/sql.ts
CHANGED
|
@@ -99,7 +99,9 @@ export function processSeedEntry(
|
|
|
99
99
|
entry: Record<string, unknown>,
|
|
100
100
|
fieldMeta: Record<string, FieldMeta>,
|
|
101
101
|
assetIndex: AssetIndex,
|
|
102
|
-
markdownToKoguma: (md: string) => { nodes: unknown[] }
|
|
102
|
+
markdownToKoguma: (md: string) => { nodes: unknown[] },
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
104
|
+
kogumaToLexical: (doc: any) => Record<string, unknown>
|
|
103
105
|
): { processed: Record<string, unknown>; resolutions: string[] } {
|
|
104
106
|
const processed: Record<string, unknown> = {
|
|
105
107
|
id: (entry.id as string) ?? crypto.randomUUID(),
|
|
@@ -121,8 +123,9 @@ export function processSeedEntry(
|
|
|
121
123
|
}
|
|
122
124
|
} else if (meta.fieldType === 'richText' && typeof value === 'string') {
|
|
123
125
|
const doc = markdownToKoguma(value);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
+
const lexical = kogumaToLexical(doc);
|
|
127
|
+
processed[fieldId] = JSON.stringify(lexical);
|
|
128
|
+
resolutions.push(`${fieldId}: markdown → Lexical JSON`);
|
|
126
129
|
} else if (meta.fieldType === 'images' && Array.isArray(value)) {
|
|
127
130
|
const ids = value.map((v: unknown) => {
|
|
128
131
|
if (typeof v !== 'string') return v;
|
package/src/rich-text/index.ts
CHANGED
|
@@ -0,0 +1,340 @@
|
|
|
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
|
+
}
|