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.
@@ -0,0 +1,164 @@
1
+ /**
2
+ * markdown-to-koguma.ts — Lightweight markdown → KogumaDocument converter.
3
+ *
4
+ * Designed for seed content (author-controlled markdown), not arbitrary user input.
5
+ * Supports: headings, paragraphs, bold, italic, code, links, lists, code blocks, HR.
6
+ */
7
+ import type {
8
+ KogumaDocument,
9
+ KogumaBlockNode,
10
+ KogumaInlineNode,
11
+ KogumaListItem
12
+ } from '../config/types.ts';
13
+
14
+ // ── Inline parsing ──────────────────────────────────────────────────
15
+
16
+ function parseInline(text: string): KogumaInlineNode[] {
17
+ const nodes: KogumaInlineNode[] = [];
18
+ // Regex: links, bold, italic, inline code
19
+ const re =
20
+ /\[([^\]]+)\]\(([^)]+)\)|`([^`]+)`|\*\*(.+?)\*\*|\*(.+?)\*|([^[`*]+)/g;
21
+ let match: RegExpExecArray | null;
22
+
23
+ while ((match = re.exec(text)) !== null) {
24
+ if (match[1] !== undefined && match[2] !== undefined) {
25
+ // Link
26
+ nodes.push({
27
+ type: 'link',
28
+ url: match[2],
29
+ children: [{ type: 'text', text: match[1] }]
30
+ });
31
+ } else if (match[3] !== undefined) {
32
+ // Inline code
33
+ nodes.push({ type: 'text', text: match[3], code: true });
34
+ } else if (match[4] !== undefined) {
35
+ // Bold
36
+ nodes.push({ type: 'text', text: match[4], bold: true });
37
+ } else if (match[5] !== undefined) {
38
+ // Italic
39
+ nodes.push({ type: 'text', text: match[5], italic: true });
40
+ } else if (match[6] !== undefined && match[6].length > 0) {
41
+ // Plain text
42
+ nodes.push({ type: 'text', text: match[6] });
43
+ }
44
+ }
45
+
46
+ return nodes.length > 0 ? nodes : [{ type: 'text', text }];
47
+ }
48
+
49
+ // ── Block parsing ───────────────────────────────────────────────────
50
+
51
+ export function markdownToKoguma(markdown: string): KogumaDocument {
52
+ const lines = markdown.split('\n');
53
+ const nodes: KogumaBlockNode[] = [];
54
+ let i = 0;
55
+
56
+ while (i < lines.length) {
57
+ const line = lines[i]!;
58
+
59
+ // Blank line — skip
60
+ if (line.trim() === '') {
61
+ i++;
62
+ continue;
63
+ }
64
+
65
+ // Fenced code block
66
+ const codeMatch = line.match(/^```(\w*)$/);
67
+ if (codeMatch) {
68
+ const language = codeMatch[1] || undefined;
69
+ const codeLines: string[] = [];
70
+ i++;
71
+ while (i < lines.length && !lines[i]!.startsWith('```')) {
72
+ codeLines.push(lines[i]!);
73
+ i++;
74
+ }
75
+ i++; // skip closing ```
76
+ nodes.push({ type: 'code', language, text: codeLines.join('\n') });
77
+ continue;
78
+ }
79
+
80
+ // Heading
81
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
82
+ if (headingMatch) {
83
+ const level = headingMatch[1]!.length as 1 | 2 | 3 | 4 | 5 | 6;
84
+ nodes.push({
85
+ type: 'heading',
86
+ level,
87
+ children: parseInline(headingMatch[2]!)
88
+ });
89
+ i++;
90
+ continue;
91
+ }
92
+
93
+ // Horizontal rule
94
+ if (/^(-{3,}|\*{3,}|_{3,})$/.test(line.trim())) {
95
+ nodes.push({ type: 'hr' });
96
+ i++;
97
+ continue;
98
+ }
99
+
100
+ // Unordered list
101
+ if (/^[-*+]\s/.test(line)) {
102
+ const items: KogumaListItem[] = [];
103
+ while (i < lines.length && /^[-*+]\s/.test(lines[i]!)) {
104
+ items.push({
105
+ children: parseInline(lines[i]!.replace(/^[-*+]\s/, ''))
106
+ });
107
+ i++;
108
+ }
109
+ nodes.push({ type: 'list', ordered: false, items });
110
+ continue;
111
+ }
112
+
113
+ // Ordered list
114
+ if (/^\d+\.\s/.test(line)) {
115
+ const items: KogumaListItem[] = [];
116
+ while (i < lines.length && /^\d+\.\s/.test(lines[i]!)) {
117
+ items.push({
118
+ children: parseInline(lines[i]!.replace(/^\d+\.\s/, ''))
119
+ });
120
+ i++;
121
+ }
122
+ nodes.push({ type: 'list', ordered: true, items });
123
+ continue;
124
+ }
125
+
126
+ // Blockquote
127
+ if (line.startsWith('> ')) {
128
+ const quoteLines: string[] = [];
129
+ while (i < lines.length && lines[i]!.startsWith('> ')) {
130
+ quoteLines.push(lines[i]!.slice(2));
131
+ i++;
132
+ }
133
+ nodes.push({
134
+ type: 'quote',
135
+ children: parseInline(quoteLines.join(' '))
136
+ });
137
+ continue;
138
+ }
139
+
140
+ // Paragraph (default)
141
+ const paraLines: string[] = [];
142
+ while (
143
+ i < lines.length &&
144
+ lines[i]!.trim() !== '' &&
145
+ !lines[i]!.startsWith('#') &&
146
+ !lines[i]!.startsWith('```') &&
147
+ !/^[-*+]\s/.test(lines[i]!) &&
148
+ !/^\d+\.\s/.test(lines[i]!) &&
149
+ !lines[i]!.startsWith('> ') &&
150
+ !/^(-{3,}|\*{3,}|_{3,})$/.test(lines[i]!.trim())
151
+ ) {
152
+ paraLines.push(lines[i]!);
153
+ i++;
154
+ }
155
+ if (paraLines.length > 0) {
156
+ nodes.push({
157
+ type: 'paragraph',
158
+ children: parseInline(paraLines.join(' '))
159
+ });
160
+ }
161
+ }
162
+
163
+ return { nodes };
164
+ }