头图

${toc}

篇章预告

:::info
!!!本文例子依赖博主自己的博客,如有需要,可移步至我的博客!!!
在上一节中,我们介绍了 line-highlight插件的源码,但是如何在markdown-it中集成使用呢?

本节作为上一节的一个补充,将带领大家实现以下功能

  • markdown-it指定行高亮
  • 实现不同样式的行高亮
  • 可链接的行号
    :::

    指定行高亮

    本节是补充节,不再介绍markdown-ittoken流render函数的使用,如果有兴趣可以自行上网查找相关资料。
    在上一节中,我们已经知道了line-highlight插件的工作原理,大致分为收集信息以及操作dom。作为使用者,我们不需要考虑太多细节,只要知道如何提供这些信息即可。
    完整信息列表如下

    标签属性/类名作用
    predata-line需要高亮的行号
    predata-line-offset行号偏移量
    prelinkable-line-numbers将元素设置为可链接的行号

对于本功能来说,只需要提供往pre标签上提供一个data-line属性即可,很自然的想到markdown-it中我们可以通过render.fence来为pre标签插入该属性,而行号则可以通过fence的token.info来提供
示例如下:

    let a = 1,
    b = 2,
    c = 3
/**
    源代码
let a = 1,
b = 2,
c = 3

\```
*/

在开始前, 让我们先思考一下,其实直接写一个插件通过render给指定的pre添加一个属性是很容易的,但是我们后续还要添加很多的功能,就要处理不同插件之间**冲突**的情况,一个直观的想法是通过复杂的正则替换来避免冲突,但那显然会让代码变得难以阅读。
幸运的是,`vuepress`为我们提供了一个**最佳实践**。

module.exports = md => {
const wrap = (wrapped) => (...args) => {

const [tokens, idx] = args
const token = tokens[idx]
const rawCode = wrapped(...args)
const tokenInfo = token.info.trim().replace(/\"/g, '\'')
return `<!--beforebegin--><div class="language-${tokenInfo} extra-class">`
+ `<!--afterbegin-->${rawCode}<!--beforeend--></div><!--afterend-->`

}
const { fence, code_block: codeBlock } = md.renderer.rules
md.renderer.rules.fence = wrap(fence)
md.renderer.rules.code_block = wrap(codeBlock)
}

参考资料

- [github-vuepress](https://github.com/vuejs/vuepress/blob/master/packages/%40vuepress/markdown/lib/preWrapper.js)
- [使用 markdown-it 解析 markdown 代码(读 vuepress 三)](https://zhuanlan.zhihu.com/p/46355549?utm_id=0)

这是个优先级很高的插件,vuepress会在后续对fence(代码块)的所有插件之前注册它
这个插件的功能也很简单,将`tokenInfo`(代码标志,如js)和`rawCode`(原始代码)进行包裹,提供一些无关紧要的注释和类名,这些类名可以充当**钩子**的作用,当我们需要在后续插件为某个元素替换类名或在此之前添加一个元素时,只需要**替换**相关字符串即可。
受此启发,我们也可以为自己的代码编写钩子。因为我们的属性是添加在pre标签上的,比如`data-line`是一个属性,而`linkable-line-numbers`是一个类名。

module.exports = md => {
const wrap = (wrapped) => (...args) => {

const [tokens, idx] = args
const token = tokens[idx]
const rawCode = wrapped(...args)
const tokenInfo = token.info.trim().replace(/\"/g, '\'')
rawCode = rawCode.replace(/<pre.*?>/, `<pre class="extra-pre-class" extra-pre-attr>`)
return `<!--beforebegin--><div class="language-${tokenInfo} extra-class">`
+ `<!--afterbegin-->${rawCode}<!--beforeend--></div><!--afterend-->`

}
const { fence, code_block: codeBlock } = md.renderer.rules
md.renderer.rules.fence = wrap(fence)
md.renderer.rules.code_block = wrap(codeBlock)
}

这里我们为**类名**添加钩子**extra-pre-class**,为属性添加钩子**extra-pre-attr**。
接下来编写fence的render插件,来为pre标签添加上`data-line`属性。

export const hightlightLinesPlugin = md => {

const fence = md.renderer.rules.fence
md.renderer.rules.fence = (...args) => {
    const [tokens, idx, options, env] = args
    const token = tokens[idx]
    const rawInfo = token.info
    const code = fence(...args)
    const infos = rawInfo.split(/\s+/)
    let lineHlFlag = infos[1] && /^[\d,-;]+$/.test(infos[1])
    const new_code = code
        .replace('extra-pre-class', 'line-numbers extra-pre-class')
        .replace('extra-pre-attr', lineHlFlag ? `data-line="${infos[1]}" extra-pre-attr` : 'extra-pre-attr')
    return new_code
}

}

先获取`token.info`来获取data-line的内容,并对其进行合法性校验,默认开启行号,根据校验结果对pre添加上属性。
注意,替换时务必带上**extra-pre-attr**这个原来的钩子,不然钩子就变成一次性的了。

// 效果

let a,
  b,
  c,
  d

/** md文本

let a,
  b,
  c,
  d
\```
*/

实现不同样式的行高亮

不同样式的行高亮本质上就是对不同类别的行给出不同的类名,即可通过css实现不同样式。
我们先明确fence的info的格式,如何区分不同的行。给出一般格式
js 1;2-3;4,5

行号类名样式
1normal-line默认样式
2-3add-line添加的行(绿色)
4,5reduce-line删除的行(红色)
const a,
  b,
  c,
  d,
  e,
  f

效果还是比较好看的。接下来是实现。我们将逻辑部分移动到插件代码的位置,故info的提取还是跟上面完全一致,代码略。
插件的逻辑

highlightLines: function highlightLines(pre, lines, classes) {
    lines = typeof lines === 'string' ? lines : pre.getAttribute('data-line') || ''
    let linesArr = lines.split(';')
    // var ranges = lines.replace(/\s+/g, '').split(',').filter(Boolean)
    var offset = +pre.getAttribute('data-line-offset') || 0

    var parseMethod = isLineHeightRounded() ? parseInt : parseFloat
    var lineHeight = parseMethod(getComputedStyle(pre).lineHeight)
    var hasLineNumbers = Prism.util.isActive(pre, LINE_NUMBERS_CLASS)
    var codeElement = pre.querySelector('code')
    var parentElement = hasLineNumbers ? pre : codeElement || pre
    var mutateActions = /** @type {(() => void)[]} */ ([])
    var lineBreakMatch = codeElement.textContent.match(NEW_LINE_EXP)
    var numberOfLines = lineBreakMatch ? lineBreakMatch.length + 1 : 1
    var codePreOffset = !codeElement || parentElement == codeElement ? 0 : getContentBoxTopOffset(pre, codeElement)
    linesArr.slice(0, 3).forEach((lines, idx) => {
        var ranges = lines.replace(/\s+/g, '').split(',').filter(Boolean)
        let extra_class
        switch (idx) {
            case 0:
                extra_class = 'normal-line'
                break
            case 1:
                extra_class = 'add-line'
                break
            case 2:
                extra_class = 'reduce-line'
                break
        }
        ranges.forEach(function (currentRange) {
            var range = currentRange.split('-')

            var start = +range[0]
            var end = +range[1] || start
            end = Math.min(numberOfLines + offset, end)

            if (end < start) {
                return
            }

            /** @type {HTMLElement} */
            var line =
                pre.querySelector('.line-highlight[data-range="' + currentRange + '"]') || document.createElement('div')

            mutateActions.push(function () {
                line.setAttribute('aria-hidden', 'true')
                line.setAttribute('data-range', currentRange)
                line.className = (classes || '') + ' line-highlight' + ` ${extra_class}`
            })
    // 以下代码省略
},

我们分割出两个数组,按idx划分,0是normal-line,1是add-line,最后是reduce-line,并在47行据此添加上对应的类名。
最后在css文件里做出相应的修改即可。

.line-highlight {
    position: absolute;
    left: 0;
    right: 0;
    padding: inherit 0;
    margin-top: 1em; /* Same as .prism’s padding-top */
    /* background: hsla(24, 20%, 50%, 0.08);
    background: linear-gradient(to right, hsla(24, 20%, 50%, 0.1) 70%, hsla(24, 20%, 50%, 0)); */
    pointer-events: none;
    line-height: inherit;
    white-space: pre;
}
.normal-line {
    background: hsla(24, 20%, 50%, 0.08);
    background: linear-gradient(to right, hsla(24, 20%, 50%, 0.1) 70%, hsla(24, 20%, 50%, 0));
}
.add-line {
    @apply bg-gradient-to-r from-green-300/40;
}
.reduce-line {
    @apply bg-gradient-to-r from-red-300/40;
}

到此即实现了不同样式高亮的效果。

可链接的行号

实际上这是最简单的,因为prism已经承揽了逻辑功能的实现,而我们需要做的只是为pre添加上一个类名linkable-line-numbers
还是一样的,我们先判断如何控制开关。我的思路是让info的第三个位置(第一个是语言类型,第二个是高亮行号)作为开关的标志, 如下。
尝试点击行号,你会发现被点击位置的行高亮了,即可链接的行号功能

let a,
b,
c,
d,
e
/**md源码

let a,
b,
c,
d,
e
\```
a只是一个占位符,它会通不过校验而被拒绝打开高亮
*/

还是在mdit插件中,注意,字符串无论任何情况下都是为真,故要使用**JSON.parse**,但是这个函数在给定值非法时会抛出错误,故要使用**try catch**去包裹它。

export const hightlightLinesPlugin = md => {

const fence = md.renderer.rules.fence
md.renderer.rules.fence = (...args) => {
    const [tokens, idx, options, env] = args
    const token = tokens[idx]
    const rawInfo = token.info
    const code = fence(...args)
    const infos = rawInfo.split(/\s+/)
    let lineHlFlag = infos[1] && /^[\d,-;]+$/.test(infos[1])
    let linkableId
    let linkableFlag
    try {
        linkableFlag = JSON.parse(infos[2])
    } catch (error) {}
    if (linkableFlag) {
        linkableId = env.linkId + 1 || (env.linkId = 1)
        env.linkId++
    }
    const new_code = code
        .replace('extra-pre-class', 'line-numbers extra-pre-class')
        .replace('extra-pre-class', linkableFlag ? `linkable-line-numbers extra-pre-class` : 'extra-pre-class')
        .replace('extra-pre-attr', lineHlFlag ? `data-line="${infos[1]}" extra-pre-attr` : 'extra-pre-attr')
        .replace(
            'extra-pre-attr',
            linkableFlag ? `id="${linkableId + 'linkable-container'}"  extra-pre-attr` : 'extra-pre-attr',
        )
    return new_code
}

}

我们做了校验,只有当为true或其他真符号时开启可链接的功能,除此之外这里使用了env记录一个有关id的**linkId**,它的功能是确保每个fence之间的id不唯一,从而保证**同一页面上两个同时开启了可链接的行号功能的fence代码块不会互相影响**。
## 写在最后
至此我们的所有功能都实现完毕了。你也可以去扩展更多的功能,比如增加一个表示修改样式的高亮。
接下来我打算将prism搁置一段时间,去探索一下codemirror以及扩展自己的博客功能,我仍旧会将自己的心得体会进行分享,请持续关注我!

张小仙人
12 声望0 粉丝

我是大睡个