Skip to content

Commit

Permalink
Merge pull request #1646 from UziTech/use-extension
Browse files Browse the repository at this point in the history
marked.use
  • Loading branch information
UziTech committed Apr 20, 2020
2 parents 904c974 + d5de6af commit 1bfbd9e
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 28 deletions.
71 changes: 43 additions & 28 deletions docs/USING_PRO.md
Expand Up @@ -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.

<h2 id="use">marked.use()</h2>

`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.

<h2 id="renderer">The renderer</h2>

The renderer defines the output of the parser.
Expand All @@ -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 `
<h${level}>
<a name="${escapedText}" class="anchor" href="#${escapedText}">
<span class="header-link"></span>
</a>
${text}
</h${level}>`;
const renderer = {
heading(text, level) {
const escapedText = text.toLowerCase().replace(/[^\w]+/g, '-');

return `
<h${level}>
<a name="${escapedText}" class="anchor" href="#${escapedText}">
<span class="header-link"></span>
</a>
${text}
</h${level}>`;
}
};

marked.use({ renderer });

// Run marked
console.log(marked('# heading+', { renderer }));
console.log(marked('# heading+'));
```

**Output:**
Expand Down Expand Up @@ -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
<p><code>latext code</code></p>
<p><code>latex code</code></p>
<p><code>other code</code></p>
```

### Block level tokenizer methods
Expand Down
1 change: 1 addition & 0 deletions docs/index.html
Expand Up @@ -154,6 +154,7 @@ <h1>Marked.js Documentation</h1>
<li>
<a href="#/USING_PRO.md">Extensibility</a>
<ul>
<li><a href="#/USING_PRO.md#use">marked.use()</a></li>
<li><a href="#/USING_PRO.md#renderer">Renderer</a></li>
<li><a href="#/USING_PRO.md#tokenizer">Tokenizer</a></li>
<li><a href="#/USING_PRO.md#lexer">Lexer</a></li>
Expand Down
37 changes: 37 additions & 0 deletions src/marked.js
Expand Up @@ -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
*/
Expand Down
133 changes: 133 additions & 0 deletions test/unit/marked-spec.js
Expand Up @@ -96,3 +96,136 @@ describe('inlineLexer', () => {
expect(renderer.html).toHaveBeenCalledWith('<img alt="MY IMAGE" src="example.png" />');
});
});

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('<p>extension</p>\n');
});

it('should use options from extension', () => {
const extension = {
headerIds: false
};
marked.use(extension);
const html = marked('# heading');
expect(html).toBe('<h1>heading</h1>\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
<html />
# heading
`);
expect(html).toBe('extension2 paragraph\nextension1 html\n<h1 id="heading">heading</h1>\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\n<p>original</p>\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
<html />
paragraph
`);
expect(html).toBe('arrow no options\nfunction options\nshorthand options\n');
});
});

1 comment on commit 1bfbd9e

@vercel
Copy link

@vercel vercel bot commented on 1bfbd9e Apr 20, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.