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 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}