Skip to content

Add markdown extension mechanism and syntax #821

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

Closed
wants to merge 11 commits into from
Closed
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
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,13 @@ Default: `false`

Use "smart" typograhic punctuation for things like quotes and dashes.

### plugins

Type: `boolean`
Default: `false`

Enable plugin syntax.

## Access to lexer and parser

You also have direct access to the lexer and parser if you so desire.
Expand Down Expand Up @@ -358,6 +365,45 @@ $ node
links: {} ]
```

## Plugin syntax

Plugins are customizable blocks which could extend default markdown behavior
on demand. It could be iframes, podcasts, UML diagrams, video, etc. Plugin block
starts with '@', plugin name and params in round braces. Plugin could contain
block of data. Data block should preserve indentation it allow to add any
content with any markup inside of the block.

```
@uml(sequence)
Alice->Bob: Hi, Bob!
Bob->Alice: Hi, Alice!
```
If plugin not exists it's just ignoring.

### Define plugin

Plugins could be defined at runtime or for all marked instances:

```javascript
// Define default plugin
marked.Renderer.plugins.video = function(params) {
return `<video src="${params}"/>`;
};

// Instance plugins
var renderer = new marked.Renderer({
plugins: {
uml(params, block) {
//...
}
}
});

renderer.plugins.audio = function(params) {
return `<audio src="${params}"/>`;
};
```

## Running Tests & Contributing

If you want to submit a pull request, make sure your changes pass the test
Expand Down
64 changes: 64 additions & 0 deletions examples/plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
'use strict';

var marked = require('marked');
var csv = require('csv-string');
var html = require('escape-html');

// Enable plugins for all instances
marked.setOptions({plugins: true});

// Initialize renderer
var renderer = new marked.Renderer({
plugins: {
// Convert `@link(title,url)` into link.
links: function(params, block) {
var parts = params.split(/\s*,\s*/);
return '<a href="' + html(parts[1]) + '">' + html(parts[0]) + '</a>';
},

// Convert `@github(user/repo)` into github link.
github: function(params, block) {
return '<a href="https://github.com/' + html(params) + '">' + html(params) + '</a>';
},

// Convert `@csv(headers)` block data into table.
csv: function(params, block) {
var thead = params.length ? params.split(/\s*,\s*/) : null;
var tbody = csv.parse(block);

var table = ['<table>']; // Use array to avoid string reallocations
if (thead) {
table.push('<thead><tr>');
thead.forEach(function(heading) {
table.push('<th>' + html(heading) + "</th>");
});
table.push('</tr></thead>');
}
table.push('<tbody>');
tbody.forEach(function(row) {
table.push('<tr>');
row.forEach(function(cell) {
table.push('<td>' + html(cell) + "</td>");
});
table.push('</tr>');
});
table.push('</tbody></table>');

return table.join('\n');
},
gallery(params, block) {
var gallery = ['<ul class="imageGallery">'];
block.replace(/^\s+|\s+$/gm, '').split(/\s+\r?\n\s+/).forEach(function(img) {
gallery.push('<li class="imageGallery-item"><img src="' + html(img) + '"/></li>');
});
gallery.push('</ul>');
return gallery.join('\n');
}
}
});

// Add plugin to initialized renderer.
// Parse block of markdown inside markdown. Yep even so :)
renderer.plugins.markdown = function(params, block) {
return marked(block, {renderer: this});
};
63 changes: 56 additions & 7 deletions lib/marked.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ var block = {
def: /^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,
table: noop,
paragraph: /^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,
text: /^[^\n]+/
text: /^[^\n]+/,
};

block.bullet = /(?:[*+-]|\d+\.)/;
Expand Down Expand Up @@ -95,6 +95,10 @@ block.tables = merge({}, block.gfm, {
table: /^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/
});

block.extra = {
plugin: /^@(!?[A-Za-z0-9_-]+) *\( *((.+) *)?\).*?\n((\s{2,}|\t{1,}).+\n(((\5).*\n)|\n)*)?/,
};

/**
* Block Lexer
*/
Expand All @@ -103,15 +107,22 @@ function Lexer(options) {
this.tokens = [];
this.tokens.links = {};
this.options = options || marked.defaults;
this.rules = block.normal;

var rules;
if (this.options.gfm) {
if (this.options.tables) {
this.rules = block.tables;
rules = merge({}, block.tables);
} else {
this.rules = block.gfm;
rules = merge({}, block.gfm);
}
} else {
rules = merge({}, block.normal);
}

if (this.options.plugins) {
rules.plugin = block.extra.plugin;
}

this.rules = rules;
}

/**
Expand Down Expand Up @@ -183,6 +194,18 @@ Lexer.prototype.token = function(src, top, bq) {
continue;
}

// code
if (this.rules.plugin && (cap = this.rules.plugin.exec(src))) {
src = src.substring(cap[0].length);
this.tokens.push({
type: 'plugin',
plugin: cap[1],
params: cap[3],
block: cap[4] !== undefined ? cap[4].replace(new RegExp('^' + cap[4].match(/^\s+/)[0], 'gm'), '') : undefined,
});
continue;
}

// fences (gfm)
if (cap = this.rules.fences.exec(src)) {
src = src.substring(cap[0].length);
Expand Down Expand Up @@ -759,6 +782,7 @@ InlineLexer.prototype.mangle = function(text) {

function Renderer(options) {
this.options = options || {};
this.plugins = merge({}, this.plugins, this.options.plugins);
}

Renderer.prototype.code = function(code, lang, escaped) {
Expand All @@ -784,6 +808,24 @@ Renderer.prototype.code = function(code, lang, escaped) {
+ '\n</code></pre>\n';
};

Renderer.prototype.plugin = function(plugin, params, block) {
if (plugin.charAt(0) === '!') {
if (! this.plugins.hasOwnProperty(plugin)) {
return block;
} else {
return '';
}
} else {
if (this.plugins.hasOwnProperty(plugin)) {
return this.plugins[plugin].call(this, params, block);
} else {
return '';
}
}
};

Renderer.prototype.plugins = Renderer.plugins = {};

Renderer.prototype.blockquote = function(quote) {
return '<blockquote>\n' + quote + '</blockquote>\n';
};
Expand Down Expand Up @@ -991,6 +1033,12 @@ Parser.prototype.tok = function() {
this.token.lang,
this.token.escaped);
}
case 'plugin': {
return this.renderer.plugin(
this.token.plugin,
this.token.params,
this.token.block);
}
case 'table': {
var header = ''
, body = ''
Expand Down Expand Up @@ -1094,7 +1142,7 @@ function escape(html, encode) {
}

function unescape(html) {
// explicitly match decimal, hex, and named HTML entities
// explicitly match decimal, hex, and named HTML entities
return html.replace(/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/g, function(_, n) {
n = n.toLowerCase();
if (n === 'colon') return ':';
Expand Down Expand Up @@ -1253,7 +1301,8 @@ marked.defaults = {
smartypants: false,
headerPrefix: '',
renderer: new Renderer,
xhtml: false
xhtml: false,
plugins: false
};

/**
Expand Down
2 changes: 1 addition & 1 deletion marked.min.js

Large diffs are not rendered by default.

15 changes: 14 additions & 1 deletion test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,19 @@ function runTests(engine, options) {
marked.setOptions(options.marked);
}

engine.setOptions({plugins: true});

var renderer = new marked.Renderer();
renderer.plugins = {};
renderer.plugins.github = function(params, body) {
return '<a href="https://github.com/' + body + '">' + body + '</a>';
};

renderer.plugins.link = function(params, body) {
var parts = params.split(/\s*,\s*/);
return '<a href="' + parts[1] + '">' + parts[0] + '</a>';
};

main:
for (; i < len; i++) {
filename = keys[i];
Expand Down Expand Up @@ -112,7 +125,7 @@ main:
}

try {
text = engine(file.text).replace(/\s/g, '');
text = engine(file.text, {renderer: renderer}).replace(/\s/g, '');
html = file.html.replace(/\s/g, '');
} catch(e) {
console.log('%s failed.', filename);
Expand Down
3 changes: 3 additions & 0 deletions test/tests/plugin_github.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<a href="https://github.com">github</a>
<a href="https://github.com/chjj/marked">chjj/marked</a>
NoYoutubePlugin
10 changes: 10 additions & 0 deletions test/tests/plugin_github.text
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
@link(github, https://github.com)

@github(https)
chjj/marked

@no()
Ignore

@!youtube()
No Youtube Plugin