diff --git a/docs/USING_PRO.md b/docs/USING_PRO.md
index 0b2aea001a..40aadea661 100644
--- a/docs/USING_PRO.md
+++ b/docs/USING_PRO.md
@@ -2,6 +2,16 @@
To champion the single-responsibility and open/closed principles, we have tried to make it relatively painless to extend marked. If you are looking to add custom functionality, this is the place to start.
+
marked.use()
+
+`marked.use(options)` is the recommended way to extend marked. The options object can contain any [option](#/USING_ADVANCED.md#options) available in marked.
+
+The `renderer` and `tokenizer` options can be an object with functions that will be merged into the `renderer` and `tokenizer` respectively.
+
+The `renderer` and `tokenizer` functions can return false to fallback to the previous function.
+
+All other options will overwrite previously set options.
+
The renderer
The renderer defines the output of the parser.
@@ -12,24 +22,25 @@ The renderer defines the output of the parser.
// Create reference instance
const marked = require('marked');
-// Get reference
-const renderer = new marked.Renderer();
-
// Override function
-renderer.heading = function(text, level) {
- const escapedText = text.toLowerCase().replace(/[^\w]+/g, '-');
-
- return `
-
-
-
-
- ${text}
- `;
+const renderer = {
+ heading(text, level) {
+ const escapedText = text.toLowerCase().replace(/[^\w]+/g, '-');
+
+ return `
+
+
+
+
+ ${text}
+ `;
+ }
};
+marked.use({ renderer });
+
// Run marked
-console.log(marked('# heading+', { renderer }));
+console.log(marked('# heading+'));
```
**Output:**
@@ -99,30 +110,34 @@ The tokenizer defines how to turn markdown text into tokens.
// Create reference instance
const marked = require('marked');
-// Get reference
-const tokenizer = new marked.Tokenizer();
-const originalCodespan = tokenizer.codespan;
// Override function
-tokenizer.codespan = function(src) {
- const match = src.match(/\$+([^\$\n]+?)\$+/);
- if (match) {
- return {
- type: 'codespan',
- raw: match[0],
- text: match[1].trim()
- };
+const tokenizer = {
+ codespan(src) {
+ const match = src.match(/\$+([^\$\n]+?)\$+/);
+ if (match) {
+ return {
+ type: 'codespan',
+ raw: match[0],
+ text: match[1].trim()
+ };
+ }
+
+ // return false to use original codespan tokenizer
+ return false;
}
- return originalCodespan.apply(this, arguments);
};
+marked.use({ tokenizer });
+
// Run marked
-console.log(marked('$ latex code $', { tokenizer }));
+console.log(marked('$ latex code $\n\n` other code `'));
```
**Output:**
```html
-latext code
+latex code
+other code
```
### Block level tokenizer methods
diff --git a/docs/index.html b/docs/index.html
index 9d68e7bb41..a7de1818c0 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -154,6 +154,7 @@ Marked.js Documentation
Extensibility
+ - marked.use()
- Renderer
- Tokenizer
- Lexer
diff --git a/src/marked.js b/src/marked.js
index 8a77ddcef6..8488e79f03 100644
--- a/src/marked.js
+++ b/src/marked.js
@@ -127,6 +127,43 @@ marked.getDefaults = getDefaults;
marked.defaults = defaults;
+/**
+ * Use Extension
+ */
+
+marked.use = function(extension) {
+ const opts = merge({}, extension);
+ if (extension.renderer) {
+ const renderer = marked.defaults.renderer || new Renderer();
+ for (const prop in extension.renderer) {
+ const prevRenderer = renderer[prop];
+ renderer[prop] = (...args) => {
+ let ret = extension.renderer[prop].apply(renderer, args);
+ if (ret === false) {
+ ret = prevRenderer.apply(renderer, args);
+ }
+ return ret;
+ };
+ }
+ opts.renderer = renderer;
+ }
+ if (extension.tokenizer) {
+ const tokenizer = marked.defaults.tokenizer || new Tokenizer();
+ for (const prop in extension.tokenizer) {
+ const prevTokenizer = tokenizer[prop];
+ tokenizer[prop] = (...args) => {
+ let ret = extension.tokenizer[prop].apply(tokenizer, args);
+ if (ret === false) {
+ ret = prevTokenizer.apply(tokenizer, args);
+ }
+ return ret;
+ };
+ }
+ opts.tokenizer = tokenizer;
+ }
+ marked.setOptions(opts);
+};
+
/**
* Expose
*/
diff --git a/test/unit/marked-spec.js b/test/unit/marked-spec.js
index cc84d16191..1b6beafdd6 100644
--- a/test/unit/marked-spec.js
+++ b/test/unit/marked-spec.js
@@ -96,3 +96,136 @@ describe('inlineLexer', () => {
expect(renderer.html).toHaveBeenCalledWith('');
});
});
+
+describe('use extension', () => {
+ it('should use renderer', () => {
+ const extension = {
+ renderer: {
+ paragraph(text) {
+ return 'extension';
+ }
+ }
+ };
+ spyOn(extension.renderer, 'paragraph').and.callThrough();
+ marked.use(extension);
+ const html = marked('text');
+ expect(extension.renderer.paragraph).toHaveBeenCalledWith('text');
+ expect(html).toBe('extension');
+ });
+
+ it('should use tokenizer', () => {
+ const extension = {
+ tokenizer: {
+ paragraph(text) {
+ return {
+ type: 'paragraph',
+ raw: text,
+ text: 'extension'
+ };
+ }
+ }
+ };
+ spyOn(extension.tokenizer, 'paragraph').and.callThrough();
+ marked.use(extension);
+ const html = marked('text');
+ expect(extension.tokenizer.paragraph).toHaveBeenCalledWith('text');
+ expect(html).toBe('extension
\n');
+ });
+
+ it('should use options from extension', () => {
+ const extension = {
+ headerIds: false
+ };
+ marked.use(extension);
+ const html = marked('# heading');
+ expect(html).toBe('heading
\n');
+ });
+
+ it('should use last extension function and not override others', () => {
+ const extension1 = {
+ renderer: {
+ paragraph(text) {
+ return 'extension1 paragraph\n';
+ },
+ html(html) {
+ return 'extension1 html\n';
+ }
+ }
+ };
+ const extension2 = {
+ renderer: {
+ paragraph(text) {
+ return 'extension2 paragraph\n';
+ }
+ }
+ };
+ marked.use(extension1);
+ marked.use(extension2);
+ const html = marked(`
+paragraph
+
+
+
+# heading
+`);
+ expect(html).toBe('extension2 paragraph\nextension1 html\nheading
\n');
+ });
+
+ it('should use previous extension when returning false', () => {
+ const extension1 = {
+ renderer: {
+ paragraph(text) {
+ if (text !== 'original') {
+ return 'extension1 paragraph\n';
+ }
+ return false;
+ }
+ }
+ };
+ const extension2 = {
+ renderer: {
+ paragraph(text) {
+ if (text !== 'extension1' && text !== 'original') {
+ return 'extension2 paragraph\n';
+ }
+ return false;
+ }
+ }
+ };
+ marked.use(extension1);
+ marked.use(extension2);
+ const html = marked(`
+paragraph
+
+extension1
+
+original
+`);
+ expect(html).toBe('extension2 paragraph\nextension1 paragraph\noriginal
\n');
+ });
+
+ it('should get options with this.options', () => {
+ const extension = {
+ renderer: {
+ heading: () => {
+ return this.options ? 'arrow options\n' : 'arrow no options\n';
+ },
+ html: function() {
+ return this.options ? 'function options\n' : 'function no options\n';
+ },
+ paragraph() {
+ return this.options ? 'shorthand options\n' : 'shorthand no options\n';
+ }
+ }
+ };
+ marked.use(extension);
+ const html = marked(`
+# heading
+
+
+
+paragraph
+`);
+ expect(html).toBe('arrow no options\nfunction options\nshorthand options\n');
+ });
+});