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
---------------------------------------------------------- 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) // 未能做正确解析
// [](/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 = {
'&': '&',
'<': '<',
'>': '>',
'"': '"'
}
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()
}