From 1b673f868984b8eca321ec83b18b5bcc57e4a34d Mon Sep 17 00:00:00 2001 From: Liang Ding Date: Wed, 11 Mar 2020 21:06:07 +0800 Subject: [PATCH] =?UTF-8?q?:art:=20Vditor=20=E6=94=AF=E6=8C=81=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E7=B1=BB=E4=BC=BC=20Typora=20=E7=9A=84=E5=8F=8A?= =?UTF-8?q?=E6=97=B6=E6=B8=B2=E6=9F=93=E6=A8=A1=E5=BC=8F=EF=BC=88=E4=BF=9D?= =?UTF-8?q?=E7=95=99=20Markdown=20=E6=A0=87=E8=AE=B0=E7=AC=A6=EF=BC=89=20h?= =?UTF-8?q?ttps://github.com/Vanessa219/vditor/issues/27?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- render/vditor_renderer.go | 2 +- render/vditorir_renderer.go | 839 ++++++++++++++++++++++++++++++++++++ 2 files changed, 840 insertions(+), 1 deletion(-) create mode 100644 render/vditorir_renderer.go diff --git a/render/vditor_renderer.go b/render/vditor_renderer.go index 86cb703979..86573ba1f4 100644 --- a/render/vditor_renderer.go +++ b/render/vditor_renderer.go @@ -29,7 +29,7 @@ type VditorRenderer struct { needRenderFootnotesDef bool } -// NewVditorRenderer 创建一个 HTML 渲染器。 +// NewVditorRenderer 创建一个 Vditor DOM 渲染器。 func NewVditorRenderer(tree *parse.Tree) *VditorRenderer { ret := &VditorRenderer{BaseRenderer: NewBaseRenderer(tree)} ret.RendererFuncs[ast.NodeDocument] = ret.renderDocument diff --git a/render/vditorir_renderer.go b/render/vditorir_renderer.go new file mode 100644 index 0000000000..2b4283c65c --- /dev/null +++ b/render/vditorir_renderer.go @@ -0,0 +1,839 @@ +// Lute - 一款对中文语境优化的 Markdown 引擎,支持 Go 和 JavaScript +// Copyright (c) 2019-present, b3log.org +// +// Lute is licensed under Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +// See the Mulan PSL v2 for more details. + +package render + +import ( + "bytes" + "strconv" + "strings" + "unicode" + "unicode/utf8" + + "github.com/88250/lute/ast" + "github.com/88250/lute/lex" + "github.com/88250/lute/parse" + "github.com/88250/lute/util" +) + +// VditorIRRenderer 描述了 Vditor Instant-Rendering DOM 渲染器。 +type VditorIRRenderer struct { + *BaseRenderer + needRenderFootnotesDef bool +} + +// NewVditorIRRenderer 创建一个 Vditor Instant-Rendering DOM 渲染器。 +func NewVditorIRRenderer(tree *parse.Tree) *VditorIRRenderer { + ret := &VditorIRRenderer{BaseRenderer: NewBaseRenderer(tree)} + ret.RendererFuncs[ast.NodeDocument] = ret.renderDocument + ret.RendererFuncs[ast.NodeParagraph] = ret.renderParagraph + ret.RendererFuncs[ast.NodeText] = ret.renderText + ret.RendererFuncs[ast.NodeCodeSpan] = ret.renderCodeSpan + ret.RendererFuncs[ast.NodeCodeSpanOpenMarker] = ret.renderCodeSpanOpenMarker + ret.RendererFuncs[ast.NodeCodeSpanContent] = ret.renderCodeSpanContent + ret.RendererFuncs[ast.NodeCodeSpanCloseMarker] = ret.renderCodeSpanCloseMarker + ret.RendererFuncs[ast.NodeCodeBlock] = ret.renderCodeBlock + ret.RendererFuncs[ast.NodeCodeBlockFenceOpenMarker] = ret.renderCodeBlockOpenMarker + ret.RendererFuncs[ast.NodeCodeBlockFenceInfoMarker] = ret.renderCodeBlockInfoMarker + ret.RendererFuncs[ast.NodeCodeBlockCode] = ret.renderCodeBlockCode + ret.RendererFuncs[ast.NodeCodeBlockFenceCloseMarker] = ret.renderCodeBlockCloseMarker + ret.RendererFuncs[ast.NodeMathBlock] = ret.renderMathBlock + ret.RendererFuncs[ast.NodeMathBlockOpenMarker] = ret.renderMathBlockOpenMarker + ret.RendererFuncs[ast.NodeMathBlockContent] = ret.renderMathBlockContent + ret.RendererFuncs[ast.NodeMathBlockCloseMarker] = ret.renderMathBlockCloseMarker + ret.RendererFuncs[ast.NodeInlineMath] = ret.renderInlineMath + ret.RendererFuncs[ast.NodeInlineMathOpenMarker] = ret.renderInlineMathOpenMarker + ret.RendererFuncs[ast.NodeInlineMathContent] = ret.renderInlineMathContent + ret.RendererFuncs[ast.NodeInlineMathCloseMarker] = ret.renderInlineMathCloseMarker + ret.RendererFuncs[ast.NodeEmphasis] = ret.renderEmphasis + ret.RendererFuncs[ast.NodeEmA6kOpenMarker] = ret.renderEmAsteriskOpenMarker + ret.RendererFuncs[ast.NodeEmA6kCloseMarker] = ret.renderEmAsteriskCloseMarker + ret.RendererFuncs[ast.NodeEmU8eOpenMarker] = ret.renderEmUnderscoreOpenMarker + ret.RendererFuncs[ast.NodeEmU8eCloseMarker] = ret.renderEmUnderscoreCloseMarker + ret.RendererFuncs[ast.NodeStrong] = ret.renderStrong + ret.RendererFuncs[ast.NodeStrongA6kOpenMarker] = ret.renderStrongA6kOpenMarker + ret.RendererFuncs[ast.NodeStrongA6kCloseMarker] = ret.renderStrongA6kCloseMarker + ret.RendererFuncs[ast.NodeStrongU8eOpenMarker] = ret.renderStrongU8eOpenMarker + ret.RendererFuncs[ast.NodeStrongU8eCloseMarker] = ret.renderStrongU8eCloseMarker + ret.RendererFuncs[ast.NodeBlockquote] = ret.renderBlockquote + ret.RendererFuncs[ast.NodeBlockquoteMarker] = ret.renderBlockquoteMarker + ret.RendererFuncs[ast.NodeHeading] = ret.renderHeading + ret.RendererFuncs[ast.NodeHeadingC8hMarker] = ret.renderHeadingC8hMarker + ret.RendererFuncs[ast.NodeList] = ret.renderList + ret.RendererFuncs[ast.NodeListItem] = ret.renderListItem + ret.RendererFuncs[ast.NodeThematicBreak] = ret.renderThematicBreak + ret.RendererFuncs[ast.NodeHardBreak] = ret.renderHardBreak + ret.RendererFuncs[ast.NodeSoftBreak] = ret.renderSoftBreak + ret.RendererFuncs[ast.NodeHTMLBlock] = ret.renderHTML + ret.RendererFuncs[ast.NodeInlineHTML] = ret.renderInlineHTML + ret.RendererFuncs[ast.NodeLink] = ret.renderLink + ret.RendererFuncs[ast.NodeImage] = ret.renderImage + ret.RendererFuncs[ast.NodeBang] = ret.renderBang + ret.RendererFuncs[ast.NodeOpenBracket] = ret.renderOpenBracket + ret.RendererFuncs[ast.NodeCloseBracket] = ret.renderCloseBracket + ret.RendererFuncs[ast.NodeOpenParen] = ret.renderOpenParen + ret.RendererFuncs[ast.NodeCloseParen] = ret.renderCloseParen + ret.RendererFuncs[ast.NodeLinkText] = ret.renderLinkText + ret.RendererFuncs[ast.NodeLinkSpace] = ret.renderLinkSpace + ret.RendererFuncs[ast.NodeLinkDest] = ret.renderLinkDest + ret.RendererFuncs[ast.NodeLinkTitle] = ret.renderLinkTitle + ret.RendererFuncs[ast.NodeStrikethrough] = ret.renderStrikethrough + ret.RendererFuncs[ast.NodeStrikethrough1OpenMarker] = ret.renderStrikethrough1OpenMarker + ret.RendererFuncs[ast.NodeStrikethrough1CloseMarker] = ret.renderStrikethrough1CloseMarker + ret.RendererFuncs[ast.NodeStrikethrough2OpenMarker] = ret.renderStrikethrough2OpenMarker + ret.RendererFuncs[ast.NodeStrikethrough2CloseMarker] = ret.renderStrikethrough2CloseMarker + ret.RendererFuncs[ast.NodeTaskListItemMarker] = ret.renderTaskListItemMarker + ret.RendererFuncs[ast.NodeTable] = ret.renderTable + ret.RendererFuncs[ast.NodeTableHead] = ret.renderTableHead + ret.RendererFuncs[ast.NodeTableRow] = ret.renderTableRow + ret.RendererFuncs[ast.NodeTableCell] = ret.renderTableCell + ret.RendererFuncs[ast.NodeEmoji] = ret.renderEmoji + ret.RendererFuncs[ast.NodeEmojiUnicode] = ret.renderEmojiUnicode + ret.RendererFuncs[ast.NodeEmojiImg] = ret.renderEmojiImg + ret.RendererFuncs[ast.NodeEmojiAlias] = ret.renderEmojiAlias + ret.RendererFuncs[ast.NodeFootnotesDef] = ret.renderFootnotesDef + ret.RendererFuncs[ast.NodeFootnotesRef] = ret.renderFootnotesRef + ret.RendererFuncs[ast.NodeToC] = ret.renderToC + ret.RendererFuncs[ast.NodeBackslash] = ret.renderBackslash + ret.RendererFuncs[ast.NodeBackslashContent] = ret.renderBackslashContent + return ret +} + +func (r *VditorIRRenderer) Render() (output []byte) { + output = r.BaseRenderer.Render() + if 1 > len(r.Tree.Context.LinkRefDefs) || r.needRenderFootnotesDef { + return + } + + // 将链接引用定义添加到末尾 + r.WriteString("
") + for _, node := range r.Tree.Context.LinkRefDefs { + label := node.LinkRefLabel + dest := node.ChildByType(ast.NodeLinkDest).Tokens + destStr := util.BytesToStr(dest) + r.WriteString("[" + util.BytesToStr(label) + "]:") + if parse.Caret != destStr { + r.WriteString(" ") + } + r.WriteString(destStr + "\n") + } + r.WriteString("
") + output = r.Writer.Bytes() + return +} + +func (r *VditorIRRenderer) RenderFootnotesDefs(context *parse.Context) []byte { + r.WriteString("
") + r.WriteString("
    ") + for _, def := range context.FootnotesDefs { + r.WriteString("
  1. ") + tree := &parse.Tree{Name: "", Context: context} + tree.Context.Tree = tree + tree.Root = &ast.Node{Type: ast.NodeDocument} + tree.Root.AppendChild(def) + defRenderer := NewVditorIRRenderer(tree) + defRenderer.needRenderFootnotesDef = true + defContent := defRenderer.Render() + r.Write(defContent) + r.WriteString("
  2. ") + } + r.WriteString("
") + return r.Writer.Bytes() +} + +func (r *VditorIRRenderer) renderBackslashContent(node *ast.Node, entering bool) ast.WalkStatus { + r.Write(util.EscapeHTML(node.Tokens)) + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderBackslash(node *ast.Node, entering bool) ast.WalkStatus { + if entering { + r.WriteString("") + r.WriteString("") + r.WriteByte(lex.ItemBackslash) + r.WriteString("") + } else { + r.WriteString("") + } + return ast.WalkContinue +} + +func (r *VditorIRRenderer) renderToC(node *ast.Node, entering bool) ast.WalkStatus { + headings := r.headings() + length := len(headings) + r.WriteString("
") + if 0 < length { + for _, heading := range headings { + spaces := (heading.HeadingLevel - 1) * 2 + r.WriteString(strings.Repeat(" ", spaces)) + r.WriteString("") + r.WriteString(heading.Text() + "
") + } + } else { + r.WriteString("[toc]
") + } + r.WriteString("
") + caretInDest := bytes.Contains(node.Tokens, []byte(parse.Caret)) + r.WriteString("

") + if caretInDest { + r.WriteString(parse.Caret) + } + r.WriteString("

") + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderFootnotesDef(node *ast.Node, entering bool) ast.WalkStatus { + if !r.needRenderFootnotesDef { + return ast.WalkStop + } + return ast.WalkContinue +} + +func (r *VditorIRRenderer) renderFootnotesRef(node *ast.Node, entering bool) ast.WalkStatus { + previousNodeText := node.PreviousNodeText() + previousNodeText = strings.ReplaceAll(previousNodeText, parse.Caret, "") + if "" == previousNodeText { + r.WriteString(parse.Zwsp) + } + idx, _ := r.Tree.Context.FindFootnotesDef(node.Tokens) + idxStr := strconv.Itoa(idx) + r.tag("sup", [][]string{{"data-type", "footnotes-ref"}, {"data-footnotes-label", string(node.FootnotesRefLabel)}}, false) + r.WriteString(idxStr) + r.WriteString("" + parse.Zwsp) + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderCodeBlockCloseMarker(node *ast.Node, entering bool) ast.WalkStatus { + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderCodeBlockInfoMarker(node *ast.Node, entering bool) ast.WalkStatus { + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderCodeBlockOpenMarker(node *ast.Node, entering bool) ast.WalkStatus { + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderEmojiAlias(node *ast.Node, entering bool) ast.WalkStatus { + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderEmojiImg(node *ast.Node, entering bool) ast.WalkStatus { + r.Write(node.Tokens) + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderEmojiUnicode(node *ast.Node, entering bool) ast.WalkStatus { + r.Write(node.Tokens) + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderEmoji(node *ast.Node, entering bool) ast.WalkStatus { + return ast.WalkContinue +} + +func (r *VditorIRRenderer) renderInlineMathCloseMarker(node *ast.Node, entering bool) ast.WalkStatus { + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderInlineMathContent(node *ast.Node, entering bool) ast.WalkStatus { + r.WriteString("") + r.tag("code", [][]string{{"data-type", "math-inline"}}, false) + tokens := bytes.ReplaceAll(node.Tokens, []byte(parse.Zwsp), []byte("")) + tokens = util.EscapeHTML(tokens) + tokens = append([]byte(parse.Zwsp), tokens...) + r.Write(tokens) + r.WriteString("" + parse.Zwsp) + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderInlineMathOpenMarker(node *ast.Node, entering bool) ast.WalkStatus { + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderInlineMath(node *ast.Node, entering bool) ast.WalkStatus { + if entering { + previousNodeText := node.PreviousNodeText() + previousNodeText = strings.ReplaceAll(previousNodeText, parse.Caret, "") + if "" == previousNodeText { + r.WriteString(parse.Zwsp) + } + } + return ast.WalkContinue +} + +func (r *VditorIRRenderer) renderMathBlockCloseMarker(node *ast.Node, entering bool) ast.WalkStatus { + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderMathBlockContent(node *ast.Node, entering bool) ast.WalkStatus { + node.Tokens = bytes.TrimSpace(node.Tokens) + codeLen := len(node.Tokens) + codeIsEmpty := 1 > codeLen || (len(parse.Caret) == codeLen && parse.Caret == string(node.Tokens)) + r.WriteString("
")
+	r.tag("code", [][]string{{"data-type", "math-block"}}, false)
+	if codeIsEmpty {
+		r.WriteString("\n")
+	} else {
+		r.Write(util.EscapeHTML(node.Tokens))
+	}
+	r.WriteString("
") + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderMathBlockOpenMarker(node *ast.Node, entering bool) ast.WalkStatus { + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderMathBlock(node *ast.Node, entering bool) ast.WalkStatus { + if entering { + r.WriteString(`
`) + } else { + r.WriteString("
") + } + return ast.WalkContinue +} + +func (r *VditorIRRenderer) renderTableCell(node *ast.Node, entering bool) ast.WalkStatus { + tag := "td" + if ast.NodeTableHead == node.Parent.Parent.Type { + tag = "th" + } + if entering { + var attrs [][]string + switch node.TableCellAlign { + case 1: + attrs = append(attrs, []string{"align", "left"}) + case 2: + attrs = append(attrs, []string{"align", "center"}) + case 3: + attrs = append(attrs, []string{"align", "right"}) + } + r.tag(tag, attrs, false) + if nil == node.FirstChild { + node.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(" ")}) + } else if bytes.Equal(node.FirstChild.Tokens, []byte(parse.Caret)) { + node.FirstChild.Tokens = []byte(parse.Caret + " ") + } else { + node.FirstChild.Tokens = bytes.TrimSpace(node.FirstChild.Tokens) + } + } else { + r.tag("/"+tag, nil, false) + } + return ast.WalkContinue +} + +func (r *VditorIRRenderer) renderTableRow(node *ast.Node, entering bool) ast.WalkStatus { + if entering { + r.tag("tr", nil, false) + } else { + r.tag("/tr", nil, false) + } + return ast.WalkContinue +} + +func (r *VditorIRRenderer) renderTableHead(node *ast.Node, entering bool) ast.WalkStatus { + if entering { + r.tag("thead", nil, false) + } else { + r.tag("/thead", nil, false) + if nil != node.Next { + r.tag("tbody", nil, false) + } + } + return ast.WalkContinue +} + +func (r *VditorIRRenderer) renderTable(node *ast.Node, entering bool) ast.WalkStatus { + if entering { + r.tag("table", [][]string{{"data-block", "0"}}, false) + } else { + if nil != node.FirstChild.Next { + r.tag("/tbody", nil, false) + } + r.tag("/table", nil, false) + } + return ast.WalkContinue +} + +func (r *VditorIRRenderer) renderStrikethrough(node *ast.Node, entering bool) ast.WalkStatus { + return ast.WalkContinue +} + +func (r *VditorIRRenderer) renderStrikethrough1OpenMarker(node *ast.Node, entering bool) ast.WalkStatus { + r.tag("s", [][]string{{"data-marker", "~"}}, false) + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderStrikethrough1CloseMarker(node *ast.Node, entering bool) ast.WalkStatus { + r.tag("/s", nil, false) + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderStrikethrough2OpenMarker(node *ast.Node, entering bool) ast.WalkStatus { + r.tag("s", [][]string{{"data-marker", "~~"}}, false) + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderStrikethrough2CloseMarker(node *ast.Node, entering bool) ast.WalkStatus { + r.tag("/s", nil, false) + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderLinkTitle(node *ast.Node, entering bool) ast.WalkStatus { + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderLinkDest(node *ast.Node, entering bool) ast.WalkStatus { + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderLinkSpace(node *ast.Node, entering bool) ast.WalkStatus { + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderLinkText(node *ast.Node, entering bool) ast.WalkStatus { + r.Write(node.Tokens) + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderCloseParen(node *ast.Node, entering bool) ast.WalkStatus { + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderOpenParen(node *ast.Node, entering bool) ast.WalkStatus { + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderCloseBracket(node *ast.Node, entering bool) ast.WalkStatus { + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderOpenBracket(node *ast.Node, entering bool) ast.WalkStatus { + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderBang(node *ast.Node, entering bool) ast.WalkStatus { + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderImage(node *ast.Node, entering bool) ast.WalkStatus { + if entering { + if 0 == r.DisableTags { + r.WriteString("\"")") + } + return ast.WalkContinue +} + +func (r *VditorIRRenderer) renderLink(node *ast.Node, entering bool) ast.WalkStatus { + if entering { + if 3 == node.LinkType { + previousNodeText := node.PreviousNodeText() + previousNodeText = strings.ReplaceAll(previousNodeText, parse.Caret, "") + if "" == previousNodeText { + r.WriteString(parse.Zwsp) + } + text := string(node.ChildByType(ast.NodeLinkText).Tokens) + label := string(node.LinkRefLabel) + attrs := [][]string{{"data-type", "link-ref"}, {"data-link-label", label}} + r.tag("span", attrs, false) + r.WriteString(text) + r.tag("/span", nil, false) + r.WriteString(parse.Zwsp) + return ast.WalkStop + } + + dest := node.ChildByType(ast.NodeLinkDest) + destTokens := dest.Tokens + destTokens = r.Tree.Context.RelativePath(destTokens) + caretInDest := bytes.Contains(destTokens, []byte(parse.Caret)) + if caretInDest { + text := node.ChildByType(ast.NodeLinkText) + text.Tokens = append(text.Tokens, []byte(parse.Caret)...) + destTokens = bytes.ReplaceAll(destTokens, []byte(parse.Caret), []byte("")) + } + attrs := [][]string{{"href", string(destTokens)}} + if title := node.ChildByType(ast.NodeLinkTitle); nil != title && nil != title.Tokens { + title.Tokens = bytes.ReplaceAll(title.Tokens, []byte(parse.Caret), []byte("")) + attrs = append(attrs, []string{"title", string(title.Tokens)}) + } + r.tag("a", attrs, false) + } else { + r.tag("/a", nil, false) + } + return ast.WalkContinue +} + +func (r *VditorIRRenderer) renderHTML(node *ast.Node, entering bool) ast.WalkStatus { + r.WriteString(`
`) + node.Tokens = bytes.TrimSpace(node.Tokens) + r.WriteString("
")
+	r.tag("code", nil, false)
+	r.Write(util.EscapeHTML(node.Tokens))
+	r.WriteString("
") + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderInlineHTML(node *ast.Node, entering bool) ast.WalkStatus { + if bytes.Equal(node.Tokens, []byte("
")) && node.ParentIs(ast.NodeTableCell) { + r.Write(node.Tokens) + return ast.WalkStop + } + + if entering { + previousNodeText := node.PreviousNodeText() + previousNodeText = strings.ReplaceAll(previousNodeText, parse.Caret, "") + if "" == previousNodeText { + r.WriteString(parse.Zwsp) + } + } + + r.WriteString("") + node.Tokens = bytes.TrimSpace(node.Tokens) + r.tag("code", [][]string{{"data-type", "html-inline"}}, false) + tokens := bytes.ReplaceAll(node.Tokens, []byte(parse.Zwsp), []byte("")) + tokens = util.EscapeHTML(tokens) + tokens = append([]byte(parse.Zwsp), tokens...) + r.Write(tokens) + r.WriteString("" + parse.Zwsp) + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderDocument(node *ast.Node, entering bool) ast.WalkStatus { + return ast.WalkContinue +} + +func (r *VditorIRRenderer) renderParagraph(node *ast.Node, entering bool) ast.WalkStatus { + if grandparent := node.Parent.Parent; nil != grandparent && ast.NodeList == grandparent.Type && grandparent.Tight { // List.ListItem.Paragraph + return ast.WalkContinue + } + + if entering { + r.tag("p", [][]string{{"data-block", "0"}}, false) + } else { + r.WriteByte(lex.ItemNewline) + r.tag("/p", nil, false) + } + return ast.WalkContinue +} + +func (r *VditorIRRenderer) renderText(node *ast.Node, entering bool) ast.WalkStatus { + if r.Option.AutoSpace { + r.Space(node) + } + if r.Option.FixTermTypo { + r.FixTermTypo(node) + } + if r.Option.ChinesePunct { + r.ChinesePunct(node) + } + + node.Tokens = bytes.TrimRight(node.Tokens, "\n") + // 有的场景需要零宽空格撑起,但如果有其他文本内容的话需要把零宽空格删掉 + if !bytes.EqualFold(node.Tokens, []byte(parse.Caret+parse.Zwsp)) { + node.Tokens = bytes.ReplaceAll(node.Tokens, []byte(parse.Zwsp), []byte("")) + } + r.Write(util.EscapeHTML(node.Tokens)) + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderCodeSpan(node *ast.Node, entering bool) ast.WalkStatus { + if entering { + previousNodeText := node.PreviousNodeText() + previousNodeText = strings.ReplaceAll(previousNodeText, parse.Caret, "") + if "" == previousNodeText { + r.WriteString(parse.Zwsp) + } else { + lastc, _ := utf8.DecodeLastRuneInString(previousNodeText) + if unicode.IsLetter(lastc) || unicode.IsDigit(lastc) { + r.WriteByte(lex.ItemSpace) + } + } + r.tag("code", [][]string{{"data-marker", strings.Repeat("`", node.CodeMarkerLen)}}, false) + } + return ast.WalkContinue +} + +func (r *VditorIRRenderer) renderCodeSpanOpenMarker(node *ast.Node, entering bool) ast.WalkStatus { + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderCodeSpanContent(node *ast.Node, entering bool) ast.WalkStatus { + tokens := bytes.ReplaceAll(node.Tokens, []byte(parse.Zwsp), []byte("")) + tokens = util.EscapeHTML(tokens) + tokens = append([]byte(parse.Zwsp), tokens...) + r.Write(tokens) + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderCodeSpanCloseMarker(node *ast.Node, entering bool) ast.WalkStatus { + r.WriteString("") + codeSpan := node.Parent + if codeSpanParent := codeSpan.Parent; nil != codeSpanParent && ast.NodeLink == codeSpanParent.Type { + return ast.WalkStop + } + r.WriteString(parse.Zwsp) + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderEmphasis(node *ast.Node, entering bool) ast.WalkStatus { + return ast.WalkContinue +} + +func (r *VditorIRRenderer) renderEmAsteriskOpenMarker(node *ast.Node, entering bool) ast.WalkStatus { + r.tag("em", [][]string{{"data-marker", "*"}}, false) + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderEmAsteriskCloseMarker(node *ast.Node, entering bool) ast.WalkStatus { + r.tag("/em", nil, false) + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderEmUnderscoreOpenMarker(node *ast.Node, entering bool) ast.WalkStatus { + r.tag("em", [][]string{{"data-marker", "_"}}, false) + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderEmUnderscoreCloseMarker(node *ast.Node, entering bool) ast.WalkStatus { + r.tag("/em", nil, false) + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderStrong(node *ast.Node, entering bool) ast.WalkStatus { + return ast.WalkContinue +} + +func (r *VditorIRRenderer) renderStrongA6kOpenMarker(node *ast.Node, entering bool) ast.WalkStatus { + r.tag("strong", [][]string{{"data-marker", "**"}}, false) + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderStrongA6kCloseMarker(node *ast.Node, entering bool) ast.WalkStatus { + r.tag("/strong", nil, false) + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderStrongU8eOpenMarker(node *ast.Node, entering bool) ast.WalkStatus { + r.tag("strong", [][]string{{"data-marker", "__"}}, false) + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderStrongU8eCloseMarker(node *ast.Node, entering bool) ast.WalkStatus { + r.tag("/strong", nil, false) + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderBlockquote(node *ast.Node, entering bool) ast.WalkStatus { + if entering { + r.WriteString(`
`) + } else { + r.WriteString("
") + } + return ast.WalkContinue +} + +func (r *VditorIRRenderer) renderBlockquoteMarker(node *ast.Node, entering bool) ast.WalkStatus { + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderHeading(node *ast.Node, entering bool) ast.WalkStatus { + if entering { + r.WriteString("") + } else { + if 1 == node.HeadingLevel { + r.WriteString(" data-marker=\"=\">") + } else { + r.WriteString(" data-marker=\"-\">") + } + } + if r.Option.HeadingAnchor { + id := r.headingID(node) + r.tag("a", [][]string{{"id", "vditorAnchor-" + id}, {"class", "vditor-anchor"}, {"href", "#" + id}}, false) + r.WriteString(``) + r.tag("/a", nil, false) + } + } else { + r.WriteString("") + } + return ast.WalkContinue +} + +func (r *VditorIRRenderer) renderHeadingC8hMarker(node *ast.Node, entering bool) ast.WalkStatus { + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderList(node *ast.Node, entering bool) ast.WalkStatus { + tag := "ul" + if 1 == node.ListData.Typ || (3 == node.ListData.Typ && 0 == node.ListData.BulletChar) { + tag = "ol" + } + if entering { + var attrs [][]string + if node.Tight { + attrs = append(attrs, []string{"data-tight", "true"}) + } + if 0 == node.BulletChar { + if 1 != node.Start { + attrs = append(attrs, []string{"start", strconv.Itoa(node.Start)}) + } + } else { + attrs = append(attrs, []string{"data-marker", string(node.BulletChar)}) + } + attrs = append(attrs, []string{"data-block", "0"}) + r.tag(tag, attrs, false) + } else { + r.tag("/"+tag, nil, false) + } + return ast.WalkContinue +} + +func (r *VditorIRRenderer) renderListItem(node *ast.Node, entering bool) ast.WalkStatus { + if entering { + var attrs [][]string + switch node.ListData.Typ { + case 0: + attrs = append(attrs, []string{"data-marker", string(node.Marker)}) + case 1: + attrs = append(attrs, []string{"data-marker", strconv.Itoa(node.Num) + string(node.ListData.Delimiter)}) + case 3: + if 0 == node.ListData.BulletChar { + attrs = append(attrs, []string{"data-marker", strconv.Itoa(node.Num) + string(node.ListData.Delimiter)}) + } else { + attrs = append(attrs, []string{"data-marker", string(node.Marker)}) + } + if nil != node.FirstChild && nil != node.FirstChild.FirstChild && ast.NodeTaskListItemMarker == node.FirstChild.FirstChild.Type { + attrs = append(attrs, []string{"class", r.Option.GFMTaskListItemClass}) + } + } + r.tag("li", attrs, false) + } else { + r.tag("/li", nil, false) + } + return ast.WalkContinue +} + +func (r *VditorIRRenderer) renderTaskListItemMarker(node *ast.Node, entering bool) ast.WalkStatus { + var attrs [][]string + if node.TaskListItemChecked { + attrs = append(attrs, []string{"checked", ""}) + } + attrs = append(attrs, []string{"type", "checkbox"}) + r.tag("input", attrs, true) + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderThematicBreak(node *ast.Node, entering bool) ast.WalkStatus { + r.tag("hr", [][]string{{"data-block", "0"}}, true) + if nil != node.Tokens { + r.tag("p", [][]string{{"data-block", "0"}}, false) + r.Write(node.Tokens) + r.WriteByte(lex.ItemNewline) + r.tag("/p", nil, false) + } + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderHardBreak(node *ast.Node, entering bool) ast.WalkStatus { + r.tag("br", nil, true) + return ast.WalkStop +} + +func (r *VditorIRRenderer) renderSoftBreak(node *ast.Node, entering bool) ast.WalkStatus { + r.WriteByte(lex.ItemNewline) + return ast.WalkStop +} + +func (r *VditorIRRenderer) tag(name string, attrs [][]string, selfclosing bool) { + if r.DisableTags > 0 { + return + } + + r.WriteString("<") + r.WriteString(name) + if 0 < len(attrs) { + for _, attr := range attrs { + r.WriteString(" " + attr[0] + "=\"" + attr[1] + "\"") + } + } + if selfclosing { + r.WriteString(" /") + } + r.WriteString(">") +} + +func (r *VditorIRRenderer) renderCodeBlock(node *ast.Node, entering bool) ast.WalkStatus { + if entering { + marker := "```" + if nil != node.FirstChild { + marker = string(node.FirstChild.Tokens) + } + r.WriteString(`
`) + } else { + r.WriteString("
") + } + return ast.WalkContinue +} + +func (r *VditorIRRenderer) renderCodeBlockCode(node *ast.Node, entering bool) ast.WalkStatus { + codeLen := len(node.Tokens) + codeIsEmpty := 1 > codeLen || (len(parse.Caret) == codeLen && parse.Caret == string(node.Tokens)) + isFenced := node.Parent.IsFencedCodeBlock + if isFenced { + node.Previous.CodeBlockInfo = bytes.ReplaceAll(node.Previous.CodeBlockInfo, []byte(parse.Caret), []byte("")) + } + var attrs [][]string + if isFenced && 0 < len(node.Previous.CodeBlockInfo) { + infoWords := lex.Split(node.Previous.CodeBlockInfo, lex.ItemSpace) + language := string(infoWords[0]) + attrs = append(attrs, []string{"class", "language-" + language}) + } + r.WriteString("
")
+	r.tag("code", attrs, false)
+
+	if codeIsEmpty {
+		r.WriteString("\n")
+	} else {
+		r.Write(util.EscapeHTML(node.Tokens))
+		r.Newline()
+	}
+	r.WriteString("
") + return ast.WalkStop +}