refactor: use markdown title plugin and remove parseHeader utils (#1235)

pull/1238/head
meteorlxy 2 years ago committed by GitHub
parent 71358ebce7
commit 19c0f43daa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,34 +0,0 @@
import { test, expect } from 'vitest'
import { deeplyParseHeader } from 'node/utils/parseHeader'
test('deeplyParseHeader', () => {
const asserts: Record<string, string> = {
// remove tail html
'# `H1` <Comp></Comp>': '# H1',
'# *H1* <Comp/>': '# H1',
// reserve code-wrapped tail html
'# `H1` `<Comp></Comp>`': '# H1 <Comp></Comp>',
'# *H1* `<Comp/>`': '# H1 <Comp/>',
// remove leading html
'# <Comp></Comp> `H1`': '# H1',
'# <Comp/> *H1*': '# H1',
// reserve code-wrapped leading html
'# `<Comp></Comp>` `H1`': '# <Comp></Comp> H1',
'# `<Comp/>` *H1*': '# <Comp/> H1',
// remove middle html
'# `H1` <Comp></Comp> `H2`': '# H1 H2',
'# `H1` <Comp/> `H2`': '# H1 H2',
// reserve middle html
'# `H1` `<Comp></Comp>` `H2`': '# H1 <Comp></Comp> H2',
'# `H1` `<Comp/>` `H2`': '# H1 <Comp/> H2'
}
Object.keys(asserts).forEach((input) => {
expect(deeplyParseHeader(input)).toBe(asserts[input])
})
})

@ -1,39 +0,0 @@
import { describe, test, expect } from 'vitest'
import { parseHeader } from 'node/utils/parseHeader'
describe('parseHeader', () => {
test('should unescape html', () => {
const input = `&lt;div :id=&quot;&#39;app&#39;&quot;&gt;`
expect(parseHeader(input)).toBe(`<div :id="'app'">`)
})
test('should remove markdown tokens correctly', () => {
const asserts: Record<string, string> = {
// vuepress #238
'[vue](vuejs.org)': 'vue',
'`vue`': 'vue',
'*vue*': 'vue',
'**vue**': 'vue',
'***vue***': 'vue',
_vue_: 'vue',
'\\_vue\\_': '_vue_',
'\\*vue\\*': '*vue*',
'\\!vue\\!': '!vue!',
// vuepress #2688
'[vue](vuejs.org) / [vue](vuejs.org)': 'vue / vue',
'[\\<ins>](vuejs.org)': '<ins>',
// vuepress #564 For multiple markdown tokens
'`a` and `b`': 'a and b',
'***bold and italic***': 'bold and italic',
'**bold** and *italic*': 'bold and italic',
// escaping \$
'\\$vue': '$vue'
}
Object.keys(asserts).forEach((input) => {
expect(parseHeader(input)).toBe(asserts[input])
})
})
})

@ -1,57 +0,0 @@
import { test, expect } from 'vitest'
import { removeNonCodeWrappedHTML } from 'node/utils/parseHeader'
test('removeNonCodeWrappedHTML', () => {
const asserts: Record<string, string> = {
// Remove tail html
'# H1 <Comp></Comp>': '# H1 ',
'# H1<Comp></Comp>': '# H1',
'# H1 <Comp a="b"></Comp>': '# H1 ',
'# H1<Comp a="b"></Comp>': '# H1',
'# H1 <Comp/>': '# H1 ',
'# H1<Comp/>': '# H1',
'# H1 <Comp a="b"/>': '# H1 ',
'# H1<Comp a="b"/>': '# H1',
// Reserve code-wrapped tail html
'# H1 `<Comp></Comp>`': '# H1 `<Comp></Comp>`',
'# H1 `<Comp a="b"></Comp>`': '# H1 `<Comp a="b"></Comp>`',
'# H1 `<Comp/>`': '# H1 `<Comp/>`',
'# H1 `<Comp a="b"/>`': '# H1 `<Comp a="b"/>`',
// Remove leading html
'# <Comp></Comp> H1': '# H1',
'# <Comp></Comp>H1': '# H1',
'# <Comp a="b"></Comp> H1': '# H1',
'# <Comp a="b"></Comp>H1': '# H1',
'# <Comp/> H1': '# H1',
'# <Comp/>H1': '# H1',
'# <Comp a="b"/> H1': '# H1',
'# <Comp a="b"/>H1': '# H1',
// Reserve code-wrapped leading html
'# `<Comp></Comp>` H1': '# `<Comp></Comp>` H1',
'# `<Comp a="b"></Comp>` H1': '# `<Comp a="b"></Comp>` H1',
'# `<Comp/>` H1': '# `<Comp/>` H1',
'# `<Comp a="b"/>` H1': '# `<Comp a="b"/>` H1',
// Remove middle html
'# H1 <Comp></Comp> H2': '# H1 H2',
'# H1 <Comp a="b"></Comp> H2': '# H1 H2',
'# H1 <Comp/> H2': '# H1 H2',
'# H1 <Comp a="b"/> H2': '# H1 H2',
// Reserve code-wrapped middle html
'# H1 `<Comp></Comp>` H2': '# H1 `<Comp></Comp>` H2',
'# H1 `<Comp a="b"></Comp>` H2': '# H1 `<Comp a="b"></Comp>` H2',
'# H1 `<Comp/>` H2': '# H1 `<Comp/>` H2',
'# H1 `<Comp a="b"/>` H2': '# H1 `<Comp a="b"/>` H2',
// vuepress #2688
'# \\<ins>': '# \\<ins>'
}
Object.keys(asserts).forEach((input) => {
expect(removeNonCodeWrappedHTML(input)).toBe(asserts[input])
})
})

@ -95,7 +95,9 @@
"@mdit-vue/plugin-component": "^0.10.0", "@mdit-vue/plugin-component": "^0.10.0",
"@mdit-vue/plugin-frontmatter": "^0.10.0", "@mdit-vue/plugin-frontmatter": "^0.10.0",
"@mdit-vue/plugin-headers": "^0.10.0", "@mdit-vue/plugin-headers": "^0.10.0",
"@mdit-vue/plugin-title": "^0.10.0",
"@mdit-vue/plugin-toc": "^0.10.0", "@mdit-vue/plugin-toc": "^0.10.0",
"@mdit-vue/shared": "^0.10.0",
"@mdit-vue/types": "^0.10.0", "@mdit-vue/types": "^0.10.0",
"@rollup/plugin-alias": "^3.1.9", "@rollup/plugin-alias": "^3.1.9",
"@rollup/plugin-commonjs": "^22.0.2", "@rollup/plugin-commonjs": "^22.0.2",

@ -9,7 +9,9 @@ importers:
'@mdit-vue/plugin-component': ^0.10.0 '@mdit-vue/plugin-component': ^0.10.0
'@mdit-vue/plugin-frontmatter': ^0.10.0 '@mdit-vue/plugin-frontmatter': ^0.10.0
'@mdit-vue/plugin-headers': ^0.10.0 '@mdit-vue/plugin-headers': ^0.10.0
'@mdit-vue/plugin-title': ^0.10.0
'@mdit-vue/plugin-toc': ^0.10.0 '@mdit-vue/plugin-toc': ^0.10.0
'@mdit-vue/shared': ^0.10.0
'@mdit-vue/types': ^0.10.0 '@mdit-vue/types': ^0.10.0
'@rollup/plugin-alias': ^3.1.9 '@rollup/plugin-alias': ^3.1.9
'@rollup/plugin-commonjs': ^22.0.2 '@rollup/plugin-commonjs': ^22.0.2
@ -94,7 +96,9 @@ importers:
'@mdit-vue/plugin-component': 0.10.0 '@mdit-vue/plugin-component': 0.10.0
'@mdit-vue/plugin-frontmatter': 0.10.0 '@mdit-vue/plugin-frontmatter': 0.10.0
'@mdit-vue/plugin-headers': 0.10.0 '@mdit-vue/plugin-headers': 0.10.0
'@mdit-vue/plugin-title': 0.10.0
'@mdit-vue/plugin-toc': 0.10.0 '@mdit-vue/plugin-toc': 0.10.0
'@mdit-vue/shared': 0.10.0
'@mdit-vue/types': 0.10.0 '@mdit-vue/types': 0.10.0
'@rollup/plugin-alias': 3.1.9_rollup@2.78.0 '@rollup/plugin-alias': 3.1.9_rollup@2.78.0
'@rollup/plugin-commonjs': 22.0.2_rollup@2.78.0 '@rollup/plugin-commonjs': 22.0.2_rollup@2.78.0
@ -423,6 +427,15 @@ packages:
markdown-it: 13.0.1 markdown-it: 13.0.1
dev: true dev: true
/@mdit-vue/plugin-title/0.10.0:
resolution: {integrity: sha512-odJ9vIazAHiomjCEEFwHNuPnmDtx/FGOYrf9xUfi3tjG9r/JZW+G++AABxvevTozwpGlpU+wkpJ7mTr+rNtBrw==}
dependencies:
'@mdit-vue/shared': 0.10.0
'@mdit-vue/types': 0.10.0
'@types/markdown-it': 12.2.3
markdown-it: 13.0.1
dev: true
/@mdit-vue/plugin-toc/0.10.0: /@mdit-vue/plugin-toc/0.10.0:
resolution: {integrity: sha512-P9aNy4jtqfjI08wUYGT/HVd5x/IpTjgSnNdJ3lU52qAO5AeFsW3v4gt+NmW0lO8We0S2YDEONRHBuBN6r40y6A==} resolution: {integrity: sha512-P9aNy4jtqfjI08wUYGT/HVd5x/IpTjgSnNdJ3lU52qAO5AeFsW3v4gt+NmW0lO8We0S2YDEONRHBuBN6r40y6A==}
dependencies: dependencies:

@ -11,6 +11,7 @@ import {
headersPlugin, headersPlugin,
type HeadersPluginOptions type HeadersPluginOptions
} from '@mdit-vue/plugin-headers' } from '@mdit-vue/plugin-headers'
import { titlePlugin } from '@mdit-vue/plugin-title'
import { tocPlugin, type TocPluginOptions } from '@mdit-vue/plugin-toc' import { tocPlugin, type TocPluginOptions } from '@mdit-vue/plugin-toc'
import { IThemeRegistration } from 'shiki' import { IThemeRegistration } from 'shiki'
import { highlight } from './plugins/highlight' import { highlight } from './plugins/highlight'
@ -106,6 +107,7 @@ export const createMarkdownRenderer = async (
slugify, slugify,
...options.headers ...options.headers
} as HeadersPluginOptions) } as HeadersPluginOptions)
.use(titlePlugin)
.use(tocPlugin, { .use(tocPlugin, {
slugify, slugify,
...options.toc ...options.toc

@ -2,14 +2,15 @@ import fs from 'fs'
import path from 'path' import path from 'path'
import c from 'picocolors' import c from 'picocolors'
import LRUCache from 'lru-cache' import LRUCache from 'lru-cache'
import { resolveTitleFromToken } from '@mdit-vue/shared'
import { PageData, HeadConfig, EXTERNAL_URL_RE, CleanUrlsMode } from './shared' import { PageData, HeadConfig, EXTERNAL_URL_RE, CleanUrlsMode } from './shared'
import { slash } from './utils/slash' import { slash } from './utils/slash'
import { deeplyParseHeader } from './utils/parseHeader'
import { getGitTimestamp } from './utils/getGitTimestamp' import { getGitTimestamp } from './utils/getGitTimestamp'
import { import {
createMarkdownRenderer, createMarkdownRenderer,
type MarkdownEnv, type MarkdownEnv,
type MarkdownOptions type MarkdownOptions,
type MarkdownRenderer
} from './markdown' } from './markdown'
import _debug from 'debug' import _debug from 'debug'
@ -83,7 +84,7 @@ export async function createMarkdownToVueRenderFn(
} }
const html = md.render(src, env) const html = md.render(src, env)
const data = md.__data const data = md.__data
const { content = '', frontmatter = {}, headers = [] } = env const { frontmatter = {}, headers = [], title = '' } = env
// validate data.links // validate data.links
const deadLinks: string[] = [] const deadLinks: string[] = []
@ -129,7 +130,7 @@ export async function createMarkdownToVueRenderFn(
} }
const pageData: PageData = { const pageData: PageData = {
title: inferTitle(frontmatter, content), title: inferTitle(md, frontmatter, title),
titleTemplate: frontmatter.titleTemplate as any, titleTemplate: frontmatter.titleTemplate as any,
description: inferDescription(frontmatter), description: inferDescription(frontmatter),
frontmatter, frontmatter,
@ -243,18 +244,21 @@ function genPageDataCode(tags: string[], data: PageData, replaceRegex: RegExp) {
return tags return tags
} }
const inferTitle = (frontmatter: Record<string, any>, content: string) => { const inferTitle = (
if (frontmatter.title) { md: MarkdownRenderer,
return deeplyParseHeader(frontmatter.title) frontmatter: Record<string, any>,
} title: string
) => {
const match = content.match(/^\s*#+\s+(.*)/m) if (typeof frontmatter.title === 'string') {
const titleToken = md.parseInline(frontmatter.title, {})[0]
if (match) { if (titleToken) {
return deeplyParseHeader(match[1].trim()) return resolveTitleFromToken(titleToken, {
shouldAllowHtml: false,
shouldEscapeText: false
})
}
} }
return title
return ''
} }
const inferDescription = (frontmatter: Record<string, any>) => { const inferDescription = (frontmatter: Record<string, any>) => {

@ -1,68 +0,0 @@
// 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 markdown
// 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.
import emojiData from 'markdown-it-emoji/lib/data/full.json'
const parseEmojis = (str: string) => {
return str.replace(
/:(.+?):/g,
(placeholder, key) => (emojiData as any)[key] || placeholder
)
}
const unescapeHtml = (html: string) =>
html
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&#x3A;/g, ':')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
const removeMarkdownTokens = (str: string) =>
str
.replace(/(\[(.[^\]]+)\]\((.[^)]+)\))/g, '$2') // []()
.replace(/(`|\*{1,3}|_)(.*?[^\\])\1/g, '$2') // `{t}` | *{t}* | **{t}** | ***{t}*** | _{t}_
.replace(/(\\)(\*|_|`|\!|<|\$)/g, '$2') // remove escape char '\'
const removeCustomAnchor = (str: string) =>
str.replace(/\{#([a-zA-Z0-9\-_]+?)\}\s*$/, '') // {#custom-header}
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 parseHeader = compose(
unescapeHtml,
parseEmojis,
removeCustomAnchor,
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 deeplyParseHeader = compose(removeNonCodeWrappedHTML, parseHeader)
Loading…
Cancel
Save