1 | const markdownIt = require('markdown-it')
|
2 | const Prism = require('prismjs')
|
3 | const container = require('markdown-it-container')
|
4 | const emoji = require('markdown-it-emoji')
|
5 | const subscript = require('markdown-it-sub')
|
6 | const superscript = require('markdown-it-sup')
|
7 | const footnote = require('markdown-it-footnote')
|
8 | const deflist = require('markdown-it-deflist')
|
9 | const abbreviation = require('markdown-it-abbr')
|
10 | const insert = require('markdown-it-ins')
|
11 | const mark = require('markdown-it-mark')
|
12 | const taskLists = require('markdown-it-task-lists')
|
13 | const imsize = require('markdown-it-imsize')
|
14 |
|
15 | const fm = require('front-matter')
|
16 |
|
17 | function removeFrontMatter (source) {
|
18 | return source.replace(/^---(.|\n)*?---\n/, '')
|
19 | }
|
20 |
|
21 | function replaceFrontMatter (source, content) {
|
22 | return String(source).replace('frontMatter: {}', `frontMatter: ${JSON.stringify(content)}`)
|
23 | }
|
24 |
|
25 | function replaceToc (source, tocData) {
|
26 | return String(source).replace('tocData: []', `tocData: ${JSON.stringify(tocData)}`)
|
27 | }
|
28 |
|
29 | function slugify (str) {
|
30 | return encodeURIComponent(String(str).trim().replace(/\s+/g, '-'))
|
31 | }
|
32 |
|
33 | function highlight (str, lang) {
|
34 | if (lang === '') {
|
35 | lang = 'js'
|
36 | } else if (lang === 'vue' || lang === 'html') {
|
37 | lang = 'html'
|
38 | }
|
39 |
|
40 | if (Prism.languages[lang] !== undefined) {
|
41 | const code = Prism.highlight(str, Prism.languages[lang], lang)
|
42 |
|
43 | return '<pre class="q-markdown--code">' +
|
44 | `<code class="q-markdown--code__inner language-${lang}">${code}</code></pre>\n`
|
45 | }
|
46 |
|
47 | return ''
|
48 | }
|
49 |
|
50 | function extendToken (md) {
|
51 | const defaultRender = md.renderer.rules.code_inline
|
52 |
|
53 | md.renderer.rules.code_inline = (tokens, idx, options, env, self) => {
|
54 | const token = tokens[idx]
|
55 |
|
56 | token.attrSet('class', 'q-markdown--token')
|
57 | return defaultRender(tokens, idx, options, env, self)
|
58 | }
|
59 | }
|
60 |
|
61 | function extendTable (md) {
|
62 | md.renderer.rules.table_open = (tokens, idx, options, env, self) => {
|
63 | const token = tokens[idx]
|
64 |
|
65 | token.attrSet('class', 'q-markdown--table')
|
66 |
|
67 | return self.renderToken(tokens, idx, options)
|
68 | }
|
69 | }
|
70 |
|
71 | function extendLink (md) {
|
72 | md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
|
73 | const token = tokens[idx]
|
74 |
|
75 | const hrefIndex = token.attrIndex('href')
|
76 |
|
77 | if (token.attrs[hrefIndex][1][0] === '#') {
|
78 | if (location) {
|
79 | token.attrs[hrefIndex][1] = location.pathname + token.attrs[hrefIndex][1]
|
80 | }
|
81 | }
|
82 |
|
83 | if (token.attrs[hrefIndex][1] === '') {
|
84 | token.attrSet('class', 'q-markdown--link q-markdown--link-local')
|
85 | if (tokens[idx + 1] && tokens[idx + 1].type === 'text' && tokens[idx + 1].content) {
|
86 | token.attrSet('id', slugify(tokens[idx + 1].content))
|
87 | }
|
88 | } else if (token.attrs[hrefIndex][1][0] === '/' ||
|
89 | token.attrs[hrefIndex][1].startsWith('..')) {
|
90 | token.attrSet('class', 'q-markdown--link q-markdown--link-local')
|
91 | } else {
|
92 | token.attrSet('class', 'q-markdown--link q-markdown--link-external')
|
93 | token.attrSet('target', '_blank')
|
94 | }
|
95 |
|
96 | return self.renderToken(tokens, idx, options)
|
97 | }
|
98 | }
|
99 |
|
100 | function extendImage (md) {
|
101 | md.renderer.rules.image = (tokens, idx, options, env, self) => {
|
102 | const token = tokens[idx]
|
103 |
|
104 | token.attrSet('class', 'q-markdown--image')
|
105 | return self.renderToken(tokens, idx, options)
|
106 | }
|
107 | }
|
108 |
|
109 | function extendHeading (md, tocData = [], toc = false, tocStart = 1, tocEnd = 3) {
|
110 | md.renderer.rules.heading_open = (tokens, idx, options, env, self) => {
|
111 | const token = tokens[idx]
|
112 |
|
113 | const label = tokens[idx + 1]
|
114 | .children
|
115 | .reduce((acc, t) => acc + t.content, '')
|
116 |
|
117 | let classes = `q-markdown--heading-${token.tag}`
|
118 |
|
119 | if (token.markup === '=') {
|
120 | classes += ' q-markdown--title-heavy'
|
121 | } else if (token.markup === '-') {
|
122 | classes += ' q-markdown--title-light'
|
123 | }
|
124 |
|
125 | const id = slugify(label)
|
126 | token.attrSet('id', id)
|
127 | token.attrSet('name', id)
|
128 | token.attrSet('class', classes)
|
129 |
|
130 | if (toc) {
|
131 | const tokenNumber = parseInt(token.tag[1])
|
132 |
|
133 | if (tocStart && tocEnd && tocStart < tocEnd && tokenNumber >= tocStart && tokenNumber <= tocEnd) {
|
134 | tocData.push({ id: id, label: label, level: tokenNumber, children: [] })
|
135 | }
|
136 | }
|
137 |
|
138 | return self.renderToken(tokens, idx, options)
|
139 | }
|
140 | }
|
141 |
|
142 | function createContainer (className, defaultTitle) {
|
143 | return [
|
144 | container,
|
145 | className,
|
146 | {
|
147 | render (tokens, idx) {
|
148 | const token = tokens[idx]
|
149 | const info = token.info.trim().slice(className.length).trim()
|
150 | if (token.nesting === 1) {
|
151 | return `<div class="q-markdown--note q-markdown--note--${className}"><p class="q-markdown--note-title">${info || defaultTitle}</p>\n`
|
152 | } else {
|
153 | return '</div>\n'
|
154 | }
|
155 | }
|
156 | }
|
157 | ]
|
158 | }
|
159 |
|
160 | function extendContainers (md) {
|
161 | md.use(...createContainer('info', 'INFO'))
|
162 | md.use(...createContainer('tip', 'TIP'))
|
163 | md.use(...createContainer('warning', 'WARNING'))
|
164 | md.use(...createContainer('danger', 'IMPORTANT'))
|
165 | md.use(...createContainer('', ''))
|
166 |
|
167 |
|
168 | md.use(container, 'v-pre', {
|
169 | render: (tokens, idx) => tokens[idx].nesting === 1
|
170 | ? '<div v-pre>\n'
|
171 | : '</div>\n'
|
172 | })
|
173 | }
|
174 |
|
175 | function extendBlockQuote (md) {
|
176 | md.renderer.rules.blockquote_open = (tokens, idx, options, env, self) => {
|
177 | const token = tokens[idx]
|
178 |
|
179 | token.attrSet('class', 'q-markdown--note')
|
180 | return self.renderToken(tokens, idx, options)
|
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 | const opts = {
|
212 | html: true,
|
213 | linkify: true,
|
214 | typographer: false,
|
215 | breaks: true,
|
216 | highlight
|
217 | }
|
218 |
|
219 | const md = markdownIt(opts)
|
220 | md.use(subscript)
|
221 | md.use(superscript)
|
222 | md.use(footnote)
|
223 | md.use(deflist)
|
224 | md.use(abbreviation)
|
225 | md.use(insert)
|
226 | md.use(mark)
|
227 | md.use(emoji)
|
228 | md.use(imsize)
|
229 | md.use(taskLists, { enabled: true, label: true, labelAfter: true })
|
230 |
|
231 | function renderMarkdown (source) {
|
232 | const tocData = []
|
233 | extendHeading(md, tocData, true)
|
234 | extendBlockQuote(md)
|
235 | extendImage(md)
|
236 | extendLink(md)
|
237 | extendTable(md)
|
238 | extendToken(md)
|
239 | extendContainers(md)
|
240 |
|
241 |
|
242 |
|
243 | const content = fm(source)
|
244 | source = removeFrontMatter(source)
|
245 | let rendered = md.render(source)
|
246 | rendered = replaceFrontMatter(rendered, content.attributes)
|
247 | rendered = replaceToc(rendered, tocData)
|
248 |
|
249 | return rendered
|
250 | }
|
251 |
|
252 | module.exports = renderMarkdown
|