@emberkit/core 0.1.2-alpha.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/LICENSE +199 -0
- package/dist/boundaries/error-boundary.js +70 -0
- package/dist/boundaries/errors.js +72 -0
- package/dist/boundaries/index.js +3 -0
- package/dist/boundaries/loading-boundary.js +106 -0
- package/dist/cache/index.js +213 -0
- package/dist/compiler/compiler.js +44 -0
- package/dist/compiler/helpers/attributes.js +35 -0
- package/dist/compiler/helpers/utils.js +31 -0
- package/dist/compiler/index.js +4 -0
- package/dist/compiler/types.js +3 -0
- package/dist/context/index.js +51 -0
- package/dist/context/types.js +1 -0
- package/dist/dev-server/index.js +121 -0
- package/dist/forms/index.js +164 -0
- package/dist/forms/mutations.js +258 -0
- package/dist/hmr/client.js +84 -0
- package/dist/hmr/index.js +2 -0
- package/dist/hmr/types.js +133 -0
- package/dist/hydration/helpers/analyzer.js +94 -0
- package/dist/hydration/helpers/hydration.js +129 -0
- package/dist/hydration/index.js +3 -0
- package/dist/hydration/types.js +18 -0
- package/dist/image/index.js +34 -0
- package/dist/image/processor.js +143 -0
- package/dist/index.js +16 -0
- package/dist/jsx-dev-runtime.js +7 -0
- package/dist/jsx-runtime.js +7 -0
- package/dist/loader/helpers/loader.js +61 -0
- package/dist/loader/index.js +2 -0
- package/dist/loader/types.js +14 -0
- package/dist/markdown/index.js +365 -0
- package/dist/mdx/index.js +156 -0
- package/dist/mdx/loader.js +6 -0
- package/dist/meta/head-registry.js +15 -0
- package/dist/meta/head.js +100 -0
- package/dist/meta/index.js +210 -0
- package/dist/navigation/helpers/navigation.js +53 -0
- package/dist/navigation/helpers/useNavigate.js +10 -0
- package/dist/navigation/index.js +3 -0
- package/dist/navigation/types.js +2 -0
- package/dist/plugin/index.js +74 -0
- package/dist/router/helpers/path.js +74 -0
- package/dist/router/helpers/route.js +109 -0
- package/dist/router/index.js +110 -0
- package/dist/router/types.js +5 -0
- package/dist/runtime/helpers/element.js +52 -0
- package/dist/runtime/helpers/render.js +121 -0
- package/dist/runtime/index.js +132 -0
- package/dist/runtime/types.js +1 -0
- package/dist/signals/helpers/core.js +96 -0
- package/dist/signals/helpers/utils.js +22 -0
- package/dist/signals/index.js +3 -0
- package/dist/signals/types.js +1 -0
- package/dist/ssg/index.js +119 -0
- package/dist/ssr/helpers/render-html.js +55 -0
- package/dist/ssr/helpers/ssr.js +90 -0
- package/dist/ssr/index.js +3 -0
- package/dist/ssr/types.js +24 -0
- package/dist/vite-plugin/index.js +650 -0
- package/dist/vite-plugin/types.js +13 -0
- package/package.json +66 -0
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
import { DEFAULT_CONFIG } from './types.js';
|
|
2
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
3
|
+
import { join, relative, dirname, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const VIRTUAL_EMBERKIT_CONFIG = 'virtual:emberkit-config';
|
|
7
|
+
const VIRTUAL_EMBERKIT_ROUTES = 'virtual:emberkit-routes';
|
|
8
|
+
function resolveConfig(userOptions = {}) {
|
|
9
|
+
return {
|
|
10
|
+
...DEFAULT_CONFIG,
|
|
11
|
+
...userOptions,
|
|
12
|
+
markdown: { ...DEFAULT_CONFIG.markdown, ...userOptions.markdown },
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function emberkitVitePlugin(userOptions = {}) {
|
|
16
|
+
const options = resolveConfig(userOptions);
|
|
17
|
+
let routesCode = `export const routes = [];`;
|
|
18
|
+
return {
|
|
19
|
+
name: 'emberkit:vite-plugin',
|
|
20
|
+
enforce: 'pre',
|
|
21
|
+
config() {
|
|
22
|
+
const pkgRoot = resolve(__dirname, '..', '..');
|
|
23
|
+
const srcDir = join(pkgRoot, 'src');
|
|
24
|
+
return {
|
|
25
|
+
resolve: {
|
|
26
|
+
alias: {
|
|
27
|
+
'@emberkit/core': srcDir,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
esbuild: {
|
|
31
|
+
jsxImportSource: '@emberkit/core',
|
|
32
|
+
},
|
|
33
|
+
optimizeDeps: {
|
|
34
|
+
exclude: ['@emberkit/core'],
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
configResolved(config) {
|
|
39
|
+
const root = config.root;
|
|
40
|
+
const routeDir = join(root, options.routeDir ?? 'src/routes');
|
|
41
|
+
const files = scanRouteFiles(routeDir);
|
|
42
|
+
routesCode = generateRoutesCode(files, routeDir);
|
|
43
|
+
},
|
|
44
|
+
resolveId(id) {
|
|
45
|
+
if (id === VIRTUAL_EMBERKIT_CONFIG) {
|
|
46
|
+
return VIRTUAL_EMBERKIT_CONFIG;
|
|
47
|
+
}
|
|
48
|
+
if (id === VIRTUAL_EMBERKIT_ROUTES) {
|
|
49
|
+
return VIRTUAL_EMBERKIT_ROUTES;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
},
|
|
53
|
+
load(id) {
|
|
54
|
+
if (id === VIRTUAL_EMBERKIT_CONFIG) {
|
|
55
|
+
return `export const config = ${JSON.stringify(options)};`;
|
|
56
|
+
}
|
|
57
|
+
if (id === VIRTUAL_EMBERKIT_ROUTES) {
|
|
58
|
+
return routesCode;
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
},
|
|
62
|
+
transform(code, id) {
|
|
63
|
+
if (id.includes('\u0000'))
|
|
64
|
+
return null;
|
|
65
|
+
const ext = id.split('.').pop() ?? '';
|
|
66
|
+
const isMD = ext === 'md';
|
|
67
|
+
const isMDX = ext === 'mdx';
|
|
68
|
+
if (!isMD && !isMDX) {
|
|
69
|
+
if (ext !== 'tsx' && ext !== 'ts' && ext !== 'jsx' && ext !== 'js') {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
if (isMD || isMDX) {
|
|
75
|
+
return transformMarkdownToJSX(code, id, options);
|
|
76
|
+
}
|
|
77
|
+
return code;
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function transformMarkdownToJSX(code, id, options) {
|
|
82
|
+
const frontmatterMatch = code.match(/^---\n([\s\S]*?)\n---\n?/);
|
|
83
|
+
let frontmatter = {};
|
|
84
|
+
let content = code;
|
|
85
|
+
if (frontmatterMatch) {
|
|
86
|
+
const fmContent = frontmatterMatch[1];
|
|
87
|
+
frontmatter = parseFrontmatter(fmContent);
|
|
88
|
+
content = code.slice(frontmatterMatch[0].length);
|
|
89
|
+
}
|
|
90
|
+
const jsxContent = markdownToJSX(content, options.markdown);
|
|
91
|
+
const exportLines = [];
|
|
92
|
+
if (frontmatter.title) {
|
|
93
|
+
exportLines.push(`export const title = ${JSON.stringify(frontmatter.title)};`);
|
|
94
|
+
}
|
|
95
|
+
if (frontmatter.description) {
|
|
96
|
+
exportLines.push(`export const description = ${JSON.stringify(frontmatter.description)};`);
|
|
97
|
+
}
|
|
98
|
+
if (frontmatter.author) {
|
|
99
|
+
exportLines.push(`export const author = ${JSON.stringify(frontmatter.author)};`);
|
|
100
|
+
}
|
|
101
|
+
if (frontmatter.date) {
|
|
102
|
+
exportLines.push(`export const date = ${JSON.stringify(frontmatter.date)};`);
|
|
103
|
+
}
|
|
104
|
+
exportLines.push(`export const metadata = ${JSON.stringify(frontmatter)};`);
|
|
105
|
+
const componentCode = `
|
|
106
|
+
import { createElement } from '@emberkit/core';
|
|
107
|
+
|
|
108
|
+
${exportLines.join('\n')}
|
|
109
|
+
|
|
110
|
+
const defaultContent = ${JSON.stringify(jsxContent)};
|
|
111
|
+
|
|
112
|
+
function MDContent(props) {
|
|
113
|
+
return createElement('div', {
|
|
114
|
+
className: 'md-content',
|
|
115
|
+
dangerouslySetInnerHTML: { __html: defaultContent }
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export default function MDComponent(props) {
|
|
120
|
+
return createElement('article', {
|
|
121
|
+
className: 'md-doc',
|
|
122
|
+
'data-file': ${JSON.stringify(id)},
|
|
123
|
+
children: [
|
|
124
|
+
createElement(MDContent, props)
|
|
125
|
+
]
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
`;
|
|
129
|
+
return { code: componentCode };
|
|
130
|
+
}
|
|
131
|
+
function parseFrontmatter(content) {
|
|
132
|
+
const result = {};
|
|
133
|
+
const lines = content.split('\n');
|
|
134
|
+
for (const line of lines) {
|
|
135
|
+
const colonIndex = line.indexOf(':');
|
|
136
|
+
if (colonIndex === -1)
|
|
137
|
+
continue;
|
|
138
|
+
const key = line.slice(0, colonIndex).trim();
|
|
139
|
+
let value = line.slice(colonIndex + 1).trim();
|
|
140
|
+
if (value === 'true')
|
|
141
|
+
value = true;
|
|
142
|
+
else if (value === 'false')
|
|
143
|
+
value = false;
|
|
144
|
+
else if (!isNaN(Number(value)))
|
|
145
|
+
value = Number(value);
|
|
146
|
+
else if (typeof value === 'string' && value.startsWith('[')) {
|
|
147
|
+
value = value.replace(/[\[\]]/g, '').split(',').map((s) => s.trim());
|
|
148
|
+
}
|
|
149
|
+
result[key] = value;
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
function markdownToJSX(content, options) {
|
|
154
|
+
let html = content;
|
|
155
|
+
html = processCodeBlocks(html);
|
|
156
|
+
html = processHeadings(html);
|
|
157
|
+
html = processHorizontalRules(html);
|
|
158
|
+
html = processTables(html);
|
|
159
|
+
html = processImages(html);
|
|
160
|
+
html = processLinks(html);
|
|
161
|
+
html = processLists(html);
|
|
162
|
+
html = processBlockquotes(html);
|
|
163
|
+
html = processEmphasis(html);
|
|
164
|
+
html = processParagraphs(html, options.breaks);
|
|
165
|
+
return html;
|
|
166
|
+
}
|
|
167
|
+
function processHeadings(html) {
|
|
168
|
+
return html.replace(/^(#{1,6})\s+(.+)$/gm, (_match, hashes, text) => {
|
|
169
|
+
const id = text.toLowerCase().replace(/[^\w]+/g, '-');
|
|
170
|
+
return `<h${hashes.length} id="${id}">${text}</h${hashes.length}>`;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
function processCodeBlocks(html) {
|
|
174
|
+
return html.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang, code) => {
|
|
175
|
+
let highlighted = code.trim();
|
|
176
|
+
if (lang === 'ts' || lang === 'tsx' || lang === 'js' || lang === 'jsx' || lang === 'typescript' || lang === 'javascript') {
|
|
177
|
+
highlighted = highlightTS(highlighted);
|
|
178
|
+
}
|
|
179
|
+
else if (lang === 'bash' || lang === 'sh' || lang === 'shell') {
|
|
180
|
+
highlighted = highlightBash(highlighted);
|
|
181
|
+
}
|
|
182
|
+
else if (lang === 'json') {
|
|
183
|
+
highlighted = highlightJSON(highlighted);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
highlighted = escapeHtml(highlighted);
|
|
187
|
+
}
|
|
188
|
+
const langAttr = lang ? ` data-lang="${lang}"` : '';
|
|
189
|
+
return `<pre${langAttr}><button class="copy-btn" onclick="(async()=>{await navigator.clipboard.writeText(this.closest('pre').querySelector('code').textContent);this.textContent='Copied!';setTimeout(()=>this.textContent='Copy',1500)})()"><svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\"/><path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"/></svg> Copy</button><code class="language-${lang}">${highlighted}</code></pre>`;
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
function escapeHtml(text) {
|
|
193
|
+
return text
|
|
194
|
+
.replace(/&/g, '&')
|
|
195
|
+
.replace(/</g, '<')
|
|
196
|
+
.replace(/>/g, '>');
|
|
197
|
+
}
|
|
198
|
+
function highlightTS(code) {
|
|
199
|
+
const tokens = [];
|
|
200
|
+
let remaining = code;
|
|
201
|
+
const controlFlow = new Set(['if', 'else', 'for', 'while', 'do', 'switch', 'case', 'break', 'continue', 'return', 'throw', 'try', 'catch', 'finally']);
|
|
202
|
+
const declarations = new Set(['import', 'export', 'from', 'const', 'let', 'var', 'function', 'class', 'extends', 'super', 'enum', 'type', 'interface', 'module', 'namespace', 'declare', 'new', 'delete', 'typeof', 'instanceof', 'in', 'of', 'default', 'as', 'satisfies', 'keyof', 'infer', 'is', 'asserts', 'abstract', 'implements']);
|
|
203
|
+
const modifiers = new Set(['readonly', 'public', 'private', 'protected', 'static', 'abstract', 'async', 'override']);
|
|
204
|
+
const literals = new Set(['true', 'false', 'null', 'undefined', 'this']);
|
|
205
|
+
const builtins = new Set(['console', 'document', 'window', 'Math', 'JSON', 'Array', 'Object', 'String', 'Number', 'Boolean', 'Promise', 'Map', 'Set', 'RegExp', 'Date', 'Error', 'Symbol', 'Record', 'Partial', 'Required', 'Pick', 'Omit', 'Exclude', 'Extract', 'ReturnType', 'Parameters', 'JSX', 'FC', 'Props', 'State', 'Effect', 'Memo', 'Signal', 'Ref', 'Context', 'React']);
|
|
206
|
+
while (remaining.length > 0) {
|
|
207
|
+
let m;
|
|
208
|
+
// Multi-line comment
|
|
209
|
+
m = remaining.match(/^\/\*[\s\S]*?\*\//);
|
|
210
|
+
if (m) {
|
|
211
|
+
tokens.push(`<span class="cm">${escapeHtml(m[0])}</span>`);
|
|
212
|
+
remaining = remaining.slice(m[0].length);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
// Single-line comment
|
|
216
|
+
m = remaining.match(/^\/\/.*/);
|
|
217
|
+
if (m) {
|
|
218
|
+
tokens.push(`<span class="cm">${escapeHtml(m[0])}</span>`);
|
|
219
|
+
remaining = remaining.slice(m[0].length);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
// Template literal with interpolations
|
|
223
|
+
m = remaining.match(/^`/);
|
|
224
|
+
if (m) {
|
|
225
|
+
let tmpl = '`';
|
|
226
|
+
remaining = remaining.slice(1);
|
|
227
|
+
while (remaining.length > 0) {
|
|
228
|
+
if (remaining[0] === '`') {
|
|
229
|
+
tmpl += '`';
|
|
230
|
+
remaining = remaining.slice(1);
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
if (remaining.startsWith('\\')) {
|
|
234
|
+
tmpl += remaining.slice(0, 2);
|
|
235
|
+
remaining = remaining.slice(2);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (remaining.startsWith('${')) {
|
|
239
|
+
tmpl += '${';
|
|
240
|
+
remaining = remaining.slice(2);
|
|
241
|
+
// Parse interpolation until matching }
|
|
242
|
+
let depth = 1;
|
|
243
|
+
let expr = '';
|
|
244
|
+
while (remaining.length > 0 && depth > 0) {
|
|
245
|
+
if (remaining[0] === '{')
|
|
246
|
+
depth++;
|
|
247
|
+
if (remaining[0] === '}') {
|
|
248
|
+
depth--;
|
|
249
|
+
if (depth === 0) {
|
|
250
|
+
remaining = remaining.slice(1);
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
expr += remaining[0];
|
|
255
|
+
remaining = remaining.slice(1);
|
|
256
|
+
}
|
|
257
|
+
tmpl += highlightInlineExpr(expr) + '}';
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
tmpl += remaining[0];
|
|
261
|
+
remaining = remaining.slice(1);
|
|
262
|
+
}
|
|
263
|
+
tokens.push(`<span class="str">${tmpl}</span>`);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
// String (single, double)
|
|
267
|
+
m = remaining.match(/^('[^']*'|"[^"]*")/);
|
|
268
|
+
if (m) {
|
|
269
|
+
tokens.push(`<span class="str">${escapeHtml(m[0])}</span>`);
|
|
270
|
+
remaining = remaining.slice(m[0].length);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
// Arrow function =>
|
|
274
|
+
m = remaining.match(/^=>/);
|
|
275
|
+
if (m) {
|
|
276
|
+
tokens.push(`<span class="op">=></span>`);
|
|
277
|
+
remaining = remaining.slice(2);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
// JSX closing tag </Component>
|
|
281
|
+
m = remaining.match(/^(<\/)([A-Za-z][\w.]*)(>)/);
|
|
282
|
+
if (m) {
|
|
283
|
+
tokens.push(`${escapeHtml(m[1])}<span class="tag">${m[2]}</span>${escapeHtml(m[3])}`);
|
|
284
|
+
remaining = remaining.slice(m[0].length);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
// JSX self-closing or opening tag <Component or <div
|
|
288
|
+
m = remaining.match(/^(<)([A-Za-z][\w.]*)/);
|
|
289
|
+
if (m) {
|
|
290
|
+
tokens.push(`${escapeHtml(m[1])}<span class="tag">${m[2]}</span>`);
|
|
291
|
+
remaining = remaining.slice(m[0].length);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
// JSX prop name (word followed by =)
|
|
295
|
+
m = remaining.match(/^([a-zA-Z_][\w.]*)\s*(?==)/);
|
|
296
|
+
if (m && !['if', 'else', 'for', 'while', 'switch', 'case', 'return', 'import', 'export', 'from', 'const', 'let', 'var', 'function', 'class', 'new', 'typeof', 'instanceof', 'void', 'null', 'undefined', 'true', 'false', 'this'].includes(m[1])) {
|
|
297
|
+
tokens.push(`<span class="attr">${escapeHtml(m[1])}</span>`);
|
|
298
|
+
remaining = remaining.slice(m[1].length);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
// Decorator @Decorator
|
|
302
|
+
m = remaining.match(/^@([A-Za-z_]\w*)/);
|
|
303
|
+
if (m) {
|
|
304
|
+
tokens.push(`<span class="dec">${m[0]}</span>`);
|
|
305
|
+
remaining = remaining.slice(m[0].length);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
// Number (with underscores and scientific notation)
|
|
309
|
+
m = remaining.match(/^(\d[\d_]*\.?[\d_]*([eE][+-]?\d+)?)/);
|
|
310
|
+
if (m) {
|
|
311
|
+
tokens.push(`<span class="num">${m[0]}</span>`);
|
|
312
|
+
remaining = remaining.slice(m[0].length);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
// Multi-char operators (check before single-char)
|
|
316
|
+
m = remaining.match(/^(&&|\|\||===|!==|==|!=|<=|>=|\+\+|--|\*\*|=>|\.\.\.)/);
|
|
317
|
+
if (m) {
|
|
318
|
+
tokens.push(`<span class="op">${escapeHtml(m[0])}</span>`);
|
|
319
|
+
remaining = remaining.slice(m[0].length);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
// Word (keyword, type, builtin, function, identifier)
|
|
323
|
+
m = remaining.match(/^([A-Za-z_$][\w$]*)/);
|
|
324
|
+
if (m) {
|
|
325
|
+
const word = m[1];
|
|
326
|
+
let cls = '';
|
|
327
|
+
if (controlFlow.has(word))
|
|
328
|
+
cls = 'kw';
|
|
329
|
+
else if (declarations.has(word))
|
|
330
|
+
cls = 'kw';
|
|
331
|
+
else if (modifiers.has(word))
|
|
332
|
+
cls = 'kw';
|
|
333
|
+
else if (literals.has(word))
|
|
334
|
+
cls = 'val';
|
|
335
|
+
else if (builtins.has(word))
|
|
336
|
+
cls = 'type';
|
|
337
|
+
else if (/^[A-Z]/.test(word) && word.length > 1)
|
|
338
|
+
cls = 'type';
|
|
339
|
+
else {
|
|
340
|
+
// Check if followed by ( → function call
|
|
341
|
+
const ahead = remaining.slice(word.length);
|
|
342
|
+
if (/^\s*\(/.test(ahead))
|
|
343
|
+
cls = 'fn';
|
|
344
|
+
}
|
|
345
|
+
tokens.push(cls ? `<span class="${cls}">${word}</span>` : word);
|
|
346
|
+
remaining = remaining.slice(word.length);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
// Single-char operators and punctuation
|
|
350
|
+
m = remaining.match(/^([{}()\[\];:,.=<>\-*/|!?~^%])/);
|
|
351
|
+
if (m) {
|
|
352
|
+
tokens.push(`<span class="op">${escapeHtml(m[0])}</span>`);
|
|
353
|
+
remaining = remaining.slice(m[0].length);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
// Whitespace
|
|
357
|
+
m = remaining.match(/^(\s+)/);
|
|
358
|
+
if (m) {
|
|
359
|
+
tokens.push(m[0]);
|
|
360
|
+
remaining = remaining.slice(m[0].length);
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
// Anything else
|
|
364
|
+
tokens.push(escapeHtml(remaining[0]));
|
|
365
|
+
remaining = remaining.slice(1);
|
|
366
|
+
}
|
|
367
|
+
return tokens.join('');
|
|
368
|
+
}
|
|
369
|
+
// Highlight a short inline expression (for template literal interpolations)
|
|
370
|
+
function highlightInlineExpr(code) {
|
|
371
|
+
return escapeHtml(code)
|
|
372
|
+
.replace(/\b(const|let|var|return|if|else|new|typeof|instanceof|async|await|function|import|from|export)\b/g, '<span class="kw">$1</span>')
|
|
373
|
+
.replace(/\b(true|false|null|undefined|this)\b/g, '<span class="val">$1</span>')
|
|
374
|
+
.replace(/(\d+)/g, '<span class="num">$1</span>')
|
|
375
|
+
.replace(/([A-Z][\w]+)/g, '<span class="type">$1</span>');
|
|
376
|
+
}
|
|
377
|
+
function highlightBash(code) {
|
|
378
|
+
const lines = code.split('\n');
|
|
379
|
+
return lines.map(line => {
|
|
380
|
+
const trimmed = line.trimStart();
|
|
381
|
+
// Comment
|
|
382
|
+
if (trimmed.startsWith('#')) {
|
|
383
|
+
return `<span class="cm">${line}</span>`;
|
|
384
|
+
}
|
|
385
|
+
// Content is already HTML-escaped by processCodeBlocks
|
|
386
|
+
// Tokenize directly without double-escaping
|
|
387
|
+
const tokens = [];
|
|
388
|
+
let remaining = line;
|
|
389
|
+
while (remaining.length > 0) {
|
|
390
|
+
// HTML entity — pass through
|
|
391
|
+
let m = remaining.match(/^(&\w+;)/);
|
|
392
|
+
if (m) {
|
|
393
|
+
tokens.push(m[1]);
|
|
394
|
+
remaining = remaining.slice(m[1].length);
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
// Quoted string
|
|
398
|
+
m = remaining.match(/^("[^&]*?"|'[^&]*?')/);
|
|
399
|
+
if (m) {
|
|
400
|
+
tokens.push(`<span class="str">${m[1]}</span>`);
|
|
401
|
+
remaining = remaining.slice(m[1].length);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
// Flag
|
|
405
|
+
m = remaining.match(/^(\s+)(--?[\w-]+)/);
|
|
406
|
+
if (m) {
|
|
407
|
+
tokens.push(`${m[1]}<span class="attr">${m[2]}</span>`);
|
|
408
|
+
remaining = remaining.slice(m[0].length);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
// Word (potential command)
|
|
412
|
+
m = remaining.match(/^([a-zA-Z][\w-]*)/);
|
|
413
|
+
if (m) {
|
|
414
|
+
const cmds = new Set(['sudo', 'cd', 'mkdir', 'rm', 'cp', 'mv', 'ls', 'cat', 'echo', 'npm', 'pnpm', 'yarn', 'git', 'curl', 'chmod', 'export', 'source', 'node', 'npx', 'bun', 'deno', 'grep', 'awk', 'sed', 'find', 'docker', 'kubectl']);
|
|
415
|
+
tokens.push(cmds.has(m[1]) ? `<span class="kw">${m[1]}</span>` : m[1]);
|
|
416
|
+
remaining = remaining.slice(m[1].length);
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
// Anything else
|
|
420
|
+
tokens.push(remaining[0]);
|
|
421
|
+
remaining = remaining.slice(1);
|
|
422
|
+
}
|
|
423
|
+
return tokens.join('');
|
|
424
|
+
}).join('\n');
|
|
425
|
+
}
|
|
426
|
+
function highlightJSON(code) {
|
|
427
|
+
let result = escapeHtml(code);
|
|
428
|
+
// Keys
|
|
429
|
+
result = result.replace(/("[^&]*"|"[^"]*")\s*:/g, '<span class="attr">$1</span>:');
|
|
430
|
+
// String values
|
|
431
|
+
result = result.replace(/:\s*("[^&]*"|"[^"]*")/g, ': <span class="val">$1</span>');
|
|
432
|
+
// Numbers
|
|
433
|
+
result = result.replace(/:\s*(\d+\.?\d*)/g, ': <span class="num">$1</span>');
|
|
434
|
+
// Booleans and null
|
|
435
|
+
result = result.replace(/:\s*(true|false|null)/g, ': <span class="kw">$1</span>');
|
|
436
|
+
return result;
|
|
437
|
+
}
|
|
438
|
+
function processTables(html) {
|
|
439
|
+
const lines = html.split('\n');
|
|
440
|
+
const result = [];
|
|
441
|
+
let i = 0;
|
|
442
|
+
while (i < lines.length) {
|
|
443
|
+
// Check if this line and the next look like a table
|
|
444
|
+
if (i + 1 < lines.length &&
|
|
445
|
+
lines[i].trim().startsWith('|') && lines[i].trim().endsWith('|') &&
|
|
446
|
+
lines[i + 1].trim().match(/^\|[\s\-:|]+\|$/)) {
|
|
447
|
+
// Parse header row
|
|
448
|
+
const headerCells = lines[i].trim().split('|').filter(c => c.trim() !== '');
|
|
449
|
+
result.push('<table>');
|
|
450
|
+
result.push('<thead><tr>');
|
|
451
|
+
for (const cell of headerCells) {
|
|
452
|
+
result.push(`<th>${cell.trim()}</th>`);
|
|
453
|
+
}
|
|
454
|
+
result.push('</tr></thead>');
|
|
455
|
+
result.push('<tbody>');
|
|
456
|
+
// Skip separator row
|
|
457
|
+
i += 2;
|
|
458
|
+
// Parse data rows
|
|
459
|
+
while (i < lines.length && lines[i].trim().startsWith('|') && lines[i].trim().endsWith('|')) {
|
|
460
|
+
const cells = lines[i].trim().split('|').filter(c => c.trim() !== '');
|
|
461
|
+
result.push('<tr>');
|
|
462
|
+
for (const cell of cells) {
|
|
463
|
+
result.push(`<td>${cell.trim()}</td>`);
|
|
464
|
+
}
|
|
465
|
+
result.push('</tr>');
|
|
466
|
+
i++;
|
|
467
|
+
}
|
|
468
|
+
result.push('</tbody>');
|
|
469
|
+
result.push('</table>');
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
result.push(lines[i]);
|
|
473
|
+
i++;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return result.join('\n');
|
|
477
|
+
}
|
|
478
|
+
function processLinks(html) {
|
|
479
|
+
return html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, href) => {
|
|
480
|
+
return `<a href="${href}">${text}</a>`;
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
function processImages(html) {
|
|
484
|
+
return html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_match, alt, src) => {
|
|
485
|
+
return `<img src="${src}" alt="${alt}" loading="lazy">`;
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
function processLists(html) {
|
|
489
|
+
return html
|
|
490
|
+
.replace(/^- \[([ x])\]\s+(.+)/gm, (_match, checked, text) => {
|
|
491
|
+
const isChecked = checked === 'x';
|
|
492
|
+
return `<li class="task"><input type="checkbox" ${isChecked ? 'checked' : ''} disabled>${text}</li>`;
|
|
493
|
+
})
|
|
494
|
+
.replace(/^[-*+]\s+(.+)/gm, '<li>$1</li>')
|
|
495
|
+
.replace(/^\d+\.\s+(.+)/gm, '<li>$1</li>')
|
|
496
|
+
.replace(/(<li>.*<\/li>\n?)+/g, (match) => {
|
|
497
|
+
if (match.includes('class="task"')) {
|
|
498
|
+
return `<ul class="task-list">${match}</ul>`;
|
|
499
|
+
}
|
|
500
|
+
return `<ul>${match}</ul>`;
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
function processBlockquotes(html) {
|
|
504
|
+
const lines = html.split('\n');
|
|
505
|
+
const result = [];
|
|
506
|
+
let inBlockquote = false;
|
|
507
|
+
let depth = 0;
|
|
508
|
+
for (const line of lines) {
|
|
509
|
+
const match = line.match(/^(\s*)>\s?(.*)/);
|
|
510
|
+
if (match) {
|
|
511
|
+
const indent = match[1].length;
|
|
512
|
+
const content = match[2];
|
|
513
|
+
const newDepth = Math.floor(indent / 2) + 1;
|
|
514
|
+
if (!inBlockquote) {
|
|
515
|
+
for (let i = 0; i < newDepth; i++) {
|
|
516
|
+
result.push('<blockquote>');
|
|
517
|
+
}
|
|
518
|
+
depth = newDepth;
|
|
519
|
+
inBlockquote = true;
|
|
520
|
+
}
|
|
521
|
+
else if (newDepth > depth) {
|
|
522
|
+
for (let i = depth; i < newDepth; i++) {
|
|
523
|
+
result.push('<blockquote>');
|
|
524
|
+
}
|
|
525
|
+
depth = newDepth;
|
|
526
|
+
}
|
|
527
|
+
else if (newDepth < depth) {
|
|
528
|
+
for (let i = depth; i > newDepth; i--) {
|
|
529
|
+
result.push('</blockquote>');
|
|
530
|
+
}
|
|
531
|
+
depth = newDepth;
|
|
532
|
+
}
|
|
533
|
+
result.push(content || '<br>');
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
if (inBlockquote) {
|
|
537
|
+
for (let i = depth; i > 0; i--) {
|
|
538
|
+
result.push('</blockquote>');
|
|
539
|
+
}
|
|
540
|
+
inBlockquote = false;
|
|
541
|
+
depth = 0;
|
|
542
|
+
}
|
|
543
|
+
result.push(line);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (inBlockquote) {
|
|
547
|
+
for (let i = depth; i > 0; i--) {
|
|
548
|
+
result.push('</blockquote>');
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return result.join('\n');
|
|
552
|
+
}
|
|
553
|
+
function processHorizontalRules(html) {
|
|
554
|
+
return html.replace(/^([-*_])\s*\1\s*\1[\s-]*$/gm, '<hr>');
|
|
555
|
+
}
|
|
556
|
+
function processEmphasis(html) {
|
|
557
|
+
return html
|
|
558
|
+
.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>')
|
|
559
|
+
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
560
|
+
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
561
|
+
.replace(/~~(.+?)~~/g, '<del>$1</del>')
|
|
562
|
+
.replace(/`([^`]+)`/g, (_match, code) => `<code>${escapeHtml(code)}</code>`);
|
|
563
|
+
}
|
|
564
|
+
function processParagraphs(html, breaks) {
|
|
565
|
+
// Split on pre blocks to avoid processing code content
|
|
566
|
+
const parts = html.split(/(<pre[\s\S]*?<\/pre>)/);
|
|
567
|
+
return parts.map((part) => {
|
|
568
|
+
// Don't process content inside pre tags
|
|
569
|
+
if (part.startsWith('<pre'))
|
|
570
|
+
return part;
|
|
571
|
+
const paragraphs = part.split('\n\n');
|
|
572
|
+
return paragraphs
|
|
573
|
+
.map((p) => {
|
|
574
|
+
p = p.trim();
|
|
575
|
+
if (!p)
|
|
576
|
+
return '';
|
|
577
|
+
if (p.startsWith('<h') || p.startsWith('<ul') || p.startsWith('<ol') ||
|
|
578
|
+
p.startsWith('<pre') || p.startsWith('<blockquote') || p.startsWith('<table') ||
|
|
579
|
+
p.startsWith('<hr')) {
|
|
580
|
+
return p;
|
|
581
|
+
}
|
|
582
|
+
p = p.replace(/\n/g, breaks ? '<br>' : ' ');
|
|
583
|
+
return `<p>${p}</p>`;
|
|
584
|
+
})
|
|
585
|
+
.join('\n');
|
|
586
|
+
}).join('');
|
|
587
|
+
}
|
|
588
|
+
function scanRouteFiles(dir) {
|
|
589
|
+
const files = [];
|
|
590
|
+
const extensions = new Set(['tsx', 'ts', 'jsx', 'js', 'md', 'mdx']);
|
|
591
|
+
function walk(currentDir) {
|
|
592
|
+
let entries;
|
|
593
|
+
try {
|
|
594
|
+
entries = readdirSync(currentDir);
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
for (const entry of entries) {
|
|
600
|
+
const fullPath = join(currentDir, entry);
|
|
601
|
+
const stat = statSync(fullPath);
|
|
602
|
+
if (stat.isDirectory()) {
|
|
603
|
+
walk(fullPath);
|
|
604
|
+
}
|
|
605
|
+
else {
|
|
606
|
+
const ext = entry.split('.').pop() ?? '';
|
|
607
|
+
if (extensions.has(ext)) {
|
|
608
|
+
files.push(fullPath);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
walk(dir);
|
|
614
|
+
return files;
|
|
615
|
+
}
|
|
616
|
+
function generateRoutesCode(files, routeDir) {
|
|
617
|
+
const routeEntries = [];
|
|
618
|
+
for (const file of files) {
|
|
619
|
+
const relativePath = relative(routeDir, file).replace(/\\/g, '/');
|
|
620
|
+
const ext = file.split('.').pop() ?? '';
|
|
621
|
+
const isMarkdown = ext === 'md' || ext === 'mdx';
|
|
622
|
+
// Skip special files
|
|
623
|
+
if (relativePath.includes('_layout') || relativePath.includes('_error') || relativePath.includes('_loading')) {
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
// Skip API routes
|
|
627
|
+
if (relativePath.startsWith('_api/') || relativePath.includes('/_api/')) {
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
let routePath = relativePath
|
|
631
|
+
.replace(/\.(tsx|ts|jsx|js|md|mdx)$/, '')
|
|
632
|
+
.replace(/(^|\/)index$/, '$1')
|
|
633
|
+
.replace(/\[\.\.\.(\w+)\]/g, ':$1*')
|
|
634
|
+
.replace(/\[([^\]]+)\]/g, ':$1');
|
|
635
|
+
if (routePath === '' || routePath === '/') {
|
|
636
|
+
routePath = '/';
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
routePath = '/' + routePath;
|
|
640
|
+
}
|
|
641
|
+
const importPath = file.replace(/\\/g, '/');
|
|
642
|
+
if (isMarkdown) {
|
|
643
|
+
routeEntries.push(` { path: ${JSON.stringify(routePath)}, component: () => import(${JSON.stringify(importPath)}), isMarkdown: true }`);
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
routeEntries.push(` { path: ${JSON.stringify(routePath)}, component: () => import(${JSON.stringify(importPath)}) }`);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return `export const routes = [\n${routeEntries.join(',\n')}\n];`;
|
|
650
|
+
}
|