simple-customize-markdown-converter 1.0.3 → 1.0.5

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
@@ -1,8 +1,8 @@
1
- # Simple Custom Markdown Converter
1
+ # Simple Customize Markdown Converter
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;
@@ -164,10 +168,7 @@ class Parser {
164
168
  if (["ListItem", "TaskItem", "ListEnd"].includes(tok.type)) {
165
169
  break;
166
170
  }
167
- children.push({
168
- type: "Paragraph",
169
- children: this.parseInlineUntil("NewLine")
170
- });
171
+ children.push(...this.parseInlineUntil("NewLine"));
171
172
  }
172
173
  return currentToken?.type === "TaskItem" ? {
173
174
  type: "TaskItem",
@@ -203,6 +204,46 @@ class Parser {
203
204
  else
204
205
  return { type: "Image", src: "", alt: "" };
205
206
  }
207
+ parseTable() {
208
+ this.next(); // skip TableStart token
209
+ const parseRow = () => {
210
+ const rowStartToken = this.peek();
211
+ if (rowStartToken?.type !== "RowStart")
212
+ return { isHeader: false, cells: [] };
213
+ this.next(); // skip RowStart token
214
+ const cells = [];
215
+ while (this.peek() && this.peek().type !== "RowEnd") {
216
+ cells.push(parseCell());
217
+ }
218
+ this.next(); // skip RowEnd token
219
+ return {
220
+ isHeader: rowStartToken.isHeader,
221
+ cells: cells
222
+ };
223
+ };
224
+ const parseCell = () => {
225
+ const cellStartToken = this.peek();
226
+ if (cellStartToken?.type !== "CellStart")
227
+ return { align: "left", children: [] };
228
+ this.next(); // skip CellStart token
229
+ const childrens = this.parseInlineUntil("CellEnd");
230
+ return {
231
+ align: cellStartToken.align,
232
+ children: childrens
233
+ };
234
+ };
235
+ const rows = [];
236
+ while (this.peek()?.type !== "TableEnd") {
237
+ rows.push(parseRow());
238
+ if (this.isEnd())
239
+ break;
240
+ }
241
+ this.next();
242
+ return {
243
+ type: "Table",
244
+ rows: rows
245
+ };
246
+ }
206
247
  parseHorizontalLine() {
207
248
  const tok = this.peek();
208
249
  this.next(); // skip marker
@@ -9,7 +9,10 @@ export default class Renderer {
9
9
  * @param node - The abstract syntax tree (AST) from the Parser
10
10
  * @returns The rendered HTML string.
11
11
  */
12
- render(node: Node): string;
12
+ render<K extends Node["type"]>(node: Extract<Node, {
13
+ type: K;
14
+ }>): string;
13
15
  private handleRender;
16
+ private renderTable;
14
17
  private escapeHtml;
15
18
  }
package/dist/renderer.js CHANGED
@@ -25,7 +25,8 @@ class Renderer {
25
25
  //Container nodes
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
- Quote: (_node, children) => `<blockquote>${children.join("")}</blockquote>`,
28
+ Quote: (_node, children) => `<blockquote style="margin:0; padding:0 1em; color:#59636e; border-left:.25em solid #d1d9e0;">${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,8 +41,29 @@ 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
- return this.option.elements?.[type] ?? defaultRender[type];
47
+ return (this.option.elements?.[type] ?? defaultRender[type]);
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.children.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>`;
45
67
  }
46
68
  escapeHtml(str) {
47
69
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -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
+ children: Node[];
79
90
  };
@@ -1,10 +1,31 @@
1
1
  import { Node } from "./node";
2
2
  /**
3
- * Option to customize how AST nodes are renderes into HTML
3
+ * Function type for rendering an AST node to HTML.
4
4
  *
5
- * @property elements? - A mapping of AST node types to custom render functions.
6
- * - The key is the `Node` type (e.g. `"Header"`, `"Text"`).
7
- * - The value is a function `(node, children) => string` that define how to render HTML string. With `node` is a AST `Node`. `children` is the node's childrens
5
+ * @template T - A subtype of `Node` corresponding to the render node
6
+ * @param node - The AST node to render
7
+ * @param children - Rendered HTML strings of the node's children
8
+ * @returns A HTML string representation of the node
9
+ */
10
+ type NodeRenderer<T extends Node = Node> = (node: T, children: string[]) => string;
11
+ /**
12
+ * A mapping of AST node types to custom render functions.
13
+ *
14
+ * - The key is a `Node["type"]` string literal (e.g. `"Header"`, `"Paragraph"`)
15
+ * - The value is a function `(node, children) => string`:
16
+ * - `node` is a `Node` with its attribute depending on its `type`.
17
+ * (e.g. `"Header"` nodes include `level`, `"CodeBlock"` nodes include `lang` and `content`, etc)
18
+ * - `children` is the array of rendered strings of its children.
19
+ */
20
+ export type RenderElements = {
21
+ [K in Node["type"]]?: NodeRenderer<Extract<Node, {
22
+ type: K;
23
+ }>>;
24
+ };
25
+ /**
26
+ * Options to customize how AST nodes are renderes into HTML
27
+ *
28
+ * @property elements - Optional custom rendered for one or more node types
8
29
  *
9
30
  * @example
10
31
  * ```ts
@@ -16,8 +37,8 @@ import { Node } from "./node";
16
37
  * }
17
38
  * ```
18
39
  *
19
- * @todo Update`node` type in value function from `any` to `Node`
20
40
  */
21
41
  export type RenderOption = {
22
- elements?: Partial<Record<Node["type"], (node: any, children: string[]) => string>>;
42
+ elements?: RenderElements;
23
43
  };
44
+ export {};
@@ -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.5",
4
4
  "description": "Convert Markdown to your customize HTML",
5
5
  "keywords": [
6
6
  "markdown",