simple-customize-markdown-converter 1.2.1 → 1.3.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.
package/README.md CHANGED
@@ -173,6 +173,85 @@ function App() {
173
173
  }
174
174
 
175
175
  ```
176
+
177
+ ## Plugin (v1.3.0+)
178
+ You can create a plugin to define custom sytax rule handler
179
+ - With default converter
180
+ ```ts
181
+ import { DefaultMarkdownConverter } from 'simple-customize-markdown-converter';
182
+
183
+ const emojiPlugin = createPlugin<string, React.ReactNode>(
184
+ "Emoji",
185
+ "inline",
186
+ {
187
+ match: (lexer) => lexer.peek() === ":",
188
+ emit: (lexer) => {
189
+ lexer.next();
190
+ const value = lexer.readUntil(":");
191
+ lexer.listToken.push({ type: "Emoji", value });
192
+ }
193
+ },
194
+ {
195
+ execute: (parser, token) => {
196
+ parser.next(1);
197
+ return { type: "Emoji", value: token.value };
198
+ }
199
+ },
200
+ {
201
+ render: (node) => React.createElement(
202
+ "span",
203
+ { className: `emoji emoji-${node.value}` },
204
+ "😲"
205
+ )
206
+ }
207
+ );
208
+
209
+ const converter = new DefaultMarkdownConverter({}, plugin).convert(input)
210
+ ```
211
+
212
+ - With React converter
213
+ ```tsx
214
+ import { MarkdownComponent } from 'simple-customize-markdown-converter/react';
215
+
216
+ function App() {
217
+ const emojiPlugin = createPlugin<string, React.ReactNode>(
218
+ "Emoji",
219
+ "inline",
220
+ {
221
+ match: (lexer) => lexer.peek() === ":",
222
+ emit: (lexer) => {
223
+ lexer.next();
224
+ const value = lexer.readUntil(":");
225
+ lexer.listToken.push({ type: "Emoji", value });
226
+ }
227
+ },
228
+ {
229
+ execute: (parser, token) => {
230
+ parser.next(1);
231
+ return { type: "Emoji", value: token.value };
232
+ }
233
+ },
234
+ {
235
+ render: (node) => React.createElement(
236
+ "span",
237
+ { className: `emoji emoji-${node.value}` },
238
+ "😲"
239
+ )
240
+ }
241
+ );
242
+
243
+ return (
244
+ <MarkdownComponent
245
+ content="Hello :omg: world"
246
+ className="md-body"
247
+ options=options
248
+ plugin=[emojiPlugin]
249
+ />
250
+ );
251
+ }
252
+ ```
253
+
254
+
176
255
  ## Security
177
256
  By default, HTML tags in Markdown are escaped. To allow raw HTML, explicitly set `allowDangerousHtml: true` in `converterOptions`. Be sure only **enable** this for **trusted** content.
178
257
 
@@ -36,7 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.UnorderedListHandler = exports.TaskListHandler = exports.TableHandler = exports.StrikethroughHandler = exports.QuoteHandler = exports.OrderedListHandler = exports.NewLineHandler = exports.LinkHandler = exports.ItalicHandler = exports.InlineCodeHandler = exports.ImageHandler = exports.HtmlHandler = exports.HorizontalLineHandler = exports.HeaderHandler = exports.FootnoteRefHandler = exports.FootnoteDefHandler = exports.EscapeCharacterHandler = exports.EndListHandler = exports.CommentHandler = exports.CodeBlockHandler = exports.BoldHandler = void 0;
37
37
  const utils = __importStar(require("../../utilities/tokenizer-utils"));
38
38
  const EscapeCharacterHandler = {
39
- name: "EscapeCharacter",
39
+ type: "EscapeCharacter",
40
40
  match: (lex) => lex.peek() === "\\" && lex.peek(1) !== undefined,
41
41
  emit: (lex) => {
42
42
  lex.next(1);
@@ -45,13 +45,13 @@ const EscapeCharacterHandler = {
45
45
  };
46
46
  exports.EscapeCharacterHandler = EscapeCharacterHandler;
47
47
  const CommentHandler = {
48
- name: "Comment",
48
+ type: "Comment",
49
49
  match: (lex) => lex.startsWith("<!--"),
50
50
  emit: (lex) => lex.readUntilMatchString("-->", true)
51
51
  };
52
52
  exports.CommentHandler = CommentHandler;
53
53
  const HtmlHandler = {
54
- name: "HTML",
54
+ type: "HTML",
55
55
  match: (lex) => lex.peek() === "<",
56
56
  emit: (lex) => {
57
57
  //Handle comment
@@ -67,7 +67,7 @@ const HtmlHandler = {
67
67
  };
68
68
  exports.HtmlHandler = HtmlHandler;
69
69
  const HorizontalLineHandler = {
70
- name: "HorizontalLine",
70
+ type: "HorizontalLine",
71
71
  match: (lex) => /^([-*_])\1{2,}$/.test(lex.peekUntil("\n").trim()) && lex.getLastToken()?.type === "NewLine",
72
72
  emit: (lex) => {
73
73
  lex.next(2); //Skip two first characters, remain will be skiped after loop
@@ -76,7 +76,7 @@ const HorizontalLineHandler = {
76
76
  };
77
77
  exports.HorizontalLineHandler = HorizontalLineHandler;
78
78
  const CodeBlockHandler = {
79
- name: "CodeBlock",
79
+ type: "CodeBlock",
80
80
  match: (lex) => lex.startsWith("```"),
81
81
  emit: (lex) => {
82
82
  let lang = "";
@@ -97,7 +97,7 @@ const CodeBlockHandler = {
97
97
  };
98
98
  exports.CodeBlockHandler = CodeBlockHandler;
99
99
  const BoldHandler = {
100
- name: "Bold",
100
+ type: "Bold",
101
101
  match: (lex) => lex.startsWith("**"),
102
102
  emit: (lex) => {
103
103
  lex.listToken.push({ type: "Bold" });
@@ -106,7 +106,7 @@ const BoldHandler = {
106
106
  };
107
107
  exports.BoldHandler = BoldHandler;
108
108
  const StrikethroughHandler = {
109
- name: "Strikethrough",
109
+ type: "Strikethrough",
110
110
  match: (lex) => lex.startsWith("~~"),
111
111
  emit: (lex) => {
112
112
  lex.listToken.push({ type: "Strikethrough" });
@@ -116,7 +116,7 @@ const StrikethroughHandler = {
116
116
  exports.StrikethroughHandler = StrikethroughHandler;
117
117
  // Footnote
118
118
  const FootnoteDefHandler = {
119
- name: "FootnoteDef",
119
+ type: "FootnoteDef",
120
120
  match: (lex) => lex.isStartOfLine() && /^\[\^[^\]]+\]:/.test(lex.peekUntil("\n")),
121
121
  emit: (lex) => {
122
122
  const line = lex.readUntil("\n");
@@ -130,7 +130,7 @@ const FootnoteDefHandler = {
130
130
  };
131
131
  exports.FootnoteDefHandler = FootnoteDefHandler;
132
132
  const FootnoteRefHandler = {
133
- name: "FootnoteRef",
133
+ type: "FootnoteRef",
134
134
  match: (lex) => lex.startsWith("[^"),
135
135
  emit: (lex) => {
136
136
  lex.next(2); //Skip [^
@@ -141,25 +141,25 @@ const FootnoteRefHandler = {
141
141
  exports.FootnoteRefHandler = FootnoteRefHandler;
142
142
  //List
143
143
  const TaskListHandler = {
144
- name: "TaskList",
144
+ type: "TaskList",
145
145
  match: (lex) => lex.isStartOfLine() && /^(\s*)([-*+]) \[( |x|X)\] /.test(lex.peekUntil("\n")),
146
146
  emit: (lex) => utils.handleList(lex, false, true)
147
147
  };
148
148
  exports.TaskListHandler = TaskListHandler;
149
149
  const UnorderedListHandler = {
150
- name: "UnorderList",
150
+ type: "UnorderList",
151
151
  match: (lex) => lex.isStartOfLine() && /^(\s*)([-*+]) /.test(lex.peekUntil("\n")),
152
152
  emit: (lex) => utils.handleList(lex, false, false)
153
153
  };
154
154
  exports.UnorderedListHandler = UnorderedListHandler;
155
155
  const OrderedListHandler = {
156
- name: "OrderedList",
156
+ type: "OrderedList",
157
157
  match: (lex) => lex.isStartOfLine() && /^(\s*)(\d+)\. /.test(lex.peekUntil("\n")),
158
158
  emit: (lex) => utils.handleList(lex, true, false)
159
159
  };
160
160
  exports.OrderedListHandler = OrderedListHandler;
161
161
  const EndListHandler = {
162
- name: "EndList",
162
+ type: "EndList",
163
163
  match: (lex) => lex.listLevelFlag > 0 && lex.isStartOfLine() && !/^(\s*)([-+*]|\d+\.) /.test(lex.peekUntil("\n")),
164
164
  emit: (lex) => {
165
165
  while (lex.listLevelFlag > 0) {
@@ -170,14 +170,14 @@ const EndListHandler = {
170
170
  exports.EndListHandler = EndListHandler;
171
171
  //Table
172
172
  const TableHandler = {
173
- name: "Table",
173
+ type: "Table",
174
174
  match: (lex) => lex.isStartOfLine() && /^\s*\|.*\|\s*$/.test(lex.peekUntil("\n")),
175
175
  emit: (lex) => utils.handleTable(lex)
176
176
  };
177
177
  exports.TableHandler = TableHandler;
178
178
  //Other common syntax
179
179
  const InlineCodeHandler = {
180
- name: "InlineCode",
180
+ type: "InlineCode",
181
181
  match: (lex) => lex.peek() === "`",
182
182
  emit: (lex) => {
183
183
  let content = "";
@@ -191,7 +191,7 @@ const InlineCodeHandler = {
191
191
  };
192
192
  exports.InlineCodeHandler = InlineCodeHandler;
193
193
  const HeaderHandler = {
194
- name: "Header",
194
+ type: "Header",
195
195
  match: (lex) => lex.peek() === "#",
196
196
  emit: (lex) => {
197
197
  let level = 0;
@@ -208,7 +208,7 @@ const HeaderHandler = {
208
208
  };
209
209
  exports.HeaderHandler = HeaderHandler;
210
210
  const ItalicHandler = {
211
- name: "Italic",
211
+ type: "Italic",
212
212
  match: (lex) => lex.peek() === "*" || lex.peek() === "_",
213
213
  emit: (lex) => {
214
214
  lex.listToken.push({ type: "Italic" });
@@ -216,7 +216,7 @@ const ItalicHandler = {
216
216
  };
217
217
  exports.ItalicHandler = ItalicHandler;
218
218
  const QuoteHandler = {
219
- name: "Quote",
219
+ type: "Quote",
220
220
  match: (lex) => lex.peek() === ">",
221
221
  emit: (lex) => {
222
222
  lex.listToken.push({ type: "Quote" });
@@ -224,7 +224,7 @@ const QuoteHandler = {
224
224
  };
225
225
  exports.QuoteHandler = QuoteHandler;
226
226
  const LinkHandler = {
227
- name: "Link",
227
+ type: "Link",
228
228
  match: (lex) => lex.peek() === "[",
229
229
  emit: (lex) => {
230
230
  lex.next(); //Skip [
@@ -242,7 +242,7 @@ const LinkHandler = {
242
242
  };
243
243
  exports.LinkHandler = LinkHandler;
244
244
  const ImageHandler = {
245
- name: "Image",
245
+ type: "Image",
246
246
  match: (lex) => lex.peek() === "!" && lex.peek(1) === "[",
247
247
  emit: (lex) => {
248
248
  lex.next(); //Skip !
@@ -263,7 +263,7 @@ const ImageHandler = {
263
263
  };
264
264
  exports.ImageHandler = ImageHandler;
265
265
  const NewLineHandler = {
266
- name: "NewLine",
266
+ type: "NewLine",
267
267
  match: (lex) => lex.peek() === "\n",
268
268
  emit: (lex) => {
269
269
  lex.listToken.push({ type: "NewLine" });
@@ -1,9 +1,10 @@
1
- import { Token } from "../../types/token";
1
+ import { Token, TokenizerStrategy } from "../../types/token";
2
2
  export interface ILexer {
3
3
  pos: number;
4
4
  input: string;
5
5
  listToken: Token[];
6
6
  listLevelFlag: number;
7
+ strategies: TokenizerStrategy[];
7
8
  peek(offset?: number): string | null;
8
9
  next(amount?: number): void;
9
10
  startsWith(str: string): boolean;
@@ -14,14 +15,19 @@ export interface ILexer {
14
15
  isEndOfFile(): boolean;
15
16
  isStartOfLine(): boolean;
16
17
  getLastToken(): Token;
18
+ registerStrategy(strategy: TokenizerStrategy): void;
17
19
  }
18
- export default class Lexer implements ILexer {
20
+ export declare class Lexer implements ILexer {
19
21
  input: string;
20
22
  pos: number;
21
23
  listToken: Token[];
22
24
  listLevelFlag: number;
23
- private strategies;
24
- constructor(input: string);
25
+ strategies: TokenizerStrategy[];
26
+ private coreStrategy;
27
+ private pluginStrategy;
28
+ private defaultStrategy;
29
+ constructor(input: string, pluginStrategy?: TokenizerStrategy[]);
30
+ private setUpStrategy;
25
31
  setInput(input: string): void;
26
32
  peek(offset?: number): string | null;
27
33
  next(amount?: number): void;
@@ -33,6 +39,7 @@ export default class Lexer implements ILexer {
33
39
  peekUntilByOffset(offset: number): string;
34
40
  isStartOfLine(): boolean;
35
41
  readUntilMatchString(str: string, isConsume?: boolean): string;
42
+ registerStrategy(strategy: TokenizerStrategy): void;
36
43
  /**
37
44
  * Tokenize the markdown into a list of tokens.
38
45
  * @param isEof - `True` when input is whole markdown, `False` if input is just a part of markdown.
@@ -33,19 +33,22 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.Lexer = void 0;
36
37
  const Handlers = __importStar(require("./handler"));
37
38
  const utils = __importStar(require("../../utilities/tokenizer-utils"));
38
39
  class Lexer {
39
- constructor(input) {
40
+ constructor(input, pluginStrategy = []) {
40
41
  this.pos = 0;
41
42
  this.listToken = [];
42
43
  // Flag for handle special syntax
43
44
  this.listLevelFlag = 0;
44
- this.input = input;
45
- this.strategies = [
45
+ this.coreStrategy = [
46
46
  Handlers.EscapeCharacterHandler,
47
47
  Handlers.CommentHandler,
48
48
  Handlers.HtmlHandler,
49
+ ];
50
+ this.pluginStrategy = [];
51
+ this.defaultStrategy = [
49
52
  Handlers.HorizontalLineHandler,
50
53
  Handlers.CodeBlockHandler,
51
54
  Handlers.BoldHandler,
@@ -65,6 +68,17 @@ class Lexer {
65
68
  Handlers.ImageHandler,
66
69
  Handlers.NewLineHandler,
67
70
  ];
71
+ this.input = input;
72
+ this.pluginStrategy = pluginStrategy;
73
+ this.strategies = [];
74
+ this.setUpStrategy();
75
+ }
76
+ setUpStrategy() {
77
+ this.strategies = [
78
+ ...this.coreStrategy,
79
+ ...this.pluginStrategy,
80
+ ...this.defaultStrategy
81
+ ];
68
82
  }
69
83
  //Reset input and other attribute
70
84
  setInput(input) {
@@ -146,6 +160,10 @@ class Lexer {
146
160
  }
147
161
  return result;
148
162
  }
163
+ registerStrategy(strategy) {
164
+ this.pluginStrategy.push(strategy);
165
+ this.setUpStrategy();
166
+ }
149
167
  /**
150
168
  * Tokenize the markdown into a list of tokens.
151
169
  * @param isEof - `True` when input is whole markdown, `False` if input is just a part of markdown.
@@ -174,4 +192,4 @@ class Lexer {
174
192
  return this.listToken;
175
193
  }
176
194
  }
177
- exports.default = Lexer;
195
+ exports.Lexer = Lexer;
@@ -1,26 +1,33 @@
1
- import { ASTNode } from '../../types/parser';
1
+ import { ASTNode, ParsingStrategy } from '../../types/parser';
2
2
  import { Token } from '../../types/token';
3
3
  import { FootnoteResolver } from '../resolver/footnote-resolver';
4
4
  export interface IParser {
5
5
  listToken: Token[];
6
6
  pos: number;
7
+ inlineStrategies: Map<string, ParsingStrategy>;
8
+ blockStrategies: Map<string, ParsingStrategy>;
7
9
  footNoteResolver: FootnoteResolver;
8
10
  peek(offset: number): Token | null;
9
11
  next(amount: number): void;
10
12
  isEnd(): boolean;
11
13
  parseBlocks(): ASTNode[];
12
14
  parseInlineUntil(stopType: Token["type"] | Token["type"][], isConsumeStopToken: boolean): ASTNode[];
15
+ registerStrategy(strategy: ParsingStrategy, type: "block" | "inline"): void;
13
16
  }
14
17
  export declare class Parser implements IParser {
15
18
  listToken: Token[];
16
19
  pos: number;
17
20
  footNoteResolver: FootnoteResolver;
18
- private inlineStrategies;
19
- private blockStrategies;
20
- constructor(listToken: Token[], footNoteResolver: FootnoteResolver);
21
+ inlineStrategies: Map<string, ParsingStrategy>;
22
+ blockStrategies: Map<string, ParsingStrategy>;
23
+ constructor(listToken: Token[], footNoteResolver: FootnoteResolver, plugin?: {
24
+ type: 'block' | 'inline';
25
+ strategy: ParsingStrategy;
26
+ }[]);
21
27
  peek(offset?: number): Token | null;
22
28
  next(amount?: number): void;
23
29
  isEnd(): boolean;
30
+ registerStrategy(strategy: ParsingStrategy, type: 'block' | 'inline'): void;
24
31
  /**
25
32
  * Parse a list token to a node
26
33
  * @return A parsed abstract syntax tree (AST)
@@ -36,7 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.Parser = void 0;
37
37
  const Handler = __importStar(require("./handler"));
38
38
  class Parser {
39
- constructor(listToken, footNoteResolver) {
39
+ constructor(listToken, footNoteResolver, plugin = []) {
40
40
  this.pos = 0;
41
41
  this.listToken = listToken;
42
42
  this.footNoteResolver = footNoteResolver;
@@ -61,6 +61,8 @@ class Parser {
61
61
  Handler.HtmlInlineHandler,
62
62
  Handler.FootnoteRefHandler,
63
63
  ].map(ele => [ele.type, ele]));
64
+ if (plugin.length > 0)
65
+ plugin.forEach(p => this.registerStrategy(p.strategy, p.type));
64
66
  }
65
67
  peek(offset = 0) {
66
68
  const i = this.pos + offset;
@@ -72,6 +74,13 @@ class Parser {
72
74
  isEnd() {
73
75
  return this.peek()?.type === "EOF";
74
76
  }
77
+ registerStrategy(strategy, type) {
78
+ if (type === "block") {
79
+ this.blockStrategies.set(strategy.type, strategy);
80
+ }
81
+ else
82
+ this.inlineStrategies.set(strategy.type, strategy);
83
+ }
75
84
  /**
76
85
  * Parse a list token to a node
77
86
  * @return A parsed abstract syntax tree (AST)
package/dist/index.d.ts CHANGED
@@ -1,8 +1,14 @@
1
+ import { ILexer, Lexer } from "./core/lexer";
2
+ import { IParser, Parser } from "./core/parser";
3
+ import { IRenderer } from "./renderers/index";
4
+ import { DefaultRenderer } from "./renderers/default";
1
5
  import { MarkdownOptions } from "./types/options";
2
6
  import { Token, TokenizerStrategy } from './types/token';
3
7
  import { ASTNode, ParsingStrategy } from './types/parser';
4
8
  import { RenderStrategy } from './types/renderer';
5
- export { MarkdownOptions, Token, TokenizerStrategy, ASTNode, ParsingStrategy, RenderStrategy };
9
+ import { MarkdownPlugin } from "./types/plugin";
10
+ import { BaseConverter } from "./types/converter";
11
+ export { MarkdownOptions, Token, TokenizerStrategy, ILexer, IParser, IRenderer, Lexer, Parser, DefaultRenderer, ASTNode, ParsingStrategy, RenderStrategy, MarkdownPlugin, };
6
12
  /**
7
13
  * Convert a Markdown string into HTML.
8
14
  * @param input - The Markdown source string
@@ -15,4 +21,24 @@ export { MarkdownOptions, Token, TokenizerStrategy, ASTNode, ParsingStrategy, Re
15
21
  * // => <p>Hello <strong>world</strong></p>
16
22
  * ```
17
23
  */
18
- export declare function convertMarkdownToHTML(input: string, options?: MarkdownOptions<string>): string;
24
+ export declare function convertMarkdownToHTML(input: string, options?: MarkdownOptions<string>, plugin?: MarkdownPlugin<string, string>[]): string;
25
+ /**
26
+ * Default Markdown converter that outputs a standard HTML string
27
+ * @extends BaseConverter<string>
28
+ * @example
29
+ * ```ts
30
+ * const converter = new DefaultMarkdownConverter(
31
+ * {
32
+ * renderOptions: {
33
+ * className: { Header: "my-title" }
34
+ * }
35
+ * },
36
+ * [MyCustomPlugin]
37
+ * );
38
+ * const html = converter.convert("# Hello");
39
+ * ```
40
+ */
41
+ export declare class DefaultMarkdownConverter extends BaseConverter<string> {
42
+ constructor(options?: MarkdownOptions<string>, plugin?: MarkdownPlugin<string, string>[]);
43
+ convert(input: string): string;
44
+ }
package/dist/index.js CHANGED
@@ -1,13 +1,14 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DefaultMarkdownConverter = exports.DefaultRenderer = exports.Parser = exports.Lexer = void 0;
6
4
  exports.convertMarkdownToHTML = convertMarkdownToHTML;
7
- const lexer_1 = __importDefault(require("./core/lexer"));
5
+ const lexer_1 = require("./core/lexer");
6
+ Object.defineProperty(exports, "Lexer", { enumerable: true, get: function () { return lexer_1.Lexer; } });
8
7
  const parser_1 = require("./core/parser");
9
- const footnote_resolver_1 = require("./core/resolver/footnote-resolver");
8
+ Object.defineProperty(exports, "Parser", { enumerable: true, get: function () { return parser_1.Parser; } });
10
9
  const default_1 = require("./renderers/default");
10
+ Object.defineProperty(exports, "DefaultRenderer", { enumerable: true, get: function () { return default_1.DefaultRenderer; } });
11
+ const converter_1 = require("./types/converter");
11
12
  /**
12
13
  * Convert a Markdown string into HTML.
13
14
  * @param input - The Markdown source string
@@ -23,9 +24,37 @@ const default_1 = require("./renderers/default");
23
24
  function convertMarkdownToHTML(input, options = {
24
25
  renderOptions: {},
25
26
  converterOptions: { allowDangerousHtml: false }
26
- }) {
27
- const tokens = new lexer_1.default(input).tokenize();
28
- const footNoteResolver = new footnote_resolver_1.FootnoteResolver();
29
- const nodes = new parser_1.Parser(tokens, footNoteResolver).parse();
30
- return new default_1.DefaultRenderer(footNoteResolver, options).render(nodes);
27
+ }, plugin = []) {
28
+ return new DefaultMarkdownConverter(options, plugin).convert(input);
31
29
  }
30
+ /**
31
+ * Default Markdown converter that outputs a standard HTML string
32
+ * @extends BaseConverter<string>
33
+ * @example
34
+ * ```ts
35
+ * const converter = new DefaultMarkdownConverter(
36
+ * {
37
+ * renderOptions: {
38
+ * className: { Header: "my-title" }
39
+ * }
40
+ * },
41
+ * [MyCustomPlugin]
42
+ * );
43
+ * const html = converter.convert("# Hello");
44
+ * ```
45
+ */
46
+ class DefaultMarkdownConverter extends converter_1.BaseConverter {
47
+ constructor(options = {
48
+ renderOptions: {},
49
+ converterOptions: { allowDangerousHtml: false }
50
+ }, plugin = []) {
51
+ super(options, plugin);
52
+ }
53
+ convert(input) {
54
+ const tokens = this.getTokens(input);
55
+ const nodes = this.getNodes(tokens);
56
+ const renderer = new default_1.DefaultRenderer(this.footnoteResolver, this.options, this.plugin.map(p => p.renderer));
57
+ return renderer.render(nodes);
58
+ }
59
+ }
60
+ exports.DefaultMarkdownConverter = DefaultMarkdownConverter;
package/dist/react.d.ts CHANGED
@@ -1,10 +1,15 @@
1
1
  import React from "react";
2
+ import { ReactRenderer } from "./renderers/react";
2
3
  import { MarkdownOptions } from "./types/options";
4
+ import { BaseConverter } from "./types/converter";
5
+ import { MarkdownPlugin } from "./types/plugin";
6
+ export { ReactRenderer };
3
7
  /**
4
8
  * Convert a Markdown string into a ReactNode.
5
9
  * @param input - The Markdown source string
6
10
  * @param renderOptions - Optional rendering options
7
11
  * @param options - Optional handle options
12
+ * @param [plugin=[]] - Optional plugin for additional render rules
8
13
  * @returns The rendered `React.ReactNode` ready to be rendered into a React component.
9
14
  *
10
15
  * @example
@@ -13,13 +18,14 @@ import { MarkdownOptions } from "./types/options";
13
18
  * return <div>{node}</div>;
14
19
  * ```
15
20
  */
16
- export declare function convertMarkdownToReactNode(input: string, options?: MarkdownOptions<React.ReactNode>): React.ReactNode;
21
+ export declare function convertMarkdownToReactNode(input: string, options?: MarkdownOptions<React.ReactNode>, plugin?: MarkdownPlugin<string, React.ReactNode>[]): React.ReactNode;
17
22
  /**
18
23
  * A React commponent that renders Markdown content.
19
24
  * Using `React.useMemo` to ensure performance and prevent unnecessary re-render.
20
25
  * @param props.content - The Markdown source to render.
21
26
  * @param props.options - Optional configuration for the renderer.
22
27
  * @param props.className - Optional CSS classes for the wrapping `div` element.
28
+ * @param props.plugin - Optional plugin for additional syntax handler.
23
29
  * @example
24
30
  * ```tsx
25
31
  * <MarkdownComponent
@@ -33,4 +39,25 @@ export declare const MarkdownComponent: React.FC<{
33
39
  content: string;
34
40
  options?: MarkdownOptions<React.ReactNode>;
35
41
  className?: string;
42
+ plugin?: MarkdownPlugin<string, React.ReactNode>[];
36
43
  }>;
44
+ /**
45
+ * React Markdown converter that outputs a React.ReactNode
46
+ * @extends BaseConverter<React.ReactNode>
47
+ * @example
48
+ * ```ts
49
+ * const converter = new ReactMarkdownConverter(
50
+ * {
51
+ * renderOptions: {
52
+ * className: { Header: "my-title" }
53
+ * }
54
+ * },
55
+ * [MyCustomPlugin]
56
+ * );
57
+ * const html = converter.convert("# Hello");
58
+ * ```
59
+ */
60
+ export declare class ReactMarkdownConverter extends BaseConverter<React.ReactNode> {
61
+ constructor(options?: MarkdownOptions<React.ReactNode>, plugin?: MarkdownPlugin<string, React.ReactNode>[]);
62
+ convert(input: string): React.ReactNode;
63
+ }
package/dist/react.js CHANGED
@@ -3,18 +3,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.MarkdownComponent = void 0;
6
+ exports.ReactMarkdownConverter = exports.MarkdownComponent = exports.ReactRenderer = void 0;
7
7
  exports.convertMarkdownToReactNode = convertMarkdownToReactNode;
8
8
  const react_1 = __importDefault(require("react"));
9
- const lexer_1 = __importDefault(require("./core/lexer"));
10
- const parser_1 = require("./core/parser");
11
- const footnote_resolver_1 = require("./core/resolver/footnote-resolver");
12
9
  const react_2 = require("./renderers/react");
10
+ Object.defineProperty(exports, "ReactRenderer", { enumerable: true, get: function () { return react_2.ReactRenderer; } });
11
+ const converter_1 = require("./types/converter");
13
12
  /**
14
13
  * Convert a Markdown string into a ReactNode.
15
14
  * @param input - The Markdown source string
16
15
  * @param renderOptions - Optional rendering options
17
16
  * @param options - Optional handle options
17
+ * @param [plugin=[]] - Optional plugin for additional render rules
18
18
  * @returns The rendered `React.ReactNode` ready to be rendered into a React component.
19
19
  *
20
20
  * @example
@@ -26,11 +26,8 @@ const react_2 = require("./renderers/react");
26
26
  function convertMarkdownToReactNode(input, options = {
27
27
  renderOptions: {},
28
28
  converterOptions: { allowDangerousHtml: false }
29
- }) {
30
- const tokens = new lexer_1.default(input).tokenize();
31
- const footNoteResolver = new footnote_resolver_1.FootnoteResolver();
32
- const nodes = new parser_1.Parser(tokens, footNoteResolver).parse();
33
- return new react_2.ReactRenderer(footNoteResolver, options).render(nodes);
29
+ }, plugin = []) {
30
+ return new ReactMarkdownConverter(options, plugin).convert(input);
34
31
  }
35
32
  /**
36
33
  * A React commponent that renders Markdown content.
@@ -38,6 +35,7 @@ function convertMarkdownToReactNode(input, options = {
38
35
  * @param props.content - The Markdown source to render.
39
36
  * @param props.options - Optional configuration for the renderer.
40
37
  * @param props.className - Optional CSS classes for the wrapping `div` element.
38
+ * @param props.plugin - Optional plugin for additional syntax handler.
41
39
  * @example
42
40
  * ```tsx
43
41
  * <MarkdownComponent
@@ -47,10 +45,41 @@ function convertMarkdownToReactNode(input, options = {
47
45
  * />
48
46
  * ```
49
47
  */
50
- const MarkdownComponent = ({ content, className, options }) => {
48
+ const MarkdownComponent = ({ content, className, options, plugin }) => {
51
49
  const rendered = react_1.default.useMemo(() => {
52
- return convertMarkdownToReactNode(content, options);
53
- }, [content]);
50
+ return new ReactMarkdownConverter(options, plugin).convert(content);
51
+ }, [content, options, plugin]);
54
52
  return react_1.default.createElement("div", { className }, rendered);
55
53
  };
56
54
  exports.MarkdownComponent = MarkdownComponent;
55
+ /**
56
+ * React Markdown converter that outputs a React.ReactNode
57
+ * @extends BaseConverter<React.ReactNode>
58
+ * @example
59
+ * ```ts
60
+ * const converter = new ReactMarkdownConverter(
61
+ * {
62
+ * renderOptions: {
63
+ * className: { Header: "my-title" }
64
+ * }
65
+ * },
66
+ * [MyCustomPlugin]
67
+ * );
68
+ * const html = converter.convert("# Hello");
69
+ * ```
70
+ */
71
+ class ReactMarkdownConverter extends converter_1.BaseConverter {
72
+ constructor(options = {
73
+ renderOptions: {},
74
+ converterOptions: { allowDangerousHtml: false }
75
+ }, plugin = []) {
76
+ super(options, plugin);
77
+ }
78
+ convert(input) {
79
+ const tokens = this.getTokens(input);
80
+ const nodes = this.getNodes(tokens);
81
+ const renderer = new react_2.ReactRenderer(this.footnoteResolver, this.options, this.plugin.map(p => p.renderer));
82
+ return renderer.render(nodes);
83
+ }
84
+ }
85
+ exports.ReactMarkdownConverter = ReactMarkdownConverter;
@@ -1,14 +1,16 @@
1
1
  import { IRenderer } from "..";
2
2
  import { MarkdownOptions } from "../../types/options";
3
3
  import { ASTNode } from "../../types/parser";
4
+ import { RenderStrategy } from "../../types/renderer";
4
5
  import { FootnoteResolver } from "../../core/resolver/footnote-resolver";
5
6
  export declare class DefaultRenderer implements IRenderer<string> {
6
7
  options: MarkdownOptions<string>;
7
8
  footnoteResolver: FootnoteResolver;
8
- private strategies;
9
- constructor(footnoteResolver: FootnoteResolver, options?: MarkdownOptions<string>);
9
+ strategies: Map<string, RenderStrategy<string>>;
10
+ constructor(footnoteResolver: FootnoteResolver, options?: MarkdownOptions<string>, plugin?: RenderStrategy<string>[]);
10
11
  private registerDefaultStrategies;
11
12
  render(node: ASTNode): string;
12
13
  renderFootnotes(): string;
13
14
  renderTable(node: ASTNode, children: string[]): string;
15
+ registerStrategy(strategy: RenderStrategy<string>): void;
14
16
  }
@@ -37,11 +37,13 @@ exports.DefaultRenderer = void 0;
37
37
  const Handlers = __importStar(require("./handler"));
38
38
  const renderer_utils_1 = require("../../utilities/renderer-utils");
39
39
  class DefaultRenderer {
40
- constructor(footnoteResolver, options = {}) {
40
+ constructor(footnoteResolver, options = {}, plugin = []) {
41
41
  this.strategies = new Map();
42
42
  this.footnoteResolver = footnoteResolver;
43
43
  this.options = options;
44
44
  this.registerDefaultStrategies();
45
+ if (plugin.length > 0)
46
+ plugin.forEach(p => this.registerStrategy(p));
45
47
  }
46
48
  registerDefaultStrategies() {
47
49
  const listDefaultStrategy = [
@@ -115,5 +117,8 @@ class DefaultRenderer {
115
117
  else
116
118
  return `<p${cls ? ` class="${cls}"` : ""}>${children.join("\n")}</p>`;
117
119
  }
120
+ registerStrategy(strategy) {
121
+ this.strategies.set(strategy.type, strategy);
122
+ }
118
123
  }
119
124
  exports.DefaultRenderer = DefaultRenderer;
@@ -1,10 +1,13 @@
1
1
  import { ASTNode } from "../types/parser";
2
2
  import { MarkdownOptions } from '../types/options/index';
3
3
  import { FootnoteResolver } from "../core/resolver/footnote-resolver";
4
+ import { RenderStrategy } from "../types/renderer";
4
5
  export interface IRenderer<TOutput> {
5
6
  options: MarkdownOptions<TOutput>;
6
7
  footnoteResolver: FootnoteResolver;
8
+ strategies: Map<string, RenderStrategy<TOutput>>;
7
9
  render(node: ASTNode): TOutput;
8
10
  renderTable(node: ASTNode, children: TOutput[]): TOutput;
9
11
  renderFootnotes(): TOutput;
12
+ registerStrategy(strategy: RenderStrategy<TOutput>): void;
10
13
  }
@@ -2,14 +2,16 @@ import React from "react";
2
2
  import { IRenderer } from "..";
3
3
  import { MarkdownOptions } from "../../types/options";
4
4
  import { ASTNode } from "../../types/parser";
5
+ import { RenderStrategy } from "../../types/renderer";
5
6
  import { FootnoteResolver } from "../../core/resolver/footnote-resolver";
6
7
  export declare class ReactRenderer implements IRenderer<React.ReactNode> {
7
8
  options: MarkdownOptions<React.ReactNode>;
8
9
  footnoteResolver: FootnoteResolver;
9
- private strategies;
10
- constructor(footnoteResolver: FootnoteResolver, options?: MarkdownOptions<React.ReactNode>);
10
+ strategies: Map<string, RenderStrategy<React.ReactNode>>;
11
+ constructor(footnoteResolver: FootnoteResolver, options?: MarkdownOptions<React.ReactNode>, plugin?: RenderStrategy<React.ReactNode>[]);
11
12
  private registerDefaultStrategies;
12
13
  render(node: ASTNode): React.ReactNode;
13
14
  renderFootnotes(): React.ReactNode;
14
15
  renderTable(node: ASTNode, children: React.ReactNode[]): React.ReactNode;
16
+ registerStrategy(strategy: RenderStrategy<React.ReactNode>): void;
15
17
  }
@@ -41,11 +41,13 @@ const react_1 = __importDefault(require("react"));
41
41
  const Handlers = __importStar(require("./handler"));
42
42
  const renderer_utils_1 = require("../../utilities/renderer-utils");
43
43
  class ReactRenderer {
44
- constructor(footnoteResolver, options = {}) {
44
+ constructor(footnoteResolver, options = {}, plugin = []) {
45
45
  this.strategies = new Map();
46
46
  this.footnoteResolver = footnoteResolver;
47
47
  this.options = options;
48
48
  this.registerDefaultStrategies();
49
+ if (plugin.length > 0)
50
+ plugin.forEach(p => this.registerStrategy(p));
49
51
  }
50
52
  registerDefaultStrategies() {
51
53
  const listDefaultStrategy = [
@@ -120,5 +122,8 @@ class ReactRenderer {
120
122
  else
121
123
  return react_1.default.createElement("p", { className: (0, renderer_utils_1.getClassName)(this, node), }, ...children);
122
124
  }
125
+ registerStrategy(strategy) {
126
+ this.strategies.set(strategy.type, strategy);
127
+ }
123
128
  }
124
129
  exports.ReactRenderer = ReactRenderer;
@@ -0,0 +1,14 @@
1
+ import { FootnoteResolver } from "../core/resolver/footnote-resolver";
2
+ import { MarkdownOptions } from "./options";
3
+ import { ASTNode } from "./parser";
4
+ import { MarkdownPlugin } from "./plugin";
5
+ import { Token } from "./token";
6
+ export declare abstract class BaseConverter<TOutput> {
7
+ protected options: MarkdownOptions<TOutput>;
8
+ protected plugin: MarkdownPlugin<string, TOutput>[];
9
+ protected footnoteResolver: FootnoteResolver;
10
+ constructor(options: MarkdownOptions<TOutput>, plugin: MarkdownPlugin<string, TOutput>[]);
11
+ protected getTokens(input: string): Token[];
12
+ protected getNodes(tokens: Token[]): ASTNode;
13
+ abstract convert(input: string): TOutput;
14
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BaseConverter = void 0;
4
+ const lexer_1 = require("../core/lexer");
5
+ const parser_1 = require("../core/parser");
6
+ const footnote_resolver_1 = require("../core/resolver/footnote-resolver");
7
+ class BaseConverter {
8
+ constructor(options, plugin) {
9
+ this.options = options;
10
+ this.plugin = plugin;
11
+ this.footnoteResolver = new footnote_resolver_1.FootnoteResolver();
12
+ }
13
+ getTokens(input) {
14
+ return new lexer_1.Lexer(input, this.plugin.map(p => p.tokenizer)).tokenize();
15
+ }
16
+ getNodes(tokens) {
17
+ return new parser_1.Parser(tokens, this.footnoteResolver, this.plugin.map(p => ({
18
+ type: p.type,
19
+ strategy: p.parser
20
+ }))).parse();
21
+ }
22
+ }
23
+ exports.BaseConverter = BaseConverter;
@@ -0,0 +1,51 @@
1
+ import { TokenizerStrategy } from "./token";
2
+ import { ParsingStrategy } from "./parser";
3
+ import { RenderStrategy } from "./renderer";
4
+ /**
5
+ * Representing a custom plugin for the Markdown converter.
6
+ * It allow to define new syntax by hooking into the `Lexing`, `Parsing` and `Rendering` stages.
7
+ * @template TOutput - The type of final rendered output
8
+ */
9
+ export interface MarkdownPlugin<T extends string, TOutput> {
10
+ /**
11
+ * Unique identifier for the plugin
12
+ */
13
+ name: T;
14
+ /**
15
+ * Define the context of plugin
16
+ */
17
+ type: "block" | "inline";
18
+ /**
19
+ * Strategy for the Lexer.
20
+ * The `type` property of this property must be same as `name` property
21
+ */
22
+ tokenizer: TokenizerStrategy & {
23
+ type: T;
24
+ };
25
+ /**
26
+ * Strategy for the Parser.
27
+ * The `type` property of this property must be same as `name` property
28
+ */
29
+ parser: ParsingStrategy & {
30
+ type: T;
31
+ };
32
+ /**
33
+ * Strategy for the Renderer.
34
+ * The `type` property of this property must be same as `name` property
35
+ */
36
+ renderer: RenderStrategy<TOutput> & {
37
+ type: T;
38
+ };
39
+ }
40
+ /**
41
+ * A helper function to create a plugin
42
+ * @template T - The literal string type for the plugin name.
43
+ * @template TOutput - The output type.
44
+ * @param name - Name of plugin
45
+ * @param type - Context of plugin, determine for parser processing
46
+ * @param tokenizer - Tokenizer strategy for Lexer
47
+ * @param parser - Parser strategy for Parser
48
+ * @param renderer - Render strategy for Renderer
49
+ * @returns - A complete Markdown plugin
50
+ */
51
+ export declare function createPlugin<T extends string, TOutput>(name: T, type: "block" | "inline", tokenizer: Omit<TokenizerStrategy, "type">, parser: Omit<ParsingStrategy, "type">, renderer: Omit<RenderStrategy<TOutput>, "type">): MarkdownPlugin<T, TOutput>;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createPlugin = createPlugin;
4
+ /**
5
+ * A helper function to create a plugin
6
+ * @template T - The literal string type for the plugin name.
7
+ * @template TOutput - The output type.
8
+ * @param name - Name of plugin
9
+ * @param type - Context of plugin, determine for parser processing
10
+ * @param tokenizer - Tokenizer strategy for Lexer
11
+ * @param parser - Parser strategy for Parser
12
+ * @param renderer - Render strategy for Renderer
13
+ * @returns - A complete Markdown plugin
14
+ */
15
+ function createPlugin(name, type, tokenizer, parser, renderer) {
16
+ const finalTokenizer = Object.assign(tokenizer, { type: name });
17
+ const finalParser = Object.assign(parser, { type: name });
18
+ const finalRenderer = Object.assign(renderer, { type: name });
19
+ return {
20
+ name,
21
+ type,
22
+ tokenizer: finalTokenizer,
23
+ parser: finalParser,
24
+ renderer: finalRenderer
25
+ };
26
+ }
@@ -114,7 +114,7 @@ export interface Token {
114
114
  * @property emit - A function handle tokenizing input to `Token`.
115
115
  */
116
116
  export interface TokenizerStrategy {
117
- name: string;
117
+ type: string;
118
118
  /**
119
119
  * Checks if the current cursor position in the Lexer matches this syntax
120
120
  * @param lex The current `ILexer` instance providing access to the input string and cursor.
@@ -1,7 +1,4 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.handleEndList = handleEndList;
7
4
  exports.handleHtmlBlock = handleHtmlBlock;
@@ -12,7 +9,7 @@ exports.handleStartList = handleStartList;
12
9
  exports.handleTable = handleTable;
13
10
  exports.handleTaskItem = handleTaskItem;
14
11
  exports.handleTextBlock = handleTextBlock;
15
- const lexer_1 = __importDefault(require("../core/lexer"));
12
+ const lexer_1 = require("../core/lexer");
16
13
  function handleTextBlock(lex) {
17
14
  const currentChar = lex.peek();
18
15
  if (currentChar === null)
@@ -106,7 +103,7 @@ function handleEndList(lex) {
106
103
  //Table utilities
107
104
  function handleTable(lex) {
108
105
  const tokenizeResult = [];
109
- const handler = new lexer_1.default("");
106
+ const handler = new lexer_1.Lexer("");
110
107
  const header = lex.readUntil("\n", true);
111
108
  const headerDetails = header.trim().replace(/^ *\|/, "").replace(/\| *$/, "").split("|");
112
109
  const align = lex.readUntil("\n", true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simple-customize-markdown-converter",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Transform Markdown into fully customizable HTML or React components.",
5
5
  "keywords": [
6
6
  "markdown",