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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koguma",
3
- "version": "0.6.5",
3
+ "version": "0.6.6",
4
4
  "description": "🐻 A little CMS with big heart — schema-driven, runs on Cloudflare's free tier",
5
5
  "type": "module",
6
6
  "license": "MIT",
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
- processed[fieldId] = JSON.stringify(doc);
125
- resolutions.push(`${fieldId}: markdown → KogumaDocument`);
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;
@@ -1,3 +1,4 @@
1
1
  export { lexicalToKoguma } from './lexical-to-koguma.ts';
2
+ export { kogumaToLexical } from './koguma-to-lexical.ts';
2
3
  export { richTextToPlain } from './plain.ts';
3
4
  export type { PlainTextOptions } from './plain.ts';
@@ -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
+ }