Используйте markdown-it для разбора кода уценки (читайте VuePress 3)

внешний интерфейс исходный код Vue.js Markdown

предисловие

в этой серии статейпервый раз, мы рассмотрели, как 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
  }
]

После токенизации получаем токен:

ast

Мы также можем вручную выполнить следующий код, чтобы получить тот же результат:

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.

Вход

исходный кодВ основном делать следующие пять вещей:

  1. Используйте плагины сообщества, такие как распознавание эмодзи, якорь и т. д.
  2. Используйте пользовательский плагин, подробно описанный ниже.
  3. Используйте markdown-it-chain для поддержки связанных вызовов markdown-it, аналогично тому, что я сделал ввторая статьяУпомянутый webpack-chain.
  4. В хуки beforeInstantiate и afterInstantiate можно передавать параметры, чтобы было удобно выставлять экземпляр markdown-it наружу.
  5. Пользовательский рендеринг 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. Информация За этой строкой следуют три обратных кавычки.

  2. вложенное свойство:

  • 1означает, что вкладка открыта.
  • 0означает, что вкладка автоматически закрывается.
  • -1означает, что вкладка закрывается.

выделить код

исходный код

  1. с помощьюprismjsэта библиотека
  2. Думайте о vue и html как об одном языке:
if (lang === 'vue' || lang === 'html') {
	lang = 'markup'
}
  1. Совместимость с языковыми аббревиатурами, такими как md, ts, py
  2. Используйте функцию переноса, чтобы снова обернуть сгенерированный выделенный код:
function wrap (code, lang) {
  if (lang === 'text') {
    code = escapeHtml(code)
  }
  return `<pre v-pre class="language-${lang}"><code>${code}</code></pre>`
}

Выделите строки кода

исходный код

  1. существуетчужой кодмодифицируется на базе.
  2. Переопределите метод 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">&nbsp;</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 делает различие между этими двумя типами ссылок, и, наконец, внешняя ссылка будет отображать значок больше, чем внутренняя ссылка:

link.png

Для этого 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 больше очарования в соответствии с их собственным воображением.