/
compile-markdown.js
219 lines (181 loc) · 6.38 KB
/
compile-markdown.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
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';
import typescript from 'highlight.js/lib/languages/typescript';
hljs.registerLanguage('javascript', javascript);
hljs.registerLanguage('js', javascript);
hljs.registerLanguage('css', css);
hljs.registerLanguage('handlebars', handlebars);
hljs.registerLanguage('htmlbars', htmlbars);
hljs.registerLanguage('hbs', htmlbars);
hljs.registerLanguage('json', json);
hljs.registerLanguage('xml', xml);
hljs.registerLanguage('diff', diff);
hljs.registerLanguage('shell', shell);
hljs.registerLanguage('sh', shell);
hljs.registerLanguage('typescript', typescript);
hljs.registerLanguage('ts', typescript);
/**
This function is used when `compileMarkdown` encounters code blocks while
rendering Markdown source.
You can use this function on its own if you have code snippets you want
to highlight at run-time, for example snippets that change based on some
user interaction.
```js
import Component from '@ember/component';
import dedent from 'dedent';
import { highlightCode } from 'ember-cli-addon-docs/utils/compile-markdown';
export default Component.extend({
snippet: dedent`
let { foo } = bar;
`,
highlightedSnippet: computed(function() {
return highlightCode(this.snippet, 'js');
})
});
```
```hbs
<div class='docs-bg-code-base text-grey overflow-x-scroll'>
<div class="p-4 w-full">
<pre>{{{highlightedSnippet}}}</pre>
</div>
</div>
```
@function highlightCode
@param {string} snippet Snippet of code
@param {string} lang Language to use for syntax highlighting
*/
export 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 compileMarkdown
@export default
@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(/[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(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"|"/g, '"')
.replace(/'|'/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, '{{')
.replace(/}}/g, '}}');
}
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>`;
}
}