предисловие
в этой серии статейпервый раз, мы рассмотрели, как Vuepress позволяет Markdown поддерживать компоненты Vue, но не упомянули, как анализируются другие части компонентов, отличных от Vue.
Сегодня давайте посмотрим, как Vuepress использует markdown-it для разбора кода уценки.
Введение в уценку-это
markdown — это библиотека, которая помогает анализировать уценку, что можно сделать из# test
прибыть<h1>test</h1>
преобразование.
Он поддерживает как среду браузера, так и среду Node, и по сути похож на babel, за исключением того, что babel анализирует JavaScript.
Говоря о синтаксическом анализе, уценке — он официально далОнлайн-пример, что позволяет интуитивно получить результат уценки после парсинга. Например, возьмите# test
Например, вы получите следующие результаты:
[
{
"type": "heading_open",
"tag": "h1",
"attrs": null,
"map": [
0,
1
],
"nesting": 1,
"level": 0,
"children": null,
"content": "",
"markup": "#",
"info": "",
"meta": null,
"block": true,
"hidden": false
},
{
"type": "inline",
"tag": "",
"attrs": null,
"map": [
0,
1
],
"nesting": 0,
"level": 1,
"children": [
{
"type": "text",
"tag": "",
"attrs": null,
"map": null,
"nesting": 0,
"level": 0,
"children": null,
"content": "test",
"markup": "",
"info": "",
"meta": null,
"block": false,
"hidden": false
}
],
"content": "test",
"markup": "",
"info": "",
"meta": null,
"block": true,
"hidden": false
},
{
"type": "heading_close",
"tag": "h1",
"attrs": null,
"map": null,
"nesting": -1,
"level": 0,
"children": null,
"content": "",
"markup": "#",
"info": "",
"meta": null,
"block": true,
"hidden": false
}
]
После токенизации получаем токен:
Мы также можем вручную выполнить следующий код, чтобы получить тот же результат:
const md = new MarkdownIt()
let tokens = md.parse('# test')
console.log(tokens)
Введение в основной API
модель
уценка-предусматривает три режима:commonmark, по умолчанию, ноль. соответствует самым строгим,GFM, самый расслабленный режим разбора.
Разобрать
Правила парсинга уценки условно делятся на два типа: блочные и встроенные. В частности, он может быть реализован какMarkdownIt.block
В соответствии с правилами блокировки синтаксического анализаParserBlock,MarkdownIt.inline
Соответствует разбору встроенных правилParserInline,MarkdownIt.renderer.render
а такжеMarkdownIt.renderer.renderInline
Генерировать HTML-код в соответствии с блочными и встроенными правилами соответственно.
правило
существуетMarkdownIt.renderer
Существует специальное свойство: rules, представляющее правила рендеринга токенов, которые могут быть обновлены или расширены пользователем:
var md = require('markdown-it')();
md.renderer.rules.strong_open = function () { return '<b>'; };
md.renderer.rules.strong_close = function () { return '</b>'; };
var result = md.renderInline(...);
Например, этот код обновляет правила рендеринга токенов strong_open и strong_close.
Система плагинов
уценка-он официально сказал:
We do a markdown parser. It should keep the "markdown spirit". Other things should be kept separate, in plugins, for example. We have no clear criteria, sorry. Probably, you will find CommonMark forum a useful read to understand us better.
Одним словом, уценка — это только чистый уценочный парсинг, если хотите больше функций, то вам придется самим писать плагины.
Итак, они предоставляют API: MarkdownIt.use
Он может загрузить указанный плагин в текущий экземпляр парсера:
var iterator = require('markdown-it-for-inline');
var md = require('markdown-it')()
.use(iterator, 'foo_replace', 'text', function (tokens, idx) {
tokens[idx].content = tokens[idx].content.replace(/foo/g, 'bar');
});
Этот пример кода заменяет все foo в коде уценки на bar.
Больше информации
Вы можете посетить переводы во время китайского фестивалякитайский документ, или официальныйДокументация API.
Приложение в vuepress
Vuepress полагается на множество подключаемых модулей сообщества markdown-it, таких как выделение кода, перенос блоков кода, смайлики и т. д., а также сам написал множество подключаемых модулей markdown-it, таких как идентификация компонентов vue, различение рендеринга между внутренними внешние цепи и т.д.
Эта статья была написана во время Национального дня в 2018 году, и соответствующая версия кода vuepress — v1.0.0-alpha.4.
Вход
исходный кодВ основном делать следующие пять вещей:
- Используйте плагины сообщества, такие как распознавание эмодзи, якорь и т. д.
- Используйте пользовательский плагин, подробно описанный ниже.
- Используйте markdown-it-chain для поддержки связанных вызовов markdown-it, аналогично тому, что я сделал ввторая статьяУпомянутый webpack-chain.
- В хуки beforeInstantiate и afterInstantiate можно передавать параметры, чтобы было удобно выставлять экземпляр markdown-it наружу.
- Пользовательский рендеринг dataReturnable:
module.exports.dataReturnable = function dataReturnable (md) {
// override render to allow custom plugins return data
const render = md.render
md.render = (...args) => {
md.__data = {}
const html = render.call(md, ...args)
return {
html,
data: md.__data
}
}
}
Это эквивалентно созданию глобальной переменной __data для хранения данных, используемых каждым плагином.
Определить компоненты vue
Просто сделал одну вещь: заменил правила htmlBlock по умолчанию, чтобы вы могли использовать пользовательские компоненты vue на корневом уровне.
module.exports = md => {
md.block.ruler.at('html_block', htmlBlock)
}
Эта функция htmlBlock и нативная уценка — этоhtml_blockВ чем ключевое отличие?
Ответ заключается в добавлении двух элементов в обычный массив HTML_SEQUENCES:
// PascalCase Components
[/^<[A-Z]/, />/, true],
// custom elements with hyphens
[/^<\w+\-/, />/, true],
Очевидно, это используется для соответствия написанию на Паскале (например,<Button/>
) и дефисы (например,<button-1/>
) написанного компонента.
блок контента
Этот компонент фактически использует плагин сообщества markdown-it-container и на его основе определяет функции рендеринга четырех блоков контента: подсказка, предупреждение, опасность и v-pre:
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`
}
}
Здесь необходимо объяснить два атрибута токена.
-
Информация За этой строкой следуют три обратных кавычки.
-
вложенное свойство:
-
1
означает, что вкладка открыта. -
0
означает, что вкладка автоматически закрывается. -
-1
означает, что вкладка закрывается.
выделить код
- с помощьюprismjsэта библиотека
- Думайте о vue и html как об одном языке:
if (lang === 'vue' || lang === 'html') {
lang = 'markup'
}
- Совместимость с языковыми аббревиатурами, такими как md, ts, py
- Используйте функцию переноса, чтобы снова обернуть сгенерированный выделенный код:
function wrap (code, lang) {
if (lang === 'text') {
code = escapeHtml(code)
}
return `<pre v-pre class="language-${lang}"><code>${code}</code></pre>`
}
Выделите строки кода
- существуетчужой кодмодифицируется на базе.
- Переопределите метод md.renderer.rules.fence, ключ в том, чтобы использовать обычное суждение, чтобы выделить строки кода:
const RE = /{([\d,-]+)}/
const lineNumbers = RE.exec(rawInfo)[1]
.split(',')
.map(v => v.split('-').map(v => parseInt(v, 10)))
Затем условно визуализируйте:
if (inRange) {
return `<div class="highlighted"> </div>`
}
return '<br>'
Наконец, верните выделенный код строки + обычный код.
повышение скрипта
Переопределите правило md.renderer.rules.html_block:
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
}
}
Сохраните теги стиля и скрипта в псевдоглобальной переменной __data. Эта часть данных будет использоваться в markdownLoader.
номер строки
Перепишите правило md.renderer.rules.fence, рассчитайте количество строк кода по количеству новых строк и снова оберните его:
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
Различие внутренней и внешней цепи
Ссылка может перейти внутрь станции или выйти за ее пределы. Vuepress делает различие между этими двумя типами ссылок, и, наконец, внешняя ссылка будет отображать значок больше, чем внутренняя ссылка:
Для этого vuepress переписывает два правила md.renderer.rules.link_open и md.renderer.rules.link_close.
Сначала взгляните на md.renderer.rules.link_open:
if (isExternal) {
Object.entries(externalAttrs).forEach(([key, val]) => {
token.attrSet(key, val)
})
if (/_blank/i.test(externalAttrs['target'])) {
hasOpenExternalLink = true
}
} else if (isSourceLink) {
hasOpenRouterLink = true
tokens[idx] = toRouterLink(token, link)
}
isExternal - это флаг внешней цепочки. В это время, если он равен true, вы можете напрямую установить атрибут токена. Если isSourceLink равен true, это означает, что передается внутренняя цепочка, и весь токен будет заменен с участиемtoRouterLink(token, link)
:
function toRouterLink (token, link) {
link[0] = 'to'
let to = link[1]
// convert link to filename and export it for existence check
const links = md.__data.links || (md.__data.links = [])
links.push(to)
const indexMatch = to.match(indexRE)
if (indexMatch) {
const [, path, , hash] = indexMatch
to = path + hash
} else {
to = to
.replace(/\.md$/, '.html')
.replace(/\.md(#.*)$/, '.html$1')
}
// relative path usage.
if (!to.startsWith('/')) {
to = ensureBeginningDotSlash(to)
}
// markdown-it encodes the uri
link[1] = decodeURI(to)
// export the router links for testing
const routerLinks = md.__data.routerLinks || (md.__data.routerLinks = [])
routerLinks.push(to)
return Object.assign({}, token, {
tag: 'router-link'
})
}
Сначала href заменяется на to, затем to заменяется действительной ссылкой, оканчивающейся на .html.
Посмотрите еще раз на md.renderer.rules.link_close:
if (hasOpenRouterLink) {
token.tag = 'router-link'
hasOpenRouterLink = false
}
if (hasOpenExternalLink) {
hasOpenExternalLink = false
// add OutBoundLink to the beforeend of this link if it opens in _blank.
return '<OutboundLink/>' + self.renderToken(tokens, idx, options)
}
return self.renderToken(tokens, idx, options)
Очевидно, что внутренняя ссылка отображает метку router-link, а внешняя ссылка отображает метку OutboundLink, которая представляет собой компонент ссылки с добавленным маленьким значком.
упаковка блока кода
Этот плагин переопределяет метод md.renderer.rules.fence для<pre>
Этикетка снова делает перенос:
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-->`
}
Разделите код забора на четыре части: beforebegin, afterbegin, beforeend, afterend. Это эквивалентно предоставлению пользователям крючков для настройки плагина markdown-it.
Привязка обработки символов, отличных от ascii
Этот код изначально был разработан для решения проблемы, связанной с тем, что китайские или специальные символы в якорях не могут правильно переходить.
Обрабатываются не-acsii-символы в следующем порядке: умлаут -> управляющий символ C0 -> специальный символ -> дефис (-), который появляется более 2 раз подряд -> дефис, используемый в качестве начала или конца.
Наконец, подчеркните число в начале и преобразуйте его в нижний регистр.
импорт фрагмента кода
Он добавляет правило фрагмента перед md.block.ruler.fence для синтаксического анализа.<<< @/filepath
такой код:
const start = pos + 3
const end = state.skipSpacesBack(max, pos)
const rawPath = state.src.slice(start, end).trim().replace(/^@/, root)
const filename = rawPath.split(/[{:\s]/).shift()
const content = fs.existsSync(filename) ? fs.readFileSync(filename).toString() : 'Not found: ' + filename
Он возьмет путь к файлу и запишет его корневым путем, а затем прочитает содержимое файла. потому что его также можно разобрать<<< @/test/markdown/fragments/snippet.js{2}
Это происходит с фрагментами кода, выделенными строками, поэтому вам нужно использовать разделение, чтобы перехватить реальное имя файла.
Эпилог
Как интерпретируемый язык, уценка может помочь людям лучше описать вещь. В то же время он действует как мост к HTML, что в конечном итоге приводит к красивым и минималистичным страницам.
Парсер, рендерер и система плагинов, предоставляемые markdown, позволяют разработчикам придавать markdown больше очарования в соответствии с их собственным воображением.