@leadertechie/md2html 0.1.0-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/.github/workflows/publish.yml +76 -0
- package/GitVersion.yml +7 -0
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/__tests__/index.test.d.ts +2 -0
- package/__tests__/index.test.d.ts.map +1 -0
- package/__tests__/index.test.js +130 -0
- package/__tests__/index.test.ts +155 -0
- package/package.json +43 -0
- package/src/index.ts +7 -0
- package/src/lit-renderer.ts +91 -0
- package/src/parser.ts +142 -0
- package/src/pipeline.ts +66 -0
- package/src/renderer.ts +67 -0
- package/src/types.ts +40 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
name: CI / Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- '**'
|
|
7
|
+
pull_request:
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build-and-test:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- uses: actions/setup-node@v4
|
|
20
|
+
with:
|
|
21
|
+
node-version: '20'
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: npm install
|
|
25
|
+
|
|
26
|
+
- name: Run tests
|
|
27
|
+
run: npm test
|
|
28
|
+
|
|
29
|
+
- name: Build package
|
|
30
|
+
run: npm run build
|
|
31
|
+
|
|
32
|
+
publish:
|
|
33
|
+
needs: build-and-test
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
|
36
|
+
permissions:
|
|
37
|
+
contents: read
|
|
38
|
+
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v4
|
|
41
|
+
with:
|
|
42
|
+
fetch-depth: 0
|
|
43
|
+
|
|
44
|
+
- name: Setup GitVersion
|
|
45
|
+
uses: gittools/actions/gitversion/setup@v0.10.2
|
|
46
|
+
with:
|
|
47
|
+
versionSpec: '5.x'
|
|
48
|
+
|
|
49
|
+
- name: Execute GitVersion
|
|
50
|
+
id: gitversion
|
|
51
|
+
uses: gittools/actions/gitversion/execute@v0.10.2
|
|
52
|
+
with:
|
|
53
|
+
useConfigFile: true
|
|
54
|
+
|
|
55
|
+
- name: Display GitVersion outputs
|
|
56
|
+
run: |
|
|
57
|
+
echo "FullSemVer: ${{ steps.gitversion.outputs.fullSemVer }}"
|
|
58
|
+
|
|
59
|
+
- uses: actions/setup-node@v4
|
|
60
|
+
with:
|
|
61
|
+
node-version: '20'
|
|
62
|
+
registry-url: 'https://registry.npmjs.org/'
|
|
63
|
+
|
|
64
|
+
- name: Install dependencies
|
|
65
|
+
run: npm install
|
|
66
|
+
|
|
67
|
+
- name: Build package
|
|
68
|
+
run: npm run build
|
|
69
|
+
|
|
70
|
+
- name: Update package version
|
|
71
|
+
run: npm version ${{ steps.gitversion.outputs.fullSemVer }} --no-git-tag-version
|
|
72
|
+
|
|
73
|
+
- name: Publish to npm
|
|
74
|
+
run: npm publish --access public
|
|
75
|
+
env:
|
|
76
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/GitVersion.yml
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 leadertechie
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# @leadertechie/md2html
|
|
2
|
+
|
|
3
|
+
A configuration-driven markdown to HTML pipeline that parses markdown to an AST (ContentNode), then renders to HTML strings or Lit templates.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Parse markdown to AST** - Converts markdown to a structured JSON AST (ContentNode[])
|
|
8
|
+
- **Render to HTML string** - Convert AST to plain HTML strings
|
|
9
|
+
- **Render to Lit templates** - Convert AST to Lit TemplateResult for web components
|
|
10
|
+
- **Configuration-driven** - No hardcoded paths or content structure
|
|
11
|
+
- **SSR-ready** - Works in both Node.js and browser environments
|
|
12
|
+
- **Image path handling** - Configurable prefix and base URL for images
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @leadertechie/md2html
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### Basic Usage
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { MarkdownPipeline } from '@leadertechie/md2html';
|
|
26
|
+
|
|
27
|
+
const pipeline = new MarkdownPipeline();
|
|
28
|
+
|
|
29
|
+
const markdown = `# Hello World
|
|
30
|
+
|
|
31
|
+
This is a paragraph with **bold** and *italic* text.
|
|
32
|
+
|
|
33
|
+
- Item 1
|
|
34
|
+
- Item 2
|
|
35
|
+
|
|
36
|
+

|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
// Parse markdown to AST
|
|
40
|
+
const ast = pipeline.parse(markdown);
|
|
41
|
+
|
|
42
|
+
// Render AST to HTML string
|
|
43
|
+
const html = pipeline.render(ast);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Configuration
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { MarkdownPipeline } from '@leadertechie/md2html';
|
|
50
|
+
|
|
51
|
+
const pipeline = new MarkdownPipeline({
|
|
52
|
+
imagePathPrefix: 'images/',
|
|
53
|
+
imageBaseUrl: 'https://cdn.example.com',
|
|
54
|
+
parseOptions: {
|
|
55
|
+
gfm: true,
|
|
56
|
+
breaks: false,
|
|
57
|
+
pedantic: false
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### API
|
|
63
|
+
|
|
64
|
+
| Method | Description |
|
|
65
|
+
|--------|-------------|
|
|
66
|
+
| `parse(markdown)` | Parse markdown string to AST |
|
|
67
|
+
| `render(nodes)` | Render AST to HTML string |
|
|
68
|
+
| `renderMarkdown(markdown)` | Parse and render in one call |
|
|
69
|
+
| `renderPage(title, nodes)` | Render AST to full HTML page |
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["index.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,130 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
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/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@leadertechie/md2html",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "Markdown to HTML pipeline - parse markdown to AST, render to HTML or Lit templates",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"test": "vitest run"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"marked": "^15.0.0"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"lit": "^3.0.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^20.0.0",
|
|
27
|
+
"typescript": "^5.0.0",
|
|
28
|
+
"vitest": "^2.0.0"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"markdown",
|
|
32
|
+
"html",
|
|
33
|
+
"ast",
|
|
34
|
+
"parser",
|
|
35
|
+
"renderer",
|
|
36
|
+
"ssr"
|
|
37
|
+
],
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/leadertechie/md2html.git"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type ContentNodeType =
|
|
2
|
+
| 'text'
|
|
3
|
+
| 'heading'
|
|
4
|
+
| 'paragraph'
|
|
5
|
+
| 'list'
|
|
6
|
+
| 'list-item'
|
|
7
|
+
| 'image'
|
|
8
|
+
| 'code'
|
|
9
|
+
| 'container'
|
|
10
|
+
| 'strong'
|
|
11
|
+
| 'emphasis';
|
|
12
|
+
|
|
13
|
+
export interface ContentNode {
|
|
14
|
+
type: ContentNodeType;
|
|
15
|
+
content?: string;
|
|
16
|
+
children?: ContentNode[];
|
|
17
|
+
attributes?: Record<string, unknown>;
|
|
18
|
+
className?: string;
|
|
19
|
+
src?: string;
|
|
20
|
+
alt?: string;
|
|
21
|
+
ordered?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface MarkdownContent {
|
|
25
|
+
title: string;
|
|
26
|
+
metadata?: Record<string, unknown>;
|
|
27
|
+
content: ContentNode[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ParseOptions {
|
|
31
|
+
gfm?: boolean;
|
|
32
|
+
breaks?: boolean;
|
|
33
|
+
pedantic?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface PipelineConfig {
|
|
37
|
+
imagePathPrefix?: string;
|
|
38
|
+
imageBaseUrl?: string;
|
|
39
|
+
parseOptions?: ParseOptions;
|
|
40
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"declarationMap": true,
|
|
13
|
+
"outDir": "./dist",
|
|
14
|
+
"rootDir": "./src"
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist", "__tests__", "**/*.test.ts"]
|
|
18
|
+
}
|