UNPKG

7.16 kBJavaScriptView Raw
1const markdownIt = require('markdown-it')
2const Prism = require('prismjs')
3const container = require('markdown-it-container')
4const emoji = require('markdown-it-emoji')
5const subscript = require('markdown-it-sub')
6const superscript = require('markdown-it-sup')
7const footnote = require('markdown-it-footnote')
8const deflist = require('markdown-it-deflist')
9const abbreviation = require('markdown-it-abbr')
10const insert = require('markdown-it-ins')
11const mark = require('markdown-it-mark')
12const taskLists = require('markdown-it-task-lists')
13const imsize = require('markdown-it-imsize')
14
15const fm = require('front-matter')
16
17function removeFrontMatter (source) {
18 return source.replace(/^---(.|\n)*?---\n/, '')
19}
20
21function replaceFrontMatter (source, content) {
22 return String(source).replace('frontMatter: {}', `frontMatter: ${JSON.stringify(content)}`)
23}
24
25function replaceToc (source, tocData) {
26 return String(source).replace('tocData: []', `tocData: ${JSON.stringify(tocData)}`)
27}
28
29function slugify (str) {
30 return encodeURIComponent(String(str).trim().replace(/\s+/g, '-'))
31}
32
33function 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
50function 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
61function 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
71function 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
100function 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
109function 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
142function 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
160function 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 // explicitly escape Vue syntax
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
175function 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// function extendFenceLineNumbers (md) {
185// const fence = md.renderer.rules.fence
186// md.renderer.rules.fence = (...args) => {
187// const rawCode = fence(...args)
188// const code = rawCode.slice(
189// rawCode.indexOf('<code>') + 6,
190// rawCode.indexOf('</code>')
191// )
192
193// const lines = code.trim().split('\n')
194// if (lines.length < 3) {
195// return rawCode
196// }
197
198// const lineNumbersCode = [...Array(lines.length)]
199// .map((line, index) => `<span class="q-markup--line-number">${index + 1}</span><br>`).join('')
200
201// const lineNumbersWrapperCode =
202// `<div class="q-markdown--line-numbers non-selectable">${lineNumbersCode}</div><div class="q-markdown--code-wrapper">${rawCode}</div>`
203
204// const finalCode =
205// `<div class="q-markdown--line-numbers-wrapper">${lineNumbersWrapperCode}</div>`
206
207// return finalCode
208// }
209// }
210
211const opts = {
212 html: true,
213 linkify: true,
214 typographer: false,
215 breaks: true,
216 highlight
217}
218
219const md = markdownIt(opts)
220md.use(subscript)
221md.use(superscript)
222md.use(footnote)
223md.use(deflist)
224md.use(abbreviation)
225md.use(insert)
226md.use(mark)
227md.use(emoji)
228md.use(imsize)
229md.use(taskLists, { enabled: true, label: true, labelAfter: true })
230
231function 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 // bug: this is causing numbers to show up twice
241 // extendFenceLineNumbers(md)
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
252module.exports = renderMarkdown