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 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
- Currently, this lib only supports:
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
- - Quote (`> text`)
14
- - List (`- Item 1,...`)
15
- - Tasklist (`- [ ], - \[x\]`)
16
- - Link (`\[link\]\(url\)`)
17
- - Image (`\[alt\]\(url\)`)
18
- - Horizontal line (`---` or `***` or `___`)
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
- this.listToken.push({ type: "EOF" });
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
@@ -25,6 +25,7 @@ export declare class Parser {
25
25
  private parseListItem;
26
26
  private parseLink;
27
27
  private parseImage;
28
+ private parseTable;
28
29
  private parseHorizontalLine;
29
30
  private parseInlineUntil;
30
31
  }
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
@@ -11,5 +11,6 @@ export default class Renderer {
11
11
  */
12
12
  render(node: Node): string;
13
13
  private handleRender;
14
+ private renderTable;
14
15
  private escapeHtml;
15
16
  }
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
48
70
  }
@@ -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
  };
@@ -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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simple-customize-markdown-converter",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Convert Markdown to your customize HTML",
5
5
  "keywords": [
6
6
  "markdown",