@leadertechie/md2html 0.1.0-alpha.0 → 0.1.0-alpha.3
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/README.md +28 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/lit-renderer.d.ts +10 -0
- package/dist/lit-renderer.d.ts.map +1 -0
- package/dist/lit-renderer.js +80 -0
- package/dist/parser.d.ts +16 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +117 -0
- package/dist/pipeline.d.ts +18 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +58 -0
- package/dist/renderer.d.ts +15 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +102 -0
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +6 -1
- package/.github/workflows/publish.yml +0 -76
- package/GitVersion.yml +0 -7
- package/__tests__/index.test.d.ts +0 -2
- package/__tests__/index.test.d.ts.map +0 -1
- package/__tests__/index.test.js +0 -130
- package/__tests__/index.test.ts +0 -155
- package/src/index.ts +0 -7
- package/src/lit-renderer.ts +0 -91
- package/src/parser.ts +0 -142
- package/src/pipeline.ts +0 -66
- package/src/renderer.ts +0 -67
- package/src/types.ts +0 -40
- package/tsconfig.json +0 -18
package/__tests__/index.test.js
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { MarkdownParser } from '../src/parser';
|
|
3
|
-
import { HTMLRenderer } from '../src/renderer';
|
|
4
|
-
import { MarkdownPipeline } from '../src/pipeline';
|
|
5
|
-
describe('MarkdownParser', () => {
|
|
6
|
-
let parser;
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
parser = new MarkdownParser();
|
|
9
|
-
});
|
|
10
|
-
describe('parse', () => {
|
|
11
|
-
it('should parse heading', () => {
|
|
12
|
-
const result = parser.parse('# Hello World');
|
|
13
|
-
expect(result.content).toHaveLength(1);
|
|
14
|
-
expect(result.content[0].type).toBe('heading');
|
|
15
|
-
expect(result.content[0].content).toBe('Hello World');
|
|
16
|
-
});
|
|
17
|
-
it('should parse multiple headings', () => {
|
|
18
|
-
const result = parser.parse('# Heading 1\n## Heading 2\n### Heading 3');
|
|
19
|
-
expect(result.content).toHaveLength(3);
|
|
20
|
-
expect(result.content[0].type).toBe('heading');
|
|
21
|
-
});
|
|
22
|
-
it('should parse paragraph', () => {
|
|
23
|
-
const result = parser.parse('This is a paragraph');
|
|
24
|
-
expect(result.content).toHaveLength(1);
|
|
25
|
-
expect(result.content[0].type).toBe('paragraph');
|
|
26
|
-
});
|
|
27
|
-
it('should parse list items', () => {
|
|
28
|
-
const result = parser.parse('- Item 1\n- Item 2\n- Item 3');
|
|
29
|
-
expect(result.content).toHaveLength(1);
|
|
30
|
-
expect(result.content[0].type).toBe('list');
|
|
31
|
-
expect(result.content[0].children).toHaveLength(3);
|
|
32
|
-
});
|
|
33
|
-
it('should parse image in paragraph', () => {
|
|
34
|
-
const result = parser.parse('');
|
|
35
|
-
expect(result.content).toHaveLength(1);
|
|
36
|
-
expect(result.content[0].type).toBe('paragraph');
|
|
37
|
-
expect(result.content[0].children?.[0].type).toBe('image');
|
|
38
|
-
});
|
|
39
|
-
it('should parse code block', () => {
|
|
40
|
-
const result = parser.parse('```javascript\nconst x = 1;\n```');
|
|
41
|
-
expect(result.content).toHaveLength(1);
|
|
42
|
-
expect(result.content[0].type).toBe('code');
|
|
43
|
-
});
|
|
44
|
-
it('should handle image path prefix', () => {
|
|
45
|
-
const parserWithPrefix = new MarkdownParser({ imagePathPrefix: 'images/' });
|
|
46
|
-
const result = parserWithPrefix.parse('');
|
|
47
|
-
expect(result.content[0].children?.[0].src).toBe('images/photo.jpg');
|
|
48
|
-
});
|
|
49
|
-
it('should handle imageBaseUrl', () => {
|
|
50
|
-
const parserWithBaseUrl = new MarkdownParser({ imageBaseUrl: 'https://cdn.example.com/' });
|
|
51
|
-
const result = parserWithBaseUrl.parse('');
|
|
52
|
-
expect(result.content[0].children?.[0].src).toBe('https://cdn.example.com/photo.jpg');
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
describe('HTMLRenderer', () => {
|
|
57
|
-
let renderer;
|
|
58
|
-
beforeEach(() => {
|
|
59
|
-
renderer = new HTMLRenderer();
|
|
60
|
-
});
|
|
61
|
-
describe('renderNode', () => {
|
|
62
|
-
it('should render heading', () => {
|
|
63
|
-
const node = { type: 'heading', content: 'Hello', attributes: { level: '1' } };
|
|
64
|
-
const html = renderer.renderNode(node);
|
|
65
|
-
expect(html).toBe('<h1>Hello</h1>');
|
|
66
|
-
});
|
|
67
|
-
it('should render paragraph', () => {
|
|
68
|
-
const node = { type: 'paragraph', content: 'Hello world' };
|
|
69
|
-
const html = renderer.renderNode(node);
|
|
70
|
-
expect(html).toBe('<p>Hello world</p>');
|
|
71
|
-
});
|
|
72
|
-
it('should render image', () => {
|
|
73
|
-
const node = { type: 'image', src: 'image.jpg', alt: 'Alt text' };
|
|
74
|
-
const html = renderer.renderNode(node);
|
|
75
|
-
expect(html).toBe('<img src="image.jpg" alt="Alt text" class="">');
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
describe('renderNodes', () => {
|
|
79
|
-
it('should render multiple nodes', () => {
|
|
80
|
-
const nodes = [
|
|
81
|
-
{ type: 'heading', content: 'Title', attributes: { level: '1' } },
|
|
82
|
-
{ type: 'paragraph', content: 'Content' }
|
|
83
|
-
];
|
|
84
|
-
const html = renderer.renderNodes(nodes);
|
|
85
|
-
expect(html).toContain('<h1>Title</h1>');
|
|
86
|
-
expect(html).toContain('<p>Content</p>');
|
|
87
|
-
});
|
|
88
|
-
it('should handle empty array', () => {
|
|
89
|
-
const html = renderer.renderNodes([]);
|
|
90
|
-
expect(html).toBe('');
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
describe('MarkdownPipeline', () => {
|
|
95
|
-
let pipeline;
|
|
96
|
-
beforeEach(() => {
|
|
97
|
-
pipeline = new MarkdownPipeline();
|
|
98
|
-
});
|
|
99
|
-
describe('parse', () => {
|
|
100
|
-
it('should parse markdown to nodes', () => {
|
|
101
|
-
const nodes = pipeline.parse('# Hello');
|
|
102
|
-
expect(nodes).toHaveLength(1);
|
|
103
|
-
expect(nodes[0].type).toBe('heading');
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
describe('renderMarkdown', () => {
|
|
107
|
-
it('should parse and render in one call', () => {
|
|
108
|
-
const html = pipeline.renderMarkdown('# Hello World\n\nThis is a paragraph.');
|
|
109
|
-
expect(html).toContain('<h1>Hello World</h1>');
|
|
110
|
-
expect(html).toContain('<p>This is a paragraph.</p>');
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
describe('renderPage', () => {
|
|
114
|
-
it('should render full HTML page', () => {
|
|
115
|
-
const nodes = [
|
|
116
|
-
{ type: 'heading', content: 'Title', attributes: { level: '1' } }
|
|
117
|
-
];
|
|
118
|
-
const page = pipeline.renderPage('My Page', nodes);
|
|
119
|
-
expect(page).toContain('<!DOCTYPE html>');
|
|
120
|
-
expect(page).toContain('<title>My Page</title>');
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
describe('configuration', () => {
|
|
124
|
-
it('should use imagePathPrefix config', () => {
|
|
125
|
-
const pipelineWithConfig = new MarkdownPipeline({ imagePathPrefix: 'images/' });
|
|
126
|
-
const nodes = pipelineWithConfig.parse('');
|
|
127
|
-
expect(nodes[0].children?.[0].src).toBe('images/test.jpg');
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
});
|
package/__tests__/index.test.ts
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { MarkdownParser } from '../src/parser';
|
|
3
|
-
import { HTMLRenderer } from '../src/renderer';
|
|
4
|
-
import { LitRenderer } from '../src/lit-renderer';
|
|
5
|
-
import { MarkdownPipeline } from '../src/pipeline';
|
|
6
|
-
import type { ContentNode } from '../src/types';
|
|
7
|
-
|
|
8
|
-
describe('MarkdownParser', () => {
|
|
9
|
-
let parser: MarkdownParser;
|
|
10
|
-
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
parser = new MarkdownParser();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
describe('parse', () => {
|
|
16
|
-
it('should parse heading', () => {
|
|
17
|
-
const result = parser.parse('# Hello World');
|
|
18
|
-
expect(result.content).toHaveLength(1);
|
|
19
|
-
expect(result.content[0].type).toBe('heading');
|
|
20
|
-
expect(result.content[0].content).toBe('Hello World');
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('should parse multiple headings', () => {
|
|
24
|
-
const result = parser.parse('# Heading 1\n## Heading 2\n### Heading 3');
|
|
25
|
-
expect(result.content).toHaveLength(3);
|
|
26
|
-
expect(result.content[0].type).toBe('heading');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('should parse paragraph', () => {
|
|
30
|
-
const result = parser.parse('This is a paragraph');
|
|
31
|
-
expect(result.content).toHaveLength(1);
|
|
32
|
-
expect(result.content[0].type).toBe('paragraph');
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('should parse list items', () => {
|
|
36
|
-
const result = parser.parse('- Item 1\n- Item 2\n- Item 3');
|
|
37
|
-
expect(result.content).toHaveLength(1);
|
|
38
|
-
expect(result.content[0].type).toBe('list');
|
|
39
|
-
expect(result.content[0].children).toHaveLength(3);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('should parse image in paragraph', () => {
|
|
43
|
-
const result = parser.parse('');
|
|
44
|
-
expect(result.content).toHaveLength(1);
|
|
45
|
-
expect(result.content[0].type).toBe('paragraph');
|
|
46
|
-
expect(result.content[0].children?.[0].type).toBe('image');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('should parse code block', () => {
|
|
50
|
-
const result = parser.parse('```javascript\nconst x = 1;\n```');
|
|
51
|
-
expect(result.content).toHaveLength(1);
|
|
52
|
-
expect(result.content[0].type).toBe('code');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('should handle image path prefix', () => {
|
|
56
|
-
const parserWithPrefix = new MarkdownParser({ imagePathPrefix: 'images/' });
|
|
57
|
-
const result = parserWithPrefix.parse('');
|
|
58
|
-
expect(result.content[0].children?.[0].src).toBe('images/photo.jpg');
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('should handle imageBaseUrl', () => {
|
|
62
|
-
const parserWithBaseUrl = new MarkdownParser({ imageBaseUrl: 'https://cdn.example.com/' });
|
|
63
|
-
const result = parserWithBaseUrl.parse('');
|
|
64
|
-
expect(result.content[0].children?.[0].src).toBe('https://cdn.example.com/photo.jpg');
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
describe('HTMLRenderer', () => {
|
|
70
|
-
let renderer: HTMLRenderer;
|
|
71
|
-
|
|
72
|
-
beforeEach(() => {
|
|
73
|
-
renderer = new HTMLRenderer();
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
describe('renderNode', () => {
|
|
77
|
-
it('should render heading', () => {
|
|
78
|
-
const node: ContentNode = { type: 'heading', content: 'Hello', attributes: { level: '1' } };
|
|
79
|
-
const html = renderer.renderNode(node);
|
|
80
|
-
expect(html).toBe('<h1>Hello</h1>');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('should render paragraph', () => {
|
|
84
|
-
const node: ContentNode = { type: 'paragraph', content: 'Hello world' };
|
|
85
|
-
const html = renderer.renderNode(node);
|
|
86
|
-
expect(html).toBe('<p>Hello world</p>');
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('should render image', () => {
|
|
90
|
-
const node: ContentNode = { type: 'image', src: 'image.jpg', alt: 'Alt text' };
|
|
91
|
-
const html = renderer.renderNode(node);
|
|
92
|
-
expect(html).toBe('<img src="image.jpg" alt="Alt text" class="">');
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
describe('renderNodes', () => {
|
|
97
|
-
it('should render multiple nodes', () => {
|
|
98
|
-
const nodes: ContentNode[] = [
|
|
99
|
-
{ type: 'heading', content: 'Title', attributes: { level: '1' } },
|
|
100
|
-
{ type: 'paragraph', content: 'Content' }
|
|
101
|
-
];
|
|
102
|
-
const html = renderer.renderNodes(nodes);
|
|
103
|
-
expect(html).toContain('<h1>Title</h1>');
|
|
104
|
-
expect(html).toContain('<p>Content</p>');
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('should handle empty array', () => {
|
|
108
|
-
const html = renderer.renderNodes([]);
|
|
109
|
-
expect(html).toBe('');
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
describe('MarkdownPipeline', () => {
|
|
115
|
-
let pipeline: MarkdownPipeline;
|
|
116
|
-
|
|
117
|
-
beforeEach(() => {
|
|
118
|
-
pipeline = new MarkdownPipeline();
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
describe('parse', () => {
|
|
122
|
-
it('should parse markdown to nodes', () => {
|
|
123
|
-
const nodes = pipeline.parse('# Hello');
|
|
124
|
-
expect(nodes).toHaveLength(1);
|
|
125
|
-
expect(nodes[0].type).toBe('heading');
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
describe('renderMarkdown', () => {
|
|
130
|
-
it('should parse and render in one call', () => {
|
|
131
|
-
const html = pipeline.renderMarkdown('# Hello World\n\nThis is a paragraph.');
|
|
132
|
-
expect(html).toContain('<h1>Hello World</h1>');
|
|
133
|
-
expect(html).toContain('<p>This is a paragraph.</p>');
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
describe('renderPage', () => {
|
|
138
|
-
it('should render full HTML page', () => {
|
|
139
|
-
const nodes: ContentNode[] = [
|
|
140
|
-
{ type: 'heading', content: 'Title', attributes: { level: '1' } }
|
|
141
|
-
];
|
|
142
|
-
const page = pipeline.renderPage('My Page', nodes);
|
|
143
|
-
expect(page).toContain('<!DOCTYPE html>');
|
|
144
|
-
expect(page).toContain('<title>My Page</title>');
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
describe('configuration', () => {
|
|
149
|
-
it('should use imagePathPrefix config', () => {
|
|
150
|
-
const pipelineWithConfig = new MarkdownPipeline({ imagePathPrefix: 'images/' });
|
|
151
|
-
const nodes = pipelineWithConfig.parse('');
|
|
152
|
-
expect(nodes[0].children?.[0].src).toBe('images/test.jpg');
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
});
|
package/src/index.ts
DELETED
package/src/lit-renderer.ts
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { TemplateResult, html } from 'lit';
|
|
2
|
-
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
|
3
|
-
import { ContentNode } from './types';
|
|
4
|
-
|
|
5
|
-
export class LitRenderer {
|
|
6
|
-
private renderTextNode(node: ContentNode): TemplateResult {
|
|
7
|
-
if (node.type === 'image') {
|
|
8
|
-
return html`<img src="${node.src}" alt="${node.alt || ''}" class="inline-image" style="max-width:100%;height:auto;">`;
|
|
9
|
-
}
|
|
10
|
-
return html`${unsafeHTML(node.content || '')}`;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
renderNode(node: ContentNode): TemplateResult {
|
|
14
|
-
switch (node.type) {
|
|
15
|
-
case 'heading': {
|
|
16
|
-
const level = node.attributes?.level || '2';
|
|
17
|
-
if (level === '1') return html`<h1>${unsafeHTML(node.content)}</h1>`;
|
|
18
|
-
if (level === '2') return html`<h2>${unsafeHTML(node.content)}</h2>`;
|
|
19
|
-
if (level === '3') return html`<h3>${unsafeHTML(node.content)}</h3>`;
|
|
20
|
-
return html`<h2>${unsafeHTML(node.content)}</h2>`;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
case 'paragraph':
|
|
24
|
-
if (node.children) {
|
|
25
|
-
return html`<p>${node.children.map(child => this.renderTextNode(child))}</p>`;
|
|
26
|
-
}
|
|
27
|
-
return html`<p>${unsafeHTML(node.content)}</p>`;
|
|
28
|
-
|
|
29
|
-
case 'list':
|
|
30
|
-
return html`<ul>${node.children?.map(child => this.renderNode(child))}</ul>`;
|
|
31
|
-
|
|
32
|
-
case 'list-item':
|
|
33
|
-
return html`<li>${unsafeHTML(node.content)}</li>`;
|
|
34
|
-
|
|
35
|
-
case 'image':
|
|
36
|
-
return html`<img src="${node.src || node.attributes?.src}" alt="${node.alt || node.attributes?.alt || ''}" class="${node.className || ''}" style="max-width:100%;height:auto;">`;
|
|
37
|
-
|
|
38
|
-
case 'container':
|
|
39
|
-
return html`<div class="${node.className || ''}" style="${node.attributes?.style || ''}">
|
|
40
|
-
${node.children?.map(child => this.renderNode(child))}
|
|
41
|
-
</div>`;
|
|
42
|
-
|
|
43
|
-
case 'code':
|
|
44
|
-
return html`<pre><code class="language-${node.attributes?.lang || ''}">${node.content || ''}</code></pre>`;
|
|
45
|
-
|
|
46
|
-
case 'text':
|
|
47
|
-
return html`${node.content}`;
|
|
48
|
-
|
|
49
|
-
default:
|
|
50
|
-
return html``;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
renderNodes(nodes: ContentNode[]): TemplateResult {
|
|
55
|
-
if (!nodes || nodes.length === 0) {
|
|
56
|
-
return html``;
|
|
57
|
-
}
|
|
58
|
-
return html`${nodes.map(node => this.renderNode(node))}`;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
renderToHTMLString(nodes: ContentNode[]): string {
|
|
62
|
-
if (!nodes || nodes.length === 0) {
|
|
63
|
-
return '';
|
|
64
|
-
}
|
|
65
|
-
return nodes.map(node => this.nodeToHTMLString(node)).join('\n');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
private nodeToHTMLString(node: ContentNode): string {
|
|
69
|
-
switch (node.type) {
|
|
70
|
-
case 'heading':
|
|
71
|
-
const level = node.attributes?.level || '2';
|
|
72
|
-
return `<h${level}>${node.content}</h${level}>`;
|
|
73
|
-
case 'paragraph':
|
|
74
|
-
return `<p>${node.content}</p>`;
|
|
75
|
-
case 'list':
|
|
76
|
-
const items = node.children?.map(child => this.nodeToHTMLString(child)).join('') || '';
|
|
77
|
-
return `<ul>${items}</ul>`;
|
|
78
|
-
case 'list-item':
|
|
79
|
-
return `<li>${node.content}</li>`;
|
|
80
|
-
case 'image':
|
|
81
|
-
return `<img src="${node.attributes?.src}" alt="${node.attributes?.alt}" class="${node.className || ''}">`;
|
|
82
|
-
case 'container':
|
|
83
|
-
const childrenHTML = node.children?.map(child => this.nodeToHTMLString(child)).join('') || '';
|
|
84
|
-
return `<div class="${node.className || ''}">${childrenHTML}</div>`;
|
|
85
|
-
case 'text':
|
|
86
|
-
return node.content || '';
|
|
87
|
-
default:
|
|
88
|
-
return '';
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
package/src/parser.ts
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { marked } from 'marked';
|
|
2
|
-
import { ContentNode, MarkdownContent, ParseOptions } from './types';
|
|
3
|
-
|
|
4
|
-
export class MarkdownParser {
|
|
5
|
-
private imagePathPrefix: string;
|
|
6
|
-
private imageBaseUrl: string;
|
|
7
|
-
|
|
8
|
-
constructor(options?: { imagePathPrefix?: string; imageBaseUrl?: string }) {
|
|
9
|
-
this.imagePathPrefix = options?.imagePathPrefix || '';
|
|
10
|
-
this.imageBaseUrl = options?.imageBaseUrl || '';
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
private processImagePath(src: string): string {
|
|
14
|
-
if (src.startsWith('http') || src.startsWith('/')) {
|
|
15
|
-
return src;
|
|
16
|
-
}
|
|
17
|
-
let path = this.imagePathPrefix ? `${this.imagePathPrefix}${src}` : src;
|
|
18
|
-
if (this.imageBaseUrl && !path.startsWith('http')) {
|
|
19
|
-
path = `${this.imageBaseUrl}${path}`;
|
|
20
|
-
}
|
|
21
|
-
return path;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
private processInlineFormatting(text: string): string {
|
|
25
|
-
return text
|
|
26
|
-
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
27
|
-
.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
private parseTokens(tokens: unknown[]): ContentNode[] {
|
|
31
|
-
const nodes: ContentNode[] = [];
|
|
32
|
-
|
|
33
|
-
for (const token of tokens) {
|
|
34
|
-
const node = this.parseToken(token as Record<string, unknown>);
|
|
35
|
-
if (node) {
|
|
36
|
-
nodes.push(node);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return nodes;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
private parseToken(token: Record<string, unknown>): ContentNode | null {
|
|
44
|
-
switch (token.type) {
|
|
45
|
-
case 'heading':
|
|
46
|
-
return {
|
|
47
|
-
type: 'heading',
|
|
48
|
-
content: token.text as string,
|
|
49
|
-
attributes: { level: String(token.depth) }
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
case 'paragraph':
|
|
53
|
-
const tokens = (token.tokens as Array<Record<string, unknown>>) || [];
|
|
54
|
-
const hasInlineImage = tokens.some(t => t.type === 'image');
|
|
55
|
-
|
|
56
|
-
if (hasInlineImage) {
|
|
57
|
-
const children = tokens.map(t => {
|
|
58
|
-
if (t.type === 'image') {
|
|
59
|
-
return {
|
|
60
|
-
type: 'image' as const,
|
|
61
|
-
src: this.processImagePath(t.href as string),
|
|
62
|
-
alt: t.text as string || ''
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
return {
|
|
66
|
-
type: 'text' as const,
|
|
67
|
-
content: this.processInlineFormatting(t.text as string || '')
|
|
68
|
-
};
|
|
69
|
-
});
|
|
70
|
-
return {
|
|
71
|
-
type: 'paragraph',
|
|
72
|
-
children
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return {
|
|
77
|
-
type: 'paragraph',
|
|
78
|
-
content: this.processInlineFormatting(token.text as string)
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
case 'list':
|
|
82
|
-
return {
|
|
83
|
-
type: 'list',
|
|
84
|
-
ordered: token.ordered as boolean,
|
|
85
|
-
children: (token.items as Array<Record<string, unknown>>).map((item) => ({
|
|
86
|
-
type: 'list-item',
|
|
87
|
-
content: this.processInlineFormatting(item.text as string)
|
|
88
|
-
}))
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
case 'image':
|
|
92
|
-
return {
|
|
93
|
-
type: 'image',
|
|
94
|
-
src: this.processImagePath(token.href as string),
|
|
95
|
-
alt: token.title as string || ''
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
case 'code':
|
|
99
|
-
return {
|
|
100
|
-
type: 'code',
|
|
101
|
-
content: token.text as string,
|
|
102
|
-
attributes: { lang: token.lang as string || '' }
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
case 'hr':
|
|
106
|
-
return { type: 'container', attributes: { tag: 'hr' } };
|
|
107
|
-
|
|
108
|
-
case 'blockquote':
|
|
109
|
-
return {
|
|
110
|
-
type: 'container',
|
|
111
|
-
attributes: { tag: 'blockquote' },
|
|
112
|
-
children: this.parseTokens((token as Record<string, unknown>).tokens as unknown[] || [])
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
case 'html':
|
|
116
|
-
return { type: 'container', content: token.raw as string };
|
|
117
|
-
|
|
118
|
-
default:
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
parse(markdown: string, options?: ParseOptions): MarkdownContent {
|
|
124
|
-
const parseOptions = {
|
|
125
|
-
gfm: options?.gfm ?? true,
|
|
126
|
-
breaks: options?.breaks ?? false,
|
|
127
|
-
pedantic: options?.pedantic ?? false
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
const tokens = marked.lexer(markdown, parseOptions as Parameters<typeof marked.lexer>[1]);
|
|
131
|
-
const content = this.parseTokens(tokens);
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
title: '',
|
|
135
|
-
content
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
parseToNodes(markdown: string, options?: ParseOptions): ContentNode[] {
|
|
140
|
-
return this.parse(markdown, options).content;
|
|
141
|
-
}
|
|
142
|
-
}
|
package/src/pipeline.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { MarkdownParser } from './parser';
|
|
2
|
-
import { HTMLRenderer } from './renderer';
|
|
3
|
-
import { ContentNode, MarkdownContent, PipelineConfig } from './types';
|
|
4
|
-
|
|
5
|
-
export class MarkdownPipeline {
|
|
6
|
-
private parser: MarkdownParser;
|
|
7
|
-
private renderer: HTMLRenderer;
|
|
8
|
-
private config: Required<PipelineConfig>;
|
|
9
|
-
|
|
10
|
-
constructor(config: PipelineConfig = {}) {
|
|
11
|
-
this.config = {
|
|
12
|
-
imagePathPrefix: config.imagePathPrefix || '',
|
|
13
|
-
imageBaseUrl: config.imageBaseUrl || '',
|
|
14
|
-
parseOptions: {
|
|
15
|
-
gfm: config.parseOptions?.gfm ?? true,
|
|
16
|
-
breaks: config.parseOptions?.breaks ?? false,
|
|
17
|
-
pedantic: config.parseOptions?.pedantic ?? false
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
this.parser = new MarkdownParser({
|
|
22
|
-
imagePathPrefix: this.config.imagePathPrefix,
|
|
23
|
-
imageBaseUrl: this.config.imageBaseUrl
|
|
24
|
-
});
|
|
25
|
-
this.renderer = new HTMLRenderer();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
parse(markdown: string): ContentNode[] {
|
|
29
|
-
return this.parser.parseToNodes(markdown, this.config.parseOptions);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
parseWithMetadata(markdown: string): MarkdownContent {
|
|
33
|
-
return this.parser.parse(markdown, this.config.parseOptions);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
render(nodes: ContentNode[]): string {
|
|
37
|
-
return this.renderer.renderNodes(nodes);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
renderMarkdown(markdown: string): string {
|
|
41
|
-
const nodes = this.parse(markdown);
|
|
42
|
-
return this.render(nodes);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
renderPage(title: string, nodes: ContentNode[], options?: {
|
|
46
|
-
lang?: string;
|
|
47
|
-
charset?: string;
|
|
48
|
-
}): string {
|
|
49
|
-
const html = this.render(nodes);
|
|
50
|
-
return `<!DOCTYPE html>
|
|
51
|
-
<html lang="${options?.lang || 'en'}">
|
|
52
|
-
<head>
|
|
53
|
-
<meta charset="${options?.charset || 'UTF-8'}">
|
|
54
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
55
|
-
<title>${title}</title>
|
|
56
|
-
</head>
|
|
57
|
-
<body>
|
|
58
|
-
${html}
|
|
59
|
-
</body>
|
|
60
|
-
</html>`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
getConfig(): Readonly<Required<PipelineConfig>> {
|
|
64
|
-
return { ...this.config };
|
|
65
|
-
}
|
|
66
|
-
}
|
package/src/renderer.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { ContentNode } from './types';
|
|
2
|
-
|
|
3
|
-
export class HTMLRenderer {
|
|
4
|
-
renderNode(node: ContentNode): string {
|
|
5
|
-
switch (node.type) {
|
|
6
|
-
case 'heading':
|
|
7
|
-
const level = node.attributes?.level || '2';
|
|
8
|
-
return `<h${level}>${node.content || ''}</h${level}>`;
|
|
9
|
-
|
|
10
|
-
case 'paragraph':
|
|
11
|
-
if (node.children) {
|
|
12
|
-
return `<p>${node.children.map(child => this.renderNode(child)).join('')}</p>`;
|
|
13
|
-
}
|
|
14
|
-
return `<p>${node.content || ''}</p>`;
|
|
15
|
-
|
|
16
|
-
case 'list':
|
|
17
|
-
const tag = node.ordered ? 'ol' : 'ul';
|
|
18
|
-
const items = node.children?.map(child => this.renderNode(child)).join('') || '';
|
|
19
|
-
return `<${tag}>${items}</${tag}>`;
|
|
20
|
-
|
|
21
|
-
case 'list-item':
|
|
22
|
-
return `<li>${node.content || ''}</li>`;
|
|
23
|
-
|
|
24
|
-
case 'image':
|
|
25
|
-
const src = node.src || node.attributes?.src || '';
|
|
26
|
-
const alt = node.alt || node.attributes?.alt || '';
|
|
27
|
-
return `<img src="${src}" alt="${alt}" class="${node.className || ''}">`;
|
|
28
|
-
|
|
29
|
-
case 'code':
|
|
30
|
-
return `<pre><code class="language-${node.attributes?.lang || ''}">${node.content || ''}</code></pre>`;
|
|
31
|
-
|
|
32
|
-
case 'container':
|
|
33
|
-
if (node.attributes?.tag === 'hr') return '<hr>';
|
|
34
|
-
if (node.attributes?.tag === 'blockquote') {
|
|
35
|
-
const children = node.children?.map(child => this.renderNode(child)).join('') || '';
|
|
36
|
-
return `<blockquote>${children}</blockquote>`;
|
|
37
|
-
}
|
|
38
|
-
const containerChildren = node.children?.map(child => this.renderNode(child)).join('') || '';
|
|
39
|
-
return `<div class="${node.className || ''}">${containerChildren}</div>`;
|
|
40
|
-
|
|
41
|
-
case 'strong':
|
|
42
|
-
return `<strong>${node.content || ''}</strong>`;
|
|
43
|
-
|
|
44
|
-
case 'emphasis':
|
|
45
|
-
return `<em>${node.content || ''}</em>`;
|
|
46
|
-
|
|
47
|
-
case 'text':
|
|
48
|
-
default:
|
|
49
|
-
return node.content || '';
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
renderNodes(nodes: ContentNode[]): string {
|
|
54
|
-
if (!nodes || nodes.length === 0) {
|
|
55
|
-
return '';
|
|
56
|
-
}
|
|
57
|
-
return nodes.map(node => this.renderNode(node)).join('\n');
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
renderToHTMLString(nodes: ContentNode[]): string {
|
|
61
|
-
return this.renderNodes(nodes);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
render(markdown: string): string {
|
|
65
|
-
return markdown;
|
|
66
|
-
}
|
|
67
|
-
}
|