koguma 0.6.4 → 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 +584 -54
- package/package.json +1 -1
- package/src/config/define.ts +3 -3
- package/src/db/migrate.ts +7 -1
- package/src/db/sql.ts +169 -0
- package/src/rich-text/index.ts +1 -0
- package/src/rich-text/koguma-to-lexical.ts +340 -0
- package/src/rich-text/markdown-to-koguma.ts +164 -0
package/package.json
CHANGED
package/src/config/define.ts
CHANGED
|
@@ -82,7 +82,7 @@ export function contentType<
|
|
|
82
82
|
displayField: string;
|
|
83
83
|
singleton?: boolean;
|
|
84
84
|
fields: F;
|
|
85
|
-
})
|
|
85
|
+
}) {
|
|
86
86
|
// Collect all field builders (from groups or flat)
|
|
87
87
|
const allFields: Record<string, FieldBuilder> = {};
|
|
88
88
|
const groups: GroupConfig[] = [];
|
|
@@ -116,11 +116,11 @@ export function contentType<
|
|
|
116
116
|
name: opts.name,
|
|
117
117
|
displayField: opts.displayField,
|
|
118
118
|
singleton: opts.singleton,
|
|
119
|
-
schema
|
|
119
|
+
schema,
|
|
120
120
|
fieldMeta,
|
|
121
121
|
groups,
|
|
122
122
|
flatFields
|
|
123
|
-
};
|
|
123
|
+
} satisfies ContentTypeConfig;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
// ── defineConfig ─────────────────────────────────────────────────────
|
package/src/db/migrate.ts
CHANGED
|
@@ -48,7 +48,13 @@ function sqlType(fieldType: string): string {
|
|
|
48
48
|
case 'longText':
|
|
49
49
|
case 'richText':
|
|
50
50
|
case 'url':
|
|
51
|
+
case 'email':
|
|
52
|
+
case 'phone':
|
|
53
|
+
case 'color':
|
|
54
|
+
case 'youtube':
|
|
55
|
+
case 'instagram':
|
|
51
56
|
case 'image':
|
|
57
|
+
case 'images':
|
|
52
58
|
case 'reference':
|
|
53
59
|
case 'date':
|
|
54
60
|
case 'select':
|
|
@@ -76,7 +82,7 @@ export function detectDrift(
|
|
|
76
82
|
|
|
77
83
|
for (const ct of contentTypes) {
|
|
78
84
|
const tableCols = existingColumns[ct.id];
|
|
79
|
-
if (!tableCols) {
|
|
85
|
+
if (!tableCols || tableCols.length === 0) {
|
|
80
86
|
// Table doesn't exist — generate CREATE TABLE
|
|
81
87
|
const columns: string[] = [
|
|
82
88
|
'id TEXT PRIMARY KEY',
|
package/src/db/sql.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sql.ts — Pure SQL generation utilities for D1 operations.
|
|
3
|
+
*
|
|
4
|
+
* These functions are used by seed, import, push, pull, and export commands.
|
|
5
|
+
* Extracted here to enable thorough unit testing without wrangler dependencies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ── Value escaping ──────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Escape a JavaScript value for safe embedding in a SQL string.
|
|
12
|
+
* Returns a SQL-safe string (already quoted if needed).
|
|
13
|
+
*/
|
|
14
|
+
export function escapeValue(v: unknown): string {
|
|
15
|
+
if (v === null || v === undefined) return 'NULL';
|
|
16
|
+
if (typeof v === 'number') return String(v);
|
|
17
|
+
if (typeof v === 'boolean') return v ? '1' : '0';
|
|
18
|
+
return `'${String(v).replace(/'/g, "''")}'`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ── INSERT OR REPLACE generation ────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generate an INSERT OR REPLACE statement from a table name and row data.
|
|
25
|
+
*/
|
|
26
|
+
export function buildInsertSql(
|
|
27
|
+
table: string,
|
|
28
|
+
row: Record<string, unknown>
|
|
29
|
+
): string {
|
|
30
|
+
const cols = Object.keys(row);
|
|
31
|
+
const vals = Object.values(row).map(escapeValue);
|
|
32
|
+
return `INSERT OR REPLACE INTO ${table} (${cols.join(', ')}) VALUES (${vals.join(', ')})`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── Shell-safe SQL escaping ─────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Wrap a SQL statement for safe use in `wrangler d1 execute --command "..."`.
|
|
39
|
+
* Escapes inner double quotes.
|
|
40
|
+
*/
|
|
41
|
+
export function wrapForShell(sql: string): string {
|
|
42
|
+
return sql.replace(/"/g, '\\"');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── Asset title resolution ──────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
export interface AssetIndex {
|
|
48
|
+
/** Map of lowercase title → asset ID */
|
|
49
|
+
titleMap: Map<string, string>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build an asset index from raw _assets rows.
|
|
54
|
+
* Maps both full title and title-without-extension to asset IDs.
|
|
55
|
+
*/
|
|
56
|
+
export function buildAssetIndex(
|
|
57
|
+
assets: { id: string; title: string }[]
|
|
58
|
+
): AssetIndex {
|
|
59
|
+
const titleMap = new Map<string, string>();
|
|
60
|
+
for (const a of assets) {
|
|
61
|
+
titleMap.set(a.title.toLowerCase(), a.id);
|
|
62
|
+
const noExt = a.title.replace(/\.\w+$/, '').toLowerCase();
|
|
63
|
+
if (noExt !== a.title.toLowerCase()) titleMap.set(noExt, a.id);
|
|
64
|
+
}
|
|
65
|
+
return { titleMap };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolve a human-friendly asset reference (title or filename) to an asset ID.
|
|
70
|
+
* Returns the original value if not found.
|
|
71
|
+
*/
|
|
72
|
+
export function resolveAssetRef(
|
|
73
|
+
value: string,
|
|
74
|
+
index: AssetIndex
|
|
75
|
+
): { id: string; resolved: boolean } {
|
|
76
|
+
const lower = value.toLowerCase();
|
|
77
|
+
const id = index.titleMap.get(lower);
|
|
78
|
+
if (id) return { id, resolved: true };
|
|
79
|
+
const noExt = value.replace(/\.\w+$/, '').toLowerCase();
|
|
80
|
+
const id2 = index.titleMap.get(noExt);
|
|
81
|
+
if (id2) return { id: id2, resolved: true };
|
|
82
|
+
return { id: value, resolved: false };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Seed entry processing ───────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
export interface FieldMeta {
|
|
88
|
+
fieldType: string;
|
|
89
|
+
required: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Process a seed entry by resolving image references and converting
|
|
94
|
+
* markdown strings in richText fields to KogumaDocument JSON.
|
|
95
|
+
*
|
|
96
|
+
* Returns a new processed entry (does not mutate the input).
|
|
97
|
+
*/
|
|
98
|
+
export function processSeedEntry(
|
|
99
|
+
entry: Record<string, unknown>,
|
|
100
|
+
fieldMeta: Record<string, FieldMeta>,
|
|
101
|
+
assetIndex: AssetIndex,
|
|
102
|
+
markdownToKoguma: (md: string) => { nodes: unknown[] },
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
104
|
+
kogumaToLexical: (doc: any) => Record<string, unknown>
|
|
105
|
+
): { processed: Record<string, unknown>; resolutions: string[] } {
|
|
106
|
+
const processed: Record<string, unknown> = {
|
|
107
|
+
id: (entry.id as string) ?? crypto.randomUUID(),
|
|
108
|
+
status: (entry.status as string) ?? 'published',
|
|
109
|
+
...entry
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const resolutions: string[] = [];
|
|
113
|
+
|
|
114
|
+
for (const [fieldId, meta] of Object.entries(fieldMeta)) {
|
|
115
|
+
const value = processed[fieldId];
|
|
116
|
+
if (value === undefined || value === null) continue;
|
|
117
|
+
|
|
118
|
+
if (meta.fieldType === 'image' && typeof value === 'string') {
|
|
119
|
+
const result = resolveAssetRef(value, assetIndex);
|
|
120
|
+
if (result.resolved) {
|
|
121
|
+
processed[fieldId] = result.id;
|
|
122
|
+
resolutions.push(`${fieldId}: "${value}" → ${result.id}`);
|
|
123
|
+
}
|
|
124
|
+
} else if (meta.fieldType === 'richText' && typeof value === 'string') {
|
|
125
|
+
const doc = markdownToKoguma(value);
|
|
126
|
+
const lexical = kogumaToLexical(doc);
|
|
127
|
+
processed[fieldId] = JSON.stringify(lexical);
|
|
128
|
+
resolutions.push(`${fieldId}: markdown → Lexical JSON`);
|
|
129
|
+
} else if (meta.fieldType === 'images' && Array.isArray(value)) {
|
|
130
|
+
const ids = value.map((v: unknown) => {
|
|
131
|
+
if (typeof v !== 'string') return v;
|
|
132
|
+
const result = resolveAssetRef(v, assetIndex);
|
|
133
|
+
if (result.resolved) {
|
|
134
|
+
resolutions.push(`${fieldId}: "${v}" → ${result.id}`);
|
|
135
|
+
return result.id;
|
|
136
|
+
}
|
|
137
|
+
return v;
|
|
138
|
+
});
|
|
139
|
+
processed[fieldId] = JSON.stringify(ids);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { processed, resolutions };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Batch SQL generation ────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Generate all INSERT OR REPLACE statements for importing a set of entries
|
|
150
|
+
* into a table, including join table rows.
|
|
151
|
+
*/
|
|
152
|
+
export function buildImportSql(
|
|
153
|
+
typeId: string,
|
|
154
|
+
entries: Record<string, unknown>[],
|
|
155
|
+
joinTables?: Record<string, Record<string, unknown>[]>
|
|
156
|
+
): string[] {
|
|
157
|
+
const statements: string[] = [];
|
|
158
|
+
for (const entry of entries) {
|
|
159
|
+
statements.push(buildInsertSql(typeId, entry));
|
|
160
|
+
}
|
|
161
|
+
if (joinTables) {
|
|
162
|
+
for (const [jtName, rows] of Object.entries(joinTables)) {
|
|
163
|
+
for (const row of rows) {
|
|
164
|
+
statements.push(buildInsertSql(jtName, row));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return statements;
|
|
169
|
+
}
|
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
|
+
}
|