A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 301 lines 8.9 kB view raw
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 model 18 19import ( 20 "bytes" 21 "regexp" 22 "sort" 23 "strings" 24 "time" 25 26 "github.com/88250/gulu" 27 "github.com/88250/lute" 28 "github.com/88250/lute/ast" 29 "github.com/88250/lute/editor" 30 "github.com/88250/lute/parse" 31 "github.com/ClarkThan/ahocorasick" 32 "github.com/dgraph-io/ristretto" 33 "github.com/siyuan-note/siyuan/kernel/search" 34 "github.com/siyuan-note/siyuan/kernel/sql" 35 "github.com/siyuan-note/siyuan/kernel/task" 36 "github.com/siyuan-note/siyuan/kernel/treenode" 37 "github.com/siyuan-note/siyuan/kernel/util" 38) 39 40// virtualBlockRefCache 用于保存块关联的虚拟引用关键字。 41// 改进打开虚拟引用后加载文档的性能 https://github.com/siyuan-note/siyuan/issues/7378 42var virtualBlockRefCache, _ = ristretto.NewCache(&ristretto.Config{ 43 NumCounters: 102400, 44 MaxCost: 10240, 45 BufferItems: 64, 46}) 47 48func getBlockVirtualRefKeywords(root *ast.Node) (ret []string) { 49 val, ok := virtualBlockRefCache.Get(root.ID) 50 if !ok { 51 buf := bytes.Buffer{} 52 ast.Walk(root, func(n *ast.Node, entering bool) ast.WalkStatus { 53 if !entering || !n.IsBlock() { 54 return ast.WalkContinue 55 } 56 57 content := sql.NodeStaticContent(n, nil, false, false, false) 58 content = strings.ReplaceAll(content, editor.Zwsp, "") 59 buf.WriteString(content) 60 return ast.WalkContinue 61 }) 62 content := buf.String() 63 ret = putBlockVirtualRefKeywords(content, root) 64 return 65 } 66 ret = val.([]string) 67 return 68} 69 70func putBlockVirtualRefKeywords(blockContent string, root *ast.Node) (ret []string) { 71 keywords := getVirtualRefKeywords(root) 72 if 1 > len(keywords) { 73 return 74 } 75 76 contentTmp := blockContent 77 var keywordsTmp []string 78 if !Conf.Search.CaseSensitive { 79 contentTmp = strings.ToLower(blockContent) 80 for _, keyword := range keywords { 81 keywordsTmp = append(keywordsTmp, strings.ToLower(keyword)) 82 } 83 } else { 84 for _, keyword := range keywords { 85 keywordsTmp = append(keywordsTmp, keyword) 86 } 87 } 88 89 m := ahocorasick.NewMatcher() 90 m.BuildWithPatterns(keywordsTmp) 91 hits := m.Search(contentTmp) 92 for _, hit := range hits { 93 ret = append(ret, hit) 94 } 95 96 if 1 > len(ret) { 97 return 98 } 99 100 ret = gulu.Str.RemoveDuplicatedElem(ret) 101 virtualBlockRefCache.SetWithTTL(root.ID, ret, 1, 10*time.Minute) 102 return 103} 104 105func CacheVirtualBlockRefJob() { 106 if !Conf.Editor.VirtualBlockRef { 107 return 108 } 109 task.AppendTask(task.CacheVirtualBlockRef, ResetVirtualBlockRefCache) 110} 111 112func ResetVirtualBlockRefCache() { 113 virtualBlockRefCache.Clear() 114 if !Conf.Editor.VirtualBlockRef { 115 return 116 } 117 118 searchIgnoreLines := getSearchIgnoreLines() 119 refSearchIgnoreLines := getRefSearchIgnoreLines() 120 keywords := sql.QueryVirtualRefKeywords(Conf.Search.VirtualRefName, Conf.Search.VirtualRefAlias, Conf.Search.VirtualRefAnchor, Conf.Search.VirtualRefDoc, searchIgnoreLines, refSearchIgnoreLines) 121 virtualBlockRefCache.Set("virtual_ref", keywords, 1) 122} 123 124func AddVirtualBlockRefInclude(keyword []string) { 125 if 1 > len(keyword) { 126 return 127 } 128 129 include := strings.ReplaceAll(Conf.Editor.VirtualBlockRefInclude, "\\,", "__comma@sep__") 130 includes := strings.Split(include, ",") 131 includes = append(includes, keyword...) 132 includes = gulu.Str.RemoveDuplicatedElem(includes) 133 Conf.Editor.VirtualBlockRefInclude = strings.Join(includes, ",") 134 Conf.Save() 135 136 ResetVirtualBlockRefCache() 137} 138 139func AddVirtualBlockRefExclude(keyword []string) { 140 if 1 > len(keyword) { 141 return 142 } 143 144 exclude := strings.ReplaceAll(Conf.Editor.VirtualBlockRefExclude, "\\,", "__comma@sep__") 145 excludes := strings.Split(exclude, ",") 146 excludes = append(excludes, keyword...) 147 excludes = gulu.Str.RemoveDuplicatedElem(excludes) 148 Conf.Editor.VirtualBlockRefExclude = strings.Join(excludes, ",") 149 Conf.Save() 150 151 ResetVirtualBlockRefCache() 152} 153 154func processVirtualRef(n *ast.Node, unlinks *[]*ast.Node, virtualBlockRefKeywords []string, refCount map[string]int, luteEngine *lute.Lute) bool { 155 if !Conf.Editor.VirtualBlockRef || 1 > len(virtualBlockRefKeywords) { 156 return false 157 } 158 159 if ast.NodeText != n.Type { 160 return false 161 } 162 163 parentBlock := treenode.ParentBlock(n) 164 if nil == parentBlock { 165 return false 166 } 167 168 if 0 < refCount[parentBlock.ID] { 169 // 如果块被引用过,则将其自身的文本排除在虚拟引用关键字之外 170 // Referenced blocks support rendering virtual references https://github.com/siyuan-note/siyuan/issues/10960 171 parentText := getNodeRefText(parentBlock) 172 virtualBlockRefKeywords = gulu.Str.RemoveElem(virtualBlockRefKeywords, parentText) 173 } 174 175 content := string(n.Tokens) 176 tmp := util.RemoveInvalid(content) 177 tmp = strings.TrimSpace(tmp) 178 if "" == tmp { 179 return false 180 } 181 182 newContent := markReplaceSpanWithSplit(content, virtualBlockRefKeywords, search.GetMarkSpanStart(search.VirtualBlockRefDataType), search.GetMarkSpanEnd()) 183 if content != newContent { 184 // 虚拟引用排除命中自身块命名和别名的情况 https://github.com/siyuan-note/siyuan/issues/3185 185 var blockKeys []string 186 if name := parentBlock.IALAttr("name"); "" != name { 187 blockKeys = append(blockKeys, name) 188 } 189 if alias := parentBlock.IALAttr("alias"); "" != alias { 190 blockKeys = append(blockKeys, alias) 191 } 192 if 0 < len(blockKeys) { 193 keys := gulu.Str.SubstringsBetween(newContent, search.GetMarkSpanStart(search.VirtualBlockRefDataType), search.GetMarkSpanEnd()) 194 for _, k := range keys { 195 if gulu.Str.Contains(k, blockKeys) { 196 return true 197 } 198 } 199 } 200 201 n.Tokens = []byte(newContent) 202 linkTree := parse.Inline("", n.Tokens, luteEngine.ParseOptions) 203 var children []*ast.Node 204 for c := linkTree.Root.FirstChild.FirstChild; nil != c; c = c.Next { 205 children = append(children, c) 206 } 207 for _, c := range children { 208 n.InsertBefore(c) 209 } 210 *unlinks = append(*unlinks, n) 211 return true 212 } 213 return false 214} 215 216func getVirtualRefKeywords(root *ast.Node) (ret []string) { 217 if !Conf.Editor.VirtualBlockRef { 218 return 219 } 220 221 if val, ok := virtualBlockRefCache.Get("virtual_ref"); ok { 222 ret = val.([]string) 223 } 224 225 if "" != strings.TrimSpace(Conf.Editor.VirtualBlockRefInclude) { 226 include := strings.ReplaceAll(Conf.Editor.VirtualBlockRefInclude, "\\,", "__comma@sep__") 227 includes := strings.Split(include, ",") 228 var tmp []string 229 for _, e := range includes { 230 e = strings.ReplaceAll(e, "__comma@sep__", ",") 231 tmp = append(tmp, e) 232 } 233 includes = tmp 234 ret = append(ret, includes...) 235 ret = gulu.Str.RemoveDuplicatedElem(ret) 236 } 237 238 if "" != strings.TrimSpace(Conf.Editor.VirtualBlockRefExclude) { 239 exclude := strings.ReplaceAll(Conf.Editor.VirtualBlockRefExclude, "\\,", "__comma@sep__") 240 excludes := strings.Split(exclude, ",") 241 var tmp, regexps []string 242 for _, e := range excludes { 243 e = strings.ReplaceAll(e, "__comma@sep__", ",") 244 if strings.HasPrefix(e, "/") && strings.HasSuffix(e, "/") { 245 regexps = append(regexps, e[1:len(e)-1]) 246 } else { 247 tmp = append(tmp, e) 248 } 249 } 250 excludes = tmp 251 ret = gulu.Str.ExcludeElem(ret, excludes) 252 if 0 < len(regexps) { 253 tmp = nil 254 for _, str := range ret { 255 matchExclude := false 256 for _, re := range regexps { 257 if ok, _ := regexp.MatchString(re, str); ok { 258 matchExclude = true 259 break 260 } 261 } 262 if !matchExclude { 263 tmp = append(tmp, str) 264 } 265 } 266 ret = tmp 267 } 268 } 269 270 // 虚拟引用排除当前文档名 https://github.com/siyuan-note/siyuan/issues/4537 271 // Virtual references exclude the name and aliases from the current document https://github.com/siyuan-note/siyuan/issues/9204 272 title := root.IALAttr("title") 273 ret = gulu.Str.ExcludeElem(ret, []string{title}) 274 if name := root.IALAttr("name"); "" != name { 275 ret = gulu.Str.ExcludeElem(ret, []string{name}) 276 } 277 if alias := root.IALAttr("alias"); "" != alias { 278 for _, a := range strings.Split(alias, ",") { 279 ret = gulu.Str.ExcludeElem(ret, []string{a}) 280 } 281 } 282 283 ret = prepareMarkKeywords(ret) 284 return 285} 286 287func prepareMarkKeywords(keywords []string) (ret []string) { 288 ret = gulu.Str.RemoveDuplicatedElem(keywords) 289 var tmp []string 290 for _, k := range ret { 291 if "" != k && "*" != k { // 提及和虚引排除 * Ignore `*` back mentions and virtual references https://github.com/siyuan-note/siyuan/issues/10873 292 tmp = append(tmp, k) 293 } 294 } 295 ret = tmp 296 297 sort.SliceStable(ret, func(i, j int) bool { 298 return len(ret[i]) > len(ret[j]) 299 }) 300 return 301}