A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
1// SiYuan - Refactor your thinking
2// Copyright (c) 2020-present, b3log.org
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program. If not, see <https://www.gnu.org/licenses/>.
16
17package treenode
18
19import (
20 "bytes"
21 "strings"
22 "sync"
23
24 "github.com/88250/gulu"
25 "github.com/88250/lute"
26 "github.com/88250/lute/ast"
27 "github.com/88250/lute/editor"
28 "github.com/88250/lute/html"
29 "github.com/88250/lute/parse"
30 "github.com/88250/lute/render"
31 "github.com/88250/vitess-sqlparser/sqlparser"
32 "github.com/siyuan-note/logging"
33 "github.com/siyuan-note/siyuan/kernel/cache"
34 "github.com/siyuan-note/siyuan/kernel/util"
35)
36
37func ResetNodeID(node *ast.Node) {
38 if nil == node {
39 return
40 }
41
42 node.ID = ast.NewNodeID()
43 node.SetIALAttr("id", node.ID)
44 resetUpdatedByID(node)
45}
46
47func resetUpdatedByID(node *ast.Node) {
48 created := util.TimeFromID(node.ID)
49 updated := node.IALAttr("updated")
50 if "" == updated {
51 updated = created
52 }
53 if updated < created {
54 updated = created
55 }
56 node.SetIALAttr("updated", updated)
57}
58
59func GetEmbedBlockRef(embedNode *ast.Node) (blockRefID string) {
60 if nil == embedNode || ast.NodeBlockQueryEmbed != embedNode.Type {
61 return
62 }
63
64 scriptNode := embedNode.ChildByType(ast.NodeBlockQueryEmbedScript)
65 if nil == scriptNode {
66 return
67 }
68
69 stmt := scriptNode.TokensStr()
70 parsedStmt, err := sqlparser.Parse(stmt)
71 if err != nil {
72 return
73 }
74
75 switch parsedStmt.(type) {
76 case *sqlparser.Select:
77 slct := parsedStmt.(*sqlparser.Select)
78 if nil == slct.Where || nil == slct.Where.Expr {
79 return
80 }
81
82 switch slct.Where.Expr.(type) {
83 case *sqlparser.ComparisonExpr: // WHERE id = '20060102150405-1a2b3c4'
84 comp := slct.Where.Expr.(*sqlparser.ComparisonExpr)
85 switch comp.Left.(type) {
86 case *sqlparser.ColName:
87 col := comp.Left.(*sqlparser.ColName)
88 if nil == col || "id" != col.Name.Lowered() {
89 return
90 }
91 }
92 switch comp.Right.(type) {
93 case *sqlparser.SQLVal:
94 val := comp.Right.(*sqlparser.SQLVal)
95 if nil == val || sqlparser.StrVal != val.Type {
96 return
97 }
98
99 idVal := string(val.Val)
100 if !ast.IsNodeIDPattern(idVal) {
101 return
102 }
103 blockRefID = idVal
104 }
105 }
106 }
107 return
108}
109
110func GetBlockRef(n *ast.Node) (blockRefID, blockRefText, blockRefSubtype string) {
111 if !IsBlockRef(n) {
112 return
113 }
114
115 blockRefID = n.TextMarkBlockRefID
116 blockRefText = n.TextMarkTextContent
117 blockRefSubtype = n.TextMarkBlockRefSubtype
118 return
119}
120
121func IsBlockRef(n *ast.Node) bool {
122 if nil == n {
123 return false
124 }
125 return (ast.NodeTextMark == n.Type && n.IsTextMarkType("block-ref")) || ast.NodeBlockRef == n.Type
126}
127
128func IsBlockLink(n *ast.Node) bool {
129 if nil == n {
130 return false
131 }
132 return ast.NodeTextMark == n.Type && n.IsTextMarkType("a") && strings.HasPrefix(n.TextMarkAHref, "siyuan://blocks/")
133}
134
135func IsFileAnnotationRef(n *ast.Node) bool {
136 if nil == n {
137 return false
138 }
139 return ast.NodeTextMark == n.Type && n.IsTextMarkType("file-annotation-ref")
140}
141
142func IsEmbedBlockRef(n *ast.Node) bool {
143 return "" != GetEmbedBlockRef(n)
144}
145
146func FormatNode(node *ast.Node, luteEngine *lute.Lute) string {
147 markdown, err := lute.FormatNodeSync(node, luteEngine.ParseOptions, luteEngine.RenderOptions)
148 if err != nil {
149 root := TreeRoot(node)
150 logging.LogFatalf(logging.ExitCodeFatal, "format node [%s] in tree [%s] failed: %s", node.ID, root.ID, err)
151 }
152 return markdown
153}
154
155func ExportNodeStdMd(node *ast.Node, luteEngine *lute.Lute) string {
156 markdown, err := lute.ProtyleExportMdNodeSync(node, luteEngine.ParseOptions, luteEngine.RenderOptions)
157 if err != nil {
158 root := TreeRoot(node)
159 logging.LogFatalf(logging.ExitCodeFatal, "export markdown for node [%s] in tree [%s] failed: %s", node.ID, root.ID, err)
160 }
161 return markdown
162}
163
164func IsNodeOCRed(node *ast.Node) (ret bool) {
165 if !util.TesseractEnabled || nil == node {
166 return true
167 }
168
169 ret = true
170 ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
171 if !entering {
172 return ast.WalkContinue
173 }
174
175 if ast.NodeImage == n.Type {
176 linkDest := n.ChildByType(ast.NodeLinkDest)
177 if nil == linkDest {
178 return ast.WalkContinue
179 }
180
181 linkDestStr := linkDest.TokensStr()
182 if !cache.ExistAsset(linkDestStr) {
183 return ast.WalkContinue
184 }
185
186 if !util.ExistsAssetText(linkDestStr) {
187 ret = false
188 return ast.WalkStop
189 }
190 }
191 return ast.WalkContinue
192 })
193 return
194}
195
196func GetNodeSrcTokens(n *ast.Node) (ret string) {
197 if index := bytes.Index(n.Tokens, []byte("src=\"")); 0 < index {
198 src := n.Tokens[index+len("src=\""):]
199 if index = bytes.Index(src, []byte("\"")); 0 < index {
200 src = src[:bytes.Index(src, []byte("\""))]
201 ret = strings.TrimSpace(string(src))
202 return
203 }
204
205 logging.LogWarnf("src is missing the closing double quote in tree [%s] ", n.Box+n.Path)
206 }
207 return
208}
209
210func FirstLeafBlock(node *ast.Node) (ret *ast.Node) {
211 ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
212 if !entering || n.IsMarker() {
213 return ast.WalkContinue
214 }
215
216 if !n.IsContainerBlock() {
217 ret = n
218 return ast.WalkStop
219 }
220 return ast.WalkContinue
221 })
222 return
223}
224
225func CountBlockNodes(node *ast.Node) (ret int) {
226 ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
227 if !entering || !n.IsBlock() || ast.NodeList == n.Type || ast.NodeBlockquote == n.Type || ast.NodeSuperBlock == n.Type {
228 return ast.WalkContinue
229 }
230
231 if "1" == n.IALAttr("fold") {
232 ret++
233 return ast.WalkSkipChildren
234 }
235
236 ret++
237 return ast.WalkContinue
238 })
239 return
240}
241
242// ParentNodesWithHeadings 返回所有父级节点。
243// 注意:返回的父级节点包括了标题节点,并且不保证父级层次顺序。
244func ParentNodesWithHeadings(node *ast.Node) (parents []*ast.Node) {
245 const maxDepth = 255
246 i := 0
247 for n := node; nil != n; n = n.Parent {
248 parent := n.Parent
249 if maxDepth < i {
250 logging.LogWarnf("parent nodes of node [%s] is too deep", node.ID)
251 return
252 }
253 i++
254
255 if nil == parent {
256 return
257 }
258
259 // 标题下方块编辑后刷新标题块更新时间
260 // The heading block update time is refreshed after editing the blocks under the heading https://github.com/siyuan-note/siyuan/issues/11374
261 parentHeadingLevel := 7
262 if ast.NodeHeading == n.Type {
263 parentHeadingLevel = n.HeadingLevel
264 }
265 for prev := n.Previous; nil != prev; prev = prev.Previous {
266 if ast.NodeHeading == prev.Type {
267 if prev.HeadingLevel >= parentHeadingLevel {
268 break
269 }
270
271 parents = append(parents, prev)
272 parentHeadingLevel = prev.HeadingLevel
273 }
274 }
275
276 parents = append(parents, parent)
277 if ast.NodeDocument == parent.Type {
278 return
279 }
280 }
281 return
282}
283
284func ChildBlockNodes(node *ast.Node) (children []*ast.Node) {
285 children = []*ast.Node{}
286 if !node.IsContainerBlock() || ast.NodeDocument == node.Type {
287 children = append(children, node)
288 return
289 }
290
291 ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
292 if !entering || !n.IsBlock() {
293 return ast.WalkContinue
294 }
295
296 children = append(children, n)
297 return ast.WalkContinue
298 })
299 return
300}
301
302func ParentBlock(node *ast.Node) *ast.Node {
303 for p := node.Parent; nil != p; p = p.Parent {
304 if "" != p.ID && p.IsBlock() {
305 return p
306 }
307 }
308 return nil
309}
310
311func PreviousBlock(node *ast.Node) *ast.Node {
312 for n := node.Previous; nil != n; n = n.Previous {
313 if "" != n.ID && n.IsBlock() {
314 return n
315 }
316 }
317 return nil
318}
319
320func NextBlock(node *ast.Node) *ast.Node {
321 for n := node.Next; nil != n; n = n.Next {
322 if "" != n.ID && n.IsBlock() {
323 return n
324 }
325 }
326 return nil
327}
328
329func FirstChildBlock(node *ast.Node) (ret *ast.Node) {
330 ast.Walk(node, func(n *ast.Node, entering bool) ast.WalkStatus {
331 if !entering {
332 return ast.WalkContinue
333 }
334
335 if n.IsBlock() {
336 ret = n
337 return ast.WalkStop
338 }
339 return ast.WalkContinue
340 })
341 return
342}
343
344func GetNodeInTree(tree *parse.Tree, id string) (ret *ast.Node) {
345 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus {
346 if !entering {
347 return ast.WalkContinue
348 }
349
350 if id == n.ID {
351 ret = n
352 ret.Box = tree.Box
353 ret.Path = tree.Path
354 return ast.WalkStop
355 }
356 return ast.WalkContinue
357 })
358 return
359}
360
361func GetDocTitleImgPath(root *ast.Node) (ret string) {
362 if nil == root {
363 return
364 }
365
366 const background = "background-image: url("
367 titleImg := root.IALAttr("title-img")
368 titleImg = strings.TrimSpace(titleImg)
369 titleImg = html.UnescapeString(titleImg)
370 titleImg = strings.ReplaceAll(titleImg, "background-image:url(", background)
371 if !strings.Contains(titleImg, background) {
372 return
373 }
374
375 start := strings.Index(titleImg, background) + len(background)
376 end := strings.LastIndex(titleImg, ")")
377 ret = titleImg[start:end]
378 ret = strings.TrimPrefix(ret, "\"")
379 ret = strings.TrimPrefix(ret, "'")
380 ret = strings.TrimSuffix(ret, "\"")
381 ret = strings.TrimSuffix(ret, "'")
382 return ret
383}
384
385var typeAbbrMap = map[string]string{
386 // 块级元素
387 "NodeDocument": "d",
388 "NodeHeading": "h",
389 "NodeList": "l",
390 "NodeListItem": "i",
391 "NodeCodeBlock": "c",
392 "NodeMathBlock": "m",
393 "NodeTable": "t",
394 "NodeBlockquote": "b",
395 "NodeSuperBlock": "s",
396 "NodeParagraph": "p",
397 "NodeHTMLBlock": "html",
398 "NodeBlockQueryEmbed": "query_embed",
399 "NodeAttributeView": "av",
400 "NodeKramdownBlockIAL": "ial",
401 "NodeIFrame": "iframe",
402 "NodeWidget": "widget",
403 "NodeThematicBreak": "tb",
404 "NodeVideo": "video",
405 "NodeAudio": "audio",
406 // 行级元素
407 "NodeText": "text",
408 "NodeImage": "img",
409 "NodeLinkText": "link_text",
410 "NodeLinkDest": "link_dest",
411 "NodeTextMark": "textmark",
412}
413
414var abbrTypeMap = map[string]string{}
415
416func init() {
417 for typ, abbr := range typeAbbrMap {
418 abbrTypeMap[abbr] = typ
419 }
420}
421
422func TypeAbbr(nodeType string) string {
423 return typeAbbrMap[nodeType]
424}
425
426func FromAbbrType(abbrType string) string {
427 return abbrTypeMap[abbrType]
428}
429
430func SubTypeAbbr(n *ast.Node) string {
431 switch n.Type {
432 case ast.NodeList, ast.NodeListItem:
433 if 0 == n.ListData.Typ {
434 return "u"
435 }
436 if 1 == n.ListData.Typ {
437 return "o"
438 }
439 if 3 == n.ListData.Typ {
440 return "t"
441 }
442 case ast.NodeHeading:
443 if 1 == n.HeadingLevel {
444 return "h1"
445 }
446 if 2 == n.HeadingLevel {
447 return "h2"
448 }
449 if 3 == n.HeadingLevel {
450 return "h3"
451 }
452 if 4 == n.HeadingLevel {
453 return "h4"
454 }
455 if 5 == n.HeadingLevel {
456 return "h5"
457 }
458 if 6 == n.HeadingLevel {
459 return "h6"
460 }
461 }
462 return ""
463}
464
465var DynamicRefTexts = sync.Map{}
466
467func SetDynamicBlockRefText(blockRef *ast.Node, refText string) {
468 if !IsBlockRef(blockRef) {
469 return
470 }
471
472 if ast.NodeBlockRef == blockRef.Type {
473 if refID := blockRef.ChildByType(ast.NodeBlockRefID); nil != refID {
474 refID.InsertAfter(&ast.Node{Type: ast.NodeBlockRefDynamicText, Tokens: []byte(refText)})
475 refID.InsertAfter(&ast.Node{Type: ast.NodeBlockRefSpace})
476 }
477 return
478 }
479
480 refText = strings.TrimSpace(refText)
481 if "" == refText {
482 refText = blockRef.TextMarkBlockRefID
483 }
484
485 blockRef.TextMarkBlockRefSubtype = "d"
486 blockRef.TextMarkTextContent = refText
487
488 // 偶发编辑文档标题后引用处的动态锚文本不更新 https://github.com/siyuan-note/siyuan/issues/5891
489 DynamicRefTexts.Store(blockRef.TextMarkBlockRefID, refText)
490}
491
492func IsChartCodeBlockCode(code *ast.Node) bool {
493 if nil == code.Previous || ast.NodeCodeBlockFenceInfoMarker != code.Previous.Type || 1 > len(code.Previous.CodeBlockInfo) {
494 return false
495 }
496
497 language := gulu.Str.FromBytes(code.Previous.CodeBlockInfo)
498 language = strings.ReplaceAll(language, editor.Caret, "")
499 return render.NoHighlight(language)
500}