simple-customize-markdown-converter 1.0.6 → 1.0.7

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/dist/index.js CHANGED
@@ -7,6 +7,7 @@ exports.convertMarkdownToHTML = convertMarkdownToHTML;
7
7
  const lexer_1 = __importDefault(require("./lexer"));
8
8
  const parser_1 = require("./parser");
9
9
  const renderer_1 = __importDefault(require("./renderer"));
10
+ const resolver_1 = require("./resolver");
10
11
  /**
11
12
  * Convert a Markdown string into HTML.
12
13
  * @param input - The Markdown source string
@@ -21,6 +22,7 @@ const renderer_1 = __importDefault(require("./renderer"));
21
22
  */
22
23
  function convertMarkdownToHTML(input, options = {}) {
23
24
  const tokens = new lexer_1.default(input).tokenize();
24
- const nodes = new parser_1.Parser(tokens).parse();
25
- return new renderer_1.default(options).render(nodes);
25
+ const footNoteResolver = new resolver_1.FootnoteResolver();
26
+ const nodes = new parser_1.Parser(tokens, footNoteResolver).parse();
27
+ return new renderer_1.default(options, footNoteResolver).render(nodes);
26
28
  }
package/dist/lexer.d.ts CHANGED
@@ -34,7 +34,13 @@ export default class Lexer {
34
34
  private handleLink;
35
35
  private handleImage;
36
36
  private handleHorizontalLine;
37
+ private handleHtmlBlock;
38
+ private handleHtmlInline;
39
+ private handleFootnoteDef;
40
+ private handleFootnoteRef;
37
41
  private readUntil;
38
42
  private peekUntil;
43
+ private peekUntilByOffset;
39
44
  private isStartOfLine;
45
+ private readUntilMatchString;
40
46
  }
package/dist/lexer.js CHANGED
@@ -30,6 +30,24 @@ class Lexer {
30
30
  lex.handleTextBlock();
31
31
  }
32
32
  },
33
+ //For HTML
34
+ //Comment
35
+ { match: (lex) => lex.startsWith("<!--"), emit: (lex) => lex.readUntilMatchString("-->", true), },
36
+ //Normal HTML
37
+ {
38
+ match: (lex) => lex.peek() === "<",
39
+ emit: (lex) => {
40
+ //Handle comment
41
+ const line = lex.peekUntil(">");
42
+ const blockRegex = /^<(h[1-6]|div|table|pre|blockquote|ul|ol|li|p|section|article|header|footer|nav|aside|hr|form|iframe)\b/i;
43
+ if (blockRegex.test(line)) {
44
+ lex.handleHtmlBlock();
45
+ }
46
+ else {
47
+ lex.handleHtmlInline();
48
+ }
49
+ }
50
+ },
33
51
  {
34
52
  //Regex: if line started with at least 3 characters: -, *, _
35
53
  match: (lex) => /^([-*_])\1{2,}$/.test(lex.peekUntil("\n").trim()) && this.getLastToken()?.type === "NewLine",
@@ -38,6 +56,10 @@ class Lexer {
38
56
  { match: (lex) => lex.startsWith("```"), emit: (lex) => lex.handleCodeBlock() },
39
57
  { match: (lex) => lex.startsWith("**"), emit: (lex) => lex.handleBold() },
40
58
  { match: (lex) => lex.startsWith("~~"), emit: (lex) => lex.handleStrikethrough() },
59
+ // Footnote Definition
60
+ { match: (lex) => lex.isStartOfLine() && /^\[\^[^\]]+\]:/.test(lex.peekUntil("\n")), emit: (lex) => lex.handleFootnoteDef() },
61
+ // Footnote Reference
62
+ { match: (lex) => lex.startsWith("[^"), emit: (lex) => lex.handleFootnoteRef() },
41
63
  //For List
42
64
  {
43
65
  match: (lex) => lex.isStartOfLine() && /^(\s*)([-*+]) \[( |x|X)\] /.test(lex.peekUntil("\n")),
@@ -299,7 +321,60 @@ class Lexer {
299
321
  this.next(2); //Skip two first characters, remain will be skiped after loop
300
322
  this.listToken.push({ type: "HorizontalLine" });
301
323
  }
302
- //Utilities function
324
+ handleHtmlBlock() {
325
+ const openTag = this.readUntil(">", true) + ">";
326
+ const matchTagName = /^<\s*([a-zA-Z0-9]+)/.exec(openTag);
327
+ const tagName = matchTagName ? matchTagName[1] : null;
328
+ //Tagname is not valid
329
+ if (!tagName) {
330
+ this.listToken.push({ type: "Text", value: "<" });
331
+ return;
332
+ }
333
+ //If it's self-closing tag
334
+ if (openTag.endsWith("/>") || ["hr", "img", "br", "input", "meta", "link"].includes(tagName)) {
335
+ this.listToken.push({ type: "HTMLBlock", value: openTag });
336
+ return;
337
+ }
338
+ let content = "";
339
+ while (!this.isEndOfFile()) {
340
+ if (this.peekUntilByOffset(`</${tagName}>`.length).toLowerCase() === `</${tagName}>`) {
341
+ break;
342
+ }
343
+ content += this.peek();
344
+ this.next();
345
+ }
346
+ const closeTag = `</${tagName}>`;
347
+ this.next(closeTag.length - 1); //Skip closing tag
348
+ this.listToken.push({ type: "HTMLBlock", value: openTag + content + closeTag });
349
+ }
350
+ handleHtmlInline() {
351
+ const openTag = this.readUntil(">", true) + ">";
352
+ const matchTagName = /^<\s*([a-zA-Z0-9]+)/.exec(openTag);
353
+ const tagName = matchTagName ? matchTagName[1] : null;
354
+ if (!tagName) {
355
+ this.listToken.push({ type: "Text", value: "<" });
356
+ return;
357
+ }
358
+ const content = this.readUntilMatchString(`</${tagName}>`);
359
+ const closeTag = `</${tagName}>`;
360
+ this.next(closeTag.length - 1); //Skip closing tag
361
+ this.listToken.push({ type: "HTMLInline", value: openTag + content + closeTag });
362
+ }
363
+ handleFootnoteDef() {
364
+ const line = this.readUntil("\n");
365
+ const match = line.match(/^\[\^([^\]]+)\]:\s*(.*)$/);
366
+ if (match) {
367
+ const id = match[1];
368
+ const content = match[2];
369
+ this.listToken.push({ type: "FootnoteDef", id, content });
370
+ }
371
+ }
372
+ handleFootnoteRef() {
373
+ this.next(2); //Skip [^
374
+ const id = this.readUntil("]");
375
+ this.listToken.push({ type: "FootnoteRef", id });
376
+ }
377
+ //Utilities function
303
378
  readUntil(char, isConsumeChar = false) {
304
379
  let result = "";
305
380
  while (this.peek() !== char) {
@@ -325,8 +400,34 @@ class Lexer {
325
400
  }
326
401
  return result;
327
402
  }
403
+ peekUntilByOffset(offset) {
404
+ let result = "";
405
+ let i = 0;
406
+ while (i !== offset) {
407
+ const current = this.peek(i++);
408
+ if (current == null)
409
+ break;
410
+ if (this.isEndOfFile())
411
+ break;
412
+ result += current;
413
+ }
414
+ return result;
415
+ }
328
416
  isStartOfLine() {
329
417
  return this.pos === 0 || this.peek(-1) === "\n";
330
418
  }
419
+ readUntilMatchString(str, isConsume = false) {
420
+ let result = "";
421
+ while (!this.isEndOfFile()) {
422
+ if (this.peekUntilByOffset(str.length) === str) {
423
+ if (isConsume)
424
+ this.next(str.length);
425
+ break;
426
+ }
427
+ result += this.peek();
428
+ this.next();
429
+ }
430
+ return result;
431
+ }
331
432
  }
332
433
  exports.default = Lexer;
package/dist/parser.d.ts CHANGED
@@ -1,9 +1,11 @@
1
+ import { FootnoteResolver } from "./resolver";
1
2
  import { Node } from "./types/node";
2
3
  import { Token } from "./types/token";
3
4
  export declare class Parser {
4
5
  listToken: Token[];
5
6
  pos: number;
6
- constructor(listToken: Token[]);
7
+ footNoteResolver: FootnoteResolver;
8
+ constructor(listToken: Token[], footNoteResolver: FootnoteResolver);
7
9
  /**
8
10
  * Parse a list token to a node
9
11
  * @return A parsed abstract syntax tree (AST)
@@ -26,6 +28,10 @@ export declare class Parser {
26
28
  private parseLink;
27
29
  private parseImage;
28
30
  private parseTable;
31
+ private parseHtmlBlock;
32
+ private parseHtmlInline;
29
33
  private parseHorizontalLine;
34
+ private parseFootnoteDef;
35
+ private parseFootnoteRef;
30
36
  private parseInlineUntil;
31
37
  }
package/dist/parser.js CHANGED
@@ -2,9 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Parser = void 0;
4
4
  class Parser {
5
- constructor(listToken) {
5
+ constructor(listToken, footNoteResolver) {
6
6
  this.pos = 0;
7
7
  this.listToken = listToken;
8
+ this.footNoteResolver = footNoteResolver;
8
9
  }
9
10
  /**
10
11
  * Parse a list token to a node
@@ -61,6 +62,15 @@ class Parser {
61
62
  listNode.push(this.parseTable());
62
63
  break;
63
64
  }
65
+ case "HTMLBlock": {
66
+ listNode.push(this.parseHtmlBlock());
67
+ break;
68
+ }
69
+ case "FootnoteDef": {
70
+ this.parseFootnoteDef();
71
+ this.next();
72
+ break;
73
+ }
64
74
  case "NewLine": {
65
75
  this.next(); // skip
66
76
  break;
@@ -244,12 +254,43 @@ class Parser {
244
254
  rows: rows
245
255
  };
246
256
  }
247
- parseHorizontalLine() {
257
+ parseHtmlBlock() {
248
258
  const tok = this.peek();
259
+ this.next(); // skip marker
260
+ if (tok?.type === "HTMLBlock") {
261
+ return { type: "HTMLBlock", value: tok.value };
262
+ }
263
+ else
264
+ return { type: "Text", value: "" };
265
+ }
266
+ parseHtmlInline() {
267
+ const tok = this.peek();
268
+ this.next(); // skip marker
269
+ if (tok?.type === "HTMLInline") {
270
+ return { type: "HTMLInline", value: tok.value };
271
+ }
272
+ else
273
+ return { type: "Text", value: "" };
274
+ }
275
+ parseHorizontalLine() {
249
276
  this.next(); // skip marker
250
277
  return { type: "HorizontalLine" };
251
278
  }
252
- parseInlineUntil(stopType) {
279
+ parseFootnoteDef() {
280
+ const tok = this.peek();
281
+ if (tok?.type !== "FootnoteDef")
282
+ return;
283
+ this.footNoteResolver.addDef(tok.id, tok.content);
284
+ }
285
+ parseFootnoteRef() {
286
+ const tok = this.peek();
287
+ this.next();
288
+ if (tok?.type !== "FootnoteRef")
289
+ return { type: "Text", value: "" };
290
+ this.footNoteResolver.addUsedRef(tok.id);
291
+ return { type: "FootnoteRef", id: tok.id };
292
+ }
293
+ parseInlineUntil(stopType, isConsumeStopToken = true) {
253
294
  const stop = Array.isArray(stopType) ? stopType : [stopType];
254
295
  const listNode = [];
255
296
  while (!this.isEnd()) {
@@ -284,10 +325,21 @@ class Parser {
284
325
  listNode.push(this.parseLink());
285
326
  break;
286
327
  }
328
+ case "HTMLInline": {
329
+ listNode.push(this.parseHtmlInline());
330
+ break;
331
+ }
332
+ case "FootnoteRef": {
333
+ listNode.push(this.parseFootnoteRef());
334
+ break;
335
+ }
336
+ //Special case
337
+ case "HTMLBlock": return listNode;
287
338
  default: this.next();
288
339
  }
289
340
  }
290
- this.next(); //Skip stop token
341
+ if (isConsumeStopToken)
342
+ this.next(); //Skip stop token
291
343
  return listNode;
292
344
  }
293
345
  }
@@ -1,8 +1,10 @@
1
+ import { FootnoteResolver } from "./resolver";
1
2
  import { Node } from "./types/node";
2
3
  import { RenderOption } from "./types/renderOptions";
3
4
  export default class Renderer {
4
5
  option: RenderOption;
5
- constructor(option: RenderOption);
6
+ footNoteResolver: FootnoteResolver;
7
+ constructor(option: RenderOption, footNoteResolver: FootnoteResolver);
6
8
  /**
7
9
  * Render a Node (AST) to a HTML string according renderer options
8
10
  *
@@ -15,4 +17,5 @@ export default class Renderer {
15
17
  private handleRender;
16
18
  private renderTable;
17
19
  private escapeHtml;
20
+ private renderFootnotes;
18
21
  }
package/dist/renderer.js CHANGED
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  class Renderer {
4
- constructor(option) {
4
+ constructor(option, footNoteResolver) {
5
5
  this.option = option;
6
+ this.footNoteResolver = footNoteResolver;
6
7
  }
7
8
  /**
8
9
  * Render a Node (AST) to a HTML string according renderer options
@@ -20,7 +21,7 @@ class Renderer {
20
21
  handleRender(type) {
21
22
  const defaultRender = {
22
23
  //Base structural nodes
23
- Document: (_node, children) => children.join(""),
24
+ Document: (_node, children) => children.join("") + this.renderFootnotes(),
24
25
  Paragraph: (_node, children) => `<p>${children.join("")}</p>`,
25
26
  //Container nodes
26
27
  CodeBlock: (node) => `<pre><code class="lang-${node.lang}">${this.escapeHtml(node.content)}</code></pre>`,
@@ -43,6 +44,14 @@ class Renderer {
43
44
  Text: (node) => node.value,
44
45
  //For table nodes
45
46
  Table: (node, children) => this.renderTable(node, children),
47
+ //For HTML
48
+ HTMLBlock: (node) => node.value,
49
+ HTMLInline: (node) => node.value,
50
+ //For footnote
51
+ FootnoteRef: (node) => {
52
+ const idx = this.footNoteResolver.getUsedRefById(node.id);
53
+ return `<sup id="fnref:${idx}"><a href="#fn:${idx}" class="footnote-ref">[${idx}]</a></sup>`;
54
+ }
46
55
  };
47
56
  return (this.option.elements?.[type] ?? defaultRender[type]);
48
57
  }
@@ -68,5 +77,20 @@ class Renderer {
68
77
  escapeHtml(str) {
69
78
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
70
79
  }
80
+ renderFootnotes() {
81
+ if (this.footNoteResolver.isResolverValid()) {
82
+ const used = this.footNoteResolver.getUsedRef();
83
+ if (used.length === 0)
84
+ return "";
85
+ const items = used.map((id, i) => {
86
+ const def = this.footNoteResolver.getDef(id) ?? "";
87
+ const idx = i + 1;
88
+ return `<li id="fn:${idx}"><p>${def} <a href="#fnref:${idx}" class="footnote-backref">↩</a></p></li>`;
89
+ });
90
+ return `<section class="footnotes"><ol>${items.join("")}</ol></section>`;
91
+ }
92
+ else
93
+ return "";
94
+ }
71
95
  }
72
96
  exports.default = Renderer;
@@ -0,0 +1,15 @@
1
+ declare abstract class Resolver {
2
+ abstract isResolverValid(): boolean;
3
+ }
4
+ export declare class FootnoteResolver extends Resolver {
5
+ private defs;
6
+ private usedRef;
7
+ addDef(id: string, content: string): void;
8
+ addUsedRef(id: string): void;
9
+ resolve(id: string): string | undefined;
10
+ getUsedRef(): string[];
11
+ getUsedRefById(id: string): number;
12
+ getDef(id: string): string | undefined;
13
+ isResolverValid(): boolean;
14
+ }
15
+ export {};
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FootnoteResolver = void 0;
4
+ class Resolver {
5
+ }
6
+ class FootnoteResolver extends Resolver {
7
+ constructor() {
8
+ super(...arguments);
9
+ this.defs = new Map();
10
+ this.usedRef = [];
11
+ }
12
+ addDef(id, content) {
13
+ this.defs.set(id, content);
14
+ }
15
+ addUsedRef(id) {
16
+ if (!this.usedRef.includes(id)) {
17
+ this.usedRef.push(id);
18
+ }
19
+ }
20
+ resolve(id) {
21
+ return this.defs.get(id);
22
+ }
23
+ getUsedRef() {
24
+ return this.usedRef;
25
+ }
26
+ getUsedRefById(id) {
27
+ return this.usedRef.indexOf(id) + 1;
28
+ }
29
+ getDef(id) {
30
+ return this.defs.get(id);
31
+ }
32
+ isResolverValid() {
33
+ return this.defs.size !== 0 && this.usedRef.length !== 0;
34
+ }
35
+ }
36
+ exports.FootnoteResolver = FootnoteResolver;
@@ -21,6 +21,10 @@
21
21
  * - Image: An image, with it's `src` and `alt`
22
22
  * - HorizontalLine: A horizontal line
23
23
  * - Text: Raw text content.
24
+ * - Table: A table, with it's rows
25
+ * - HTMLBlock: A HTML block element, with it's `value`
26
+ * - HTMLInline: An inline HTML element, with it's `value`
27
+ * - FootnoteRef: A refernce with it's `id`
24
28
  */
25
29
  export type Node = {
26
30
  type: "Document";
@@ -79,11 +83,30 @@ export type Node = {
79
83
  } | {
80
84
  type: "Table";
81
85
  rows: TableRow[];
86
+ } | {
87
+ type: "HTMLBlock";
88
+ value: string;
89
+ } | {
90
+ type: "HTMLInline";
91
+ value: string;
92
+ } | {
93
+ type: "FootnoteRef";
94
+ id: string;
82
95
  };
96
+ /**
97
+ * A subtype represent a row of table
98
+ * @property isHeader - If this row is header
99
+ * @property cells: List cells of this row
100
+ */
83
101
  export type TableRow = {
84
102
  isHeader: boolean;
85
103
  cells: TableCell[];
86
104
  };
105
+ /**
106
+ * A subtype represent a table cell
107
+ * @property align - Cell's align
108
+ * @property children - Cell's children nodes
109
+ */
87
110
  export type TableCell = {
88
111
  align: "left" | "center" | "right";
89
112
  children: Node[];
@@ -21,6 +21,16 @@
21
21
  * - Image: An image (`![alt](url)`)
22
22
  * - HorizontalLine: A horizontal line (`---` or `___` or `***`)
23
23
  * - Text: Plain text content.
24
+ * - TableStart: Start of a table
25
+ * - TableEnd: End of a table
26
+ * - RowStart: Start of a table row
27
+ * - RowEnd: End of a table row
28
+ * - CellStart: Start of a table cell, with it's align accroding to it's row
29
+ * - CellEnd: End of a table cell
30
+ * - HTMLBlock: A HTML block element
31
+ * - HTMLInline: An inline HTML element
32
+ * - FootnodeDef: Definition of a footnote
33
+ * - FootnodeRef: The reference of a footnote
24
34
  * - EOF: A special token, this is the end of input.
25
35
  */
26
36
  export type Token = {
@@ -67,8 +77,6 @@ export type Token = {
67
77
  } | {
68
78
  type: "Text";
69
79
  value: string;
70
- } | {
71
- type: "EOF";
72
80
  } | {
73
81
  type: "TableStart";
74
82
  } | {
@@ -83,4 +91,19 @@ export type Token = {
83
91
  align: "left" | "center" | "right";
84
92
  } | {
85
93
  type: "CellEnd";
94
+ } | {
95
+ type: "HTMLBlock";
96
+ value: string;
97
+ } | {
98
+ type: "HTMLInline";
99
+ value: string;
100
+ } | {
101
+ type: "FootnoteDef";
102
+ id: string;
103
+ content: string;
104
+ } | {
105
+ type: "FootnoteRef";
106
+ id: string;
107
+ } | {
108
+ type: "EOF";
86
109
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simple-customize-markdown-converter",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Convert Markdown to your customize HTML",
5
5
  "keywords": [
6
6
  "markdown",