Skip to content

Commit ac5147b

Browse files
authored
Expose compileMarkdown as public API (#321)
- closes #318
1 parent a00a28e commit ac5147b

File tree

11 files changed

+1600
-205
lines changed

11 files changed

+1600
-205
lines changed

addon/utils/compile-markdown.js

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import marked from 'marked';
2+
3+
import hljs from 'highlight.js/lib/highlight';
4+
5+
// Installed languages
6+
import javascript from 'highlight.js/lib/languages/javascript';
7+
import css from 'highlight.js/lib/languages/css';
8+
import handlebars from 'highlight.js/lib/languages/handlebars';
9+
import htmlbars from 'highlight.js/lib/languages/htmlbars';
10+
import json from 'highlight.js/lib/languages/json';
11+
import xml from 'highlight.js/lib/languages/xml';
12+
import diff from 'highlight.js/lib/languages/diff';
13+
import shell from 'highlight.js/lib/languages/shell';
14+
15+
hljs.registerLanguage('javascript', javascript);
16+
hljs.registerLanguage('css', css);
17+
hljs.registerLanguage('handlebars', handlebars);
18+
hljs.registerLanguage('htmlbars', htmlbars);
19+
hljs.registerLanguage('json', json);
20+
hljs.registerLanguage('xml', xml);
21+
hljs.registerLanguage('diff', diff);
22+
hljs.registerLanguage('shell', shell);
23+
24+
function highlightCode(code, lang) {
25+
return hljs.getLanguage(lang) ? hljs.highlight(lang, code).value : code
26+
}
27+
28+
/**
29+
This is the function used by AddonDocs to compile Markdown into HTML, for
30+
example when turning `template.md` files into `template.hbs`. It includes
31+
some parsing options, as well as syntax highlighting for code blocks.
32+
33+
You can use it in your own code, so your Markdown-rendered content shares the
34+
same styling & syntax highlighting as the content AddonDocs already handles.
35+
36+
For example, you can use it if your Ember App has Markdown data that is
37+
fetched at runtime from an API:
38+
39+
```js
40+
import Component from '@ember/component';
41+
import compileMarkdown from 'ember-cli-addon-docs/utils/compile-markdown';
42+
import { htmlSafe } from '@ember/string';
43+
44+
export default Component.extend({
45+
htmlBody: computed('post.body', function() {
46+
return htmlSafe(compileMarkdown(this.post.body));
47+
});
48+
});
49+
```
50+
51+
@function
52+
@param {string} source Markdown string representing the source content
53+
@param {object} options? Options. Pass `targetHandlebars: true` if turning MD into HBS
54+
*/
55+
export default function compileMarkdown(source, config) {
56+
let tokens = marked.lexer(source);
57+
let markedOptions = {
58+
highlight: highlightCode,
59+
renderer: new HBSRenderer(config)
60+
};
61+
62+
if (config && config.targetHandlebars) {
63+
tokens = compactParagraphs(tokens);
64+
}
65+
66+
return `<div class="docs-md">${marked.parser(tokens, markedOptions).trim()}</div>`;
67+
}
68+
69+
// Whitespace can imply paragraphs in Markdown, which can result
70+
// in interleaving between <p> tags and block component invocations,
71+
// so this scans the Marked tokens to turn things like this:
72+
// <p>{{#my-component}}<p>
73+
// <p>{{/my-component}}</p>
74+
// Into this:
75+
// <p>{{#my-component}} {{/my-component}}</p>
76+
function compactParagraphs(tokens) {
77+
let compacted = [];
78+
79+
compacted.links = tokens.links;
80+
81+
let balance = 0;
82+
for (let token of tokens) {
83+
if (balance === 0) {
84+
compacted.push(token);
85+
} else if (token.text) {
86+
let last = compacted[compacted.length - 1];
87+
last.text = `${last.text} ${token.text}`;
88+
}
89+
90+
let tokenText = token.text || '';
91+
let textWithoutCode = tokenText.replace(/`[\s\S]*?`/g, '');
92+
93+
balance += count(/{{#/g, textWithoutCode);
94+
balance += count(/<[A-Z]/g, textWithoutCode);
95+
balance -= count(/{{\//g, textWithoutCode);
96+
balance -= count(/<\/[A-Z]/g, textWithoutCode);
97+
}
98+
99+
return compacted;
100+
}
101+
102+
function count(regex, string) {
103+
let total = 0;
104+
while (regex.exec(string)) total++;
105+
return total;
106+
}
107+
108+
class HBSRenderer extends marked.Renderer {
109+
constructor(config) {
110+
super();
111+
this.config = config || {};
112+
}
113+
114+
codespan() {
115+
return this._processCode(super.codespan.apply(this, arguments));
116+
}
117+
118+
code() {
119+
let code = this._processCode(super.code.apply(this, arguments));
120+
121+
return code.replace(/^<pre>/, '<pre class="docs-md__code">');
122+
}
123+
124+
// Unescape markdown escaping in general, since it can interfere with
125+
// Handlebars templating
126+
text() {
127+
let text = super.text.apply(this, arguments);
128+
if (this.config.targetHandlebars) {
129+
text = text
130+
.replace(/&amp;/g, '&')
131+
.replace(/&lt;/g, '<')
132+
.replace(/&gt;/g, '>')
133+
.replace(/&quot;|&#34;/g, '"')
134+
.replace(/&apos;|&#39;/g, '\'');
135+
}
136+
return text;
137+
}
138+
139+
// Escape curlies in code spans/blocks to avoid treating them as Handlebars
140+
_processCode(string) {
141+
if (this.config.targetHandlebars) {
142+
string = this._escapeCurlies(string);
143+
}
144+
145+
return string;
146+
}
147+
148+
_escapeCurlies(string) {
149+
return string
150+
.replace(/{{/g, '&#123;&#123;')
151+
.replace(/}}/g, '&#125;&#125;');
152+
}
153+
154+
heading(text, level) {
155+
let id = text.toLowerCase().replace(/<\/?.*?>/g, '').replace(/[^\w]+/g, '-');
156+
let inner = level === 1 ? text : `<a href='#${id}' class='heading-anchor'>${text}</a>`;
157+
158+
return `
159+
<h${level} id='${id}' class='docs-md__h${level}'>${inner}</h${level}>
160+
`;
161+
}
162+
163+
hr() {
164+
return `<hr class='docs-md__hr'>`;
165+
}
166+
167+
blockquote(text) {
168+
return `<blockquote class='docs-md__blockquote'>${text}</blockquote>`;
169+
}
170+
171+
link(href, title, text) {
172+
const titleAttribute = title ? `title="${title}"` : '';
173+
return `<a href="${href}" ${titleAttribute} class="docs-md__a">${text}</a>`;
174+
}
175+
}

index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ module.exports = {
9090
includer.options.includehighlightJS = false;
9191
includer.options.includeHighlightStyle = false;
9292
includer.options.snippetExtensions = ['js', 'css', 'scss', 'hbs', 'md', 'text', 'json', 'handlebars', 'htmlbars', 'html', 'diff'];
93+
includer.options.autoImport = {
94+
exclude: [ 'qunit' ]
95+
};
9396

9497
// This must come after we add our own options above, or else other addons won't see them.
9598
this._super.included.apply(this, arguments);

lib/utils/compile-markdown.js

Lines changed: 2 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -1,150 +1,2 @@
1-
'use strict';
2-
3-
const marked = require('marked');
4-
5-
const hljs = require('highlight.js/lib/highlight');
6-
7-
// Installed languages
8-
const javascript = require('highlight.js/lib/languages/javascript');
9-
const css = require('highlight.js/lib/languages/css');
10-
const handlebars = require('highlight.js/lib/languages/handlebars');
11-
const htmlbars = require('highlight.js/lib/languages/htmlbars');
12-
const json = require('highlight.js/lib/languages/json');
13-
const xml = require('highlight.js/lib/languages/xml');
14-
const diff = require('highlight.js/lib/languages/diff');
15-
const shell = require('highlight.js/lib/languages/shell');
16-
17-
hljs.registerLanguage('javascript', javascript);
18-
hljs.registerLanguage('css', css);
19-
hljs.registerLanguage('handlebars', handlebars);
20-
hljs.registerLanguage('htmlbars', htmlbars);
21-
hljs.registerLanguage('json', json);
22-
hljs.registerLanguage('xml', xml);
23-
hljs.registerLanguage('diff', diff);
24-
hljs.registerLanguage('shell', shell);
25-
26-
function highlightCode(code, lang) {
27-
return hljs.getLanguage(lang) ? hljs.highlight(lang, code).value : code
28-
}
29-
30-
module.exports = function compileMarkdown(source, config) {
31-
let tokens = marked.lexer(source);
32-
let markedOptions = {
33-
highlight: highlightCode,
34-
renderer: new HBSRenderer(config)
35-
};
36-
37-
if (config && config.targetHandlebars) {
38-
tokens = compactParagraphs(tokens);
39-
}
40-
41-
return `<div class="docs-md">${marked.parser(tokens, markedOptions).trim()}</div>`;
42-
};
43-
44-
// Whitespace can imply paragraphs in Markdown, which can result
45-
// in interleaving between <p> tags and block component invocations,
46-
// so this scans the Marked tokens to turn things like this:
47-
// <p>{{#my-component}}<p>
48-
// <p>{{/my-component}}</p>
49-
// Into this:
50-
// <p>{{#my-component}} {{/my-component}}</p>
51-
function compactParagraphs(tokens) {
52-
let compacted = [];
53-
54-
compacted.links = tokens.links;
55-
56-
let balance = 0;
57-
for (let token of tokens) {
58-
if (balance === 0) {
59-
compacted.push(token);
60-
} else if (token.text) {
61-
let last = compacted[compacted.length - 1];
62-
last.text = `${last.text} ${token.text}`;
63-
}
64-
65-
let tokenText = token.text || '';
66-
let textWithoutCode = tokenText.replace(/`[\s\S]*?`/g, '');
67-
68-
balance += count(/{{#/g, textWithoutCode);
69-
balance += count(/<[A-Z]/g, textWithoutCode);
70-
balance -= count(/{{\//g, textWithoutCode);
71-
balance -= count(/<\/[A-Z]/g, textWithoutCode);
72-
}
73-
74-
return compacted;
75-
}
76-
77-
function count(regex, string) {
78-
let total = 0;
79-
while (regex.exec(string)) total++;
80-
return total;
81-
}
82-
83-
class HBSRenderer extends marked.Renderer {
84-
constructor(config) {
85-
super();
86-
this.config = config || {};
87-
}
88-
89-
codespan() {
90-
return this._processCode(super.codespan.apply(this, arguments));
91-
}
92-
93-
code() {
94-
let code = this._processCode(super.code.apply(this, arguments));
95-
96-
return code.replace(/^<pre>/, '<pre class="docs-md__code">');
97-
}
98-
99-
// Unescape markdown escaping in general, since it can interfere with
100-
// Handlebars templating
101-
text() {
102-
let text = super.text.apply(this, arguments);
103-
if (this.config.targetHandlebars) {
104-
text = text
105-
.replace(/&amp;/g, '&')
106-
.replace(/&lt;/g, '<')
107-
.replace(/&gt;/g, '>')
108-
.replace(/&quot;|&#34;/g, '"')
109-
.replace(/&apos;|&#39;/g, '\'');
110-
}
111-
return text;
112-
}
113-
114-
// Escape curlies in code spans/blocks to avoid treating them as Handlebars
115-
_processCode(string) {
116-
if (this.config.targetHandlebars) {
117-
string = this._escapeCurlies(string);
118-
}
119-
120-
return string;
121-
}
122-
123-
_escapeCurlies(string) {
124-
return string
125-
.replace(/{{/g, '&#123;&#123;')
126-
.replace(/}}/g, '&#125;&#125;');
127-
}
128-
129-
heading(text, level) {
130-
let id = text.toLowerCase().replace(/<\/?.*?>/g, '').replace(/[^\w]+/g, '-');
131-
let inner = level === 1 ? text : `<a href='#${id}' class='heading-anchor'>${text}</a>`;
132-
133-
return `
134-
<h${level} id='${id}' class='docs-md__h${level}'>${inner}</h${level}>
135-
`;
136-
}
137-
138-
hr() {
139-
return `<hr class='docs-md__hr'>`;
140-
}
141-
142-
blockquote(text) {
143-
return `<blockquote class='docs-md__blockquote'>${text}</blockquote>`;
144-
}
145-
146-
link(href, title, text) {
147-
const titleAttribute = title ? `title="${title}"` : '';
148-
return `<a href="${href}" ${titleAttribute} class="docs-md__a">${text}</a>`;
149-
}
150-
}
1+
const esmRequire = require("esm")(module, { cjs: true });
2+
module.exports = esmRequire('../../addon/utils/compile-markdown').default;

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"broccoli-plugin": "^1.3.1",
3737
"broccoli-source": "^1.1.0",
3838
"broccoli-stew": "^2.0.0",
39+
"ember-auto-import": "^1.2.19",
3940
"ember-cli-autoprefixer": "^0.8.1",
4041
"ember-cli-babel": "^6.16.0",
4142
"ember-cli-clipboard": "^0.11.1",
@@ -58,6 +59,7 @@
5859
"ember-svg-jar": "^1.2.2",
5960
"ember-tether": "^1.0.0-beta.2",
6061
"ember-truth-helpers": "^2.1.0",
62+
"esm": "^3.2.4",
6163
"execa": "^1.0.0",
6264
"fs-extra": "^7.0.0",
6365
"git-repo-info": "^2.0.0",
@@ -116,7 +118,7 @@
116118
"eslint-plugin-node": "^7.0.1",
117119
"loader.js": "^4.7.0",
118120
"qunit": "^2.6.2",
119-
"qunit-dom": "^0.8.0"
121+
"qunit-dom": "^0.8.4"
120122
},
121123
"resolutions": {
122124
"**/tough-cookie": "~2.4.0",

test-apps/new-addon/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@
4444
"ember-try": "*",
4545
"eslint-plugin-ember": "*",
4646
"eslint-plugin-node": "*",
47-
"loader.js": "*"
47+
"loader.js": "*",
48+
"qunit-dom": "*"
4849
},
4950
"engines": {
5051
"node": "^4.5 || 6.* || >= 7.*"

0 commit comments

Comments
 (0)