From 885496e7460d4a9653518e8c23d66a5773bbcdfa Mon Sep 17 00:00:00 2001 From: ULIVZ <472590061@qq.com> Date: Wed, 8 Aug 2018 13:44:50 +0800 Subject: [PATCH] feat: support writing HTML(Vue) anywhere in the header. (#711) 1. You can write HTML(Vue) anywhere in the header as long as it is not wrapped by code(`). 2. The HTML wrapped by code will be shown as it is. 3. A good practice when using HTML in a header is to leave a space between plain text and HTML. --- lib/util/parseHeaders.js | 37 ++++++++++---- test/util/parseHeaders.spec.js | 88 ++++++++++++++++++++++++++++++---- 2 files changed, 108 insertions(+), 17 deletions(-) diff --git a/lib/util/parseHeaders.js b/lib/util/parseHeaders.js index 77b701c528..02740540c6 100644 --- a/lib/util/parseHeaders.js +++ b/lib/util/parseHeaders.js @@ -1,3 +1,15 @@ +// 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 (markdown token: '`') tag. + const { compose } = require('./shared') const parseEmojis = str => { @@ -12,26 +24,33 @@ const unescapeHtml = html => String(html) .replace(/</g, '<') .replace(/>/g, '>') -const removeMarkdownToken = str => String(str) +const removeMarkdownTokens = str => String(str) .replace(/\[(.*)\]\(.*\)/, '$1') // []() .replace(/(`|\*{1,3}|_)(.*?[^\\])\1/g, '$2') // `{t}` | *{t}* | **{t}** | ***{t}*** | _{t}_ .replace(/(\\)(\*|_|`)/g, '$2') // remove escape char '\' -exports.removeTailHtml = (str) => { - return String(str).replace(/\s*?<.*>\s*$/g, '') +const trim = str => str.trim() + +// This method remove the raw HTML but reserve the HTML wrapped by ``. +// e.g. +// Input: " b", Output: "b" +// Input: "`` b", Output: "`` b" +exports.removeNonCodeWrappedHTML = (str) => { + return String(str).replace(/(^|[^><`])<.*>([^><`]|$)/g, '$1$2') } -// Only remove some md tokens. +// Unescape html, parse emojis and remove some md tokens. exports.parseHeaders = compose( unescapeHtml, parseEmojis, - removeMarkdownToken + removeMarkdownTokens, + trim ) -// Also clean the tail html in headers. -// Since we want to support tailed badge in headers. -// See: https://vuepress.vuejs.org/guide/using-vue.html#badge +// 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 exports.deeplyParseHeaders = compose( - exports.removeTailHtml, + exports.removeNonCodeWrappedHTML, exports.parseHeaders, ) diff --git a/test/util/parseHeaders.spec.js b/test/util/parseHeaders.spec.js index 3d1eb6a8be..da8c19aa21 100644 --- a/test/util/parseHeaders.spec.js +++ b/test/util/parseHeaders.spec.js @@ -1,6 +1,6 @@ import { parseHeaders, - removeTailHtml, + removeNonCodeWrappedHTML, deeplyParseHeaders } from '@/util/parseHeaders' @@ -32,15 +32,87 @@ describe('parseHeaders', () => { }) }) - test('should remove tail html correctly', () => { - expect(removeTailHtml('# H1 ')).toBe('# H1') - expect(removeTailHtml('# H1 ')).toBe('# H1') - expect(removeTailHtml('# H1 ')).toBe('# H1') - expect(removeTailHtml('# H1 ')).toBe('# H1') + test('should remove non-code-wrapped html correctly', () => { + const asserts = { + // Remove tail html + '# H1 ': '# H1 ', + '# H1': '# H1', + '# H1 ': '# H1 ', + '# H1': '# H1', + '# H1 ': '# H1 ', + '# H1': '# H1', + '# H1 ': '# H1 ', + '# H1': '# H1', + + // Reserve code-wrapped tail html + '# H1 ``': '# H1 ``', + '# H1 ``': '# H1 ``', + '# H1 ``': '# H1 ``', + '# H1 ``': '# H1 ``', + + // Remove leading html + '# H1': '# H1', + '# H1': '# H1', + '# H1': '# H1', + '# H1': '# H1', + '# H1': '# H1', + '# H1': '# H1', + '# H1': '# H1', + '# H1': '# H1', + + // Reserve code-wrapped leading html + '# `` H1': '# `` H1', + '# `` H1': '# `` H1', + '# `` H1': '# `` H1', + '# `` H1': '# `` H1', + + // Remove middle html + '# H1 H2': '# H1 H2', + '# H1 H2': '# H1 H2', + '# H1 H2': '# H1 H2', + '# H1 H2': '# H1 H2', + + // Reserve code-wrapped middle html + '# H1 `` H2': '# H1 `` H2', + '# H1 `` H2': '# H1 `` H2', + '# H1 `` H2': '# H1 `` H2', + '# H1 `` H2': '# H1 `` H2' + } + + Object.keys(asserts).forEach(input => { + expect(removeNonCodeWrappedHTML(input)).toBe(asserts[input]) + }) }) test('should deeplyParseHeaders transformed as expected', () => { - expect(deeplyParseHeaders('# `H1` ')).toBe('# H1') - expect(deeplyParseHeaders('# *H1* ')).toBe('# H1') + const asserts = { + // Remove tail html + '# `H1` ': '# H1', + '# *H1* ': '# H1', + + // Reserve code-wrapped tail html + '# `H1` ``': '# H1 ', + '# *H1* ``': '# H1 ', + + // Remove leading html + '# `H1`': '# H1', + '# *H1*': '# H1', + + // Reserve code-wrapped leading html + '# `` `H1`': '# H1', + '# `` *H1*': '# H1', + + // Remove middle html + '# `H1` `H2`': '# H1 H2', + '# `H1` `H2`': '# H1 H2', + + // Reserve middle html + '# `H1` `` `H2`': '# H1 H2', + '# `H1` `` `H2`': '# H1 H2' + } + + Object.keys(asserts).forEach(input => { + expect(deeplyParseHeaders(input)).toBe(asserts[input]) + }) }) })