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

add walkTokens and fix highlight async #1664

Merged
merged 7 commits into from May 14, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/USING_ADVANCED.md
Expand Up @@ -58,6 +58,7 @@ console.log(marked(markdownString));
|smartLists |`boolean` |`false` |v0.2.8 |If true, use smarter list behavior than those found in `markdown.pl`.|
|smartypants |`boolean` |`false` |v0.2.9 |If true, use "smart" typographic punctuation for things like quotes and dashes.|
|tokenizer |`object` |`new Tokenizer()`|v1.0.0|An object containing functions to create tokens from markdown. See [extensibility](/#/USING_PRO.md) for more details.|
|walkTokens |`function` |`null`|v1.1.0|A function which is called for every token. See [extensibility](/#/USING_PRO.md) for more details.|
|xhtml |`boolean` |`false` |v0.3.2 |If true, emit self-closing HTML tags for void elements (<br/>, <img/>, etc.) with a "/" as required by XHTML.|

<h2 id="highlight">Asynchronous highlighting</h2>
Expand Down
31 changes: 31 additions & 0 deletions docs/USING_PRO.md
Expand Up @@ -10,6 +10,8 @@ The `renderer` and `tokenizer` options can be an object with functions that will

The `renderer` and `tokenizer` functions can return false to fallback to the previous function.

The `walkTokens` option can be a function that will be called with every token before rendering. When calling `use` multiple times with different `walkTokens` functions each function will be called in the **reverse** order in which they were assigned.

All other options will overwrite previously set options.

<h2 id="renderer">The renderer</h2>
Expand Down Expand Up @@ -188,6 +190,35 @@ smartypants('"this ... string"')
// "“this … string”"
```

<h2 id="walk-tokens">Walk Tokens</h2>

The walkTokens function gets called with every token. Child tokens are called before moving on to sibling tokens. Each token is passed by reference so updates are persisted when passed to the parser. The return value of the function is ignored.

**Example:** Overriding heading tokens to start at h2.

```js
const marked = require('marked');

// Override function
const walkTokens = (token) => {
if (token.type === 'heading') {
token.depth += 1;
}
};

marked.use({ walkTokens });

// Run marked
console.log(marked('# heading 2\n\n## heading 3'));
```

**Output:**

```html
<h2 id="heading-2">heading 2</h2>
<h3 id="heading-3">heading 3</h3>
```

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

The lexer takes a markdown string and calls the tokenizer functions.
Expand Down
1 change: 1 addition & 0 deletions docs/index.html
Expand Up @@ -157,6 +157,7 @@ <h1>Marked.js Documentation</h1>
<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#walk-tokens">Walk Tokens</a></li>
<li><a href="#/USING_PRO.md#lexer">Lexer</a></li>
<li><a href="#/USING_PRO.md#parser">Parser</a></li>
</ul>
Expand Down
1 change: 1 addition & 0 deletions src/Tokenizer.js
Expand Up @@ -269,6 +269,7 @@ module.exports = class Tokenizer {
}

list.items.push({
type: 'list_item',
raw,
task: istask,
checked: ischecked,
Expand Down
1 change: 1 addition & 0 deletions src/defaults.js
Expand Up @@ -16,6 +16,7 @@ function getDefaults() {
smartLists: false,
smartypants: false,
tokenizer: null,
walkTokens: null,
xhtml: false
};
}
Expand Down
124 changes: 85 additions & 39 deletions src/marked.js
Expand Up @@ -28,39 +28,33 @@ function marked(src, opt, callback) {
+ Object.prototype.toString.call(src) + ', string expected');
}

if (callback || typeof opt === 'function') {
if (!callback) {
callback = opt;
opt = null;
}
if (typeof opt === 'function') {
callback = opt;
opt = null;
}

opt = merge({}, marked.defaults, opt || {});
checkSanitizeDeprecation(opt);
opt = merge({}, marked.defaults, opt || {});
checkSanitizeDeprecation(opt);

if (callback) {
const highlight = opt.highlight;
let tokens,
pending,
i = 0;
let tokens;

try {
tokens = Lexer.lex(src, opt);
} catch (e) {
return callback(e);
}

pending = tokens.length;

const done = function(err) {
if (err) {
opt.highlight = highlight;
return callback(err);
}

let out;

try {
out = Parser.parse(tokens, opt);
} catch (e) {
err = e;
if (!err) {
try {
out = Parser.parse(tokens, opt);
} catch (e) {
err = e;
}
}

opt.highlight = highlight;
Expand All @@ -76,34 +70,45 @@ function marked(src, opt, callback) {

delete opt.highlight;

if (!pending) return done();
if (!tokens.length) return done();

for (; i < tokens.length; i++) {
(function(token) {
if (token.type !== 'code') {
return --pending || done();
}
return highlight(token.text, token.lang, function(err, code) {
if (err) return done(err);
UziTech marked this conversation as resolved.
Show resolved Hide resolved
if (code == null || code === token.text) {
return --pending || done();
let pending = 0;
marked.walkTokens(tokens, function(token) {
if (token.type === 'code') {
pending++;
highlight(token.text, token.lang, function(err, code) {
if (err) {
return done(err);
}
if (code != null && code !== token.text) {
token.text = code;
token.escaped = true;
}

pending--;
if (pending === 0) {
done();
}
token.text = code;
token.escaped = true;
--pending || done();
});
})(tokens[i]);
}
});

if (pending === 0) {
done();
}

return;
}

try {
opt = merge({}, marked.defaults, opt || {});
checkSanitizeDeprecation(opt);
return Parser.parse(Lexer.lex(src, opt), opt);
const tokens = Lexer.lex(src, opt);
if (opt.walkTokens) {
marked.walkTokens(tokens, opt.walkTokens);
}
return Parser.parse(tokens, opt);
} catch (e) {
e.message += '\nPlease report this to https://github.com/markedjs/marked.';
if ((opt || marked.defaults).silent) {
if (opt.silent) {
return '<p>An error occurred:</p><pre>'
+ escape(e.message + '', true)
+ '</pre>';
Expand Down Expand Up @@ -161,9 +166,50 @@ marked.use = function(extension) {
}
opts.tokenizer = tokenizer;
}
if (extension.walkTokens) {
const walkTokens = marked.defaults.walkTokens;
opts.walkTokens = (token) => {
extension.walkTokens(token);
if (walkTokens) {
walkTokens(token);
}
};
}
marked.setOptions(opts);
};

/**
* Run callback for every token
*/

marked.walkTokens = function(tokens, callback) {
for (const token of tokens) {
callback(token);
switch (token.type) {
case 'table': {
for (const cell of token.tokens.header) {
marked.walkTokens(cell, callback);
}
for (const row of token.tokens.cells) {
for (const cell of row) {
marked.walkTokens(cell, callback);
}
}
break;
}
case 'list': {
marked.walkTokens(token.items, callback);
break;
}
default: {
if (token.tokens) {
marked.walkTokens(token.tokens, callback);
}
}
}
}
};

/**
* Expose
*/
Expand Down
2 changes: 2 additions & 0 deletions test/unit/Lexer-spec.js
Expand Up @@ -307,6 +307,7 @@ a | b
loose: false,
items: [
{
type: 'list_item',
raw: '- item 1',
task: false,
checked: undefined,
Expand All @@ -320,6 +321,7 @@ a | b
}]
},
{
type: 'list_item',
raw: '- item 2\n',
task: false,
checked: undefined,
Expand Down