Skip to content

Commit

Permalink
✨ Support for inserting custom blocks siyuan-note/siyuan#8418
Browse files Browse the repository at this point in the history
  • Loading branch information
88250 committed Jun 1, 2023
1 parent 0435fd1 commit d3e4735
Show file tree
Hide file tree
Showing 18 changed files with 293 additions and 19 deletions.
17 changes: 14 additions & 3 deletions ast/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,11 @@ type Node struct {

AttributeViewID string `json:",omitempty"` // 属性视图 data-av-id 属性
AttributeViewType string `json:",omitempty"` // 属性视图 data-av-type 属性

// 自定义块 https://github.com/siyuan-note/siyuan/issues/8418

CustomBlockFenceOffset int `json:",omitempty"` // 自定义块标记符起始偏移量
CustomBlockInfo string `json:",omitempty"` // 自定义块信息
}

// ListData 用于记录列表或列表项节点的附加信息。
Expand Down Expand Up @@ -686,7 +691,7 @@ func (n *Node) IsBlock() bool {
case NodeDocument, NodeParagraph, NodeHeading, NodeThematicBreak, NodeBlockquote, NodeList, NodeListItem, NodeHTMLBlock,
NodeCodeBlock, NodeTable, NodeMathBlock, NodeFootnotesDefBlock, NodeFootnotesDef, NodeToC, NodeYamlFrontMatter,
NodeBlockQueryEmbed, NodeKramdownBlockIAL, NodeSuperBlock, NodeGitConflict, NodeAudio, NodeVideo, NodeIFrame, NodeWidget,
NodeAttributeView:
NodeAttributeView, NodeCustomBlock:
return true
}
return false
Expand Down Expand Up @@ -719,7 +724,8 @@ func (n *Node) IsMarker() bool {
// AcceptLines 判断是否节点是否可以接受更多的文本行。比如 HTML 块、代码块和段落是可以接受更多的文本行的。
func (n *Node) AcceptLines() bool {
switch n.Type {
case NodeParagraph, NodeCodeBlock, NodeHTMLBlock, NodeMathBlock, NodeYamlFrontMatter, NodeBlockQueryEmbed, NodeGitConflict, NodeIFrame, NodeWidget, NodeVideo, NodeAudio, NodeAttributeView:
case NodeParagraph, NodeCodeBlock, NodeHTMLBlock, NodeMathBlock, NodeYamlFrontMatter, NodeBlockQueryEmbed,
NodeGitConflict, NodeIFrame, NodeWidget, NodeVideo, NodeAudio, NodeAttributeView, NodeCustomBlock:
return true
}
return false
Expand All @@ -729,7 +735,8 @@ func (n *Node) AcceptLines() bool {
// 块引用节点(块级容器)可以包含任意节点;段落节点(叶子块节点)不能包含任何其他块级节点。
func (n *Node) CanContain(nodeType NodeType) bool {
switch n.Type {
case NodeCodeBlock, NodeHTMLBlock, NodeParagraph, NodeThematicBreak, NodeTable, NodeMathBlock, NodeYamlFrontMatter, NodeGitConflict, NodeIFrame, NodeWidget, NodeVideo, NodeAudio, NodeAttributeView:
case NodeCodeBlock, NodeHTMLBlock, NodeParagraph, NodeThematicBreak, NodeTable, NodeMathBlock, NodeYamlFrontMatter,
NodeGitConflict, NodeIFrame, NodeWidget, NodeVideo, NodeAudio, NodeAttributeView, NodeCustomBlock:
return false
case NodeList:
return NodeListItem == nodeType
Expand Down Expand Up @@ -981,5 +988,9 @@ const (

NodeAttributeView NodeType = 550 // 属性视图

// 自定义块 https://github.com/siyuan-note/siyuan/issues/8418 ;;;info

NodeCustomBlock NodeType = 560 // 自定义块

NodeTypeMaxVal NodeType = 1024 // 节点类型最大值
)
6 changes: 4 additions & 2 deletions ast/nodetype_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions javascript/lute.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion javascript/lute.min.js.map

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions parse/block_start.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ func blockStarts() []blockStartFunc {
BlockquoteStart,
ATXHeadingStart,
FenceCodeBlockStart,
CustomBlockStart,
SetextHeadingStart,
HtmlBlockStart,
YamlFrontMatterStart,
Expand All @@ -36,7 +37,8 @@ func blockStarts() []blockStartFunc {
}

// blockStartFunc 定义了用于判断块是否开始的函数签名,返回值:
// 0:不匹配
// 1:匹配到容器块,需要继续迭代下降
// 2:匹配到叶子块
//
// 0:不匹配
// 1:匹配到容器块,需要继续迭代下降
// 2:匹配到叶子块
type blockStartFunc func(t *Tree, container *ast.Node) int
6 changes: 5 additions & 1 deletion parse/blocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ func (t *Tree) incorporateLine(line []byte) {
lex.ItemHyphen != maybeMarker && lex.ItemAsterisk != maybeMarker && lex.ItemPlus != maybeMarker && // 无序列表
!lex.IsDigit(maybeMarker) && // 有序列表
lex.ItemBacktick != maybeMarker && lex.ItemTilde != maybeMarker && // 代码块
lex.ItemSemicolon != maybeMarker && // 定义块
lex.ItemCrosshatch != maybeMarker && // ATX 标题
lex.ItemGreater != maybeMarker && // 块引用
lex.ItemLess != maybeMarker && // HTML 块
Expand Down Expand Up @@ -202,6 +203,7 @@ func (t *Tree) incorporateLine(line []byte) {
!(typ == ast.NodeFootnotesDef ||
typ == ast.NodeBlockquote || // 块引用行肯定不会是空行因为至少有一个 >
(typ == ast.NodeCodeBlock && isFenced) || // 围栏代码块不计入空行判断
(typ == ast.NodeCustomBlock) || // 自定义块不计入空行判断
(typ == ast.NodeMathBlock) || // 数学公式块不计入空行判断
(typ == ast.NodeGitConflict) || // Git 冲突标记不计入空行判断
(typ == ast.NodeListItem && nil == container.FirstChild)) // 内容为空的列表项也不计入空行判断
Expand Down Expand Up @@ -261,7 +263,7 @@ func (t *Tree) addLine() {
}

// _continue 判断节点是否可以继续处理,比如块引用需要 >,缩进代码块需要 4 空格,围栏代码块需要 ```。
// 如果可以继续处理返回 0,如果不能接续处理返回 1,如果返回 2(仅在围栏代码块或超级块闭合时)则说明可以继续下一行处理了。
// 如果可以继续处理返回 0,如果不能接续处理返回 1,如果返回 2(仅在围栏代码块、超级块或自定义块闭合时)则说明可以继续下一行处理了。
func _continue(n *ast.Node, context *Context) int {
switch n.Type {
case ast.NodeCodeBlock:
Expand All @@ -284,6 +286,8 @@ func _continue(n *ast.Node, context *Context) int {
return SuperBlockContinue(n, context)
case ast.NodeGitConflict:
return GitConflictContinue(n, context)
case ast.NodeCustomBlock:
return CustomBlockContinue(n, context)
case ast.NodeHeading, ast.NodeThematicBreak, ast.NodeKramdownBlockIAL, ast.NodeLinkRefDefBlock, ast.NodeBlockQueryEmbed,
ast.NodeIFrame, ast.NodeVideo, ast.NodeAudio, ast.NodeWidget, ast.NodeAttributeView:
return 1
Expand Down
130 changes: 130 additions & 0 deletions parse/custom_block.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// 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 parse

import (
"bytes"
"strings"

"github.com/88250/lute/ast"
"github.com/88250/lute/editor"
"github.com/88250/lute/html"
"github.com/88250/lute/lex"
)

// CustomBlockStart 判断围栏自定义块(;;;)是否开始。
func CustomBlockStart(t *Tree, container *ast.Node) int {
if t.Context.indented {
return 0
}

if ok, offset, info := t.parseCustomBlock(); ok {
t.Context.closeUnmatchedBlocks()
container := t.Context.addChild(ast.NodeCustomBlock)
container.CustomBlockFenceOffset = offset
container.CustomBlockInfo = info
t.Context.advanceNextNonspace()
t.Context.advanceOffset(3, false)
return 2
}
return 0
}

func CustomBlockContinue(customBlock *ast.Node, context *Context) int {
ln := context.currentLine
indent := context.indent
if ok := context.isCustomBlockClose(ln[context.nextNonspace:]); indent <= 3 && ok {
context.finalize(customBlock)
return 2
} else {
// 跳过围栏标记符 ; 之前可能存在的空格
i := customBlock.CustomBlockFenceOffset
var token byte
for i > 0 {
token = lex.Peek(ln, context.offset)
if lex.ItemSpace != token && lex.ItemTab != token {
break
}
context.advanceOffset(1, true)
i--
}
}
return 0
}

func (context *Context) customBlockFinalize(customBlock *ast.Node) {
content := customBlock.Tokens
length := len(content)
if 1 > length {
return
}

var i int
for ; i < length; i++ {
if lex.ItemNewline == content[i] {
break
}
}
customBlock.Tokens = content[i+1:]
}

func (t *Tree) parseCustomBlock() (ok bool, fenceOffset int, info string) {
marker := t.Context.currentLine[t.Context.nextNonspace]
if lex.ItemSemicolon != marker {
return
}

var fenceLen int
for i := t.Context.nextNonspace; i < t.Context.currentLineLen && lex.ItemSemicolon == t.Context.currentLine[i]; i++ {
fenceLen++
}

if 3 > fenceLen {
return
}

infoTokens := t.Context.currentLine[t.Context.nextNonspace+fenceLen:]
if lex.ItemSemicolon == marker && 0 < bytes.IndexByte(infoTokens, lex.ItemSemicolon) {
// info 部分不能包含 ;
return
}
info = string(lex.TrimWhitespace(infoTokens))
info = html.UnescapeString(info)
if idx := strings.IndexByte(info, ' '); 0 <= idx {
info = info[:idx]
}
return true, t.Context.indent, info
}

func (context *Context) isCustomBlockClose(tokens []byte) (ok bool) {
closeMarker := tokens[0]
if closeMarker != lex.ItemSemicolon {
return false
}
if 3 > lex.Accept(tokens, closeMarker) {
return false
}
tokens = lex.TrimWhitespace(tokens)
endCaret := bytes.HasSuffix(tokens, editor.CaretTokens)
if context.ParseOption.VditorWYSIWYG || context.ParseOption.VditorIR || context.ParseOption.VditorSV || context.ParseOption.ProtyleWYSIWYG {
tokens = bytes.ReplaceAll(tokens, editor.CaretTokens, nil)
if endCaret {
context.Tip.Tokens = bytes.TrimSuffix(context.Tip.Tokens, []byte("\n"))
context.Tip.Tokens = append(context.Tip.Tokens, editor.CaretTokens...)
}
}
for _, token := range tokens {
if token != lex.ItemSemicolon {
return false
}
}
return true
}
2 changes: 2 additions & 0 deletions parse/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,8 @@ func (context *Context) finalize(block *ast.Node) {
context.superBlockFinalize(block)
case ast.NodeGitConflict:
context.gitConflictFinalize(block)
case ast.NodeCustomBlock:
context.customBlockFinalize(block)
}

context.Tip = parent
Expand Down
8 changes: 7 additions & 1 deletion protyle.go
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,12 @@ func (lute *Lute) genASTByBlockDOM(n *html.Node, tree *parse.Tree) {
node.AttributeViewType = util.DomAttrValue(n, "data-av-type")
tree.Context.Tip.AppendChild(node)
return
case ast.NodeCustomBlock:
node.Type = ast.NodeCustomBlock
node.CustomBlockInfo = util.DomAttrValue(n, "data-info")
node.Tokens = []byte(html.UnescapeHTMLStr(util.DomAttrValue(n, "data-content")))
tree.Context.Tip.AppendChild(node)
return
default:
switch n.DataAtom {
case 0:
Expand Down Expand Up @@ -1011,7 +1017,7 @@ func (lute *Lute) genASTByBlockDOM(n *html.Node, tree *parse.Tree) {
}

func (lute *Lute) genASTContenteditable(n *html.Node, tree *parse.Tree) {
if ast.NodeCodeBlock == tree.Context.Tip.Type {
if ast.NodeCodeBlock == tree.Context.Tip.Type || ast.NodeCustomBlock == tree.Context.Tip.Type {
return
}

Expand Down
19 changes: 19 additions & 0 deletions render/format_renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,28 @@ func NewFormatRenderer(tree *parse.Tree, options *Options) *FormatRenderer {
ret.RendererFuncs[ast.NodeBr] = ret.renderBr
ret.RendererFuncs[ast.NodeTextMark] = ret.renderTextMark
ret.RendererFuncs[ast.NodeAttributeView] = ret.renderAttributeView
ret.RendererFuncs[ast.NodeCustomBlock] = ret.renderCustomBlock
return ret
}

func (r *FormatRenderer) renderCustomBlock(node *ast.Node, entering bool) ast.WalkStatus {
if entering {
r.Newline()
r.WriteString(";;;")
r.WriteString(node.CustomBlockInfo)
r.Newline()
r.Write(node.Tokens)
r.Newline()
r.WriteString(";;;")
if !r.isLastNode(r.Tree.Root, node) {
if r.withoutKramdownBlockIAL(node) {
r.WriteByte(lex.ItemNewline)
}
}
}
return ast.WalkContinue
}

func (r *FormatRenderer) renderAttributeView(node *ast.Node, entering bool) ast.WalkStatus {
if entering {
r.Newline()
Expand Down
15 changes: 15 additions & 0 deletions render/html_renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ func NewHtmlRenderer(tree *parse.Tree, options *Options) *HtmlRenderer {
ret.RendererFuncs[ast.NodeBr] = ret.renderBr
ret.RendererFuncs[ast.NodeTextMark] = ret.renderTextMark
ret.RendererFuncs[ast.NodeAttributeView] = ret.renderAttributeView
ret.RendererFuncs[ast.NodeCustomBlock] = ret.renderCustomBlock
return ret
}

Expand All @@ -174,6 +175,20 @@ func (r *HtmlRenderer) Render() (output []byte) {
return
}

func (r *HtmlRenderer) renderCustomBlock(node *ast.Node, entering bool) ast.WalkStatus {
if entering {
r.Newline()
r.Tag("div", [][]string{
{"data-type", "NodeCustomBlock"},
{"data-info", node.CustomBlockInfo},
{"data-content", string(html.EscapeHTML(node.Tokens))},
}, false)
r.WriteString("</div>")
r.Newline()
}
return ast.WalkContinue
}

func (r *HtmlRenderer) renderAttributeView(node *ast.Node, entering bool) ast.WalkStatus {
if entering {
r.Newline()
Expand Down
15 changes: 15 additions & 0 deletions render/protyle_export_docx_renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ func NewProtyleExportDocxRenderer(tree *parse.Tree, options *Options) *ProtyleEx
ret.RendererFuncs[ast.NodeBr] = ret.renderBr
ret.RendererFuncs[ast.NodeTextMark] = ret.renderTextMark
ret.RendererFuncs[ast.NodeAttributeView] = ret.renderAttributeView
ret.RendererFuncs[ast.NodeCustomBlock] = ret.renderCustomBlock
return ret
}

Expand All @@ -171,6 +172,20 @@ func (r *ProtyleExportDocxRenderer) Render() (output []byte) {
return
}

func (r *ProtyleExportDocxRenderer) renderCustomBlock(node *ast.Node, entering bool) ast.WalkStatus {
if entering {
r.Newline()
r.Tag("div", [][]string{
{"data-type", "NodeCustomBlock"},
{"data-info", node.CustomBlockInfo},
{"data-content", string(html.EscapeHTML(node.Tokens))},
}, false)
r.WriteString("</div>")
r.Newline()
}
return ast.WalkContinue
}

func (r *ProtyleExportDocxRenderer) renderAttributeView(node *ast.Node, entering bool) ast.WalkStatus {
if entering {
r.Newline()
Expand Down
Loading

0 comments on commit d3e4735

Please sign in to comment.