feat: add markdown processing

pull/1/head
Evan You 4 years ago
parent 21d3cd8cbe
commit 5c47bbb463

@ -1,3 +0,0 @@
export function markdownToVue(content: string): string {
return `<template>${content}</template>`
}

@ -1,85 +0,0 @@
import path from 'path'
import {
createServer as createViteServer,
cachedRead,
Plugin,
Resolver
} from 'vite'
import { markdownToVue } from './markdown'
const debug = require('debug')('vitepress')
// built ts files are placed into /dist
const appPath = path.join(__dirname, '../lib/app')
// TODO detect user configured theme
const themePath = path.join(__dirname, '../lib/theme-default')
const VitePressResolver: Resolver = {
publicToFile(publicPath) {
if (publicPath.startsWith('/@app')) {
return path.join(appPath, publicPath.replace(/^\/@app\/?/, ''))
}
if (publicPath.startsWith('/@theme')) {
return path.join(themePath, publicPath.replace(/^\/@theme\/?/, ''))
}
},
fileToPublic(filePath) {
if (filePath.startsWith(appPath)) {
return `/@app/${path.relative(appPath, filePath)}`
}
if (filePath.startsWith(themePath)) {
return `/@theme/${path.relative(themePath, filePath)}`
}
}
}
const VitePressPlugin: Plugin = ({ app, root, watcher, resolver }) => {
// watch theme files if it's outside of project root
if (path.relative(root, themePath).startsWith('..')) {
debug(`watching theme dir outside of project root: ${themePath}`)
watcher.add(themePath)
}
// hot reload .md files as .vue files
watcher.on('change', async (file) => {
if (file.endsWith('.md')) {
const content = await cachedRead(null, file)
watcher.handleVueReload(file, Date.now(), markdownToVue(content))
}
})
app.use(async (ctx, next) => {
if (ctx.path.endsWith('.md')) {
await cachedRead(ctx, resolver.publicToFile(ctx.path))
// let vite know this is supposed to be treated as vue file
ctx.vue = true
ctx.body = markdownToVue(ctx.body)
debug(`serving ${ctx.url}`)
return next()
}
// detect and serve vitepress files
const file = VitePressResolver.publicToFile(ctx.path, root)
if (file) {
ctx.type = path.extname(file)
await cachedRead(ctx, file)
debug(`serving file: ${ctx.url}`)
return next()
}
await next()
// serve our index.html after vite history fallback
if (ctx.url === '/index.html') {
await cachedRead(ctx, path.join(appPath, 'index-dev.html'))
}
})
}
export function createServer() {
return createViteServer({
plugins: [VitePressPlugin],
resolvers: [VitePressResolver]
})
}

@ -1,9 +1,16 @@
<template>
<div class="theme-container">
<h1>Hello VitePress {{ a }}</h1>
<Content/>
</div>
</template>
<script>
export default {
data: () => ({ a: 111 })
}
</script>
<style>
.theme-container {
font-family: Arial, Helvetica, sans-serif;

@ -18,9 +18,21 @@
"license": "MIT",
"dependencies": {
"debug": "^4.1.1",
"diacritics": "^1.3.0",
"escape-html": "^1.0.3",
"gray-matter": "^4.0.2",
"lru-cache": "^5.1.1",
"markdown-it": "^10.0.0",
"markdown-it-anchor": "^5.2.7",
"markdown-it-container": "^2.0.0",
"markdown-it-emoji": "^1.4.0",
"markdown-it-table-of-contents": "^0.4.4",
"prismjs": "^1.20.0",
"vite": "^0.6.0"
},
"devDependencies": {
"@types/lru-cache": "^5.1.0",
"@types/markdown-it": "^10.0.1",
"@types/node": "^13.13.4",
"typescript": "^3.8.3"
}

@ -0,0 +1,105 @@
import MarkdownIt from 'markdown-it'
import { RuleBlock } from 'markdown-it/lib/parser_block'
// Replacing the default htmlBlock rule to allow using custom components at
// root level
const blockNames: string[] = require('markdown-it/lib/common/html_blocks')
const HTML_OPEN_CLOSE_TAG_RE: RegExp = require('markdown-it/lib/common/html_re')
.HTML_OPEN_CLOSE_TAG_RE
// An array of opening and corresponding closing sequences for html tags,
// last argument defines whether it can terminate a paragraph or not
const HTML_SEQUENCES: [RegExp, RegExp, boolean][] = [
[/^<(script|pre|style)(?=(\s|>|$))/i, /<\/(script|pre|style)>/i, true],
[/^<!--/, /-->/, true],
[/^<\?/, /\?>/, true],
[/^<![A-Z]/, />/, true],
[/^<!\[CDATA\[/, /\]\]>/, true],
// PascalCase Components
[/^<[A-Z]/, />/, true],
// custom elements with hyphens
[/^<\w+\-/, />/, true],
[
new RegExp('^</?(' + blockNames.join('|') + ')(?=(\\s|/?>|$))', 'i'),
/^$/,
true
],
[new RegExp(HTML_OPEN_CLOSE_TAG_RE.source + '\\s*$'), /^$/, false]
]
export const componentPlugin = (md: MarkdownIt) => {
md.block.ruler.at('html_block', htmlBlock)
}
const htmlBlock: RuleBlock = (
state,
startLine,
endLine,
silent
): boolean => {
let i, nextLine, lineText
let pos = state.bMarks[startLine] + state.tShift[startLine]
let max = state.eMarks[startLine]
// if it's indented more than 3 spaces, it should be a code block
if (state.sCount[startLine] - state.blkIndent >= 4) {
return false
}
if (!state.md.options.html) {
return false
}
if (state.src.charCodeAt(pos) !== 0x3c /* < */) {
return false
}
lineText = state.src.slice(pos, max)
for (i = 0; i < HTML_SEQUENCES.length; i++) {
if (HTML_SEQUENCES[i][0].test(lineText)) {
break
}
}
if (i === HTML_SEQUENCES.length) {
return false
}
if (silent) {
// true if this sequence can be a terminator, false otherwise
return HTML_SEQUENCES[i][2]
}
nextLine = startLine + 1
// If we are here - we detected HTML block.
// Let's roll down till block end.
if (!HTML_SEQUENCES[i][1].test(lineText)) {
for (; nextLine < endLine; nextLine++) {
if (state.sCount[nextLine] < state.blkIndent) {
break
}
pos = state.bMarks[nextLine] + state.tShift[nextLine]
max = state.eMarks[nextLine]
lineText = state.src.slice(pos, max)
if (HTML_SEQUENCES[i][1].test(lineText)) {
if (lineText.length !== 0) {
nextLine++
}
break
}
}
}
state.line = nextLine
const token = state.push('html_block', '', 0)
token.map = [startLine, nextLine]
token.content = state.getLines(startLine, nextLine, state.blkIndent, true)
return true
}

@ -0,0 +1,43 @@
import MarkdownIt from 'markdown-it'
import Token from 'markdown-it/lib/token'
const container = require('markdown-it-container')
export const containerPlugin = (md: MarkdownIt) => {
md.use(...createContainer('tip', 'TIP'))
.use(...createContainer('warning', 'WARNING'))
.use(...createContainer('danger', 'WARNING'))
// explicitly escape Vue syntax
.use(container, 'v-pre', {
render: (tokens: Token[], idx: number) =>
tokens[idx].nesting === 1 ? `<div v-pre>\n` : `</div>\n`
})
}
type ContainerArgs = [
typeof container,
string,
{
render(tokens: Token[], idx: number): string
}
]
function createContainer(klass: string, defaultTitle: string): ContainerArgs {
return [
container,
klass,
{
render(tokens, idx) {
const token = tokens[idx]
const info = token.info.trim().slice(klass.length).trim()
if (token.nesting === 1) {
return `<div class="${klass} custom-block"><p class="custom-block-title">${
info || defaultTitle
}</p>\n`
} else {
return `</div>\n`
}
}
}
]
}

@ -0,0 +1,50 @@
const chalk = require('chalk')
const prism = require('prismjs')
const loadLanguages = require('prismjs/components/index')
const escapeHtml = require('escape-html')
// required to make embedded highlighting work...
loadLanguages(['markup', 'css', 'javascript'])
function wrap(code: string, lang: string): string {
if (lang === 'text') {
code = escapeHtml(code)
}
return `<pre v-pre class="language-${lang}"><code>${code}</code></pre>`
}
export const highlight = (str: string, lang: string) => {
if (!lang) {
return wrap(str, 'text')
}
lang = lang.toLowerCase()
const rawLang = lang
if (lang === 'vue' || lang === 'html') {
lang = 'markup'
}
if (lang === 'md') {
lang = 'markdown'
}
if (lang === 'ts') {
lang = 'typescript'
}
if (lang === 'py') {
lang = 'python'
}
if (!prism.languages[lang]) {
try {
loadLanguages([lang])
} catch (e) {
console.warn(
chalk.yellow(
`[vuepress] Syntax highlight for language "${lang}" is not supported.`
)
)
}
}
if (prism.languages[lang]) {
const code = prism.highlight(str, prism.languages[lang], lang)
return wrap(code, rawLang)
}
return wrap(str, 'text')
}

@ -0,0 +1,50 @@
// Modified from https://github.com/egoist/markdown-it-highlight-lines
import MarkdownIt from 'markdown-it'
const RE = /{([\d,-]+)}/
const wrapperRE = /^<pre .*?><code>/
export const highlightLinePlugin = (md: MarkdownIt) => {
const fence = md.renderer.rules.fence!
md.renderer.rules.fence = (...args) => {
const [tokens, idx, options] = args
const token = tokens[idx]
const rawInfo = token.info
if (!rawInfo || !RE.test(rawInfo)) {
return fence(...args)
}
const langName = rawInfo.replace(RE, '').trim()
// ensure the next plugin get the correct lang.
token.info = langName
const lineNumbers = RE.exec(rawInfo)![1]
.split(',')
.map(v => v.split('-').map(v => parseInt(v, 10)))
const code = options.highlight
? options.highlight(token.content, langName)
: token.content
const rawCode = code.replace(wrapperRE, '')
const highlightLinesCode = rawCode.split('\n').map((split, index) => {
const lineNumber = index + 1
const inRange = lineNumbers.some(([start, end]) => {
if (start && end) {
return lineNumber >= start && lineNumber <= end
}
return lineNumber === start
})
if (inRange) {
return `<div class="highlighted">&nbsp;</div>`
}
return '<br>'
}).join('')
const highlightLinesWrapperCode =
`<div class="highlight-lines">${highlightLinesCode}</div>`
return highlightLinesWrapperCode + code
}
}

@ -0,0 +1,16 @@
import MarkdownIt from 'markdown-it'
export const hoistPlugin = (md: MarkdownIt & { __data: any }) => {
const RE = /^<(script|style)(?=(\s|>|$))/i
md.renderer.rules.html_block = (tokens, idx) => {
const content = tokens[idx].content
const hoistedTags = md.__data.hoistedTags || (md.__data.hoistedTags = [])
if (RE.test(content.trim())) {
hoistedTags.push(content)
return ''
} else {
return content
}
}
}

@ -0,0 +1,112 @@
import MarkdownIt from 'markdown-it'
import { parseHeaders } from '../utils/parseHeaders'
import { highlight } from './highlight'
import { slugify } from './slugify'
import { highlightLinePlugin } from './highlightLines'
import { lineNumberPlugin } from './lineNumbers'
import { componentPlugin } from './component'
import { containerPlugin } from './containers'
import { snippetPlugin } from './snippet'
import { hoistPlugin } from './hoist'
import { preWrapperPlugin } from './preWrapper'
import { linkPlugin } from './link'
const emoji = require('markdown-it-emoji')
const anchor = require('markdown-it-anchor')
const toc = require('markdown-it-table-of-contents')
export interface MarkdownOpitons extends MarkdownIt.Options {
lineNumbers?: boolean
config?: (md: MarkdownIt) => void
anchor?: {
permalink?: boolean
permalinkBefore?: boolean
permalinkSymbol?: string
}
// https://github.com/Oktavilla/markdown-it-table-of-contents
toc?: any
externalLinks?: Record<string, string>
}
export interface MarkdownRenderer {
__data?: any
render: (src: string, env?: any) => { html: string; data: any }
}
export const createMarkdownRenderer = (
options: MarkdownOpitons = {}
): MarkdownRenderer => {
const md = MarkdownIt({
html: true,
highlight,
...options
})
// custom plugins
md
.use(componentPlugin)
.use(highlightLinePlugin)
.use(preWrapperPlugin)
.use(snippetPlugin)
.use(hoistPlugin)
.use(containerPlugin)
.use(linkPlugin, {
target: '_blank',
rel: 'noopener noreferrer',
...options.externalLinks
})
// 3rd party plugins
.use(emoji)
.use(
anchor,
Object.assign(
{
slugify,
permalink: true,
permalinkBefore: true,
permalinkSymbol: '#'
},
options.anchor
)
)
.use(
toc,
Object.assign(
{
slugify,
includeLevel: [2, 3],
format: parseHeaders
},
options.toc
)
)
// apply user config
if (options.config) {
options.config(md)
}
if (options.lineNumbers) {
md.use(lineNumberPlugin)
}
dataReturnable(md)
return md as any
}
export const dataReturnable = (md: MarkdownIt) => {
// override render to allow custom plugins return data
const render = md.render
const wrappedRender: MarkdownRenderer['render'] = (src) => {
(md as any).__data = {}
const html = render.call(md, src)
return {
html,
data: (md as any).__data
}
}
;(md as any).render = wrappedRender
}

@ -0,0 +1,28 @@
// markdown-it plugin for generating line numbers.
// It depends on preWrapper plugin.
import MarkdownIt from 'markdown-it'
export const lineNumberPlugin = (md: MarkdownIt) => {
const fence = md.renderer.rules.fence!
md.renderer.rules.fence = (...args) => {
const rawCode = fence(...args)
const code = rawCode.slice(
rawCode.indexOf('<code>'),
rawCode.indexOf('</code>')
)
const lines = code.split('\n')
const lineNumbersCode = [...Array(lines.length - 1)]
.map((line, index) => `<span class="line-number">${index + 1}</span><br>`)
.join('')
const lineNumbersWrapperCode = `<div class="line-numbers-wrapper">${lineNumbersCode}</div>`
const finalCode = rawCode
.replace('<!--beforeend-->', `${lineNumbersWrapperCode}<!--beforeend-->`)
.replace('extra-class', 'line-numbers-mode')
return finalCode
}
}

@ -0,0 +1,64 @@
// markdown-it plugin for:
// 1. adding target="_blank" to external links
// 2. normalize internal links to end with `.html`
import MarkdownIt from 'markdown-it'
const indexRE = /(^|.*\/)(index|readme).md(#?.*)$/i
export const linkPlugin = (
md: MarkdownIt,
externalAttrs: Record<string, string>
) => {
md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
const token = tokens[idx]
const hrefIndex = token.attrIndex('href')
if (hrefIndex >= 0) {
const hrefAttr = token.attrs![hrefIndex]
const url = hrefAttr[1]
const isExternal = /^https?:/.test(url)
const isSourceLink = /(\/|\.md|\.html)(#.*)?$/.test(url)
if (isExternal) {
Object.entries(externalAttrs).forEach(([key, val]) => {
token.attrSet(key, val)
})
} else if (isSourceLink) {
normalizeHref(hrefAttr)
}
}
return self.renderToken(tokens, idx, options)
}
function normalizeHref(hrefAttr: [string, string]) {
const data = (md as any).__data
let url = hrefAttr[1]
// convert link to filename and export it for existence check
const links = data.links || (data.links = [])
links.push(url)
const indexMatch = url.match(indexRE)
if (indexMatch) {
const [, path, , hash] = indexMatch
url = path + hash
} else {
url = url.replace(/\.md$/, '.html').replace(/\.md(#.*)$/, '.html$1')
}
// relative path usage.
if (!url.startsWith('/')) {
url = ensureBeginningDotSlash(url)
}
// markdown-it encodes the uri
hrefAttr[1] = decodeURI(url)
}
}
const beginningSlashRE = /^\.\//
const ensureBeginningDotSlash = (path: string) => {
if (beginningSlashRE.test(path)) {
return path
}
return './' + path
}

@ -0,0 +1,23 @@
// markdown-it plugin for wrapping <pre> ... </pre>.
//
// If your plugin was chained before preWrapper, you can add additional eleemnt directly.
// If your plugin was chained after preWrapper, you can use these slots:
// 1. <!--beforebegin-->
// 2. <!--afterbegin-->
// 3. <!--beforeend-->
// 4. <!--afterend-->
import MarkdownIt from 'markdown-it'
export const preWrapperPlugin = (md: MarkdownIt) => {
const fence = md.renderer.rules.fence!
md.renderer.rules.fence = (...args) => {
const [tokens, idx] = args
const token = tokens[idx]
const rawCode = fence(...args)
return (
`<!--beforebegin--><div class="language-${token.info.trim()} extra-class">` +
`<!--afterbegin-->${rawCode}<!--beforeend--></div><!--afterend-->`
)
}
}

@ -0,0 +1,22 @@
// string.js slugify drops non ascii chars so we have to
// use a custom implementation here
const removeDiacritics = require('diacritics').remove
// eslint-disable-next-line no-control-regex
const rControl = /[\u0000-\u001f]/g
const rSpecial = /[\s~`!@#$%^&*()\-_+=[\]{}|\\;:"'<>,.?/]+/g
export const slugify = (str: string): string => {
return removeDiacritics(str)
// Remove control characters
.replace(rControl, '')
// Replace special characters
.replace(rSpecial, '-')
// Remove continous separators
.replace(/\-{2,}/g, '-')
// Remove prefixing and trailing separtors
.replace(/^\-+|\-+$/g, '')
// ensure it doesn't start with a number (#121)
.replace(/^(\d)/, '_$1')
// lowercase
.toLowerCase()
}

@ -0,0 +1,46 @@
import fs from 'fs'
import MarkdownIt from 'markdown-it'
import { RuleBlock } from 'markdown-it/lib/parser_block'
export const snippetPlugin = (md: MarkdownIt, root: string) => {
const parser: RuleBlock = (state, startLine, endLine, silent) => {
const CH = '<'.charCodeAt(0)
const pos = state.bMarks[startLine] + state.tShift[startLine]
const max = state.eMarks[startLine]
// if it's indented more than 3 spaces, it should be a code block
if (state.sCount[startLine] - state.blkIndent >= 4) {
return false
}
for (let i = 0; i < 3; ++i) {
const ch = state.src.charCodeAt(pos + i)
if (ch !== CH || pos + i >= max) return false
}
if (silent) {
return true
}
const start = pos + 3
const end = state.skipSpacesBack(max, pos)
const rawPath = state.src.slice(start, end).trim().replace(/^@/, root)
const filename = rawPath.split(/{/).shift()!.trim()
const content = fs.existsSync(filename)
? fs.readFileSync(filename).toString()
: 'Not found: ' + filename
const meta = rawPath.replace(filename, '')
state.line = startLine + 1
const token = state.push('fence', 'code', 0)
token.info = filename.split('.').pop() + meta
token.content = content
token.markup = '```'
token.map = [startLine, startLine + 1]
return true
}
md.block.ruler.before('fence', 'snippet', parser)
}

@ -0,0 +1,31 @@
import path from 'path'
import { createMarkdownRenderer, MarkdownOpitons } from './markdown/index'
import LRUCache from 'lru-cache'
const debug = require('debug')('vitepress:md')
const cache = new LRUCache<string, string>({ max: 1024 })
const matter = require('gray-matter')
export function createMarkdownFn(root: string, options: MarkdownOpitons = {}) {
const md = createMarkdownRenderer(options)
return (src: string, file: string) => {
file = path.relative(root, file)
const cached = cache.get(src)
if (cached) {
debug(`[cache hit] ${file}`)
return cached
}
const start = Date.now()
const { content, data } = matter(src)
const { html } = md.render(content)
// TODO make use of data
const vueSrc = `<template>${html}</template>`
debug(`[render] ${file} in ${Date.now() - start}ms.`, data)
cache.set(src, vueSrc)
return vueSrc
}
}

@ -0,0 +1,26 @@
import path from 'path'
import { Resolver } from "vite"
// built ts files are placed into /dist
export const APP_PATH = path.join(__dirname, '../lib/app')
// TODO detect user configured theme
export const THEME_PATH = path.join(__dirname, '../lib/theme-default')
export const VitePressResolver: Resolver = {
publicToFile(publicPath) {
if (publicPath.startsWith('/@app')) {
return path.join(APP_PATH, publicPath.replace(/^\/@app\/?/, ''))
}
if (publicPath.startsWith('/@theme')) {
return path.join(THEME_PATH, publicPath.replace(/^\/@theme\/?/, ''))
}
},
fileToPublic(filePath) {
if (filePath.startsWith(APP_PATH)) {
return `/@app/${path.relative(APP_PATH, filePath)}`
}
if (filePath.startsWith(THEME_PATH)) {
return `/@theme/${path.relative(THEME_PATH, filePath)}`
}
}
}

@ -0,0 +1,60 @@
import path from 'path'
import { createServer as createViteServer, cachedRead, Plugin } from 'vite'
import { createMarkdownFn } from './markdownToVue'
import { VitePressResolver, THEME_PATH, APP_PATH } from './resolver'
const debug = require('debug')('vitepress')
const VitePressPlugin: Plugin = ({ app, root, watcher, resolver }) => {
const markdownToVue = createMarkdownFn(root)
// watch theme files if it's outside of project root
if (path.relative(root, THEME_PATH).startsWith('..')) {
debug(`watching theme dir outside of project root: ${THEME_PATH}`)
watcher.add(THEME_PATH)
}
// hot reload .md files as .vue files
watcher.on('change', async (file) => {
if (file.endsWith('.md')) {
const content = await cachedRead(null, file)
watcher.handleVueReload(file, Date.now(), markdownToVue(content, file))
}
})
app.use(async (ctx, next) => {
if (ctx.path.endsWith('.md')) {
const file = resolver.publicToFile(ctx.path)
await cachedRead(ctx, file)
// let vite know this is supposed to be treated as vue file
ctx.vue = true
ctx.body = markdownToVue(ctx.body, file)
debug(`serving ${ctx.url}`)
return next()
}
// detect and serve vitepress files
const file = VitePressResolver.publicToFile(ctx.path, root)
if (file) {
ctx.type = path.extname(file)
await cachedRead(ctx, file)
debug(`serving file: ${ctx.url}`)
return next()
}
await next()
// serve our index.html after vite history fallback
if (ctx.url === '/index.html') {
await cachedRead(ctx, path.join(APP_PATH, 'index-dev.html'))
}
})
}
export function createServer() {
return createViteServer({
plugins: [VitePressPlugin],
resolvers: [VitePressResolver]
})
}

@ -0,0 +1,62 @@
// Since VuePress needs to extract the header from the markdown source
// file and display it in the sidebar or title (#238), this file simply
// removes some unnecessary elements to make header displays well at
// sidebar or title.
//
// But header's parsing in the markdown content is done by the markdown
// loader based on markdown-it. markdown-it parser will will always keep
// HTML in headers, so in VuePress, after being parsed by the markdiwn
// loader, the raw HTML in headers will finally be parsed by Vue-loader.
// so that we can write HTML/Vue in the header. One exception is the HTML
// wrapped by <code>(markdown token: '`') tag.
const parseEmojis = (str: string) => {
const emojiData = require('markdown-it-emoji/lib/data/full.json')
return String(str).replace(/:(.+?):/g, (placeholder, key) => emojiData[key] || placeholder)
}
const unescapeHtml = (html: string) => String(html)
.replace(/&quot;/g, '"')
.replace(/&#39;/g, '\'')
.replace(/&#x3A;/g, ':')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
const removeMarkdownTokens = (str: string) => String(str)
.replace(/\[(.*)\]\(.*\)/, '$1') // []()
.replace(/(`|\*{1,3}|_)(.*?[^\\])\1/g, '$2') // `{t}` | *{t}* | **{t}** | ***{t}*** | _{t}_
.replace(/(\\)(\*|_|`|\!)/g, '$2') // remove escape char '\'
const trim = (str: string) => str.trim()
// This method remove the raw HTML but reserve the HTML wrapped by `<code>`.
// e.g.
// Input: "<a> b", Output: "b"
// Input: "`<a>` b", Output: "`<a>` b"
export const removeNonCodeWrappedHTML = (str: string) => {
return String(str).replace(/(^|[^><`])<.*>([^><`]|$)/g, '$1$2')
}
const compose = (...processors: ((str: string) => string)[]) => {
if (processors.length === 0) return (input: string) => input
if (processors.length === 1) return processors[0]
return processors.reduce((prev, next) => {
return (str) => next(prev(str))
})
}
// Unescape html, parse emojis and remove some md tokens.
export const parseHeaders = compose(
unescapeHtml,
parseEmojis,
removeMarkdownTokens,
trim
)
// Also clean the html that isn't wrapped by code.
// Because we want to support using VUE components in headers.
// e.g. https://vuepress.vuejs.org/guide/using-vue.html#badge
export const deeplyParseHeaders = compose(
removeNonCodeWrappedHTML,
parseHeaders
)

@ -17,5 +17,5 @@
"removeComments": false,
"preserveSymlinks": true
},
"include": ["lib"]
"include": ["src"]
}

@ -167,6 +167,29 @@
"@types/koa-compose" "*"
"@types/node" "*"
"@types/linkify-it@*":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-2.1.0.tgz#ea3dd64c4805597311790b61e872cbd1ed2cd806"
integrity sha512-Q7DYAOi9O/+cLLhdaSvKdaumWyHbm7HAk/bFwwyTuU0arR5yyCeW5GOoqt4tJTpDRxhpx9Q8kQL6vMpuw9hDSw==
"@types/lru-cache@^5.1.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.0.tgz#57f228f2b80c046b4a1bd5cac031f81f207f4f03"
integrity sha512-RaE0B+14ToE4l6UqdarKPnXwVDuigfFv+5j9Dze/Nqr23yyuqdNvzcZi3xB+3Agvi5R4EOgAksfv3lXX4vBt9w==
"@types/markdown-it@^10.0.1":
version "10.0.1"
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-10.0.1.tgz#94e252ab689c8e9ceb9aff2946e0a458390105eb"
integrity sha512-L1ibTdA5IUe/cRBlf3N3syAOBQSN1WCMGtAWir6mKxibiRl4LmpZM4jLz+7zAqiMnhQuAP1sqZOF9wXgn2kpEg==
dependencies:
"@types/linkify-it" "*"
"@types/mdurl" "*"
"@types/mdurl@*":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9"
integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==
"@types/mime@*":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d"
@ -471,6 +494,15 @@ chokidar@^3.3.1:
optionalDependencies:
fsevents "~2.1.2"
clipboard@^2.0.0:
version "2.0.6"
resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.6.tgz#52921296eec0fdf77ead1749421b21c968647376"
integrity sha512-g5zbiixBRk/wyKakSwCKd7vQXDjFnAMGHoEyBogG/bw9kTD9GvdAvaoRR1ALcEzt3pVKxZR0pViekPMIS0QyGg==
dependencies:
good-listener "^1.2.2"
select "^1.1.2"
tiny-emitter "^2.0.0"
co@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
@ -755,6 +787,11 @@ define-properties@^1.1.2, define-properties@^1.1.3:
dependencies:
object-keys "^1.0.12"
delegate@^3.1.2:
version "3.2.0"
resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166"
integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==
delegates@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
@ -775,6 +812,11 @@ destroy@^1.0.4:
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
diacritics@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/diacritics/-/diacritics-1.3.0.tgz#3efa87323ebb863e6696cebb0082d48ff3d6f7a1"
integrity sha1-PvqHMj67hj5mls67AILUj/PW96E=
dom-serializer@0:
version "0.2.2"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
@ -828,7 +870,7 @@ encodeurl@^1.0.2:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
entities@^2.0.0:
entities@^2.0.0, entities@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4"
integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==
@ -911,6 +953,13 @@ etag@^1.3.0:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
extend-shallow@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=
dependencies:
is-extendable "^0.1.0"
fastparse@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9"
@ -959,6 +1008,23 @@ glob-parent@~5.1.0:
dependencies:
is-glob "^4.0.1"
good-listener@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
integrity sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=
dependencies:
delegate "^3.1.2"
gray-matter@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-4.0.2.tgz#9aa379e3acaf421193fce7d2a28cebd4518ac454"
integrity sha512-7hB/+LxrOjq/dd8APlK0r24uL/67w7SkYnfwhNFwg/VDIGWGmduTDYf3WNstLW2fbbmRwrDGCVSJ2isuf2+4Hw==
dependencies:
js-yaml "^3.11.0"
kind-of "^6.0.2"
section-matter "^1.0.0"
strip-bom-string "^1.0.0"
has-ansi@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
@ -1124,6 +1190,11 @@ is-directory@^0.3.1:
resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1"
integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=
is-extendable@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
@ -1195,7 +1266,7 @@ js-tokens@^4.0.0:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
js-yaml@^3.13.1:
js-yaml@^3.11.0, js-yaml@^3.13.1:
version "3.13.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==
@ -1227,6 +1298,11 @@ keygrip@~1.1.0:
dependencies:
tsscmp "1.0.6"
kind-of@^6.0.0, kind-of@^6.0.2:
version "6.0.3"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
koa-compose@^3.0.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7"
@ -1308,6 +1384,13 @@ koa@^2.11.0:
type-is "^1.6.16"
vary "^1.1.2"
linkify-it@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.2.0.tgz#e3b54697e78bf915c70a38acd78fd09e0058b1cf"
integrity sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==
dependencies:
uc.micro "^1.0.1"
loader-utils@^1.1.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
@ -1359,6 +1442,37 @@ magic-string@^0.25.5, magic-string@^0.25.7:
dependencies:
sourcemap-codec "^1.4.4"
markdown-it-anchor@^5.2.7:
version "5.2.7"
resolved "https://registry.yarnpkg.com/markdown-it-anchor/-/markdown-it-anchor-5.2.7.tgz#ec740f6bd03258a582cd0c65b9644b9f9852e5a3"
integrity sha512-REFmIaSS6szaD1bye80DMbp7ePwsPNvLTR5HunsUcZ0SG0rWJQ+Pz24R4UlTKtjKBPhxo0v0tOBDYjZQQknW8Q==
markdown-it-container@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/markdown-it-container/-/markdown-it-container-2.0.0.tgz#0019b43fd02eefece2f1960a2895fba81a404695"
integrity sha1-ABm0P9Au7+zi8ZYKKJX7qBpARpU=
markdown-it-emoji@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz#9bee0e9a990a963ba96df6980c4fddb05dfb4dcc"
integrity sha1-m+4OmpkKljupbfaYDE/dsF37Tcw=
markdown-it-table-of-contents@^0.4.4:
version "0.4.4"
resolved "https://registry.yarnpkg.com/markdown-it-table-of-contents/-/markdown-it-table-of-contents-0.4.4.tgz#3dc7ce8b8fc17e5981c77cc398d1782319f37fbc"
integrity sha512-TAIHTHPwa9+ltKvKPWulm/beozQU41Ab+FIefRaQV1NRnpzwcV9QOe6wXQS5WLivm5Q/nlo0rl6laGkMDZE7Gw==
markdown-it@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-10.0.0.tgz#abfc64f141b1722d663402044e43927f1f50a8dc"
integrity sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==
dependencies:
argparse "^1.0.7"
entities "~2.0.0"
linkify-it "^2.0.0"
mdurl "^1.0.1"
uc.micro "^1.0.5"
mdn-data@2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b"
@ -1369,6 +1483,11 @@ mdn-data@2.0.6:
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978"
integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA==
mdurl@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@ -1922,6 +2041,13 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.27:
source-map "^0.6.1"
supports-color "^6.1.0"
prismjs@^1.20.0:
version "1.20.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.20.0.tgz#9b685fc480a3514ee7198eac6a3bf5024319ff03"
integrity sha512-AEDjSrVNkynnw6A+B1DsFkd6AVdTnp+/WoUixFRULlCLZVRZlVQMVWio/16jv7G1FscUxQxOQhWwApgbnxr6kQ==
optionalDependencies:
clipboard "^2.0.0"
q@^1.1.2:
version "1.5.1"
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
@ -2050,6 +2176,19 @@ sax@~1.2.4:
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
section-matter@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167"
integrity sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==
dependencies:
extend-shallow "^2.0.1"
kind-of "^6.0.0"
select@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=
serialize-javascript@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61"
@ -2161,6 +2300,11 @@ strip-ansi@^3.0.0:
dependencies:
ansi-regex "^2.0.0"
strip-bom-string@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92"
integrity sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI=
stylehacks@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5"
@ -2250,6 +2394,11 @@ timsort@^0.3.0:
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
tiny-emitter@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
to-fast-properties@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
@ -2285,6 +2434,11 @@ typescript@^3.8.3:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061"
integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==
uc.micro@^1.0.1, uc.micro@^1.0.5:
version "1.0.6"
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
unicode-canonical-property-names-ecmascript@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"

Loading…
Cancel
Save