simple-customize-markdown-converter 1.0.2 → 1.0.3

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
@@ -3,12 +3,19 @@ This simple library help you convert Markdown to HTML and customize it.
3
3
 
4
4
  ## Feature
5
5
  Currently, this lib only supports:
6
- - Headings (#, ##, )
6
+ - Headings (`#, ##, …`)
7
7
  - Paragraphs
8
- - Bold (\*\*text\*\*)
9
- - Italic (\*text\* or \_text\_)
10
- - Inline code (\`code\`)
11
- - Code blocks (\`\`\`lang ... \`\`\`)
8
+ - Bold (`\*\*text\*\*`)
9
+ - Italic (`\*text\* or \_text\_`)
10
+ - Strikethrough (`\~\~text\~\~`)
11
+ - Inline code (`\`code\``)
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 `___`)
12
19
 
13
20
  And customizable renderer for all elements
14
21
 
package/dist/lexer.d.ts CHANGED
@@ -3,6 +3,7 @@ export default class Lexer {
3
3
  input: string;
4
4
  pos: number;
5
5
  listToken: Token[];
6
+ listLevelFlag: number;
6
7
  constructor(input: string);
7
8
  /**
8
9
  * Tokenize the markdown into a list of tokens.
@@ -19,9 +20,18 @@ export default class Lexer {
19
20
  private handleTextBlock;
20
21
  private handleItalic;
21
22
  private handleBold;
23
+ private handleStrikethrough;
22
24
  private handleInlineBlock;
23
25
  private handleQuoteBlock;
26
+ private handleList;
27
+ private handleStartList;
28
+ private handleListItem;
29
+ private handleTaskItem;
30
+ private handleEndList;
24
31
  private handleLink;
25
32
  private handleImage;
33
+ private handleHorizontalLine;
26
34
  private readUntil;
35
+ private peekUntil;
36
+ private isStartOfLine;
27
37
  }
package/dist/lexer.js CHANGED
@@ -4,6 +4,8 @@ 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
  }
9
11
  /**
@@ -12,8 +14,46 @@ class Lexer {
12
14
  */
13
15
  tokenize() {
14
16
  const TOKEN_HANDLER = [
17
+ //Handle escape character first
18
+ {
19
+ match: (lex) => lex.peek() === "\\" && lex.peek(1) !== undefined,
20
+ emit: (lex) => {
21
+ lex.next(1);
22
+ lex.handleTextBlock();
23
+ }
24
+ },
25
+ {
26
+ //Regex: if line started with at least 3 characters: -, *, _
27
+ match: (lex) => /^([-*_])\1{2,}$/.test(lex.peekUntil("\n").trim()) && this.getLastToken()?.type === "NewLine",
28
+ emit: (lex) => lex.handleHorizontalLine()
29
+ },
15
30
  { match: (lex) => lex.startsWith("```"), emit: (lex) => lex.handleCodeBlock() },
16
31
  { match: (lex) => lex.startsWith("**"), emit: (lex) => lex.handleBold() },
32
+ { match: (lex) => lex.startsWith("~~"), emit: (lex) => lex.handleStrikethrough() },
33
+ //For List
34
+ {
35
+ match: (lex) => lex.isStartOfLine() && /^(\s*)([-*+]) \[( |x|X)\] /.test(lex.peekUntil("\n")),
36
+ emit: (lex) => lex.handleList(false, true)
37
+ },
38
+ {
39
+ //Regex: if line started with zero or more spaces, then have - or + or * + 1 space
40
+ match: (lex) => lex.isStartOfLine() && /^(\s*)([-*+]) /.test(lex.peekUntil("\n")),
41
+ emit: (lex) => lex.handleList(false, false)
42
+ },
43
+ {
44
+ //Regex: if line started with zero or more spaces, then have number. character + 1 space
45
+ match: (lex) => lex.isStartOfLine() && /^(\s*)(\d+)\. /.test(lex.peekUntil("\n")),
46
+ emit: (lex) => lex.handleList(true, false)
47
+ },
48
+ {
49
+ match: (lex) => lex.listLevelFlag > 0 && lex.isStartOfLine() && !/^(\s*)([-+*]|\d+\.) /.test(lex.peekUntil("\n")),
50
+ emit: (lex) => {
51
+ while (lex.listLevelFlag > 0) {
52
+ lex.handleEndList();
53
+ }
54
+ }
55
+ },
56
+ //For common syntax
17
57
  { match: (lex) => lex.peek() === "`", emit: (lex) => lex.handleInlineBlock() },
18
58
  { match: (lex) => lex.peek() === "#", emit: (lex) => lex.handleHeader() },
19
59
  { match: (lex) => lex.peek() === "*" || lex.peek() === "_", emit: (lex) => lex.handleItalic() },
@@ -36,6 +76,9 @@ class Lexer {
36
76
  }
37
77
  this.next();
38
78
  }
79
+ while (this.listLevelFlag > 0) {
80
+ this.handleEndList();
81
+ }
39
82
  this.listToken.push({ type: "EOF" });
40
83
  return this.listToken;
41
84
  }
@@ -101,7 +144,11 @@ class Lexer {
101
144
  }
102
145
  handleBold() {
103
146
  this.listToken.push({ type: "Bold" });
104
- this.next(); //Skip *
147
+ this.next(); //Skip remain *
148
+ }
149
+ handleStrikethrough() {
150
+ this.listToken.push({ type: "Strikethrough" });
151
+ this.next(); //Skip remain ~
105
152
  }
106
153
  handleInlineBlock() {
107
154
  let content = "";
@@ -110,12 +157,51 @@ class Lexer {
110
157
  content += this.peek();
111
158
  this.next();
112
159
  }
113
- // this.next() //Skip close block
114
160
  this.listToken.push({ "type": "InlineCode", content: content });
115
161
  }
116
162
  handleQuoteBlock() {
117
163
  this.listToken.push({ type: "Quote" });
118
164
  }
165
+ handleList(isOrdered, isTask) {
166
+ const line = this.peekUntil("\n");
167
+ if (isTask) {
168
+ const m = line.match(/^(\s*)([-*+]) \[( |x|X)\] (.*)$/);
169
+ const indent = Math.floor(m[1].length / 2) + 1;
170
+ while (this.listLevelFlag < indent)
171
+ this.handleStartList(false);
172
+ while (this.listLevelFlag > indent)
173
+ this.handleEndList();
174
+ this.next(m[1].length + 4);
175
+ this.handleTaskItem(m[3].toLowerCase() === "x");
176
+ }
177
+ else {
178
+ //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
179
+ const m = isOrdered ? line.match(/^(\s*)(\d+)\. (.*)$/) : line.match(/^(\s*)([-*+]) (.*)$/);
180
+ const indent = Math.floor(m[1].length / 2) + 1; //m[1] to get the spaces in group 1
181
+ while (this.listLevelFlag < indent)
182
+ this.handleStartList(isOrdered);
183
+ while (this.listLevelFlag > indent)
184
+ this.handleEndList();
185
+ this.next(m[1].length + (isOrdered ? 1 : 0)); //+1 due to marker have 2 characters (e.g: 1.) instead 1 like unordered list
186
+ this.handleListItem();
187
+ }
188
+ }
189
+ handleStartList(isOrder) {
190
+ this.listLevelFlag++;
191
+ this.listToken.push({ type: "ListStart", level: this.listLevelFlag, ordered: isOrder });
192
+ }
193
+ handleListItem() {
194
+ this.next(); // Skip space between - and text
195
+ this.listToken.push({ type: "ListItem" });
196
+ }
197
+ handleTaskItem(isChecked) {
198
+ this.next(); // Skip space between last ] and text
199
+ this.listToken.push({ type: "TaskItem", checked: isChecked });
200
+ }
201
+ handleEndList() {
202
+ this.listLevelFlag === 0 ? 0 : this.listLevelFlag--;
203
+ this.listToken.push({ type: "ListEnd" });
204
+ }
119
205
  handleLink() {
120
206
  this.next(); //Skip [
121
207
  const text = this.readUntil("]");
@@ -145,6 +231,11 @@ class Lexer {
145
231
  else
146
232
  this.listToken.push({ type: "Text", value: `![${alt}]` });
147
233
  }
234
+ handleHorizontalLine() {
235
+ this.next(2); //Skip two first characters, remain will be skiped after loop
236
+ this.listToken.push({ type: "HorizontalLine" });
237
+ }
238
+ //Utilities function
148
239
  readUntil(char) {
149
240
  let result = "";
150
241
  while (this.peek() !== char) {
@@ -153,5 +244,21 @@ class Lexer {
153
244
  }
154
245
  return result;
155
246
  }
247
+ peekUntil(char) {
248
+ let result = "";
249
+ let i = 0;
250
+ while (true) {
251
+ const current = this.peek(i++);
252
+ if (current == null)
253
+ break;
254
+ if (current == char)
255
+ break;
256
+ result += current;
257
+ }
258
+ return result;
259
+ }
260
+ isStartOfLine() {
261
+ return this.pos === 0 || this.peek(-1) === "\n";
262
+ }
156
263
  }
157
264
  exports.default = Lexer;
package/dist/parser.d.ts CHANGED
@@ -18,9 +18,13 @@ 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 parseHorizontalLine;
25
29
  private parseInlineUntil;
26
30
  }
package/dist/parser.js CHANGED
@@ -49,6 +49,14 @@ 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
+ }
52
60
  case "NewLine": {
53
61
  this.next(); // skip
54
62
  break;
@@ -90,6 +98,10 @@ class Parser {
90
98
  this.next(); // skip marker
91
99
  return { type: "Italic", children: this.parseInlineUntil("Italic") };
92
100
  }
101
+ parseStrikethrough() {
102
+ this.next(); // skip marker
103
+ return { type: "Strikethrough", children: this.parseInlineUntil("Strikethrough") };
104
+ }
93
105
  parseInlineCode() {
94
106
  const tok = this.peek();
95
107
  this.next();
@@ -102,6 +114,70 @@ class Parser {
102
114
  this.next(); //skip marker
103
115
  return { type: "Quote", children: [{ type: "Paragraph", children: this.parseInlineUntil("NewLine") }] };
104
116
  }
117
+ parseList() {
118
+ const tok = this.peek();
119
+ if (tok?.type === "ListStart") {
120
+ this.next(); //skip marker
121
+ const result = {
122
+ type: "List",
123
+ level: tok.level,
124
+ ordered: tok.ordered,
125
+ children: [],
126
+ };
127
+ let nextToken = this.peek();
128
+ while (!this.isEnd()) {
129
+ if (nextToken?.type === "ListItem" || nextToken?.type === "TaskItem") {
130
+ result.children.push(this.parseListItem());
131
+ nextToken = this.peek();
132
+ }
133
+ else if (nextToken?.type === "ListEnd") {
134
+ this.next();
135
+ break;
136
+ }
137
+ else
138
+ break;
139
+ }
140
+ return result;
141
+ }
142
+ //Temp return
143
+ return {
144
+ type: "Text",
145
+ value: ""
146
+ };
147
+ }
148
+ parseListItem() {
149
+ const currentToken = this.peek();
150
+ this.next(); // skip marker
151
+ const children = [];
152
+ while (!this.isEnd()) {
153
+ const tok = this.peek();
154
+ if (!tok)
155
+ break;
156
+ if (tok.type === "NewLine") {
157
+ this.next();
158
+ continue;
159
+ }
160
+ if (tok.type === "ListStart") {
161
+ children.push(this.parseList());
162
+ continue;
163
+ }
164
+ if (["ListItem", "TaskItem", "ListEnd"].includes(tok.type)) {
165
+ break;
166
+ }
167
+ children.push({
168
+ type: "Paragraph",
169
+ children: this.parseInlineUntil("NewLine")
170
+ });
171
+ }
172
+ return currentToken?.type === "TaskItem" ? {
173
+ type: "TaskItem",
174
+ checked: currentToken.type === "TaskItem" ? currentToken.checked : false,
175
+ children: children
176
+ } : {
177
+ type: "ListItem",
178
+ children: children
179
+ };
180
+ }
105
181
  parseLink() {
106
182
  const tok = this.peek();
107
183
  this.next();
@@ -127,12 +203,20 @@ class Parser {
127
203
  else
128
204
  return { type: "Image", src: "", alt: "" };
129
205
  }
206
+ parseHorizontalLine() {
207
+ const tok = this.peek();
208
+ this.next(); // skip marker
209
+ return { type: "HorizontalLine" };
210
+ }
130
211
  parseInlineUntil(stopType) {
212
+ const stop = Array.isArray(stopType) ? stopType : [stopType];
131
213
  const listNode = [];
132
- while (!this.isEnd() && this.peek()?.type !== stopType) {
214
+ while (!this.isEnd()) {
133
215
  const currentNode = this.peek();
134
216
  if (!currentNode)
135
217
  break;
218
+ if (stop.includes(currentNode.type))
219
+ break;
136
220
  switch (currentNode.type) {
137
221
  case "Bold": {
138
222
  listNode.push(this.parseBold());
@@ -142,6 +226,10 @@ class Parser {
142
226
  listNode.push(this.parseItalic());
143
227
  break;
144
228
  }
229
+ case "Strikethrough": {
230
+ listNode.push(this.parseStrikethrough());
231
+ break;
232
+ }
145
233
  case "InlineCode": {
146
234
  listNode.push(this.parseInlineCode());
147
235
  break;
package/dist/renderer.js CHANGED
@@ -19,16 +19,26 @@ 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
- Header: (node, children) => `<h${node.level}>${children.join("")}</h${node.level}>`,
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
+ List: (node, children) => node.ordered ? `<ol>${children.join("")}</ol>` : `<ul>${children.join("")}</ul>`,
30
+ ListItem: (_node, children) => `<li>${children.join("")}</li>`,
31
+ TaskItem: (node, children) => `<li><input type="checkbox" disabled ${node.checked ? "checked" : ""}>${children.join("")}</li>`,
32
+ //Styling nodes
27
33
  Bold: (_node, children) => `<strong>${children.join("")}</strong>`,
28
34
  Italic: (_node, children) => `<em>${children.join("")}</em>`,
29
- Quote: (_node, children) => `<blockquote>${children.join("")}</blockquote>`,
35
+ Strikethrough: (_node, children) => `<s>${children.join("")}</s>`,
36
+ InlineCode: (node) => `<code>${this.escapeHtml(node.content)}</code>`,
37
+ //Media nodes
30
38
  Link: (node) => `<a href="${node.href}">${node.text}</a>`,
31
39
  Image: (node) => `<img src="${node.src}" alt="${node.alt}"/>`,
40
+ //Leaf nodes
41
+ HorizontalLine: (_node) => `<hr>`,
32
42
  Text: (node) => node.value,
33
43
  };
34
44
  return this.option.elements?.[type] ?? defaultRender[type];
@@ -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,6 +71,8 @@ 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;
@@ -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 (`![alt](url)`)
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,11 @@ 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";
51
72
  };
package/package.json CHANGED
@@ -1,41 +1,41 @@
1
- {
2
- "name": "simple-customize-markdown-converter",
3
- "version": "1.0.2",
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.3",
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
  }