A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 421 lines 11 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 "errors" 21 "fmt" 22 "sort" 23 "strings" 24 25 "github.com/88250/gulu" 26 "github.com/88250/lute/ast" 27 "github.com/emirpasic/gods/sets/hashset" 28 "github.com/siyuan-note/siyuan/kernel/search" 29 "github.com/siyuan-note/siyuan/kernel/sql" 30 "github.com/siyuan-note/siyuan/kernel/treenode" 31 "github.com/siyuan-note/siyuan/kernel/util" 32) 33 34func RemoveTag(label string) (err error) { 35 if "" == label { 36 return 37 } 38 39 util.PushEndlessProgress(Conf.Language(116)) 40 util.RandomSleep(1000, 2000) 41 42 tags := sql.QueryTagSpansByLabel(label) 43 treeBlocks := map[string][]string{} 44 for _, tag := range tags { 45 if blocks, ok := treeBlocks[tag.RootID]; !ok { 46 treeBlocks[tag.RootID] = []string{tag.BlockID} 47 } else { 48 treeBlocks[tag.RootID] = append(blocks, tag.BlockID) 49 } 50 } 51 52 var reloadTreeIDs []string 53 updateNodes := map[string]*ast.Node{} 54 for treeID, blocks := range treeBlocks { 55 util.PushEndlessProgress("[" + treeID + "]") 56 tree, e := LoadTreeByBlockIDWithReindex(treeID) 57 if nil != e { 58 util.ClearPushProgress(100) 59 return e 60 } 61 62 var unlinks []*ast.Node 63 for _, blockID := range blocks { 64 node := treenode.GetNodeInTree(tree, blockID) 65 if nil == node { 66 continue 67 } 68 69 if ast.NodeDocument == node.Type { 70 if docTagsVal := node.IALAttr("tags"); strings.Contains(docTagsVal, label) { 71 docTags := strings.Split(docTagsVal, ",") 72 var tmp []string 73 for _, docTag := range docTags { 74 if docTag != label { 75 tmp = append(tmp, docTag) 76 continue 77 } 78 } 79 node.SetIALAttr("tags", strings.Join(tmp, ",")) 80 } 81 continue 82 } 83 84 nodeTags := node.ChildrenByType(ast.NodeTextMark) 85 for _, nodeTag := range nodeTags { 86 if nodeTag.IsTextMarkType("tag") { 87 if label == nodeTag.TextMarkTextContent { 88 unlinks = append(unlinks, nodeTag) 89 } 90 } 91 } 92 93 updateNodes[node.ID] = node 94 } 95 for _, n := range unlinks { 96 n.Unlink() 97 } 98 util.PushEndlessProgress(fmt.Sprintf(Conf.Language(111), util.EscapeHTML(tree.Root.IALAttr("title")))) 99 if err = writeTreeUpsertQueue(tree); err != nil { 100 util.ClearPushProgress(100) 101 return 102 } 103 util.RandomSleep(50, 150) 104 reloadTreeIDs = append(reloadTreeIDs, tree.ID) 105 } 106 107 sql.FlushQueue() 108 109 reloadTreeIDs = gulu.Str.RemoveDuplicatedElem(reloadTreeIDs) 110 for _, id := range reloadTreeIDs { 111 ReloadProtyle(id) 112 } 113 114 updateAttributeViewBlockText(updateNodes) 115 116 sql.FlushQueue() 117 util.PushClearProgress() 118 return 119} 120 121func RenameTag(oldLabel, newLabel string) (err error) { 122 if invalidChar := treenode.ContainsMarker(newLabel); "" != invalidChar { 123 return errors.New(fmt.Sprintf(Conf.Language(112), invalidChar)) 124 } 125 126 newLabel = strings.TrimSpace(newLabel) 127 newLabel = strings.TrimPrefix(newLabel, "/") 128 newLabel = strings.TrimSuffix(newLabel, "/") 129 newLabel = strings.TrimSpace(newLabel) 130 131 if "" == newLabel { 132 return errors.New(Conf.Language(114)) 133 } 134 135 if oldLabel == newLabel { 136 return 137 } 138 139 util.PushEndlessProgress(Conf.Language(110)) 140 util.RandomSleep(500, 1000) 141 142 tags := sql.QueryTagSpansByLabel(oldLabel) 143 treeBlocks := map[string][]string{} 144 for _, tag := range tags { 145 if blocks, ok := treeBlocks[tag.RootID]; !ok { 146 treeBlocks[tag.RootID] = []string{tag.BlockID} 147 } else { 148 treeBlocks[tag.RootID] = append(blocks, tag.BlockID) 149 } 150 } 151 152 var reloadTreeIDs []string 153 updateNodes := map[string]*ast.Node{} 154 155 for treeID, blocks := range treeBlocks { 156 util.PushEndlessProgress("[" + treeID + "]") 157 tree, e := LoadTreeByBlockIDWithReindex(treeID) 158 if nil != e { 159 util.ClearPushProgress(100) 160 return e 161 } 162 163 for _, blockID := range blocks { 164 node := treenode.GetNodeInTree(tree, blockID) 165 if nil == node { 166 continue 167 } 168 169 if ast.NodeDocument == node.Type { 170 if docTagsVal := node.IALAttr("tags"); strings.Contains(docTagsVal, oldLabel) { 171 docTags := strings.Split(docTagsVal, ",") 172 var tmp []string 173 for _, docTag := range docTags { 174 if strings.HasPrefix(docTag, oldLabel+"/") || docTag == oldLabel { 175 docTag = strings.Replace(docTag, oldLabel, newLabel, 1) 176 tmp = append(tmp, docTag) 177 } else { 178 tmp = append(tmp, docTag) 179 } 180 } 181 node.SetIALAttr("tags", strings.Join(tmp, ",")) 182 } 183 continue 184 } 185 186 nodeTags := node.ChildrenByType(ast.NodeTextMark) 187 for _, nodeTag := range nodeTags { 188 if nodeTag.IsTextMarkType("tag") { 189 if strings.HasPrefix(nodeTag.TextMarkTextContent, oldLabel+"/") || nodeTag.TextMarkTextContent == oldLabel { 190 nodeTag.TextMarkTextContent = strings.Replace(nodeTag.TextMarkTextContent, oldLabel, newLabel, 1) 191 } 192 } 193 } 194 195 updateNodes[node.ID] = node 196 } 197 util.PushEndlessProgress(fmt.Sprintf(Conf.Language(111), util.EscapeHTML(tree.Root.IALAttr("title")))) 198 if err = writeTreeUpsertQueue(tree); err != nil { 199 util.ClearPushProgress(100) 200 return 201 } 202 util.RandomSleep(50, 150) 203 reloadTreeIDs = append(reloadTreeIDs, tree.ID) 204 } 205 206 sql.FlushQueue() 207 208 reloadTreeIDs = gulu.Str.RemoveDuplicatedElem(reloadTreeIDs) 209 for _, id := range reloadTreeIDs { 210 ReloadProtyle(id) 211 } 212 213 updateAttributeViewBlockText(updateNodes) 214 215 sql.FlushQueue() 216 util.PushClearProgress() 217 return 218} 219 220type TagBlocks []*Block 221 222func (s TagBlocks) Len() int { return len(s) } 223func (s TagBlocks) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 224func (s TagBlocks) Less(i, j int) bool { return s[i].ID < s[j].ID } 225 226type Tag struct { 227 Name string `json:"name"` 228 Label string `json:"label"` 229 Children Tags `json:"children"` 230 Type string `json:"type"` // "tag" 231 Depth int `json:"depth"` 232 Count int `json:"count"` 233 234 tags Tags 235} 236 237type Tags []*Tag 238 239func BuildTags(ignoreMaxListHintArg bool, appID string) (ret *Tags) { 240 FlushTxQueue() 241 sql.FlushQueue() 242 243 ret = &Tags{} 244 labels := labelTags() 245 tags := Tags{} 246 for label := range labels { 247 tags = buildTags(tags, strings.Split(label, "/"), 0) 248 } 249 appendTagChildren(&tags, labels) 250 sortTags(tags) 251 252 var total int 253 tmp := &Tags{} 254 for _, tag := range tags { 255 *tmp = append(*tmp, tag) 256 countTag(tag, &total) 257 if Conf.FileTree.MaxListCount < total && !ignoreMaxListHintArg { 258 util.PushMsgWithApp(appID, fmt.Sprintf(Conf.Language(243), Conf.FileTree.MaxListCount), 7000) 259 break 260 } 261 } 262 263 ret = tmp 264 return 265} 266 267func countTag(tag *Tag, total *int) { 268 *total += 1 269 for _, child := range tag.tags { 270 countTag(child, total) 271 } 272} 273 274func sortTags(tags Tags) { 275 switch Conf.Tag.Sort { 276 case util.SortModeNameASC: 277 sort.Slice(tags, func(i, j int) bool { 278 return util.PinYinCompare(tags[i].Name, tags[j].Name) 279 }) 280 case util.SortModeNameDESC: 281 sort.Slice(tags, func(j, i int) bool { 282 return util.PinYinCompare(tags[i].Name, tags[j].Name) 283 }) 284 case util.SortModeAlphanumASC: 285 sort.Slice(tags, func(i, j int) bool { 286 return util.NaturalCompare((tags)[i].Name, (tags)[j].Name) 287 }) 288 case util.SortModeAlphanumDESC: 289 sort.Slice(tags, func(i, j int) bool { 290 return util.NaturalCompare((tags)[j].Name, (tags)[i].Name) 291 }) 292 case util.SortModeRefCountASC: 293 sort.Slice(tags, func(i, j int) bool { return (tags)[i].Count < (tags)[j].Count }) 294 case util.SortModeRefCountDESC: 295 sort.Slice(tags, func(i, j int) bool { return (tags)[i].Count > (tags)[j].Count }) 296 default: 297 sort.Slice(tags, func(i, j int) bool { 298 return util.NaturalCompare((tags)[i].Name, (tags)[j].Name) 299 }) 300 } 301} 302 303func SearchTags(keyword string) (ret []string) { 304 ret = []string{} 305 306 sql.FlushQueue() 307 308 labels := labelBlocksByKeyword(keyword) 309 keyword = strings.Join(strings.Split(keyword, " "), search.TermSep) 310 for label := range labels { 311 if "" == keyword { 312 ret = append(ret, util.EscapeHTML(label)) 313 continue 314 } 315 316 _, t := search.MarkText(label, keyword, 1024, Conf.Search.CaseSensitive) 317 ret = append(ret, t) 318 } 319 sort.Strings(ret) 320 return 321} 322 323func labelBlocksByKeyword(keyword string) (ret map[string]TagBlocks) { 324 ret = map[string]TagBlocks{} 325 326 tags := sql.QueryTagSpansByKeyword(keyword, Conf.Search.Limit) 327 set := hashset.New() 328 for _, tag := range tags { 329 set.Add(tag.BlockID) 330 } 331 var blockIDs []string 332 for _, v := range set.Values() { 333 blockIDs = append(blockIDs, v.(string)) 334 } 335 sort.SliceStable(blockIDs, func(i, j int) bool { 336 return blockIDs[i] > blockIDs[j] 337 }) 338 339 sqlBlocks := sql.GetBlocks(blockIDs) 340 blockMap := map[string]*sql.Block{} 341 for _, block := range sqlBlocks { 342 if nil == block { 343 continue 344 } 345 346 blockMap[block.ID] = block 347 } 348 349 for _, tag := range tags { 350 label := tag.Content 351 352 parentSQLBlock := blockMap[tag.BlockID] 353 block := fromSQLBlock(parentSQLBlock, "", 0) 354 if blocks, ok := ret[label]; ok { 355 blocks = append(blocks, block) 356 ret[label] = blocks 357 } else { 358 ret[label] = []*Block{block} 359 } 360 } 361 return 362} 363 364func labelTags() (ret map[string]Tags) { 365 ret = map[string]Tags{} 366 367 tagSpans := sql.QueryTagSpans("") 368 for _, tagSpan := range tagSpans { 369 label := util.UnescapeHTML(tagSpan.Content) 370 if _, ok := ret[label]; ok { 371 ret[label] = append(ret[label], &Tag{}) 372 } else { 373 ret[label] = Tags{} 374 } 375 } 376 return 377} 378 379func appendTagChildren(tags *Tags, labels map[string]Tags) { 380 for _, tag := range *tags { 381 tag.Label = tag.Name 382 if _, ok := labels[tag.Label]; ok { 383 tag.Count = len(labels[tag.Label]) + 1 384 } 385 appendChildren0(tag, labels) 386 sortTags(tag.Children) 387 } 388} 389 390func appendChildren0(tag *Tag, labels map[string]Tags) { 391 sortTags(tag.tags) 392 for _, t := range tag.tags { 393 t.Label = tag.Label + "/" + t.Name 394 if _, ok := labels[t.Label]; ok { 395 t.Count = len(labels[t.Label]) + 1 396 } 397 tag.Children = append(tag.Children, t) 398 } 399 for _, child := range tag.tags { 400 appendChildren0(child, labels) 401 } 402} 403 404func buildTags(root Tags, labels []string, depth int) Tags { 405 if 1 > len(labels) { 406 return root 407 } 408 409 i := 0 410 for ; i < len(root); i++ { 411 if (root)[i].Name == labels[0] { 412 break 413 } 414 } 415 if i == len(root) { 416 root = append(root, &Tag{Name: util.EscapeHTML(labels[0]), Type: "tag", Depth: depth}) 417 } 418 depth++ 419 root[i].tags = buildTags(root[i].tags, labels[1:], depth) 420 return root 421}