lezer markdown render1

i can`t del the first post ~~

lezer markdown render and No third-party plugins available

use:

import 'CmdRender' from 'CmdRender'

let md = = `
[foo][baz]

[baz]: /url
`

let html = CmdRender.render(md)

console.log(html)

// result

foo


---------------------------------------------------------- the source

import { Tree } from '@lezer/common';
import { parser, GFM } from '@lezer/markdown'

export class CmdRender{

    // 前一个节点的信息 开始,结束,名称
    prev = { from: 0, to: 0, name: void 0}

    // 块堆栈如果嵌套会存放 close, 如果close 被消耗,会弹出
    stack = []

    // 关闭标签 当前块的最后位置
    close = { from: 0, to: 0, name:'', tag: '', }

    // 光标当前位置, 全局用, 后面需要替换 close.from, 不在主动变更 close.from 位置
    pos = 0

    //---------------------------s link img
    inLink = false

    // 用于存放link信息
    linkInfo = {
        from: 0, to: 0, url: '', title: '', label: '', img: '',  // 只有在嵌套img时才有用
    }

    inImg = false
    // 用于计算link或者图片嵌套
    imgInfo = { from: 0, to: 0, pos: 0, url: '', title: '', alt: ''}

    // link 嵌套位置 nesting
    nt = { from: 0, to: 0 }

    // 存放引用链接 { id: '', url: '', title: '', text: '', refs: Set }
    refUrls = new Map()

    // ref 当前正在处理的ref
    refInfo = { label: '', content: '', url: '', title: '', init: true, readonly: true }

    inRef = false
    //---------------------------e link img

    inline = { from: 0, to: 0, is: false}

    // 行堆栈
    lineStack = []

    // 输出内容
    output = ''

    constructor(input, tree = null, gfm = true)
    {
        this.input = input

        if(!(tree instanceof Tree)) {
            let _parser = gfm
                ? parser.configure(GFM)
                : parser
            tree = _parser.parse(input)
        }

        this.tree = tree
    }

    static render(input, tree = null, gfm = true)
    {
        const instance = new CmdRender(input, tree, gfm)

        return instance.render()
    }

    // 只返回块的内容,不包含块
    static renderLine(input, tree = null, gfm = true)
    {
        const instance = new CmdRender(input, tree, gfm)

        return instance.renderSome()
    }

    render()
    {
        let cursor = this.tree.cursor()

        do {
            let str = this['md' + cursor.name]?.(cursor)

            // 需要判断是否在 link 中,并且判断是否结束 link
            if(this.inRef || this.inImg || this.inLink) {
                // do-nothing
            }
            else if(str) {
                this.output += str
            }

            this.prev.from = cursor.from
            this.prev.to = cursor.to
            this.prev.name = cursor.name
        } while (cursor.next())

        // 最后闭尾
        this.output += this._closePrevBlock(this.input.length)

        console.info(this.refUrls)
        // 替换引用链接 // key, value
        for (let ref of this.refUrls.values()) {  // entries
            // url 的内容,在这里需要重新渲染

            if(ref.readonly) continue

            // 对其进行排序确保 ![ 即 img 标签排在 a 标签前
            const img = []
            const link = []

            ;[...ref.refs].map(item => {
                item.startsWith('![')
                    ? img.push(item)
                    : link.push(item)
            })

            if(img.length) {
                img.map(item => {
                    this.output = ref.init
                        ? this.output.replaceAll(item, this._makeImg(ref))
                        : this.output.replaceAll(item, this._linkContent(item))
                })
            }

            if(link.length) {
                link.sort((a, b) => b.length - a.length).map(item => {
                    if(ref.init) {

                        let content = item
                        // [foo][bar], [foo][]
                        if(/^\[.+\](\[.+\]$|\[\]$)/.test(item)) {
                            content = item.replace(/\](\[.+\]$|\[\]$)/, '')
                            content += ']'
                        }

                        this.output = this.output.replaceAll(item, this._makeLink(ref, content))
                    }
                    else {
                        this.output = this.output.replaceAll(item, this._linkContent(item, true, ref))
                    }
                })
            }
        }

        return this.output
    }

    // 只渲染行的内容
    renderSome()
    {
        let cursor = this.tree.cursor()

        while (cursor.next()) {

            if(!this._isLine(cursor.name)) {
                continue
            }

            let str = this['md' + cursor.name]?.(cursor)

            if(str) {
                this.output += str
            }

            this.prev.from = cursor.from
            this.prev.to = cursor.to
            this.prev.name = cursor.name
        }

        this.output += this._closePrevBlock(this.input.length)

        return this.output
    }

    // 这里方法的名称和 md 解析里面的名称一样,方便调用
    //
    // 容器块是一种包含其他块的块的集合
    //----------------------------------------------s 容器块
    mdDocument({from, to, name})
    {
        this._enterBlock(from, to, name, '</div>')

        return '<div class="md-html">'
    }

    // 一个块引用标记由 0-3 个初始缩进空格组成,另外加上(a)字符 `>` 与一个空格,或(b)单个字符 > 后面没有空格。
    mdBlockquote({from, to, name})
    {
        // 这里可能就会有嵌套
        let str = this._closePrevBlock(from) + '<blockquote>'

        this._enterBlock(from, to, name, '</blockquote>')

        return str
    }

    // Blockquote 的子块
    mdQuoteMark({from, to})
    {
        // 开启 inline 嵌套模式

        // let str = this.prev.name !== 'Blockquote'
        //     ? this.input.slice(this.prev.from, from)
        //     : this.input.slice(this.pos, from)
        let str = this.input.slice(this.pos, from)

        this.pos = to

        return str
    }

    // 列表
    mdBulletList({from, to, name})
    {
        let str = this._closePrevBlock(from) + '<ul>'

        this._enterBlock(from, to, name, '</ul>')

        return str
    }

    mdOrderedList({from, to, name})
    {
        let str = this._closePrevBlock(from) + '<ol>'

        this._enterBlock(from, to, name, '</ol>')

        return str
    }

    // 1,缩进问题 no-ok 不影响大题
    mdListItem({from, to, name})
    {
        // 有序列表不是从第一开始的问题
        if(this.prev.name === 'OrderedList') {

            // 空格的第一字段
            const o = this.input.slice(from, to).trim().split(' ')[0]

            if(!o.endsWith('.') && !o.endsWith(')')) {
                return
            }

            let num = +o.slice(0, -1)

            if(!Number.isNaN(num) && -1 < num && num < 1000000000) {
                this.output = this.output.slice(0, -1)
                this.output += ' start="' + num + '">'
            }
        }

        let str = this._closePrevBlock(from) + '<li>'

        this._enterBlock(from, to, name, '</li>')

        return str
    }

    mdListMark({to})
    {
        this.pos = to
        // this.close.from = to
    }

    // 任务列表
    mdTaskMarker({from, to})
    {
        let str = this.input.slice(from, to).toLowerCase()

        // 此时this.output 以 '<li>' 结尾
        this.output += str.includes('x')
            ? '<input checked disabled type="checkbox" />'
            : '<input disabled type="checkbox" />'

        this.pos = to
    }
    //----------------------------------------------e 容器块

    // 叶子块不能包含其他块
    //----------------------------------------------s 叶子块

    // 换行符
    // 由 0-3 个缩进空格组成的行,后面跟三个或更多的匹配 -, _, 或 * 字符的序列,
    // 每个字符后跟任意数量的空格或制表符,形成一个专门的换行。
    mdHorizontalRule({from, to})
    {
        let str = this._closePrevBlock(from)

        this.pos = to

        return str + '<hr />'
    }

    // ATX 标题
    mdATXHeading1({from, to, name})
    {
        let str = this._closePrevBlock(from) + '<h1>'

        this._enterBlock(from, to, name, '</h1>')

        return str
    }

    mdATXHeading2({from, to, name})
    {
        let str = this._closePrevBlock(from) + '<h2>'

        this._enterBlock(from, to, name, '</h2>')

        return str
    }

    mdATXHeading3({from, to, name})
    {
        let str = this._closePrevBlock(from) + '<h3>'

        this._enterBlock(from, to, name, '</h3>')

        return str
    }

    mdATXHeading4({from, to, name})
    {
        let str = this._closePrevBlock(from) + '<h4>'

        this._enterBlock(from, to, name, '</h4>')

        return str
    }

    mdATXHeading5({from, to, name})
    {
        let str = this._closePrevBlock(from) + '<h5>'

        this._enterBlock(from, to, name, '</h5>')

        return str
    }

    mdATXHeading6({from, to, name})
    {
        let str = this._closePrevBlock(from) + '<h6>'

        this._enterBlock(from, to, name, '</h6>')

        return str
    }

    // Setext 标题
    mdSetextHeading1({from, to, name})
    {
        let str = this._closePrevBlock(from) + '<h1>'

        this._enterBlock(from, to, name, '</h1>')

        return str
    }

    mdSetextHeading2({from, to, name})
    {
        let str = this._closePrevBlock(from) + '<h2>'

        this._enterBlock(from, to, name, '</h2>')

        return str
    }

    mdHeaderMark({from, to})
    {
        // 紧跟在 heading 之后,
        // 需要做的只是改变一下当前的 close 起始位置,
        // heading 分为二种情况,一种是 ATX, 别一种是 setext
        if(this.close.name === 'SetextHeading1' || this.close.name === 'SetextHeading2') {
            // close.to = from
            // 表明需要打断一部分,在闭合时使用和close.to的值一样,重新定义只是为了区分
            // 移动close的起始位置,
        }
        else {
            this.pos = to
            // ## hhh ## 这些情况
            if(this.prev.name === 'HeaderMark') {
                return this.input.slice(this.prev.to, from)
            }
        }
    }

    // 缩进代码块
    mdCodeBlock({from, to, name})
    {
        let str = this._closePrevBlock(from) + '<pre><code>'

        this._enterBlock(from, to, name, '</pre></code>')

        return str
    }

    // 1.缩进问题 - no-ok 不影响大局
    // 围栏代码 ```之间的代码```, 需要用到其他的语言块来解析,
    mdFencedCode({from, to, name})
    {    // 缩进没有解决,Blockquote 嵌套未解决
        let str = this._closePrevBlock(from) + '<pre><code>'

        this._enterBlock(from, to, name, '</pre></code>')

        return str
    }

    // FencedCode 后面紧跟着 CodeMark
    mdCodeMark({from, to})
    {
        // 使用位置来判断? 和 from 或者 to 做比较来判断是第一个mark还是后一个mark
        // 和第二次进入,可能只进入一次,在结尾时
        // 可能会进入一次,或者二次
        // 表示闭合
        let str = ''

        // 行级处理 InlineCode
        if(this.inline.is && to === this.inline.to) {
            str = escapeHtml(this.input.slice(this.pos, from))

            str += this.lineStack.pop()
            this.inline = {from: 0, to: 0, is: false}
        }

        this.pos = to

        return str
    }

    // ``` 后成跟的语言信息
    mdCodeInfo({from, to})
    {
        let info = this.input.slice(from, to).trim().split(' ')[0]

        this.output = this.output.slice(0, -1)

        return ' class="lang-' + info + '">'
    }

    // 父级可能是 FencedCode 或者 CodeBlock 或者 InlineCode
    mdCodeText({from, to})
    {
        // 需要转换成对应的语言,暂时跳过转换
        // 应该传入一个转换的参数
        let str =  this.input.slice(from, to)

        // 转换 按语言换转 str ....

        this.pos = to

        return str
    }

    // html 块
    mdHTMLBlock({from, to, name})
    {
        let str = this._closePrevBlock(from)

        // 情况比较复杂,需要参考GFM的标准,有7种情况, 暂时还跳过这里
        this._enterBlock(from, to, name, '')

        return str
    }

    // 注释块 <!-- sss -->
    mdCommentBlock({from, to})
    {
        let str = this._closePrevBlock(from)

        str += this.input.slice(from, to)

        this.pos = to

        return str
    }

    // 原生 HTML
    mdHTMLTag({from, to})
    {
        let str = this.input.slice(this.pos, to)

        this.pos = to

        return str
    }

    mdParagraph({from, to, name})
    {
        // 先关闭之前的块,在记录本次块的信息
        let str = this._closePrevBlock(from) + '<p>'

        this._enterBlock(from, to, name, '</p>')

        return str
    }

    //----------------------------------------------e 叶子块
    //----------------------------------------------s 内联inline,行级别的

    mdComment({from, to})
    {
        let str = this.input.slice(this.pos, to)

        this.pos = to

        return str
    }

    // 进入行后,先判断是否应关闭上个块
    // 1,from > this.close.to 应关闭上个块
    // 2,from < this.close.to 不用关闭上个块
    // 3, 遇到mark 当前close.from 应向前跳到 to

    // 反斜杠转义 在反html 标签时有问题, 这个 lezer md 在解析的时候就存在问题
    mdEscape({from, to})
    {
        let str = this.input.slice(this.pos, from)

        // 跳过反转标签
        str += this.input.slice(from + 1, to)

        this.pos = to

        return escapeHtml(str)
    }

    // 实体和数字字符引用
    mdEntity({from, to})
    {
        let str = this.input.slice(this.pos, to)

        // str += this.input.slice(this.prev.from, from)

        this.pos = to

        return str

        // 这里似乎不在需要做其他的事情, --整理做完后,在看情况--
    }

    // 行内代码
    mdInlineCode({from, to})
    {
        // 进入行之后,可能之前还有其他文字
        let str = this.input.slice(this.pos, from) + '<code>'

        this.lineStack.push('</code>')
        this.pos = to
        this.inline = {from, to, is: true}

        return str
    }

    // 强调
    mdEmphasis({from, to})
    {
        let str = this.input.slice(this.pos, from) + '<em>'

        this.lineStack.push('</em>')
        this.pos = to

        return str
    }

    // 强调
    mdStrongEmphasis({from, to})
    {
        let str = this.input.slice(this.pos, from) + '<strong>'

        this.lineStack.push('</strong>')
        this.pos = to

        return str
    }

    mdEmphasisMark({from, to})
    {
        // 每一个行级标签都后都是紧跟着一个push 操作,其他情况下视为 pop操作
        let str = ''

        let preName = this.prev.name

        if(preName !== 'StrongEmphasis' && preName !== 'Emphasis') {
            str += this.input.slice(this.pos, from)
            str += this.lineStack.pop()
        }

        this.pos = to
        return str
    }

    // 删除线 (拓展)
    mdStrikethrough({from, to})
    {
        let str = this.input.slice(this.pos, from) + '<del>'

        this.lineStack.push('</del>')
        this.pos = to

        return str
    }

    mdStrikethroughMark({from, to})
    {
        let str = ''

        // 每一个行级标签都后都是紧跟着一个push 操作,其他情况下视为 pop操作
        if(this.prev.name !== 'Strikethrough') {
            str += this.input.slice(this.pos, from)
            str += this.lineStack.pop()
        }

        this.pos = to

        return str
    }

    //------------------------------------------------
    // link 和img 放在一起,他们有共用的部分
    //------------------------------------------------
    // 链接, 链接不能嵌套链接
    // 链接包含链接文本(可见文本),目标链接(作为目标链接的 URI),以及可选的链接标题。
    // Markdown 中有两种基本类型的链接。
    // 在内联链接中,目标和标题在链接文本后立即给出。
    // 在引用链接中,目标和标题在文档的其他地方定义。
    // 链接的情况比较复杂,稍后在补充 no-ok
    // [link [foo [bar]]](/uri) // 未能做正确解析
    // [![moon](moon.jpg)](/uri) // 链接嵌套img
    mdLink({from, to})
    {
        if(this.inImg) {
            this.nt.nt = true
            this.nt.txt += ' ' + this.input.slice(this.prev.to, from).trim()
            return
        }

        let str = this.input.slice(this.pos, from)

        // 标记当前进入行模式
        // 记录信息,在闭合时在拼接出信息
        this.linkInfo = {
            from, to, url: '', title: '', label: '', img: '',
        }

        this.inLink = true
        this.pos = to

        this.output += str
        // 因开启了 inLink 不能直接返回str
        // return str
    }

    // 这是一个闭合块,不需要在文档中显示,给引用链接做参数
    // 记录位置信息和name
    mdLinkReference({from, to, name})
    {
        // 先关闭之前的块,在记录本次块的信息
        let str = this._closePrevBlock(from)

        this._enterBlock(from, to, name, '')

        // 接下来会经历 -> LinkLabel -> LinkMark -> URL -> LinkTitle
        // 开启
        this.inRef = true

        // 每次进入会初始化一下
        this.refInfo = {
            from, to,
            label: '', content: '', url: '', title: '', init: true, readonly: true
        }

        // 因开启了 inRef 不能直接返回str
        this.output += str
    }

    // 图片 同链接的情况一样复杂,有二种表现形式,一种正常在一行内,别一种引用式的 稍后在做处理
    mdImage({from, to})
    {
        // 图片不能嵌套图片或者链接,至少是第二次进入
        if(this.inImg) {
            this.nt.nt = true
            this.nt.txt += ' ' + this.input.slice(this.prev.to, from).trim()
            return
        }

        let str = this.input.slice(this.pos, from)

        // 标记当前进入行模式
        this.imgInfo = {
            from, to, alt: '', title: '', url: '',
        }
        // 以下几个用于嵌套
        this.nt = { pos: 0, nesting: 0, nt: false, txt: ''}
        this.inImg = true
        this.pos = to

        this.output += str
        // 因开启了 inImg 不能直接返回str
        // return str
    }

    mdAutolink({from, to})
    {
        let str = this.input.slice(this.pos, from)

        // this.inAutoLink = true
        this.pos = to

        return str
    }

    mdLinkLabel({from, to})
    {
        let label = this.input.slice(from, to)

        if(this.inRef) {
            this.refInfo.label = label
        }
        else {

            // 不是在引用中,而是在 url中,这种情况稍后在处理
            let str = this.input.slice(this.linkInfo.from, to)
            this.linkInfo.as = str

            this.linkInfo.label = label === '[]'
                ? this.input.slice(this.linkInfo.from, from)
                : label

            this.linkInfo.readonly = false
            this._storeToRefsMap(this.linkInfo)
            this.output += str
            this.pos = to
        }
    }

    mdLinkMark({from, to})
    {
        if(this.inRef) {
            return  // 不做任何事
        }

        let mark = this.input.slice(from, to)

        // mark === [ 或 ( ![
        if(mark === '![' || mark === '['){
            // 标记链接开始, 分别放在了 img 和 link 中
            // 不做位置调整, 为了获取 [] 之间的内容
            if(this.nt.nt) {
                this.nt.pos = to
                this.nt.nesting++
            }
            return
        }

        let str = ''

        if(mark === ']') {

            if(this.nt.nesting > 0) {
                this.nt.txt += ' ' + this.input.slice(this.nt.pos, from).trim()
                this.nt.nesting--
                return
            }
            else if(this.nt.nt) {
                this.nt.txt += ' ' + this.input.slice(this.prev.to, from).trim()
            }

            // 提前结束, 这里会和 refUrls 配合使用,如果有的话
            if(this.inImg) {
                str = this.input.slice(this.imgInfo.from, to)

                if(to === this.imgInfo.to) {
                    this._storeToRefsMap({label: str, readonly: false})
                    this.inImg = false
                }
                else {
                    this.imgInfo.label = str
                }
            }
            else {
                str = this.input.slice(this.linkInfo.from, to)

                if(to === this.linkInfo.to) {
                    this._storeToRefsMap({label: str, readonly: false})

                    this.inLink = false
                }
                else {
                    this.linkInfo.label = str
                }
            }
        }
        else if(mark === ')') {
            // 结束
            if(this.inImg) {
                // 说明有嵌套,未结束
                if(to !== this.imgInfo.to) {
                    return
                }

                if(this.inLink) {
                    this.linkInfo.img = this._makeImg(this.imgInfo)
                }
                else {
                    str = this._makeImg(this.imgInfo)
                }

                this.inImg = false
            }
            else {
                str = this._makeLink(this.linkInfo)

                this.inLink = false
            }
        }

        this.pos = to

        return str
    }

    // 这里 lezer md 未做详细解析
    // 可以试着启用一个子程度来对此片段进行解析
    mdURL({from, to})
    {
        let url = this.input.slice(from, to)

        if(this.inRef) {
            this.refInfo.url = url

            // 可能会提前结束
            if(to === this.refInfo.to) {
                this._storeToRefsMap(this.refInfo)
                this.inRef = false
                this.pos = to
            }

            return
        }

        if(this.inImg) {
            this.imgInfo.url = url
        }
        else if(this.inLink) {
            this.linkInfo.url = url
        }
        else if(this.prev.name === 'LinkMark') {

            this.pos = to
            return this._makeAutolink(url)
        }
        else {
            let str = this.input.slice(this.pos, from)
            this.pos = to
            return str + this._makeAutolink(url)
        }
    }

    // 这里需要进行下详细判断,比如(title), 'title', "title", 是否要做区分
    // 参考mdURl中的参考
    mdLinkTitle({from, to})
    {
        let title = this.input.slice(from, to)

        if(this.inRef) {
            this.refInfo.title = title

            this._storeToRefsMap(this.refInfo)
            this.inRef = false
            this.pos = to
            return
        }

        if(this.inImg) {
            this.imgInfo.title = title
        }
        else {
            this.linkInfo.title = title
        }
    }

    // 处理指令块
    mdProcessingInstructionBlock({from, to})
    {
        let str = this.input.slice(this.pos, to)

        this.pos = to

        return str
    }

    // 处理指令
    mdProcessingInstruction({from, to})
    {
        let str = this.input.slice(this.pos, to)

        this.pos = to

        return str
    }

    // 强制换行
    mdHardBreak({from, to})
    {
        let str = this.input.slice(this.pos, from)

        str += '<br />'

        this.pos = to

        return str
    }

    //----------------------------------------------e 内联inline,行级别的

    // 存储到 map 中
    _storeToRefsMap({
                        label, url = '', title= '', init = false,
                        as = '', img = '', readonly = true } = {})
    {
        let key = normalizeReference(label)

        let ref

        if(this.refUrls.has(key)) {

            ref = this.refUrls.get(key)

            // 是否初始化
            if(init && !ref.init) {
                ref.init = true
                ref.url = url
                ref.title = title
            }

            img && (ref.img = img)
            !readonly && (ref.readonly = false)
        }
        else {

            // 第一次初始化
            ref = {
                url, title, label, init, img, readonly,
                refs: new Set
            }
        }

        ref.refs.add(label)

        as && ref.refs.add(as)

        this.refUrls.set(key, ref)
    }

    _makeImg({url, title, label})
    {
        let img = '<img src="' + url + '"'

        if(title) {
            img += ' title=' + title
        }

        if(label) {
            // CmdRender.renderLine(label.slice(2, -1).trim())
            // 正则提取内容 因为可能是嵌套
            let alt

            if(this.nt.nt) {
                alt = this.nt.txt.trim()
                this.nt.nt = false
                this.nt.txt = ''
            }
            else {
                alt = label.slice(2, -1).trim()
            }

            alt = CmdRender.renderLine(alt)

            img += ' alt="' + alt + '"'
        }

        return img + ' />'
    }

    // 处理 链接中 [] 或者 ![] 之间的内容
    _linkContent(str, mark = true, {img = ''} = {})
    {
        let start = str.startsWith('![') ? 2 : 1

        str = CmdRender.renderLine(str.slice(start, -1)).trim()

        if(!mark) {
            return str
        }

        if(img) {
            str = str.replace(/^!\[.+\]\(.+\)/i, img)
        }

        return start === 2
                ? '![' + str + ']'
                : '[' + str + ']'
    }

    _makeAutolink(url)
    {
        return '<a href="' + url + '">' + url + '</a>'
    }

    _makeLink({url, title, label, img}, content = null)
    {
        let link = '<a href="' + url + '"'

        if(title) {
            link += ' title=' + title
        }

        link += '>'

        content = content
            ? content.slice(1, -1).trim()
            : label.slice(1, -1).trim()

        if(/^!\[.+\]\(.+\)$/.test(content) && img) {
            link += img
        }
        else {
            link += CmdRender.renderLine(content)
        }

        link += '</a>'

        return link
    }

    _enterBlock(from, to, name, tag)
    {
        this.close = {
            from, to, name, tag
        }
        this.pos = from
        this.stack.push(this.close)
    }

    // 不要改变end的值
    _closePrevBlock(end)
    {
        let str = ''

        // 这里的 close 是上一次的块
        let { to, tag, name } = this.close

        if(end >=  to) {

            // 先处理最后一个闭合, 块之间的文本
            if(name === 'SetextHeading1' || name === 'SetextHeading2') {
                // 闭合setext heading 需要跳过之后的 HeaderMark 字符的长度
                let arr = this.input.slice(this.pos, end).trim().split('\n')
                for (let i = 0; i < arr.length - 1; i++) {
                    str += arr[i]
                }
                this.pos = to
            }
            else {
                str += this.input.slice(this.pos, end) // 是否要加上? 行级的不加, 块级的需测试
            }

            if(name === 'CodeBlock' || name === 'FencedCode') {
                str = str.trim()
            }

            str = escapeHtml(str)

            str += tag
            // 消耗了一个close, 弹出
            this.stack.pop()

            // 将 stack 中最后一个设置为当前 close
            this.close = this.stack[this.stack.length - 1] // 加载前一个位置,
            // this.pos = to

            while (this.close && end >= this.close.to) {

                if(this.close.name === 'CodeBlock') {
                    str = str.trim()
                }

                str += this.close.tag

                this.stack.pop()
                this.close = this.stack[this.stack.length - 1]
            }
        }

        return str
    }

    _isLine(name)
    {
        return name.startsWith('ATXHeading')
            || name.startsWith('SetextHeading')
            || [
                'Escape', 'Entity', 'HardBreak', 'Emphasis',
                'StrongEmphasis', 'HeaderMark', 'EmphasisMark',
            ].includes(name)
    }
}

function htmlToPlainText(html) {
    return html.replace(/<[^>]*>/g, '')
}

// from markdown-it
const HTML_ESCAPE_TEST_RE = /[&<>"]/
const HTML_ESCAPE_REPLACE_RE = /[&<>"]/g
const HTML_REPLACEMENTS = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;'
}

function replaceUnsafeChar (ch) {
    return HTML_REPLACEMENTS[ch]
}

function escapeHtml (str) {
    if (HTML_ESCAPE_TEST_RE.test(str)) {
        return str.replace(HTML_ESCAPE_REPLACE_RE, replaceUnsafeChar)
    }
    return str
}

function normalizeReference (str) {

    let start = str.startsWith('![') ? 2 : 1

    // Trim and collapse whitespace
    str = str.slice(start, -1).trim().replace(/\s+/g, ' ')

    if ('ẞ'.toLowerCase() === 'Ṿ') {
        str = str.replace(/ẞ/g, 'ß')
    }

    return '[' + str.toLowerCase().toUpperCase() + ']'
    //return str.toLowerCase().toUpperCase()
}