Skip to content

Expose compileMarkdown as public API #321

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

Merged
merged 4 commits into from
Feb 12, 2019
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
175 changes: 175 additions & 0 deletions addon/utils/compile-markdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import marked from 'marked';

import hljs from 'highlight.js/lib/highlight';

// Installed languages
import javascript from 'highlight.js/lib/languages/javascript';
import css from 'highlight.js/lib/languages/css';
import handlebars from 'highlight.js/lib/languages/handlebars';
import htmlbars from 'highlight.js/lib/languages/htmlbars';
import json from 'highlight.js/lib/languages/json';
import xml from 'highlight.js/lib/languages/xml';
import diff from 'highlight.js/lib/languages/diff';
import shell from 'highlight.js/lib/languages/shell';

hljs.registerLanguage('javascript', javascript);
hljs.registerLanguage('css', css);
hljs.registerLanguage('handlebars', handlebars);
hljs.registerLanguage('htmlbars', htmlbars);
hljs.registerLanguage('json', json);
hljs.registerLanguage('xml', xml);
hljs.registerLanguage('diff', diff);
hljs.registerLanguage('shell', shell);

function highlightCode(code, lang) {
return hljs.getLanguage(lang) ? hljs.highlight(lang, code).value : code
}

/**
This is the function used by AddonDocs to compile Markdown into HTML, for
example when turning `template.md` files into `template.hbs`. It includes
some parsing options, as well as syntax highlighting for code blocks.

You can use it in your own code, so your Markdown-rendered content shares the
same styling & syntax highlighting as the content AddonDocs already handles.

For example, you can use it if your Ember App has Markdown data that is
fetched at runtime from an API:

```js
import Component from '@ember/component';
import compileMarkdown from 'ember-cli-addon-docs/utils/compile-markdown';
import { htmlSafe } from '@ember/string';

export default Component.extend({
htmlBody: computed('post.body', function() {
return htmlSafe(compileMarkdown(this.post.body));
});
});
```

@function
@param {string} source Markdown string representing the source content
@param {object} options? Options. Pass `targetHandlebars: true` if turning MD into HBS
*/
export default function compileMarkdown(source, config) {
let tokens = marked.lexer(source);
let markedOptions = {
highlight: highlightCode,
renderer: new HBSRenderer(config)
};

if (config && config.targetHandlebars) {
tokens = compactParagraphs(tokens);
}

return `<div class="docs-md">${marked.parser(tokens, markedOptions).trim()}</div>`;
}

// Whitespace can imply paragraphs in Markdown, which can result
// in interleaving between <p> tags and block component invocations,
// so this scans the Marked tokens to turn things like this:
// <p>{{#my-component}}<p>
// <p>{{/my-component}}</p>
// Into this:
// <p>{{#my-component}} {{/my-component}}</p>
function compactParagraphs(tokens) {
let compacted = [];

compacted.links = tokens.links;

let balance = 0;
for (let token of tokens) {
if (balance === 0) {
compacted.push(token);
} else if (token.text) {
let last = compacted[compacted.length - 1];
last.text = `${last.text} ${token.text}`;
}

let tokenText = token.text || '';
let textWithoutCode = tokenText.replace(/`[\s\S]*?`/g, '');

balance += count(/{{#/g, textWithoutCode);
balance += count(/<[A-Z]/g, textWithoutCode);
balance -= count(/{{\//g, textWithoutCode);
balance -= count(/<\/[A-Z]/g, textWithoutCode);
}

return compacted;
}

function count(regex, string) {
let total = 0;
while (regex.exec(string)) total++;
return total;
}

class HBSRenderer extends marked.Renderer {
constructor(config) {
super();
this.config = config || {};
}

codespan() {
return this._processCode(super.codespan.apply(this, arguments));
}

code() {
let code = this._processCode(super.code.apply(this, arguments));

return code.replace(/^<pre>/, '<pre class="docs-md__code">');
}

// Unescape markdown escaping in general, since it can interfere with
// Handlebars templating
text() {
let text = super.text.apply(this, arguments);
if (this.config.targetHandlebars) {
text = text
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;|&#34;/g, '"')
.replace(/&apos;|&#39;/g, '\'');
}
return text;
}

// Escape curlies in code spans/blocks to avoid treating them as Handlebars
_processCode(string) {
if (this.config.targetHandlebars) {
string = this._escapeCurlies(string);
}

return string;
}

_escapeCurlies(string) {
return string
.replace(/{{/g, '&#123;&#123;')
.replace(/}}/g, '&#125;&#125;');
}

heading(text, level) {
let id = text.toLowerCase().replace(/<\/?.*?>/g, '').replace(/[^\w]+/g, '-');
let inner = level === 1 ? text : `<a href='#${id}' class='heading-anchor'>${text}</a>`;

return `
<h${level} id='${id}' class='docs-md__h${level}'>${inner}</h${level}>
`;
}

hr() {
return `<hr class='docs-md__hr'>`;
}

blockquote(text) {
return `<blockquote class='docs-md__blockquote'>${text}</blockquote>`;
}

link(href, title, text) {
const titleAttribute = title ? `title="${title}"` : '';
return `<a href="${href}" ${titleAttribute} class="docs-md__a">${text}</a>`;
}
}
3 changes: 3 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ module.exports = {
includer.options.includehighlightJS = false;
includer.options.includeHighlightStyle = false;
includer.options.snippetExtensions = ['js', 'css', 'scss', 'hbs', 'md', 'text', 'json', 'handlebars', 'htmlbars', 'html', 'diff'];
includer.options.autoImport = {
exclude: [ 'qunit' ]
};

// This must come after we add our own options above, or else other addons won't see them.
this._super.included.apply(this, arguments);
Expand Down
152 changes: 2 additions & 150 deletions lib/utils/compile-markdown.js
Original file line number Diff line number Diff line change
@@ -1,150 +1,2 @@
'use strict';

const marked = require('marked');

const hljs = require('highlight.js/lib/highlight');

// Installed languages
const javascript = require('highlight.js/lib/languages/javascript');
const css = require('highlight.js/lib/languages/css');
const handlebars = require('highlight.js/lib/languages/handlebars');
const htmlbars = require('highlight.js/lib/languages/htmlbars');
const json = require('highlight.js/lib/languages/json');
const xml = require('highlight.js/lib/languages/xml');
const diff = require('highlight.js/lib/languages/diff');
const shell = require('highlight.js/lib/languages/shell');

hljs.registerLanguage('javascript', javascript);
hljs.registerLanguage('css', css);
hljs.registerLanguage('handlebars', handlebars);
hljs.registerLanguage('htmlbars', htmlbars);
hljs.registerLanguage('json', json);
hljs.registerLanguage('xml', xml);
hljs.registerLanguage('diff', diff);
hljs.registerLanguage('shell', shell);

function highlightCode(code, lang) {
return hljs.getLanguage(lang) ? hljs.highlight(lang, code).value : code
}

module.exports = function compileMarkdown(source, config) {
let tokens = marked.lexer(source);
let markedOptions = {
highlight: highlightCode,
renderer: new HBSRenderer(config)
};

if (config && config.targetHandlebars) {
tokens = compactParagraphs(tokens);
}

return `<div class="docs-md">${marked.parser(tokens, markedOptions).trim()}</div>`;
};

// Whitespace can imply paragraphs in Markdown, which can result
// in interleaving between <p> tags and block component invocations,
// so this scans the Marked tokens to turn things like this:
// <p>{{#my-component}}<p>
// <p>{{/my-component}}</p>
// Into this:
// <p>{{#my-component}} {{/my-component}}</p>
function compactParagraphs(tokens) {
let compacted = [];

compacted.links = tokens.links;

let balance = 0;
for (let token of tokens) {
if (balance === 0) {
compacted.push(token);
} else if (token.text) {
let last = compacted[compacted.length - 1];
last.text = `${last.text} ${token.text}`;
}

let tokenText = token.text || '';
let textWithoutCode = tokenText.replace(/`[\s\S]*?`/g, '');

balance += count(/{{#/g, textWithoutCode);
balance += count(/<[A-Z]/g, textWithoutCode);
balance -= count(/{{\//g, textWithoutCode);
balance -= count(/<\/[A-Z]/g, textWithoutCode);
}

return compacted;
}

function count(regex, string) {
let total = 0;
while (regex.exec(string)) total++;
return total;
}

class HBSRenderer extends marked.Renderer {
constructor(config) {
super();
this.config = config || {};
}

codespan() {
return this._processCode(super.codespan.apply(this, arguments));
}

code() {
let code = this._processCode(super.code.apply(this, arguments));

return code.replace(/^<pre>/, '<pre class="docs-md__code">');
}

// Unescape markdown escaping in general, since it can interfere with
// Handlebars templating
text() {
let text = super.text.apply(this, arguments);
if (this.config.targetHandlebars) {
text = text
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;|&#34;/g, '"')
.replace(/&apos;|&#39;/g, '\'');
}
return text;
}

// Escape curlies in code spans/blocks to avoid treating them as Handlebars
_processCode(string) {
if (this.config.targetHandlebars) {
string = this._escapeCurlies(string);
}

return string;
}

_escapeCurlies(string) {
return string
.replace(/{{/g, '&#123;&#123;')
.replace(/}}/g, '&#125;&#125;');
}

heading(text, level) {
let id = text.toLowerCase().replace(/<\/?.*?>/g, '').replace(/[^\w]+/g, '-');
let inner = level === 1 ? text : `<a href='#${id}' class='heading-anchor'>${text}</a>`;

return `
<h${level} id='${id}' class='docs-md__h${level}'>${inner}</h${level}>
`;
}

hr() {
return `<hr class='docs-md__hr'>`;
}

blockquote(text) {
return `<blockquote class='docs-md__blockquote'>${text}</blockquote>`;
}

link(href, title, text) {
const titleAttribute = title ? `title="${title}"` : '';
return `<a href="${href}" ${titleAttribute} class="docs-md__a">${text}</a>`;
}
}
const esmRequire = require("esm")(module, { cjs: true });
module.exports = esmRequire('../../addon/utils/compile-markdown').default;
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"broccoli-plugin": "^1.3.1",
"broccoli-source": "^1.1.0",
"broccoli-stew": "^2.0.0",
"ember-auto-import": "^1.2.19",
"ember-cli-autoprefixer": "^0.8.1",
"ember-cli-babel": "^6.16.0",
"ember-cli-clipboard": "^0.11.1",
Expand All @@ -58,6 +59,7 @@
"ember-svg-jar": "^1.2.2",
"ember-tether": "^1.0.0-beta.2",
"ember-truth-helpers": "^2.1.0",
"esm": "^3.2.4",
"execa": "^1.0.0",
"fs-extra": "^7.0.0",
"git-repo-info": "^2.0.0",
Expand Down Expand Up @@ -116,7 +118,7 @@
"eslint-plugin-node": "^7.0.1",
"loader.js": "^4.7.0",
"qunit": "^2.6.2",
"qunit-dom": "^0.8.0"
"qunit-dom": "^0.8.4"
},
"resolutions": {
"**/tough-cookie": "~2.4.0",
Expand Down
3 changes: 2 additions & 1 deletion test-apps/new-addon/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
"ember-try": "*",
"eslint-plugin-ember": "*",
"eslint-plugin-node": "*",
"loader.js": "*"
"loader.js": "*",
"qunit-dom": "*"
},
"engines": {
"node": "^4.5 || 6.* || >= 7.*"
Expand Down
Loading