simple-customize-markdown-converter 1.0.7 → 1.1.0

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.
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ class DefaultRenderer {
4
+ constructor(options, footNoteResolver) {
5
+ this.options = options;
6
+ this.footNoteResolver = footNoteResolver;
7
+ }
8
+ /**
9
+ * Render a Node (AST) to a HTML string according renderer options
10
+ *
11
+ * @param node - The abstract syntax tree (AST) from the Parser
12
+ * @returns The rendered HTML string.
13
+ */
14
+ render(node) {
15
+ //Get proper handler type
16
+ const handler = this.handleRender(node.type);
17
+ //If node have children, recursive to handle all node's children
18
+ const children = "children" in node ? node.children.map((ele) => this.render(ele)) : [];
19
+ return handler(node, children);
20
+ }
21
+ /**
22
+ * Select the appropriate rendering handler for a specific node type
23
+ * @param type - The type of AST Note
24
+ * @returns A function take a node and its children to procude a string.
25
+ */
26
+ handleRender(type) {
27
+ const defaultRender = {
28
+ //Base structural nodes
29
+ Document: (_node, children) => children.join("") + this.renderFootnotes(),
30
+ Paragraph: (_node, children) => `<p>${children.join("")}</p>`,
31
+ //Container nodes
32
+ CodeBlock: (node) => `<pre><code class="lang-${node.lang}">${this.escapeHtml(node.content)}</code></pre>`,
33
+ Header: (node, children) => `<h${node.level}${node.level <= 2 ? ' style="border-bottom: 1px solid #d1d9e0b3"' : ''}>${children.join("")}</h${node.level}>`,
34
+ Quote: (_node, children) => `<blockquote style="margin:0; padding:0 1em; color:#59636e; border-left:.25em solid #d1d9e0;">${children.join("")}</blockquote>`,
35
+ //For list nodes
36
+ List: (node, children) => node.ordered ? `<ol>${children.join("")}</ol>` : `<ul>${children.join("")}</ul>`,
37
+ ListItem: (_node, children) => `<li>${children.join("")}</li>`,
38
+ TaskItem: (node, children) => `<li><input type="checkbox" disabled ${node.checked ? "checked" : ""}>${children.join("")}</li>`,
39
+ //Styling nodes
40
+ Bold: (_node, children) => `<strong>${children.join("")}</strong>`,
41
+ Italic: (_node, children) => `<em>${children.join("")}</em>`,
42
+ Strikethrough: (_node, children) => `<s>${children.join("")}</s>`,
43
+ InlineCode: (node) => `<code>${this.escapeHtml(node.content)}</code>`,
44
+ //Media nodes
45
+ Link: (node) => `<a href="${node.href}">${node.text}</a>`,
46
+ Image: (node) => `<img src="${node.src}" alt="${node.alt}"/>`,
47
+ //Leaf nodes
48
+ HorizontalLine: (_node) => `<hr>`,
49
+ Text: (node) => node.value,
50
+ //For table nodes
51
+ Table: (node, children) => this.renderTable(node, children),
52
+ //For HTML
53
+ HTMLBlock: (node) => this.options.converterOptions?.allowDangerousHtml
54
+ ? node.value
55
+ : this.escapeHtml(node.value),
56
+ HTMLInline: (node) => this.options.converterOptions?.allowDangerousHtml
57
+ ? node.value
58
+ : this.escapeHtml(node.value),
59
+ //For footnote
60
+ FootnoteRef: (node) => {
61
+ const idx = this.footNoteResolver.getUsedRefById(node.id);
62
+ return `<sup id="fnref:${idx}"><a href="#fn:${idx}" class="footnote-ref">[${idx}]</a></sup>`;
63
+ }
64
+ };
65
+ return (this.options.renderOptions?.elements?.[type] ?? defaultRender[type]);
66
+ }
67
+ renderTable(node, children) {
68
+ if (node.type === "Table") {
69
+ const header = node.rows.filter(row => row.isHeader);
70
+ const body = node.rows.filter(row => !row.isHeader);
71
+ const renderRows = (row) => {
72
+ const tag = row.isHeader ? "th" : "td";
73
+ const cells = row.cells.map(cell => {
74
+ const align = `style="text-align:${cell.align}"`;
75
+ return `<${tag} ${align}>${cell.children.map(c => this.render(c)).join("")}</${tag}>`;
76
+ }).join("");
77
+ return `<tr>${cells}</tr>`;
78
+ };
79
+ const tHead = header.length ? `<thead>${header.map(renderRows).join("")}</thead>` : "";
80
+ const tBody = body.length ? `<tbody>${body.map(renderRows).join("")}</tbody>` : "";
81
+ return `<table>${tHead}${tBody}</table>`;
82
+ }
83
+ else
84
+ return `<p>${children.join("\n")}</p>`;
85
+ }
86
+ escapeHtml(str) {
87
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
88
+ }
89
+ renderFootnotes() {
90
+ if (this.footNoteResolver.isResolverValid()) {
91
+ const used = this.footNoteResolver.getUsedRef();
92
+ if (used.length === 0)
93
+ return "";
94
+ const items = used.map((id, i) => {
95
+ const def = this.footNoteResolver.getDef(id) ?? "";
96
+ const idx = i + 1;
97
+ return `<li id="fn:${idx}"><p>${def} <a href="#fnref:${idx}" class="footnote-backref">↩</a></p></li>`;
98
+ });
99
+ return `<section class="footnotes"><ol>${items.join("")}</ol></section>`;
100
+ }
101
+ else
102
+ return "";
103
+ }
104
+ }
105
+ exports.default = DefaultRenderer;
@@ -0,0 +1,26 @@
1
+ import { ReactNode } from "react";
2
+ import { FootnoteResolver } from "../core/resolver";
3
+ import { Node } from "../types/node";
4
+ import { MarkdownReactOptions } from "../types/options";
5
+ export default class ReactRenderer {
6
+ options: MarkdownReactOptions;
7
+ footNoteResolver: FootnoteResolver;
8
+ constructor(footNoteResolver: FootnoteResolver, options: MarkdownReactOptions);
9
+ /**
10
+ * Render a Node (AST) to a ReactNode according renderer options
11
+ *
12
+ * @param node - The abstract syntax tree (AST) from the Parser
13
+ * @returns The ReactNode representing the rendered Markdown element.
14
+ */
15
+ render<K extends Node["type"]>(node: Extract<Node, {
16
+ type: K;
17
+ }>): ReactNode;
18
+ /**
19
+ * Select the appropriate rendering handler for a specific node type
20
+ * @param type - The type of AST Note
21
+ * @returns A function take a node and its children to procude a ReactNode.
22
+ */
23
+ private handleRender;
24
+ private renderTable;
25
+ private renderFootnotes;
26
+ }
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const react_1 = __importDefault(require("react"));
7
+ class ReactRenderer {
8
+ constructor(footNoteResolver, options) {
9
+ this.footNoteResolver = footNoteResolver;
10
+ this.options = options;
11
+ }
12
+ /**
13
+ * Render a Node (AST) to a ReactNode according renderer options
14
+ *
15
+ * @param node - The abstract syntax tree (AST) from the Parser
16
+ * @returns The ReactNode representing the rendered Markdown element.
17
+ */
18
+ render(node) {
19
+ //Get proper handler type
20
+ const handler = this.handleRender(node.type);
21
+ //If node have children, recursive to handle all node's children
22
+ const children = "children" in node ? node.children.map((ele) => this.render(ele)) : [];
23
+ return handler(node, children);
24
+ }
25
+ /**
26
+ * Select the appropriate rendering handler for a specific node type
27
+ * @param type - The type of AST Note
28
+ * @returns A function take a node and its children to procude a ReactNode.
29
+ */
30
+ handleRender(type) {
31
+ const defaultRender = {
32
+ //Base structural nodes
33
+ Document: (_node, children) => react_1.default.createElement(react_1.default.Fragment, null, ...children, this.renderFootnotes()),
34
+ Paragraph: (_node, children) => react_1.default.createElement("p", null, ...children),
35
+ //Container nodes
36
+ CodeBlock: (node) => react_1.default.createElement("pre", null, react_1.default.createElement("code", { className: `lang-${node.lang}` }, node.content)),
37
+ Header: (node, children) => react_1.default.createElement(`h${node.level}`, { style: { borderBottom: node.level <= 2 ? "1px solid #d1d9e0b3" : undefined } }, ...children),
38
+ Quote: (_node, children) => react_1.default.createElement("blockquote", { style: { margin: "0", padding: "0 1em", color: "#59636e", borderLeft: ".25em solid #d1d9e0" } }, ...children),
39
+ //For list nodes
40
+ List: (node, children) => node.ordered ?
41
+ react_1.default.createElement("ol", null, ...children) :
42
+ react_1.default.createElement("ul", null, ...children),
43
+ ListItem: (_node, children) => react_1.default.createElement("li", null, ...children),
44
+ TaskItem: (node, children) => react_1.default.createElement("li", null, react_1.default.createElement("input", {
45
+ type: "checkbox",
46
+ disabled: true,
47
+ checked: !!node.checked,
48
+ readOnly: true
49
+ }), ...children),
50
+ //Styling nodes
51
+ Bold: (_node, children) => react_1.default.createElement("strong", null, ...children),
52
+ Italic: (_node, children) => react_1.default.createElement("em", null, ...children),
53
+ Strikethrough: (_node, children) => react_1.default.createElement("s", null, ...children),
54
+ InlineCode: (node) => react_1.default.createElement("code", null, node.content),
55
+ //Media nodes
56
+ Link: (node) => react_1.default.createElement("a", {
57
+ href: node.href,
58
+ //Security reason
59
+ target: "_blank",
60
+ rel: "noopener"
61
+ }, node.text),
62
+ Image: (node) => react_1.default.createElement("img", {
63
+ src: node.src,
64
+ alt: node.alt,
65
+ }, null),
66
+ //Leaf nodes
67
+ HorizontalLine: (_node) => react_1.default.createElement("hr"),
68
+ Text: (node) => node.value || "",
69
+ //For table nodes
70
+ Table: (node, children) => this.renderTable(node, children),
71
+ //For HTML
72
+ HTMLBlock: (node) => this.options.converterOptions?.allowDangerousHtml
73
+ ? react_1.default.createElement("div", { dangerouslySetInnerHTML: { __html: node.value } })
74
+ : react_1.default.createElement("code", null, node.value),
75
+ HTMLInline: (node) => this.options.converterOptions?.allowDangerousHtml
76
+ ? react_1.default.createElement("span", { dangerouslySetInnerHTML: { __html: node.value } })
77
+ : react_1.default.createElement("code", null, node.value),
78
+ //For footnote
79
+ FootnoteRef: (node) => {
80
+ const idx = this.footNoteResolver.getUsedRefById(node.id);
81
+ return react_1.default.createElement("sup", { id: `fnref:${idx}` }, react_1.default.createElement("a", { href: `#fn:${idx}`, className: "footnote-ref" }, `[${idx}]`));
82
+ }
83
+ };
84
+ return (this.options.renderOptions?.elements?.[type] ?? defaultRender[type]);
85
+ }
86
+ renderTable(node, children) {
87
+ if (node.type === "Table") {
88
+ const header = node.rows.filter(row => row.isHeader);
89
+ const body = node.rows.filter(row => !row.isHeader);
90
+ const renderRows = (row, rowIndex) => {
91
+ return react_1.default.createElement("tr", { key: rowIndex }, row.cells.map((cell, cellIndex) => {
92
+ const tag = row.isHeader ? "th" : "td";
93
+ return react_1.default.createElement(tag, { key: cellIndex, style: { textAlign: cell.align } }, ...cell.children.map(c => this.render(c)));
94
+ }));
95
+ };
96
+ const tHead = header.length
97
+ ? react_1.default.createElement("thead", null, header.map((row, i) => renderRows(row, i)))
98
+ : null;
99
+ const tBody = body.length
100
+ ? react_1.default.createElement("tbody", null, body.map((row, i) => renderRows(row, i)))
101
+ : null;
102
+ return react_1.default.createElement("table", null, tHead, tBody);
103
+ }
104
+ else
105
+ return react_1.default.createElement("p", null, ...children);
106
+ }
107
+ renderFootnotes() {
108
+ if (this.footNoteResolver.isResolverValid()) {
109
+ const used = this.footNoteResolver.getUsedRef();
110
+ if (used.length === 0)
111
+ return null;
112
+ const items = used.map((id, i) => {
113
+ const def = this.footNoteResolver.getDef(id) ?? "";
114
+ const idx = i + 1;
115
+ return react_1.default.createElement("li", { id: `fn:${idx}` }, react_1.default.createElement("p", null, def + " ", react_1.default.createElement("a", { href: `#fnref:${idx}`, className: "footnote-backref" }, "↩")));
116
+ });
117
+ return react_1.default.createElement("section", { className: "footnotes" }, react_1.default.createElement("ol", null, ...items));
118
+ }
119
+ else
120
+ return null;
121
+ }
122
+ }
123
+ exports.default = ReactRenderer;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * General option for the render process,
3
+ * These rules define high-level behavior for render process.
4
+ */
5
+ export type ConvertOption = {
6
+ /**
7
+ * Allow raw HTML rendered in the Markdown input.
8
+ * When false (default), HTML tags are escaped for security
9
+ */
10
+ allowDangerousHtml: boolean;
11
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,17 @@
1
+ import { ConvertOption } from "./converterOptions";
2
+ import { ReactRenderOption } from "./reactRenderOptions";
3
+ import { RenderOption } from "./renderOptions";
4
+ /**
5
+ * General option for rendering Markdown into HTML strings
6
+ */
7
+ export type MarkdownDefaultOptions = {
8
+ renderOptions?: RenderOption;
9
+ converterOptions?: ConvertOption;
10
+ };
11
+ /**
12
+ * General option for rendering Markdown into `React.ReactNode` elements
13
+ */
14
+ export type MarkdownReactOptions = {
15
+ renderOptions?: ReactRenderOption;
16
+ converterOptions?: ConvertOption;
17
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,50 @@
1
+ import { Node } from "../node";
2
+ /**
3
+ * Function type for rendering an AST node to a ReactNode.
4
+ *
5
+ * @template T - A subtype of `Node` corresponding to the render node
6
+ * @param node - The AST node to render
7
+ * @param children - An array of rendered `ReactNode` from the node's children
8
+ * @returns A `React.ReactNode` representation of the node
9
+ */
10
+ type ReactNodeRenderer<T extends Node = Node> = (node: T, children: React.ReactNode[]) => React.ReactNode;
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 `ReactNodeRenderer` function:
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 `ReactNode` of its children.
19
+ */
20
+ export type ReactRenderElements = {
21
+ [K in Node["type"]]?: ReactNodeRenderer<Extract<Node, {
22
+ type: K;
23
+ }>>;
24
+ };
25
+ /**
26
+ * Options to customize how AST nodes are renderes into ReactNode elements
27
+ *
28
+ * @property elements - Optional custom rendered for one or more node types
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * // Using JSX (Recommended for most users)
33
+ * const renderOptions: ReactRenderOption = {
34
+ * elements: {
35
+ * Paragraph: (_node, children) => <p className="paragraph">{children}</p>,
36
+ * Bold: (_node, children) => <strong className="bold-text">{children}</strong>,
37
+ * }
38
+ * }
39
+ * // Or using React.createElement (Common in library core or without JSX)
40
+ * const renderOptions: ReactRenderOption = {
41
+ * elements: {
42
+ * Bold: (_node, children) => React.createElement("b", { className: "bold" }, ...children),
43
+ * }
44
+ * }
45
+ * ```
46
+ */
47
+ export type ReactRenderOption = {
48
+ elements?: ReactRenderElements;
49
+ };
50
+ export {};
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,44 @@
1
+ import { Node } from "../node";
2
+ /**
3
+ * Function type for rendering an AST node to HTML.
4
+ *
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
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * const renderOptions: RenderOption = {
33
+ * elements: {
34
+ * Paragraph: (_node, children) => `<div class="paragraph">${children.join("")}</div>`,
35
+ * Bold: (_node, children) => `<b class="bold-text">${children.join("")}</b>`,
36
+ * }
37
+ * }
38
+ * ```
39
+ *
40
+ */
41
+ export type RenderOption = {
42
+ elements?: RenderElements;
43
+ };
44
+ export {};
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simple-customize-markdown-converter",
3
- "version": "1.0.7",
3
+ "version": "1.1.0",
4
4
  "description": "Convert Markdown to your customize HTML",
5
5
  "keywords": [
6
6
  "markdown",
@@ -17,15 +17,34 @@
17
17
  "LICENSE",
18
18
  "README.md"
19
19
  ],
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "default": "./dist/index.js"
24
+ },
25
+ "./react": {
26
+ "types": "./dist/react.d.ts",
27
+ "default": "./dist/react.js"
28
+ }
29
+ },
20
30
  "scripts": {
21
31
  "test": "jest",
22
32
  "build": "tsc",
23
33
  "start": "ts-node src/index.ts"
24
34
  },
35
+ "peerDependencies": {
36
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.2.0",
37
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.2.0"
38
+ },
25
39
  "devDependencies": {
26
40
  "@types/jest": "^30.0.0",
27
41
  "@types/node": "^24.3.3",
42
+ "@types/react": "^19.2.7",
43
+ "@types/react-dom": "^19.2.3",
28
44
  "jest": "^30.1.3",
45
+ "jest-environment-jsdom": "^30.2.0",
46
+ "react": "^19.2.3",
47
+ "react-dom": "^19.2.3",
29
48
  "ts-jest": "^29.4.1",
30
49
  "ts-node": "^10.9.2",
31
50
  "typescript": "^5.9.2"