simple-customize-markdown-converter 1.0.2 → 1.0.4
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 +14 -6
- package/dist/lexer.d.ts +14 -1
- package/dist/lexer.js +180 -5
- package/dist/parser.d.ts +5 -0
- package/dist/parser.js +133 -1
- package/dist/renderer.d.ts +1 -0
- package/dist/renderer.js +35 -3
- package/dist/types/node.d.ts +33 -0
- package/dist/types/token.d.ts +39 -4
- package/package.json +40 -40
package/README.md
CHANGED
|
@@ -2,13 +2,21 @@
|
|
|
2
2
|
This simple library help you convert Markdown to HTML and customize it.
|
|
3
3
|
|
|
4
4
|
## Feature
|
|
5
|
-
|
|
6
|
-
- Headings (
|
|
5
|
+
This library currently supports the most common Markdown syntaxes:
|
|
6
|
+
- Headings (`#, ##, …`)
|
|
7
7
|
- Paragraphs
|
|
8
|
-
- Bold (
|
|
9
|
-
- Italic (
|
|
10
|
-
-
|
|
11
|
-
-
|
|
8
|
+
- Bold (`\*\*text\*\*`)
|
|
9
|
+
- Italic (`\*text\* or \_text\_`)
|
|
10
|
+
- Strikethrough (`\~\~text\~\~`)
|
|
11
|
+
- Inline code (`\`code\``)
|
|
12
|
+
- Code blocks (`\`\`\`lang ... \`\`\``)
|
|
13
|
+
- Quotes (`> text`)
|
|
14
|
+
- Lists (`- Item 1,...`)
|
|
15
|
+
- Tasklists (`- [ ], - \[x\]`)
|
|
16
|
+
- Links (`\[link\]\(url\)`)
|
|
17
|
+
- Images (`\[alt\]\(url\)`)
|
|
18
|
+
- Horizontal lines (`---` or `***` or `___`)
|
|
19
|
+
- Tables
|
|
12
20
|
|
|
13
21
|
And customizable renderer for all elements
|
|
14
22
|
|
package/dist/lexer.d.ts
CHANGED
|
@@ -3,25 +3,38 @@ export default class Lexer {
|
|
|
3
3
|
input: string;
|
|
4
4
|
pos: number;
|
|
5
5
|
listToken: Token[];
|
|
6
|
+
listLevelFlag: number;
|
|
6
7
|
constructor(input: string);
|
|
8
|
+
setInput(input: string): void;
|
|
7
9
|
/**
|
|
8
10
|
* Tokenize the markdown into a list of tokens.
|
|
11
|
+
* @param isEof - `True` when input is whole markdown, `False` if input is just a part of markdown.
|
|
9
12
|
* @returns List of tokens
|
|
10
13
|
*/
|
|
11
|
-
tokenize(): Token[];
|
|
14
|
+
tokenize(isEof?: boolean): Token[];
|
|
12
15
|
private peek;
|
|
13
16
|
private next;
|
|
14
17
|
private startsWith;
|
|
15
18
|
private isEndOfFile;
|
|
16
19
|
private getLastToken;
|
|
20
|
+
private handleTable;
|
|
17
21
|
private handleHeader;
|
|
18
22
|
private handleCodeBlock;
|
|
19
23
|
private handleTextBlock;
|
|
20
24
|
private handleItalic;
|
|
21
25
|
private handleBold;
|
|
26
|
+
private handleStrikethrough;
|
|
22
27
|
private handleInlineBlock;
|
|
23
28
|
private handleQuoteBlock;
|
|
29
|
+
private handleList;
|
|
30
|
+
private handleStartList;
|
|
31
|
+
private handleListItem;
|
|
32
|
+
private handleTaskItem;
|
|
33
|
+
private handleEndList;
|
|
24
34
|
private handleLink;
|
|
25
35
|
private handleImage;
|
|
36
|
+
private handleHorizontalLine;
|
|
26
37
|
private readUntil;
|
|
38
|
+
private peekUntil;
|
|
39
|
+
private isStartOfLine;
|
|
27
40
|
}
|
package/dist/lexer.js
CHANGED
|
@@ -4,16 +4,66 @@ class Lexer {
|
|
|
4
4
|
constructor(input) {
|
|
5
5
|
this.pos = 0;
|
|
6
6
|
this.listToken = [];
|
|
7
|
+
// Flag for handle special syntax
|
|
8
|
+
this.listLevelFlag = 0;
|
|
7
9
|
this.input = input;
|
|
8
10
|
}
|
|
11
|
+
//Reset input and other attribute
|
|
12
|
+
setInput(input) {
|
|
13
|
+
this.input = input;
|
|
14
|
+
this.pos = 0;
|
|
15
|
+
this.listLevelFlag = 0;
|
|
16
|
+
this.listToken = [];
|
|
17
|
+
}
|
|
9
18
|
/**
|
|
10
19
|
* Tokenize the markdown into a list of tokens.
|
|
20
|
+
* @param isEof - `True` when input is whole markdown, `False` if input is just a part of markdown.
|
|
11
21
|
* @returns List of tokens
|
|
12
22
|
*/
|
|
13
|
-
tokenize() {
|
|
23
|
+
tokenize(isEof = true) {
|
|
14
24
|
const TOKEN_HANDLER = [
|
|
25
|
+
//Handle escape character first
|
|
26
|
+
{
|
|
27
|
+
match: (lex) => lex.peek() === "\\" && lex.peek(1) !== undefined,
|
|
28
|
+
emit: (lex) => {
|
|
29
|
+
lex.next(1);
|
|
30
|
+
lex.handleTextBlock();
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
//Regex: if line started with at least 3 characters: -, *, _
|
|
35
|
+
match: (lex) => /^([-*_])\1{2,}$/.test(lex.peekUntil("\n").trim()) && this.getLastToken()?.type === "NewLine",
|
|
36
|
+
emit: (lex) => lex.handleHorizontalLine()
|
|
37
|
+
},
|
|
15
38
|
{ match: (lex) => lex.startsWith("```"), emit: (lex) => lex.handleCodeBlock() },
|
|
16
39
|
{ match: (lex) => lex.startsWith("**"), emit: (lex) => lex.handleBold() },
|
|
40
|
+
{ match: (lex) => lex.startsWith("~~"), emit: (lex) => lex.handleStrikethrough() },
|
|
41
|
+
//For List
|
|
42
|
+
{
|
|
43
|
+
match: (lex) => lex.isStartOfLine() && /^(\s*)([-*+]) \[( |x|X)\] /.test(lex.peekUntil("\n")),
|
|
44
|
+
emit: (lex) => lex.handleList(false, true)
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
//Regex: if line started with zero or more spaces, then have - or + or * + 1 space
|
|
48
|
+
match: (lex) => lex.isStartOfLine() && /^(\s*)([-*+]) /.test(lex.peekUntil("\n")),
|
|
49
|
+
emit: (lex) => lex.handleList(false, false)
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
//Regex: if line started with zero or more spaces, then have number. character + 1 space
|
|
53
|
+
match: (lex) => lex.isStartOfLine() && /^(\s*)(\d+)\. /.test(lex.peekUntil("\n")),
|
|
54
|
+
emit: (lex) => lex.handleList(true, false)
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
match: (lex) => lex.listLevelFlag > 0 && lex.isStartOfLine() && !/^(\s*)([-+*]|\d+\.) /.test(lex.peekUntil("\n")),
|
|
58
|
+
emit: (lex) => {
|
|
59
|
+
while (lex.listLevelFlag > 0) {
|
|
60
|
+
lex.handleEndList();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
//For table
|
|
65
|
+
{ match: (lex) => lex.isStartOfLine() && /^\s*\|.*\|\s*$/.test(lex.peekUntil("\n")), emit: (lex) => lex.handleTable() },
|
|
66
|
+
//For common syntax
|
|
17
67
|
{ match: (lex) => lex.peek() === "`", emit: (lex) => lex.handleInlineBlock() },
|
|
18
68
|
{ match: (lex) => lex.peek() === "#", emit: (lex) => lex.handleHeader() },
|
|
19
69
|
{ match: (lex) => lex.peek() === "*" || lex.peek() === "_", emit: (lex) => lex.handleItalic() },
|
|
@@ -36,7 +86,11 @@ class Lexer {
|
|
|
36
86
|
}
|
|
37
87
|
this.next();
|
|
38
88
|
}
|
|
39
|
-
this.
|
|
89
|
+
while (this.listLevelFlag > 0) {
|
|
90
|
+
this.handleEndList();
|
|
91
|
+
}
|
|
92
|
+
if (isEof)
|
|
93
|
+
this.listToken.push({ type: "EOF" });
|
|
40
94
|
return this.listToken;
|
|
41
95
|
}
|
|
42
96
|
//Get current character with offset
|
|
@@ -58,6 +112,59 @@ class Lexer {
|
|
|
58
112
|
getLastToken() {
|
|
59
113
|
return this.listToken[this.listToken.length - 1];
|
|
60
114
|
}
|
|
115
|
+
handleTable() {
|
|
116
|
+
const tokenizeResult = [];
|
|
117
|
+
const handler = new Lexer("");
|
|
118
|
+
const header = this.readUntil("\n", true);
|
|
119
|
+
const headerDetails = header.trim().replace(/^ *\|/, "").replace(/\| *$/, "").split("|");
|
|
120
|
+
const align = this.readUntil("\n", true);
|
|
121
|
+
const alignDetails = align.trim().replace(/^ *\|/, "").replace(/\| *$/, "").split("|");
|
|
122
|
+
if (alignDetails.length !== headerDetails.length || !alignDetails.every(c => /^:?-{3,}:?$/.test(c))) {
|
|
123
|
+
this.listToken.push({ type: "Text", value: `${header}\n${align}\n` });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
//Handle alignment
|
|
128
|
+
const normalizeAlign = alignDetails.map(value => {
|
|
129
|
+
if (value.startsWith(":") && value.endsWith(":"))
|
|
130
|
+
return "center";
|
|
131
|
+
else if (value.endsWith(":"))
|
|
132
|
+
return "right";
|
|
133
|
+
else
|
|
134
|
+
return "left";
|
|
135
|
+
});
|
|
136
|
+
tokenizeResult.push({ type: "TableStart" });
|
|
137
|
+
//Handle header
|
|
138
|
+
tokenizeResult.push({ type: "RowStart", isHeader: true });
|
|
139
|
+
headerDetails.forEach((cell, index) => {
|
|
140
|
+
tokenizeResult.push({ type: "CellStart", align: normalizeAlign[index] ?? "left" });
|
|
141
|
+
handler.setInput(cell.trim());
|
|
142
|
+
tokenizeResult.push(...handler.tokenize(false));
|
|
143
|
+
tokenizeResult.push({ type: "CellEnd" });
|
|
144
|
+
});
|
|
145
|
+
tokenizeResult.push({ type: "RowEnd" });
|
|
146
|
+
//Handle body
|
|
147
|
+
while (!this.isEndOfFile()) {
|
|
148
|
+
const body = this.readUntil("\n", true);
|
|
149
|
+
if (!body)
|
|
150
|
+
break;
|
|
151
|
+
const line = body.trim();
|
|
152
|
+
if (!line.startsWith("|") || !line.endsWith("|"))
|
|
153
|
+
break; //End of table
|
|
154
|
+
const bodyDetail = body.trim().replace(/^ *\|/, "").replace(/\| *$/, "").split("|");
|
|
155
|
+
tokenizeResult.push({ type: "RowStart", isHeader: false });
|
|
156
|
+
bodyDetail.forEach((cell, index) => {
|
|
157
|
+
tokenizeResult.push({ type: "CellStart", align: normalizeAlign[index] ?? "left" });
|
|
158
|
+
handler.setInput(cell.trim());
|
|
159
|
+
tokenizeResult.push(...handler.tokenize(false));
|
|
160
|
+
tokenizeResult.push({ type: "CellEnd" });
|
|
161
|
+
});
|
|
162
|
+
tokenizeResult.push({ type: "RowEnd" });
|
|
163
|
+
}
|
|
164
|
+
tokenizeResult.push({ type: "TableEnd" });
|
|
165
|
+
this.listToken.push(...tokenizeResult);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
61
168
|
handleHeader() {
|
|
62
169
|
let level = 0;
|
|
63
170
|
while (this.peek() === "#") {
|
|
@@ -101,7 +208,11 @@ class Lexer {
|
|
|
101
208
|
}
|
|
102
209
|
handleBold() {
|
|
103
210
|
this.listToken.push({ type: "Bold" });
|
|
104
|
-
this.next(); //Skip *
|
|
211
|
+
this.next(); //Skip remain *
|
|
212
|
+
}
|
|
213
|
+
handleStrikethrough() {
|
|
214
|
+
this.listToken.push({ type: "Strikethrough" });
|
|
215
|
+
this.next(); //Skip remain ~
|
|
105
216
|
}
|
|
106
217
|
handleInlineBlock() {
|
|
107
218
|
let content = "";
|
|
@@ -110,12 +221,51 @@ class Lexer {
|
|
|
110
221
|
content += this.peek();
|
|
111
222
|
this.next();
|
|
112
223
|
}
|
|
113
|
-
// this.next() //Skip close block
|
|
114
224
|
this.listToken.push({ "type": "InlineCode", content: content });
|
|
115
225
|
}
|
|
116
226
|
handleQuoteBlock() {
|
|
117
227
|
this.listToken.push({ type: "Quote" });
|
|
118
228
|
}
|
|
229
|
+
handleList(isOrdered, isTask) {
|
|
230
|
+
const line = this.peekUntil("\n");
|
|
231
|
+
if (isTask) {
|
|
232
|
+
const m = line.match(/^(\s*)([-*+]) \[( |x|X)\] (.*)$/);
|
|
233
|
+
const indent = Math.floor(m[1].length / 2) + 1;
|
|
234
|
+
while (this.listLevelFlag < indent)
|
|
235
|
+
this.handleStartList(false);
|
|
236
|
+
while (this.listLevelFlag > indent)
|
|
237
|
+
this.handleEndList();
|
|
238
|
+
this.next(m[1].length + 4);
|
|
239
|
+
this.handleTaskItem(m[3].toLowerCase() === "x");
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
//Regex: line started with: Group 1: zero or more spaces, group 2: (- or + or * + 1 space) or (number with . character), group 3: everything else in line
|
|
243
|
+
const m = isOrdered ? line.match(/^(\s*)(\d+)\. (.*)$/) : line.match(/^(\s*)([-*+]) (.*)$/);
|
|
244
|
+
const indent = Math.floor(m[1].length / 2) + 1; //m[1] to get the spaces in group 1
|
|
245
|
+
while (this.listLevelFlag < indent)
|
|
246
|
+
this.handleStartList(isOrdered);
|
|
247
|
+
while (this.listLevelFlag > indent)
|
|
248
|
+
this.handleEndList();
|
|
249
|
+
this.next(m[1].length + (isOrdered ? 1 : 0)); //+1 due to marker have 2 characters (e.g: 1.) instead 1 like unordered list
|
|
250
|
+
this.handleListItem();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
handleStartList(isOrder) {
|
|
254
|
+
this.listLevelFlag++;
|
|
255
|
+
this.listToken.push({ type: "ListStart", level: this.listLevelFlag, ordered: isOrder });
|
|
256
|
+
}
|
|
257
|
+
handleListItem() {
|
|
258
|
+
this.next(); // Skip space between - and text
|
|
259
|
+
this.listToken.push({ type: "ListItem" });
|
|
260
|
+
}
|
|
261
|
+
handleTaskItem(isChecked) {
|
|
262
|
+
this.next(); // Skip space between last ] and text
|
|
263
|
+
this.listToken.push({ type: "TaskItem", checked: isChecked });
|
|
264
|
+
}
|
|
265
|
+
handleEndList() {
|
|
266
|
+
this.listLevelFlag === 0 ? 0 : this.listLevelFlag--;
|
|
267
|
+
this.listToken.push({ type: "ListEnd" });
|
|
268
|
+
}
|
|
119
269
|
handleLink() {
|
|
120
270
|
this.next(); //Skip [
|
|
121
271
|
const text = this.readUntil("]");
|
|
@@ -145,13 +295,38 @@ class Lexer {
|
|
|
145
295
|
else
|
|
146
296
|
this.listToken.push({ type: "Text", value: `![${alt}]` });
|
|
147
297
|
}
|
|
148
|
-
|
|
298
|
+
handleHorizontalLine() {
|
|
299
|
+
this.next(2); //Skip two first characters, remain will be skiped after loop
|
|
300
|
+
this.listToken.push({ type: "HorizontalLine" });
|
|
301
|
+
}
|
|
302
|
+
//Utilities function
|
|
303
|
+
readUntil(char, isConsumeChar = false) {
|
|
149
304
|
let result = "";
|
|
150
305
|
while (this.peek() !== char) {
|
|
151
306
|
result += this.peek();
|
|
152
307
|
this.next();
|
|
308
|
+
if (this.isEndOfFile())
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
if (isConsumeChar)
|
|
312
|
+
this.next(char.length); //Make cursor skip the char
|
|
313
|
+
return result;
|
|
314
|
+
}
|
|
315
|
+
peekUntil(char) {
|
|
316
|
+
let result = "";
|
|
317
|
+
let i = 0;
|
|
318
|
+
while (true) {
|
|
319
|
+
const current = this.peek(i++);
|
|
320
|
+
if (current == null)
|
|
321
|
+
break;
|
|
322
|
+
if (current == char)
|
|
323
|
+
break;
|
|
324
|
+
result += current;
|
|
153
325
|
}
|
|
154
326
|
return result;
|
|
155
327
|
}
|
|
328
|
+
isStartOfLine() {
|
|
329
|
+
return this.pos === 0 || this.peek(-1) === "\n";
|
|
330
|
+
}
|
|
156
331
|
}
|
|
157
332
|
exports.default = Lexer;
|
package/dist/parser.d.ts
CHANGED
|
@@ -18,9 +18,14 @@ export declare class Parser {
|
|
|
18
18
|
private parseHeader;
|
|
19
19
|
private parseBold;
|
|
20
20
|
private parseItalic;
|
|
21
|
+
private parseStrikethrough;
|
|
21
22
|
private parseInlineCode;
|
|
22
23
|
private parseQuote;
|
|
24
|
+
private parseList;
|
|
25
|
+
private parseListItem;
|
|
23
26
|
private parseLink;
|
|
24
27
|
private parseImage;
|
|
28
|
+
private parseTable;
|
|
29
|
+
private parseHorizontalLine;
|
|
25
30
|
private parseInlineUntil;
|
|
26
31
|
}
|
package/dist/parser.js
CHANGED
|
@@ -49,6 +49,18 @@ class Parser {
|
|
|
49
49
|
listNode.push(this.parseImage());
|
|
50
50
|
break;
|
|
51
51
|
}
|
|
52
|
+
case "HorizontalLine": {
|
|
53
|
+
listNode.push(this.parseHorizontalLine());
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
case "ListStart": {
|
|
57
|
+
listNode.push(this.parseList());
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
case "TableStart": {
|
|
61
|
+
listNode.push(this.parseTable());
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
52
64
|
case "NewLine": {
|
|
53
65
|
this.next(); // skip
|
|
54
66
|
break;
|
|
@@ -90,6 +102,10 @@ class Parser {
|
|
|
90
102
|
this.next(); // skip marker
|
|
91
103
|
return { type: "Italic", children: this.parseInlineUntil("Italic") };
|
|
92
104
|
}
|
|
105
|
+
parseStrikethrough() {
|
|
106
|
+
this.next(); // skip marker
|
|
107
|
+
return { type: "Strikethrough", children: this.parseInlineUntil("Strikethrough") };
|
|
108
|
+
}
|
|
93
109
|
parseInlineCode() {
|
|
94
110
|
const tok = this.peek();
|
|
95
111
|
this.next();
|
|
@@ -102,6 +118,70 @@ class Parser {
|
|
|
102
118
|
this.next(); //skip marker
|
|
103
119
|
return { type: "Quote", children: [{ type: "Paragraph", children: this.parseInlineUntil("NewLine") }] };
|
|
104
120
|
}
|
|
121
|
+
parseList() {
|
|
122
|
+
const tok = this.peek();
|
|
123
|
+
if (tok?.type === "ListStart") {
|
|
124
|
+
this.next(); //skip marker
|
|
125
|
+
const result = {
|
|
126
|
+
type: "List",
|
|
127
|
+
level: tok.level,
|
|
128
|
+
ordered: tok.ordered,
|
|
129
|
+
children: [],
|
|
130
|
+
};
|
|
131
|
+
let nextToken = this.peek();
|
|
132
|
+
while (!this.isEnd()) {
|
|
133
|
+
if (nextToken?.type === "ListItem" || nextToken?.type === "TaskItem") {
|
|
134
|
+
result.children.push(this.parseListItem());
|
|
135
|
+
nextToken = this.peek();
|
|
136
|
+
}
|
|
137
|
+
else if (nextToken?.type === "ListEnd") {
|
|
138
|
+
this.next();
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
else
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
//Temp return
|
|
147
|
+
return {
|
|
148
|
+
type: "Text",
|
|
149
|
+
value: ""
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
parseListItem() {
|
|
153
|
+
const currentToken = this.peek();
|
|
154
|
+
this.next(); // skip marker
|
|
155
|
+
const children = [];
|
|
156
|
+
while (!this.isEnd()) {
|
|
157
|
+
const tok = this.peek();
|
|
158
|
+
if (!tok)
|
|
159
|
+
break;
|
|
160
|
+
if (tok.type === "NewLine") {
|
|
161
|
+
this.next();
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (tok.type === "ListStart") {
|
|
165
|
+
children.push(this.parseList());
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (["ListItem", "TaskItem", "ListEnd"].includes(tok.type)) {
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
children.push({
|
|
172
|
+
type: "Paragraph",
|
|
173
|
+
children: this.parseInlineUntil("NewLine")
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return currentToken?.type === "TaskItem" ? {
|
|
177
|
+
type: "TaskItem",
|
|
178
|
+
checked: currentToken.type === "TaskItem" ? currentToken.checked : false,
|
|
179
|
+
children: children
|
|
180
|
+
} : {
|
|
181
|
+
type: "ListItem",
|
|
182
|
+
children: children
|
|
183
|
+
};
|
|
184
|
+
}
|
|
105
185
|
parseLink() {
|
|
106
186
|
const tok = this.peek();
|
|
107
187
|
this.next();
|
|
@@ -127,12 +207,60 @@ class Parser {
|
|
|
127
207
|
else
|
|
128
208
|
return { type: "Image", src: "", alt: "" };
|
|
129
209
|
}
|
|
210
|
+
parseTable() {
|
|
211
|
+
this.next(); // skip TableStart token
|
|
212
|
+
const parseRow = () => {
|
|
213
|
+
const rowStartToken = this.peek();
|
|
214
|
+
if (rowStartToken?.type !== "RowStart")
|
|
215
|
+
return { isHeader: false, cells: [] };
|
|
216
|
+
this.next(); // skip RowStart token
|
|
217
|
+
const cells = [];
|
|
218
|
+
while (this.peek() && this.peek().type !== "RowEnd") {
|
|
219
|
+
cells.push(parseCell());
|
|
220
|
+
}
|
|
221
|
+
this.next(); // skip RowEnd token
|
|
222
|
+
return {
|
|
223
|
+
isHeader: rowStartToken.isHeader,
|
|
224
|
+
cells: cells
|
|
225
|
+
};
|
|
226
|
+
};
|
|
227
|
+
const parseCell = () => {
|
|
228
|
+
const cellStartToken = this.peek();
|
|
229
|
+
if (cellStartToken?.type !== "CellStart")
|
|
230
|
+
return { align: "left", chlidren: [] };
|
|
231
|
+
this.next(); // skip CellStart token
|
|
232
|
+
const childrens = this.parseInlineUntil("CellEnd");
|
|
233
|
+
return {
|
|
234
|
+
align: cellStartToken.align,
|
|
235
|
+
chlidren: [{ type: "Paragraph", children: childrens }]
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
const rows = [];
|
|
239
|
+
while (this.peek()?.type !== "TableEnd") {
|
|
240
|
+
rows.push(parseRow());
|
|
241
|
+
if (this.isEnd())
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
this.next();
|
|
245
|
+
return {
|
|
246
|
+
type: "Table",
|
|
247
|
+
rows: rows
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
parseHorizontalLine() {
|
|
251
|
+
const tok = this.peek();
|
|
252
|
+
this.next(); // skip marker
|
|
253
|
+
return { type: "HorizontalLine" };
|
|
254
|
+
}
|
|
130
255
|
parseInlineUntil(stopType) {
|
|
256
|
+
const stop = Array.isArray(stopType) ? stopType : [stopType];
|
|
131
257
|
const listNode = [];
|
|
132
|
-
while (!this.isEnd()
|
|
258
|
+
while (!this.isEnd()) {
|
|
133
259
|
const currentNode = this.peek();
|
|
134
260
|
if (!currentNode)
|
|
135
261
|
break;
|
|
262
|
+
if (stop.includes(currentNode.type))
|
|
263
|
+
break;
|
|
136
264
|
switch (currentNode.type) {
|
|
137
265
|
case "Bold": {
|
|
138
266
|
listNode.push(this.parseBold());
|
|
@@ -142,6 +270,10 @@ class Parser {
|
|
|
142
270
|
listNode.push(this.parseItalic());
|
|
143
271
|
break;
|
|
144
272
|
}
|
|
273
|
+
case "Strikethrough": {
|
|
274
|
+
listNode.push(this.parseStrikethrough());
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
145
277
|
case "InlineCode": {
|
|
146
278
|
listNode.push(this.parseInlineCode());
|
|
147
279
|
break;
|
package/dist/renderer.d.ts
CHANGED
package/dist/renderer.js
CHANGED
|
@@ -19,20 +19,52 @@ class Renderer {
|
|
|
19
19
|
}
|
|
20
20
|
handleRender(type) {
|
|
21
21
|
const defaultRender = {
|
|
22
|
+
//Base structural nodes
|
|
22
23
|
Document: (_node, children) => children.join(""),
|
|
23
24
|
Paragraph: (_node, children) => `<p>${children.join("")}</p>`,
|
|
24
|
-
|
|
25
|
-
InlineCode: (node) => `<code>${this.escapeHtml(node.content)}</code>`,
|
|
25
|
+
//Container nodes
|
|
26
26
|
CodeBlock: (node) => `<pre><code class="lang-${node.lang}">${this.escapeHtml(node.content)}</code></pre>`,
|
|
27
|
+
Header: (node, children) => `<h${node.level}${node.level <= 2 ? ' style="border-bottom: 1px solid #d1d9e0b3"' : ''}>${children.join("")}</h${node.level}>`,
|
|
28
|
+
Quote: (_node, children) => `<blockquote>${children.join("")}</blockquote>`,
|
|
29
|
+
//For list nodes
|
|
30
|
+
List: (node, children) => node.ordered ? `<ol>${children.join("")}</ol>` : `<ul>${children.join("")}</ul>`,
|
|
31
|
+
ListItem: (_node, children) => `<li>${children.join("")}</li>`,
|
|
32
|
+
TaskItem: (node, children) => `<li><input type="checkbox" disabled ${node.checked ? "checked" : ""}>${children.join("")}</li>`,
|
|
33
|
+
//Styling nodes
|
|
27
34
|
Bold: (_node, children) => `<strong>${children.join("")}</strong>`,
|
|
28
35
|
Italic: (_node, children) => `<em>${children.join("")}</em>`,
|
|
29
|
-
|
|
36
|
+
Strikethrough: (_node, children) => `<s>${children.join("")}</s>`,
|
|
37
|
+
InlineCode: (node) => `<code>${this.escapeHtml(node.content)}</code>`,
|
|
38
|
+
//Media nodes
|
|
30
39
|
Link: (node) => `<a href="${node.href}">${node.text}</a>`,
|
|
31
40
|
Image: (node) => `<img src="${node.src}" alt="${node.alt}"/>`,
|
|
41
|
+
//Leaf nodes
|
|
42
|
+
HorizontalLine: (_node) => `<hr>`,
|
|
32
43
|
Text: (node) => node.value,
|
|
44
|
+
//For table nodes
|
|
45
|
+
Table: (node, children) => this.renderTable(node, children),
|
|
33
46
|
};
|
|
34
47
|
return this.option.elements?.[type] ?? defaultRender[type];
|
|
35
48
|
}
|
|
49
|
+
renderTable(node, children) {
|
|
50
|
+
if (node.type === "Table") {
|
|
51
|
+
const header = node.rows.filter(row => row.isHeader);
|
|
52
|
+
const body = node.rows.filter(row => !row.isHeader);
|
|
53
|
+
const renderRows = (row) => {
|
|
54
|
+
const tag = row.isHeader ? "th" : "td";
|
|
55
|
+
const cells = row.cells.map(cell => {
|
|
56
|
+
const align = `style="text-align:${cell.align}"`;
|
|
57
|
+
return `<${tag} ${align}>${cell.chlidren.map(c => this.render(c)).join("")}</${tag}>`;
|
|
58
|
+
}).join("");
|
|
59
|
+
return `<tr>${cells}</tr>`;
|
|
60
|
+
};
|
|
61
|
+
const tHead = header.length ? `<thead>${header.map(renderRows).join("")}</thead>` : "";
|
|
62
|
+
const tBody = body.length ? `<tbody>${body.map(renderRows).join("")}</tbody>` : "";
|
|
63
|
+
return `<table>${tHead}${tBody}</table>`;
|
|
64
|
+
}
|
|
65
|
+
else
|
|
66
|
+
return `<p>${children.join("\n")}</p>`;
|
|
67
|
+
}
|
|
36
68
|
escapeHtml(str) {
|
|
37
69
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
38
70
|
}
|
package/dist/types/node.d.ts
CHANGED
|
@@ -10,11 +10,16 @@
|
|
|
10
10
|
* - Header: A header with given `level` (1-6)
|
|
11
11
|
* - Bold: Bold text
|
|
12
12
|
* - Italic: Italic text
|
|
13
|
+
* - Strikethrough: Strilethrough text
|
|
13
14
|
* - InlineCode: Inline code snippet, with it's `content`
|
|
14
15
|
* - Quote: A quote block
|
|
15
16
|
* - CodeBlock: A code block, with it's `lang` and `content`
|
|
17
|
+
* - List: A list, with it's level and children
|
|
18
|
+
* - ListItem: An item of a list, with it's children
|
|
19
|
+
* - TaskItem: An item for tasklist, with it's checked state
|
|
16
20
|
* - Link: A link, with it's `text` and `href`
|
|
17
21
|
* - Image: An image, with it's `src` and `alt`
|
|
22
|
+
* - HorizontalLine: A horizontal line
|
|
18
23
|
* - Text: Raw text content.
|
|
19
24
|
*/
|
|
20
25
|
export type Node = {
|
|
@@ -33,6 +38,9 @@ export type Node = {
|
|
|
33
38
|
} | {
|
|
34
39
|
type: "Italic";
|
|
35
40
|
children: Node[];
|
|
41
|
+
} | {
|
|
42
|
+
type: "Strikethrough";
|
|
43
|
+
children: Node[];
|
|
36
44
|
} | {
|
|
37
45
|
type: "InlineCode";
|
|
38
46
|
content: string;
|
|
@@ -43,6 +51,18 @@ export type Node = {
|
|
|
43
51
|
} | {
|
|
44
52
|
type: "Quote";
|
|
45
53
|
children: Node[];
|
|
54
|
+
} | {
|
|
55
|
+
type: "List";
|
|
56
|
+
ordered: boolean;
|
|
57
|
+
level: number;
|
|
58
|
+
children: Node[];
|
|
59
|
+
} | {
|
|
60
|
+
type: "ListItem";
|
|
61
|
+
children: Node[];
|
|
62
|
+
} | {
|
|
63
|
+
type: "TaskItem";
|
|
64
|
+
checked: boolean;
|
|
65
|
+
children: Node[];
|
|
46
66
|
} | {
|
|
47
67
|
type: "Link";
|
|
48
68
|
href: string;
|
|
@@ -51,7 +71,20 @@ export type Node = {
|
|
|
51
71
|
type: "Image";
|
|
52
72
|
src: string;
|
|
53
73
|
alt: string;
|
|
74
|
+
} | {
|
|
75
|
+
type: "HorizontalLine";
|
|
54
76
|
} | {
|
|
55
77
|
type: "Text";
|
|
56
78
|
value: string;
|
|
79
|
+
} | {
|
|
80
|
+
type: "Table";
|
|
81
|
+
rows: TableRow[];
|
|
82
|
+
};
|
|
83
|
+
export type TableRow = {
|
|
84
|
+
isHeader: boolean;
|
|
85
|
+
cells: TableCell[];
|
|
86
|
+
};
|
|
87
|
+
export type TableCell = {
|
|
88
|
+
align: "left" | "center" | "right";
|
|
89
|
+
chlidren: Node[];
|
|
57
90
|
};
|
package/dist/types/token.d.ts
CHANGED
|
@@ -8,13 +8,19 @@
|
|
|
8
8
|
* - Header: Markdown header (`#`), with a `level` (1–6).
|
|
9
9
|
* - CodeBlock: Fenced code block (` ``` `), with optional `lang` and its `content`.
|
|
10
10
|
* - NewLine: Line break (`\n`).
|
|
11
|
-
* - Text: Plain text content.
|
|
12
11
|
* - Bold: Bold marker (`**`).
|
|
13
12
|
* - Italic: Italic marker (`*` or `_`).
|
|
13
|
+
* - Strikethrough: Strikethrough marker (`~~`)
|
|
14
14
|
* - InlineCode: Inline code snippet (`` ` ``), with its `content`.
|
|
15
15
|
* - Quote: A quote block (`>`).
|
|
16
|
+
* - ListStart: Start a list
|
|
17
|
+
* - ListItem: A list's item (`* ` or `+ ` or `- ` or `number with dot`)
|
|
18
|
+
* - TaskItem: A task item in a list (`- [ ]` or `- [x]`)
|
|
19
|
+
* - ListEnd: End a list
|
|
16
20
|
* - Link: A link (`[text](url)`)
|
|
17
21
|
* - Image: An image (``)
|
|
22
|
+
* - HorizontalLine: A horizontal line (`---` or `___` or `***`)
|
|
23
|
+
* - Text: Plain text content.
|
|
18
24
|
* - EOF: A special token, this is the end of input.
|
|
19
25
|
*/
|
|
20
26
|
export type Token = {
|
|
@@ -26,18 +32,28 @@ export type Token = {
|
|
|
26
32
|
content: string;
|
|
27
33
|
} | {
|
|
28
34
|
type: "NewLine";
|
|
29
|
-
} | {
|
|
30
|
-
type: "Text";
|
|
31
|
-
value: string;
|
|
32
35
|
} | {
|
|
33
36
|
type: "Bold";
|
|
34
37
|
} | {
|
|
35
38
|
type: "Italic";
|
|
39
|
+
} | {
|
|
40
|
+
type: "Strikethrough";
|
|
36
41
|
} | {
|
|
37
42
|
type: "InlineCode";
|
|
38
43
|
content: string;
|
|
39
44
|
} | {
|
|
40
45
|
type: "Quote";
|
|
46
|
+
} | {
|
|
47
|
+
type: "ListStart";
|
|
48
|
+
ordered: boolean;
|
|
49
|
+
level: number;
|
|
50
|
+
} | {
|
|
51
|
+
type: "ListItem";
|
|
52
|
+
} | {
|
|
53
|
+
type: "TaskItem";
|
|
54
|
+
checked: boolean;
|
|
55
|
+
} | {
|
|
56
|
+
type: "ListEnd";
|
|
41
57
|
} | {
|
|
42
58
|
type: "Link";
|
|
43
59
|
text: string;
|
|
@@ -46,6 +62,25 @@ export type Token = {
|
|
|
46
62
|
type: "Image";
|
|
47
63
|
src: string;
|
|
48
64
|
alt: string;
|
|
65
|
+
} | {
|
|
66
|
+
type: "HorizontalLine";
|
|
67
|
+
} | {
|
|
68
|
+
type: "Text";
|
|
69
|
+
value: string;
|
|
49
70
|
} | {
|
|
50
71
|
type: "EOF";
|
|
72
|
+
} | {
|
|
73
|
+
type: "TableStart";
|
|
74
|
+
} | {
|
|
75
|
+
type: "TableEnd";
|
|
76
|
+
} | {
|
|
77
|
+
type: "RowStart";
|
|
78
|
+
isHeader: boolean;
|
|
79
|
+
} | {
|
|
80
|
+
type: "RowEnd";
|
|
81
|
+
} | {
|
|
82
|
+
type: "CellStart";
|
|
83
|
+
align: "left" | "center" | "right";
|
|
84
|
+
} | {
|
|
85
|
+
type: "CellEnd";
|
|
51
86
|
};
|
package/package.json
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "simple-customize-markdown-converter",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Convert Markdown to your customize HTML",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"markdown",
|
|
7
|
-
"html",
|
|
8
|
-
"converter"
|
|
9
|
-
],
|
|
10
|
-
"author": "Regiko04",
|
|
11
|
-
"license": "MIT",
|
|
12
|
-
"main": "dist/index.js",
|
|
13
|
-
"module": "dist/index.js",
|
|
14
|
-
"types": "dist/index.d.ts",
|
|
15
|
-
"files": [
|
|
16
|
-
"dist",
|
|
17
|
-
"LICENSE",
|
|
18
|
-
"README.md"
|
|
19
|
-
],
|
|
20
|
-
"scripts": {
|
|
21
|
-
"test": "jest",
|
|
22
|
-
"build": "tsc",
|
|
23
|
-
"start": "ts-node src/index.ts"
|
|
24
|
-
},
|
|
25
|
-
"devDependencies": {
|
|
26
|
-
"@types/jest": "^30.0.0",
|
|
27
|
-
"@types/node": "^24.3.3",
|
|
28
|
-
"jest": "^30.1.3",
|
|
29
|
-
"ts-jest": "^29.4.1",
|
|
30
|
-
"ts-node": "^10.9.2",
|
|
31
|
-
"typescript": "^5.9.2"
|
|
32
|
-
},
|
|
33
|
-
"repository": {
|
|
34
|
-
"type": "git",
|
|
35
|
-
"url": "git+https://github.com/Riiichan04/simple-custom-markdown-converter.git"
|
|
36
|
-
},
|
|
37
|
-
"bugs": {
|
|
38
|
-
"url": "https://github.com/Riiichan04/simple-custom-markdown-converter/issues"
|
|
39
|
-
},
|
|
40
|
-
"homepage": "https://github.com/Riiichan04/simple-custom-markdown-converter#readme"
|
|
1
|
+
{
|
|
2
|
+
"name": "simple-customize-markdown-converter",
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"description": "Convert Markdown to your customize HTML",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"markdown",
|
|
7
|
+
"html",
|
|
8
|
+
"converter"
|
|
9
|
+
],
|
|
10
|
+
"author": "Regiko04",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"main": "dist/index.js",
|
|
13
|
+
"module": "dist/index.js",
|
|
14
|
+
"types": "dist/index.d.ts",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"LICENSE",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "jest",
|
|
22
|
+
"build": "tsc",
|
|
23
|
+
"start": "ts-node src/index.ts"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/jest": "^30.0.0",
|
|
27
|
+
"@types/node": "^24.3.3",
|
|
28
|
+
"jest": "^30.1.3",
|
|
29
|
+
"ts-jest": "^29.4.1",
|
|
30
|
+
"ts-node": "^10.9.2",
|
|
31
|
+
"typescript": "^5.9.2"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/Riiichan04/simple-custom-markdown-converter.git"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/Riiichan04/simple-custom-markdown-converter/issues"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/Riiichan04/simple-custom-markdown-converter#readme"
|
|
41
41
|
}
|