Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

marked.use #1646

Merged
merged 6 commits into from
Apr 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 43 additions & 28 deletions docs/USING_PRO.md
Original file line number Diff line number Diff line change
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;
styfle marked this conversation as resolved.
Show resolved Hide resolved
}
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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) => {
styfle marked this conversation as resolved.
Show resolved Hide resolved
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
Original file line number Diff line number Diff line change
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');
});
});