@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,18 @@
|
|
|
1
|
+
export const INTERACTIVE_ATTRIBUTES = new Set([
|
|
2
|
+
'onClick', 'onMouseDown', 'onMouseUp', 'onMouseEnter', 'onMouseLeave',
|
|
3
|
+
'onFocus', 'onBlur', 'onChange', 'onInput', 'onSubmit',
|
|
4
|
+
'onKeyDown', 'onKeyUp', 'onKeyPress', 'onScroll',
|
|
5
|
+
'onTouchStart', 'onTouchEnd', 'onTouchMove',
|
|
6
|
+
'onDragStart', 'onDrag', 'onDragEnd',
|
|
7
|
+
'onWheel', 'onAnimationStart', 'onAnimationEnd',
|
|
8
|
+
]);
|
|
9
|
+
export const HYDRATABLE_TAGS = new Set([
|
|
10
|
+
'button', 'a', 'input', 'select', 'textarea',
|
|
11
|
+
'form', 'details', 'dialog', 'summary',
|
|
12
|
+
]);
|
|
13
|
+
export function isInteractiveTag(tagName) {
|
|
14
|
+
return HYDRATABLE_TAGS.has(tagName.toLowerCase());
|
|
15
|
+
}
|
|
16
|
+
export function hasEventHandlers(props) {
|
|
17
|
+
return Object.keys(props).some((key) => INTERACTIVE_ATTRIBUTES.has(key));
|
|
18
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function imagePlugin(config = {}) {
|
|
2
|
+
const processor = createImageConfig(config);
|
|
3
|
+
const images = new Set();
|
|
4
|
+
return {
|
|
5
|
+
name: 'emberkit-image',
|
|
6
|
+
version: '0.1.0',
|
|
7
|
+
setup(context) {
|
|
8
|
+
context.onHook('buildEnd', () => {
|
|
9
|
+
optimizeImages(images);
|
|
10
|
+
});
|
|
11
|
+
context.addWatchFile(config.watchDir ?? './src/images');
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function createImageConfig(config) {
|
|
16
|
+
return {
|
|
17
|
+
provider: config.provider ?? 'none',
|
|
18
|
+
baseUrl: config.baseUrl ?? '',
|
|
19
|
+
quality: config.quality ?? 80,
|
|
20
|
+
formats: config.formats ?? ['webp', 'jpeg'],
|
|
21
|
+
sizes: config.sizes ?? [320, 640, 1024, 1920],
|
|
22
|
+
lazy: config.lazy ?? true,
|
|
23
|
+
placeholder: config.placeholder ?? 'blur',
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async function optimizeImages(images) {
|
|
27
|
+
for (const imagePath of images) {
|
|
28
|
+
console.log(`[Image Plugin] Optimizing: ${imagePath}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function createImageVitePlugin(config) {
|
|
32
|
+
return imagePlugin(config);
|
|
33
|
+
}
|
|
34
|
+
export { createImageProcessor } from './processor.js';
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
export class ImageProcessor {
|
|
2
|
+
config;
|
|
3
|
+
constructor(config = {}) {
|
|
4
|
+
this.config = {
|
|
5
|
+
provider: config.provider ?? 'none',
|
|
6
|
+
baseUrl: config.baseUrl ?? '',
|
|
7
|
+
quality: config.quality ?? 80,
|
|
8
|
+
formats: config.formats ?? ['webp', 'jpeg'],
|
|
9
|
+
sizes: config.sizes ?? [320, 640, 1024, 1920],
|
|
10
|
+
lazy: config.lazy ?? true,
|
|
11
|
+
placeholder: config.placeholder ?? 'blur',
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
transform(src, options = {}) {
|
|
15
|
+
const { width = 800, height, format = 'webp', quality } = options;
|
|
16
|
+
const url = this.buildUrl(src, { width, height, format, quality: quality ?? this.config.quality });
|
|
17
|
+
const srcset = this.buildSrcset(src, {
|
|
18
|
+
format,
|
|
19
|
+
quality: quality ?? this.config.quality,
|
|
20
|
+
});
|
|
21
|
+
return {
|
|
22
|
+
src: url,
|
|
23
|
+
srcset,
|
|
24
|
+
width,
|
|
25
|
+
height: height ?? Math.round(width * 0.75),
|
|
26
|
+
format,
|
|
27
|
+
blurDataURL: this.config.placeholder !== 'none' ? this.generatePlaceholder() : undefined,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
buildUrl(src, params) {
|
|
31
|
+
const url = new URL(src, this.config.baseUrl || 'https://example.com');
|
|
32
|
+
if (params.width)
|
|
33
|
+
url.searchParams.set('w', String(params.width));
|
|
34
|
+
if (params.height)
|
|
35
|
+
url.searchParams.set('h', String(params.height));
|
|
36
|
+
if (params.format)
|
|
37
|
+
url.searchParams.set('f', params.format);
|
|
38
|
+
if (params.quality)
|
|
39
|
+
url.searchParams.set('q', String(params.quality));
|
|
40
|
+
return url.toString();
|
|
41
|
+
}
|
|
42
|
+
buildSrcset(src, params) {
|
|
43
|
+
return this.config.sizes
|
|
44
|
+
.map((size) => {
|
|
45
|
+
const url = this.buildUrl(src, { width: size, ...params });
|
|
46
|
+
return `${url} ${size}w`;
|
|
47
|
+
})
|
|
48
|
+
.join(', ');
|
|
49
|
+
}
|
|
50
|
+
generatePlaceholder() {
|
|
51
|
+
return 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMDAiIGhlaWdodD0iMjAwIj48cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZmZmIj48L3JlY3Q+PC9zdmc+';
|
|
52
|
+
}
|
|
53
|
+
analyze(stats) {
|
|
54
|
+
const savingsPercent = parseFloat(((stats.savings / stats.originalSize) * 100).toFixed(1));
|
|
55
|
+
if (savingsPercent > 50) {
|
|
56
|
+
console.log(`[Image] Significant savings: ${savingsPercent}% reduction`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function createImageProcessor(config) {
|
|
61
|
+
return new ImageProcessor(config);
|
|
62
|
+
}
|
|
63
|
+
export function getOptimalFormat(acceptHeader) {
|
|
64
|
+
if (acceptHeader.includes('image/avif'))
|
|
65
|
+
return 'avif';
|
|
66
|
+
if (acceptHeader.includes('image/webp'))
|
|
67
|
+
return 'webp';
|
|
68
|
+
return 'jpeg';
|
|
69
|
+
}
|
|
70
|
+
export function calculateSizes(sizes, viewport) {
|
|
71
|
+
const parsed = sizes.replace(/(\d+)px/g, (_, n) => {
|
|
72
|
+
const vw = (parseInt(n) / viewport) * 100;
|
|
73
|
+
return `${vw.toFixed(2)}vw`;
|
|
74
|
+
});
|
|
75
|
+
return parsed || '100vw';
|
|
76
|
+
}
|
|
77
|
+
export const ASPECT_RATIOS = new Map([
|
|
78
|
+
['1:1', 1],
|
|
79
|
+
['4:3', 4 / 3],
|
|
80
|
+
['16:9', 16 / 9],
|
|
81
|
+
['21:9', 21 / 9],
|
|
82
|
+
['3:2', 3 / 2],
|
|
83
|
+
['2:3', 2 / 3],
|
|
84
|
+
]);
|
|
85
|
+
export function calculateDimensions(width, aspectRatio) {
|
|
86
|
+
const ratio = aspectRatio ? ASPECT_RATIOS.get(aspectRatio) : undefined;
|
|
87
|
+
return {
|
|
88
|
+
width,
|
|
89
|
+
height: ratio ? Math.round(width / ratio) : Math.round(width * 0.75),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export const LOW_QUALITY_IMAGE_SIZES = [20, 40, 80];
|
|
93
|
+
export function generatePlaceholderSVG(width, height, color = '#e0e0e0') {
|
|
94
|
+
return `data:image/svg+xml;base64,${btoa(`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
|
|
95
|
+
<rect width="100%" height="100%" fill="${color}"/>
|
|
96
|
+
<text x="50%" y="50%" text-anchor="middle" fill="#999">Loading...</text>
|
|
97
|
+
</svg>`)}`;
|
|
98
|
+
}
|
|
99
|
+
export function createPictureElement(image, alt, options = {}) {
|
|
100
|
+
const { lazy = true, loading = 'lazy', fetchpriority = 'auto' } = options;
|
|
101
|
+
const sources = image.format !== 'avif'
|
|
102
|
+
? `<source type="image/avif" srcset="${image.srcset.replace(/f=webp/g, 'f=avif')}">`
|
|
103
|
+
: '';
|
|
104
|
+
const webpSource = image.format !== 'webp'
|
|
105
|
+
? `<source type="image/webp" srcset="${image.srcset.replace(/f=[\w]+/g, 'f=webp')}">`
|
|
106
|
+
: '';
|
|
107
|
+
return `<picture>
|
|
108
|
+
${sources}
|
|
109
|
+
${webpSource}
|
|
110
|
+
<img
|
|
111
|
+
src="${image.src}"
|
|
112
|
+
width="${image.width}"
|
|
113
|
+
height="${image.height}"
|
|
114
|
+
alt="${escapeHtml(alt)}"
|
|
115
|
+
loading="${loading}"
|
|
116
|
+
${lazy ? 'decoding="async"' : ''}
|
|
117
|
+
fetchpriority="${fetchpriority}"
|
|
118
|
+
${image.blurDataURL ? `style="background-image:url(${image.blurDataURL})"` : ''}
|
|
119
|
+
>
|
|
120
|
+
</picture>`;
|
|
121
|
+
}
|
|
122
|
+
function escapeHtml(str) {
|
|
123
|
+
return str.replace(/[<>&"']/g, (c) => ({
|
|
124
|
+
'<': '<',
|
|
125
|
+
'>': '>',
|
|
126
|
+
'&': '&',
|
|
127
|
+
'"': '"',
|
|
128
|
+
"'": ''',
|
|
129
|
+
})[c] ?? c);
|
|
130
|
+
}
|
|
131
|
+
export function parseImageSrc(src) {
|
|
132
|
+
const [path, query] = src.split('?');
|
|
133
|
+
const params = {};
|
|
134
|
+
if (query) {
|
|
135
|
+
for (const pair of query.split('&')) {
|
|
136
|
+
const [key, value] = pair.split('=');
|
|
137
|
+
if (key && value) {
|
|
138
|
+
params[key] = value;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { path, params };
|
|
143
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const VERSION = '0.1.0';
|
|
2
|
+
export { createElement, render, hydrate } from './runtime/index.js';
|
|
3
|
+
export { createSignal, createMemo, createEffect, batch, untrack } from './signals/index.js';
|
|
4
|
+
export { createContext, useContext } from './context/index.js';
|
|
5
|
+
export { navigate, preload, useNavigate } from './navigation/index.js';
|
|
6
|
+
export { createRouter, matchRoute } from './router/index.js';
|
|
7
|
+
export { createLoaderData } from './loader/index.js';
|
|
8
|
+
export { createErrorBoundary, createLoadingBoundary } from './boundaries/index.js';
|
|
9
|
+
export { createMarkdownParser, parseMarkdown, renderMarkdown, extractFrontmatter } from './markdown/index.js';
|
|
10
|
+
export { compileMDX, compileSync, useMDX } from './mdx/index.js';
|
|
11
|
+
export { DataCache, createCache, getCached, setCache, prefetch } from './cache/index.js';
|
|
12
|
+
export { Head } from './meta/index.js';
|
|
13
|
+
export { generateMeta, generateBreadcrumbs, generateArticleSchema, generateProductSchema } from './meta/index.js';
|
|
14
|
+
export function defineConfig(config) {
|
|
15
|
+
return config;
|
|
16
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export async function runLoader(loader, context) {
|
|
2
|
+
if (!loader) {
|
|
3
|
+
return { data: undefined };
|
|
4
|
+
}
|
|
5
|
+
try {
|
|
6
|
+
const result = await loader(context);
|
|
7
|
+
return result;
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
if (error instanceof Error) {
|
|
11
|
+
return {
|
|
12
|
+
error: {
|
|
13
|
+
code: 'LOADER_ERROR',
|
|
14
|
+
message: error.message,
|
|
15
|
+
status: 500,
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
error: {
|
|
21
|
+
code: 'UNKNOWN_ERROR',
|
|
22
|
+
message: String(error),
|
|
23
|
+
status: 500,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function extractLoaderData(result) {
|
|
29
|
+
if ('data' in result) {
|
|
30
|
+
return result.data;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
export function extractLoaderError(result) {
|
|
35
|
+
if ('error' in result) {
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
export function mergeLoaderResults(results) {
|
|
41
|
+
const errors = [];
|
|
42
|
+
const data = [];
|
|
43
|
+
for (const result of results) {
|
|
44
|
+
if ('error' in result) {
|
|
45
|
+
errors.push(result);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
data.push(result.data);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (errors.length > 0) {
|
|
52
|
+
return {
|
|
53
|
+
error: {
|
|
54
|
+
code: 'MULTIPLE_ERRORS',
|
|
55
|
+
message: `${errors.length} loaders failed`,
|
|
56
|
+
status: 500,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return { data };
|
|
61
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function isLoaderError(result) {
|
|
2
|
+
return 'error' in result;
|
|
3
|
+
}
|
|
4
|
+
export function isLoaderData(result) {
|
|
5
|
+
return 'data' in result;
|
|
6
|
+
}
|
|
7
|
+
export function createLoaderError(code, message, status = 500) {
|
|
8
|
+
return {
|
|
9
|
+
error: { code, message, status },
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export function createLoaderData(data) {
|
|
13
|
+
return { data };
|
|
14
|
+
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
class MarkdownParser {
|
|
2
|
+
config;
|
|
3
|
+
constructor(config = {}) {
|
|
4
|
+
this.config = {
|
|
5
|
+
gfm: config.gfm ?? true,
|
|
6
|
+
breaks: config.breaks ?? false,
|
|
7
|
+
html: config.html ?? true,
|
|
8
|
+
tables: config.tables ?? true,
|
|
9
|
+
taskLists: config.taskLists ?? true,
|
|
10
|
+
footnotes: config.footnotes ?? false,
|
|
11
|
+
remarkPlugins: config.remarkPlugins ?? [],
|
|
12
|
+
rehypePlugins: config.rehypePlugins ?? [],
|
|
13
|
+
components: config.components ?? {},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
parse(markdown) {
|
|
17
|
+
const frontmatter = this.extractFrontmatter(markdown);
|
|
18
|
+
const content = frontmatter ? this.removeFrontmatter(markdown) : markdown;
|
|
19
|
+
const html = this.renderMarkdown(content);
|
|
20
|
+
const headings = this.extractHeadings(html);
|
|
21
|
+
const links = this.extractLinks(content);
|
|
22
|
+
const images = this.extractImages(content);
|
|
23
|
+
const codeBlocks = this.extractCodeBlocks(content);
|
|
24
|
+
return { html, frontmatter, headings, links, images, codeBlocks };
|
|
25
|
+
}
|
|
26
|
+
extractFrontmatter(markdown) {
|
|
27
|
+
const match = markdown.match(/^---\n([\s\S]*?)\n---/);
|
|
28
|
+
if (!match)
|
|
29
|
+
return undefined;
|
|
30
|
+
const result = {};
|
|
31
|
+
const lines = match[1].split('\n');
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
const colonIndex = line.indexOf(':');
|
|
34
|
+
if (colonIndex === -1)
|
|
35
|
+
continue;
|
|
36
|
+
const key = line.slice(0, colonIndex).trim();
|
|
37
|
+
let value = line.slice(colonIndex + 1).trim();
|
|
38
|
+
if (value === 'true')
|
|
39
|
+
value = true;
|
|
40
|
+
else if (value === 'false')
|
|
41
|
+
value = false;
|
|
42
|
+
else if (!isNaN(Number(value)))
|
|
43
|
+
value = Number(value);
|
|
44
|
+
else if (typeof value === 'string' && value.startsWith('[')) {
|
|
45
|
+
value = value.replace(/[\[\]]/g, '').split(',').map((s) => s.trim());
|
|
46
|
+
}
|
|
47
|
+
result[key] = value;
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
removeFrontmatter(markdown) {
|
|
52
|
+
return markdown.replace(/^---\n[\s\S]*?\n---\n?/, '');
|
|
53
|
+
}
|
|
54
|
+
renderMarkdown(content) {
|
|
55
|
+
let html = content;
|
|
56
|
+
html = this.processCodeBlocks(html);
|
|
57
|
+
html = this.processHeadings(html);
|
|
58
|
+
html = this.processHorizontalRules(html);
|
|
59
|
+
html = this.processLists(html);
|
|
60
|
+
html = this.processTaskLists(html);
|
|
61
|
+
html = this.processTables(html);
|
|
62
|
+
html = this.processImages(html);
|
|
63
|
+
html = this.processLinks(html);
|
|
64
|
+
html = this.processBlockquotes(html);
|
|
65
|
+
html = this.processEmphasis(html);
|
|
66
|
+
html = this.processLineBreaks(html);
|
|
67
|
+
html = this.processComponents(html);
|
|
68
|
+
return html;
|
|
69
|
+
}
|
|
70
|
+
processCodeBlocks(html) {
|
|
71
|
+
return html.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang, code) => {
|
|
72
|
+
const escaped = this.escapeHtml(code.trim());
|
|
73
|
+
return `<pre><code class="language-${lang}">${escaped}</code></pre>`;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
processHeadings(html) {
|
|
77
|
+
return html.replace(/^(#{1,6})\s+(.+)$/gm, (_match, hashes, text) => {
|
|
78
|
+
const level = hashes.length;
|
|
79
|
+
const id = this.slugify(text);
|
|
80
|
+
return `<h${level} id="${id}">${text}</h${level}>`;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
processLists(html) {
|
|
84
|
+
const lines = html.split('\n');
|
|
85
|
+
const result = [];
|
|
86
|
+
let inList = false;
|
|
87
|
+
for (const line of lines) {
|
|
88
|
+
const orderedMatch = line.match(/^(\d+)\.\s+(.+)/);
|
|
89
|
+
const unorderedMatch = line.match(/^[-*+]\s+(.+)/);
|
|
90
|
+
if (orderedMatch || unorderedMatch) {
|
|
91
|
+
const tag = orderedMatch ? 'ol' : 'ul';
|
|
92
|
+
const content = orderedMatch ? orderedMatch[2] : unorderedMatch[2];
|
|
93
|
+
if (!inList) {
|
|
94
|
+
result.push(`<${tag}>`);
|
|
95
|
+
inList = true;
|
|
96
|
+
}
|
|
97
|
+
result.push(`<li>${content}</li>`);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
if (inList) {
|
|
101
|
+
const lastTag = result[result.length - 1]?.startsWith('<ol') ? 'ol' : 'ul';
|
|
102
|
+
result.push(`</${lastTag}>`);
|
|
103
|
+
inList = false;
|
|
104
|
+
}
|
|
105
|
+
result.push(line);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (inList) {
|
|
109
|
+
const lastTag = result[result.length - 1]?.startsWith('<ol') ? 'ol' : 'ul';
|
|
110
|
+
result.push(`</${lastTag}>`);
|
|
111
|
+
}
|
|
112
|
+
return result.join('\n');
|
|
113
|
+
}
|
|
114
|
+
processTaskLists(html) {
|
|
115
|
+
return html.replace(/^- \[([ x])\]\s+(.+)/gm, (_match, checked, text) => {
|
|
116
|
+
const isChecked = checked === 'x';
|
|
117
|
+
return `<li class="task"><input type="checkbox" ${isChecked ? 'checked' : ''} disabled>${text}</li>`;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
processTables(html) {
|
|
121
|
+
const rows = html.split('\n');
|
|
122
|
+
let inTable = false;
|
|
123
|
+
const result = [];
|
|
124
|
+
let headerProcessed = false;
|
|
125
|
+
for (const row of rows) {
|
|
126
|
+
if (row.match(/^\|.*\|$/)) {
|
|
127
|
+
if (!inTable) {
|
|
128
|
+
result.push('<table><thead><tr>');
|
|
129
|
+
inTable = true;
|
|
130
|
+
headerProcessed = false;
|
|
131
|
+
}
|
|
132
|
+
const cells = row.split('|').filter((c) => c.trim()).map((c) => c.trim());
|
|
133
|
+
if (row.match(/^\|[\s-:]+\|$/)) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (!headerProcessed) {
|
|
137
|
+
for (const cell of cells) {
|
|
138
|
+
result.push(`<th>${cell}</th>`);
|
|
139
|
+
}
|
|
140
|
+
result.push('</tr></thead><tbody>');
|
|
141
|
+
headerProcessed = true;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
result.push('<tr>');
|
|
145
|
+
for (const cell of cells) {
|
|
146
|
+
result.push(`<td>${cell}</td>`);
|
|
147
|
+
}
|
|
148
|
+
result.push('</tr>');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
if (inTable) {
|
|
153
|
+
result.push('</tbody></table>');
|
|
154
|
+
inTable = false;
|
|
155
|
+
}
|
|
156
|
+
result.push(row);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (inTable) {
|
|
160
|
+
result.push('</tbody></table>');
|
|
161
|
+
}
|
|
162
|
+
return result.join('\n');
|
|
163
|
+
}
|
|
164
|
+
processLinks(html) {
|
|
165
|
+
return html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, text, href) => {
|
|
166
|
+
const titleMatch = href.match(/^([^"]+)"([^"]+)"/);
|
|
167
|
+
if (titleMatch) {
|
|
168
|
+
return `<a href="${titleMatch[1]}" title="${titleMatch[2]}">${text}</a>`;
|
|
169
|
+
}
|
|
170
|
+
return `<a href="${href}">${text}</a>`;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
processImages(html) {
|
|
174
|
+
return html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_match, alt, src) => {
|
|
175
|
+
return `<img src="${src}" alt="${alt}" loading="lazy">`;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
processBlockquotes(html) {
|
|
179
|
+
const lines = html.split('\n');
|
|
180
|
+
const result = [];
|
|
181
|
+
let inBlockquote = false;
|
|
182
|
+
let depth = 0;
|
|
183
|
+
for (const line of lines) {
|
|
184
|
+
const match = line.match(/^(\s*)>\s?(.*)/);
|
|
185
|
+
if (match) {
|
|
186
|
+
const indent = match[1].length;
|
|
187
|
+
const content = match[2];
|
|
188
|
+
const newDepth = Math.floor(indent / 2) + 1;
|
|
189
|
+
if (!inBlockquote) {
|
|
190
|
+
for (let i = 0; i < newDepth; i++) {
|
|
191
|
+
result.push('<blockquote>');
|
|
192
|
+
}
|
|
193
|
+
depth = newDepth;
|
|
194
|
+
inBlockquote = true;
|
|
195
|
+
}
|
|
196
|
+
else if (newDepth > depth) {
|
|
197
|
+
for (let i = depth; i < newDepth; i++) {
|
|
198
|
+
result.push('<blockquote>');
|
|
199
|
+
}
|
|
200
|
+
depth = newDepth;
|
|
201
|
+
}
|
|
202
|
+
else if (newDepth < depth) {
|
|
203
|
+
for (let i = depth; i > newDepth; i--) {
|
|
204
|
+
result.push('</blockquote>');
|
|
205
|
+
}
|
|
206
|
+
depth = newDepth;
|
|
207
|
+
}
|
|
208
|
+
result.push(content || '<br>');
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
if (inBlockquote) {
|
|
212
|
+
for (let i = depth; i > 0; i--) {
|
|
213
|
+
result.push('</blockquote>');
|
|
214
|
+
}
|
|
215
|
+
inBlockquote = false;
|
|
216
|
+
depth = 0;
|
|
217
|
+
}
|
|
218
|
+
result.push(line);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (inBlockquote) {
|
|
222
|
+
for (let i = depth; i > 0; i--) {
|
|
223
|
+
result.push('</blockquote>');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return result.join('\n');
|
|
227
|
+
}
|
|
228
|
+
processEmphasis(html) {
|
|
229
|
+
return html
|
|
230
|
+
.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>')
|
|
231
|
+
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
232
|
+
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
233
|
+
.replace(/~~(.+?)~~/g, '<del>$1</del>')
|
|
234
|
+
.replace(/`([^`]+)`/g, (_match, code) => `<code>${this.escapeHtml(code)}</code>`);
|
|
235
|
+
}
|
|
236
|
+
processHorizontalRules(html) {
|
|
237
|
+
return html.replace(/^([-*_])\s*\1\s*\1[\s-]*$/gm, '<hr>');
|
|
238
|
+
}
|
|
239
|
+
processLineBreaks(html) {
|
|
240
|
+
if (this.config.breaks) {
|
|
241
|
+
return html.replace(/\n/g, '<br>');
|
|
242
|
+
}
|
|
243
|
+
return html.replace(/\n\n/g, '</p><p>').replace(/\n/g, ' ');
|
|
244
|
+
}
|
|
245
|
+
processComponents(html) {
|
|
246
|
+
for (const [component, tag] of Object.entries(this.config.components)) {
|
|
247
|
+
html = html.replace(new RegExp(`<${component}>`, 'g'), `<${tag}>`);
|
|
248
|
+
html = html.replace(new RegExp(`</${component}>`, 'g'), `</${tag}>`);
|
|
249
|
+
}
|
|
250
|
+
return html;
|
|
251
|
+
}
|
|
252
|
+
extractHeadings(html) {
|
|
253
|
+
const headings = [];
|
|
254
|
+
const regex = /<h(\d) id="([^"]+)">([^<]+)<\/h\1>/g;
|
|
255
|
+
let match;
|
|
256
|
+
while ((match = regex.exec(html)) !== null) {
|
|
257
|
+
headings.push({
|
|
258
|
+
level: parseInt(match[1]),
|
|
259
|
+
text: match[3],
|
|
260
|
+
id: match[2],
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return headings;
|
|
264
|
+
}
|
|
265
|
+
extractLinks(content) {
|
|
266
|
+
const links = [];
|
|
267
|
+
const regex = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
268
|
+
let match;
|
|
269
|
+
while ((match = regex.exec(content)) !== null) {
|
|
270
|
+
const href = match[2];
|
|
271
|
+
if (!href.startsWith('#') && !links.includes(href)) {
|
|
272
|
+
links.push(href);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return links;
|
|
276
|
+
}
|
|
277
|
+
extractImages(content) {
|
|
278
|
+
const images = [];
|
|
279
|
+
const regex = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
|
280
|
+
let match;
|
|
281
|
+
while ((match = regex.exec(content)) !== null) {
|
|
282
|
+
const src = match[2];
|
|
283
|
+
const alt = match[1];
|
|
284
|
+
const titleMatch = src.match(/"([^"]+)"/);
|
|
285
|
+
const title = titleMatch ? titleMatch[1] : undefined;
|
|
286
|
+
images.push({ src, alt, title });
|
|
287
|
+
}
|
|
288
|
+
return images;
|
|
289
|
+
}
|
|
290
|
+
extractCodeBlocks(content) {
|
|
291
|
+
const blocks = [];
|
|
292
|
+
const regex = /```(\w*)\n([\s\S]*?)```/g;
|
|
293
|
+
let match;
|
|
294
|
+
while ((match = regex.exec(content)) !== null) {
|
|
295
|
+
blocks.push({
|
|
296
|
+
lang: match[1],
|
|
297
|
+
code: match[2].trim(),
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
return blocks;
|
|
301
|
+
}
|
|
302
|
+
slugify(text) {
|
|
303
|
+
return text
|
|
304
|
+
.toLowerCase()
|
|
305
|
+
.replace(/[^\w\s-]/g, '')
|
|
306
|
+
.replace(/\s+/g, '-')
|
|
307
|
+
.replace(/-+/g, '-')
|
|
308
|
+
.trim();
|
|
309
|
+
}
|
|
310
|
+
escapeHtml(text) {
|
|
311
|
+
return text
|
|
312
|
+
.replace(/&/g, '&')
|
|
313
|
+
.replace(/</g, '<')
|
|
314
|
+
.replace(/>/g, '>')
|
|
315
|
+
.replace(/"/g, '"');
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
export function createMarkdownParser(options) {
|
|
319
|
+
return new MarkdownParser(options);
|
|
320
|
+
}
|
|
321
|
+
export function parseMarkdown(content, options) {
|
|
322
|
+
const parser = new MarkdownParser(options);
|
|
323
|
+
return parser.parse(content);
|
|
324
|
+
}
|
|
325
|
+
export function renderMarkdown(content, options) {
|
|
326
|
+
const parser = new MarkdownParser(options);
|
|
327
|
+
return parser.parse(content).html;
|
|
328
|
+
}
|
|
329
|
+
export function extractFrontmatter(content) {
|
|
330
|
+
const parser = new MarkdownParser();
|
|
331
|
+
const parsed = parser.parse(content);
|
|
332
|
+
if (!parsed.frontmatter) {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
return {
|
|
336
|
+
data: parsed.frontmatter,
|
|
337
|
+
content: parsed.html,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
export function markdownToPlainText(markdown) {
|
|
341
|
+
return markdown
|
|
342
|
+
.replace(/```[\s\S]*?```/g, '')
|
|
343
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
344
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
345
|
+
.replace(/\*([^*]+)\*/g, '$1')
|
|
346
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
347
|
+
.replace(/!\[([^\]]*)\]\([^)]+\)/g, '')
|
|
348
|
+
.replace(/^#+\s+/gm, '')
|
|
349
|
+
.replace(/^>\s+/gm, '')
|
|
350
|
+
.replace(/^[-*+]\s+/gm, '')
|
|
351
|
+
.replace(/^\d+\.\s+/gm, '');
|
|
352
|
+
}
|
|
353
|
+
export function getReadingTime(text, wpm = 200) {
|
|
354
|
+
const words = text.trim().split(/\s+/).length;
|
|
355
|
+
return Math.ceil(words / wpm);
|
|
356
|
+
}
|
|
357
|
+
export function getWordCount(text) {
|
|
358
|
+
return text.trim().split(/\s+/).length;
|
|
359
|
+
}
|
|
360
|
+
export function getCharacterCount(text, includeSpaces = false) {
|
|
361
|
+
if (includeSpaces) {
|
|
362
|
+
return text.length;
|
|
363
|
+
}
|
|
364
|
+
return text.replace(/\s/g, '').length;
|
|
365
|
+
}
|