simple-customize-markdown-converter 1.0.3 → 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 +8 -7
- package/dist/lexer.d.ts +4 -1
- package/dist/lexer.js +71 -3
- package/dist/parser.d.ts +1 -0
- package/dist/parser.js +44 -0
- package/dist/renderer.d.ts +1 -0
- package/dist/renderer.js +22 -0
- package/dist/types/node.d.ts +11 -0
- package/dist/types/token.d.ts +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
This simple library help you convert Markdown to HTML and customize it.
|
|
3
3
|
|
|
4
4
|
## Feature
|
|
5
|
-
|
|
5
|
+
This library currently supports the most common Markdown syntaxes:
|
|
6
6
|
- Headings (`#, ##, …`)
|
|
7
7
|
- Paragraphs
|
|
8
8
|
- Bold (`\*\*text\*\*`)
|
|
@@ -10,12 +10,13 @@ Currently, this lib only supports:
|
|
|
10
10
|
- Strikethrough (`\~\~text\~\~`)
|
|
11
11
|
- Inline code (`\`code\``)
|
|
12
12
|
- Code blocks (`\`\`\`lang ... \`\`\``)
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
- Horizontal
|
|
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
|
|
19
20
|
|
|
20
21
|
And customizable renderer for all elements
|
|
21
22
|
|
package/dist/lexer.d.ts
CHANGED
|
@@ -5,16 +5,19 @@ export default class Lexer {
|
|
|
5
5
|
listToken: Token[];
|
|
6
6
|
listLevelFlag: number;
|
|
7
7
|
constructor(input: string);
|
|
8
|
+
setInput(input: string): void;
|
|
8
9
|
/**
|
|
9
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.
|
|
10
12
|
* @returns List of tokens
|
|
11
13
|
*/
|
|
12
|
-
tokenize(): Token[];
|
|
14
|
+
tokenize(isEof?: boolean): Token[];
|
|
13
15
|
private peek;
|
|
14
16
|
private next;
|
|
15
17
|
private startsWith;
|
|
16
18
|
private isEndOfFile;
|
|
17
19
|
private getLastToken;
|
|
20
|
+
private handleTable;
|
|
18
21
|
private handleHeader;
|
|
19
22
|
private handleCodeBlock;
|
|
20
23
|
private handleTextBlock;
|
package/dist/lexer.js
CHANGED
|
@@ -8,11 +8,19 @@ class Lexer {
|
|
|
8
8
|
this.listLevelFlag = 0;
|
|
9
9
|
this.input = input;
|
|
10
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
|
+
}
|
|
11
18
|
/**
|
|
12
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.
|
|
13
21
|
* @returns List of tokens
|
|
14
22
|
*/
|
|
15
|
-
tokenize() {
|
|
23
|
+
tokenize(isEof = true) {
|
|
16
24
|
const TOKEN_HANDLER = [
|
|
17
25
|
//Handle escape character first
|
|
18
26
|
{
|
|
@@ -53,6 +61,8 @@ class Lexer {
|
|
|
53
61
|
}
|
|
54
62
|
}
|
|
55
63
|
},
|
|
64
|
+
//For table
|
|
65
|
+
{ match: (lex) => lex.isStartOfLine() && /^\s*\|.*\|\s*$/.test(lex.peekUntil("\n")), emit: (lex) => lex.handleTable() },
|
|
56
66
|
//For common syntax
|
|
57
67
|
{ match: (lex) => lex.peek() === "`", emit: (lex) => lex.handleInlineBlock() },
|
|
58
68
|
{ match: (lex) => lex.peek() === "#", emit: (lex) => lex.handleHeader() },
|
|
@@ -79,7 +89,8 @@ class Lexer {
|
|
|
79
89
|
while (this.listLevelFlag > 0) {
|
|
80
90
|
this.handleEndList();
|
|
81
91
|
}
|
|
82
|
-
|
|
92
|
+
if (isEof)
|
|
93
|
+
this.listToken.push({ type: "EOF" });
|
|
83
94
|
return this.listToken;
|
|
84
95
|
}
|
|
85
96
|
//Get current character with offset
|
|
@@ -101,6 +112,59 @@ class Lexer {
|
|
|
101
112
|
getLastToken() {
|
|
102
113
|
return this.listToken[this.listToken.length - 1];
|
|
103
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
|
+
}
|
|
104
168
|
handleHeader() {
|
|
105
169
|
let level = 0;
|
|
106
170
|
while (this.peek() === "#") {
|
|
@@ -236,12 +300,16 @@ class Lexer {
|
|
|
236
300
|
this.listToken.push({ type: "HorizontalLine" });
|
|
237
301
|
}
|
|
238
302
|
//Utilities function
|
|
239
|
-
readUntil(char) {
|
|
303
|
+
readUntil(char, isConsumeChar = false) {
|
|
240
304
|
let result = "";
|
|
241
305
|
while (this.peek() !== char) {
|
|
242
306
|
result += this.peek();
|
|
243
307
|
this.next();
|
|
308
|
+
if (this.isEndOfFile())
|
|
309
|
+
break;
|
|
244
310
|
}
|
|
311
|
+
if (isConsumeChar)
|
|
312
|
+
this.next(char.length); //Make cursor skip the char
|
|
245
313
|
return result;
|
|
246
314
|
}
|
|
247
315
|
peekUntil(char) {
|
package/dist/parser.d.ts
CHANGED
package/dist/parser.js
CHANGED
|
@@ -57,6 +57,10 @@ class Parser {
|
|
|
57
57
|
listNode.push(this.parseList());
|
|
58
58
|
break;
|
|
59
59
|
}
|
|
60
|
+
case "TableStart": {
|
|
61
|
+
listNode.push(this.parseTable());
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
60
64
|
case "NewLine": {
|
|
61
65
|
this.next(); // skip
|
|
62
66
|
break;
|
|
@@ -203,6 +207,46 @@ class Parser {
|
|
|
203
207
|
else
|
|
204
208
|
return { type: "Image", src: "", alt: "" };
|
|
205
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
|
+
}
|
|
206
250
|
parseHorizontalLine() {
|
|
207
251
|
const tok = this.peek();
|
|
208
252
|
this.next(); // skip marker
|
package/dist/renderer.d.ts
CHANGED
package/dist/renderer.js
CHANGED
|
@@ -26,6 +26,7 @@ class Renderer {
|
|
|
26
26
|
CodeBlock: (node) => `<pre><code class="lang-${node.lang}">${this.escapeHtml(node.content)}</code></pre>`,
|
|
27
27
|
Header: (node, children) => `<h${node.level}${node.level <= 2 ? ' style="border-bottom: 1px solid #d1d9e0b3"' : ''}>${children.join("")}</h${node.level}>`,
|
|
28
28
|
Quote: (_node, children) => `<blockquote>${children.join("")}</blockquote>`,
|
|
29
|
+
//For list nodes
|
|
29
30
|
List: (node, children) => node.ordered ? `<ol>${children.join("")}</ol>` : `<ul>${children.join("")}</ul>`,
|
|
30
31
|
ListItem: (_node, children) => `<li>${children.join("")}</li>`,
|
|
31
32
|
TaskItem: (node, children) => `<li><input type="checkbox" disabled ${node.checked ? "checked" : ""}>${children.join("")}</li>`,
|
|
@@ -40,9 +41,30 @@ class Renderer {
|
|
|
40
41
|
//Leaf nodes
|
|
41
42
|
HorizontalLine: (_node) => `<hr>`,
|
|
42
43
|
Text: (node) => node.value,
|
|
44
|
+
//For table nodes
|
|
45
|
+
Table: (node, children) => this.renderTable(node, children),
|
|
43
46
|
};
|
|
44
47
|
return this.option.elements?.[type] ?? defaultRender[type];
|
|
45
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
|
+
}
|
|
46
68
|
escapeHtml(str) {
|
|
47
69
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
48
70
|
}
|
package/dist/types/node.d.ts
CHANGED
|
@@ -76,4 +76,15 @@ export type Node = {
|
|
|
76
76
|
} | {
|
|
77
77
|
type: "Text";
|
|
78
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[];
|
|
79
90
|
};
|
package/dist/types/token.d.ts
CHANGED
|
@@ -69,4 +69,18 @@ export type Token = {
|
|
|
69
69
|
value: string;
|
|
70
70
|
} | {
|
|
71
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";
|
|
72
86
|
};
|