siyuan/kernel/model/tag.go

383 lines
9.6 KiB
Go

// SiYuan - Refactor your thinking
// Copyright (c) 2020-present, b3log.org
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package model
import (
"errors"
"fmt"
"sort"
"strings"
"github.com/88250/lute/ast"
"github.com/emirpasic/gods/sets/hashset"
"github.com/siyuan-note/logging"
"github.com/siyuan-note/siyuan/kernel/search"
"github.com/siyuan-note/siyuan/kernel/sql"
"github.com/siyuan-note/siyuan/kernel/treenode"
"github.com/siyuan-note/siyuan/kernel/util"
)
func RemoveTag(label string) (err error) {
if "" == label {
return
}
util.PushEndlessProgress(Conf.Language(116))
util.RandomSleep(1000, 2000)
tags := sql.QueryTagSpansByLabel(label)
treeBlocks := map[string][]string{}
for _, tag := range tags {
if blocks, ok := treeBlocks[tag.RootID]; !ok {
treeBlocks[tag.RootID] = []string{tag.BlockID}
} else {
treeBlocks[tag.RootID] = append(blocks, tag.BlockID)
}
}
for treeID, blocks := range treeBlocks {
util.PushEndlessProgress("[" + treeID + "]")
tree, e := LoadTreeByBlockID(treeID)
if nil != e {
util.ClearPushProgress(100)
return e
}
var unlinks []*ast.Node
for _, blockID := range blocks {
node := treenode.GetNodeInTree(tree, blockID)
if nil == node {
continue
}
if ast.NodeDocument == node.Type {
if docTagsVal := node.IALAttr("tags"); strings.Contains(docTagsVal, label) {
docTags := strings.Split(docTagsVal, ",")
var tmp []string
for _, docTag := range docTags {
if docTag != label {
tmp = append(tmp, docTag)
continue
}
}
node.SetIALAttr("tags", strings.Join(tmp, ","))
}
continue
}
nodeTags := node.ChildrenByType(ast.NodeTextMark)
for _, nodeTag := range nodeTags {
if nodeTag.IsTextMarkType("tag") {
if label == nodeTag.TextMarkTextContent {
unlinks = append(unlinks, nodeTag)
}
}
}
}
for _, n := range unlinks {
n.Unlink()
}
util.PushEndlessProgress(fmt.Sprintf(Conf.Language(111), util.EscapeHTML(tree.Root.IALAttr("title"))))
if err = writeTreeUpsertQueue(tree); err != nil {
util.ClearPushProgress(100)
return
}
util.RandomSleep(50, 150)
}
util.ReloadUI()
return
}
func RenameTag(oldLabel, newLabel string) (err error) {
if invalidChar := treenode.ContainsMarker(newLabel); "" != invalidChar {
return errors.New(fmt.Sprintf(Conf.Language(112), invalidChar))
}
newLabel = strings.TrimSpace(newLabel)
newLabel = strings.TrimPrefix(newLabel, "/")
newLabel = strings.TrimSuffix(newLabel, "/")
newLabel = strings.TrimSpace(newLabel)
if "" == newLabel {
return errors.New(Conf.Language(114))
}
if oldLabel == newLabel {
return
}
util.PushEndlessProgress(Conf.Language(110))
util.RandomSleep(500, 1000)
tags := sql.QueryTagSpansByLabel(oldLabel)
treeBlocks := map[string][]string{}
for _, tag := range tags {
if blocks, ok := treeBlocks[tag.RootID]; !ok {
treeBlocks[tag.RootID] = []string{tag.BlockID}
} else {
treeBlocks[tag.RootID] = append(blocks, tag.BlockID)
}
}
for treeID, blocks := range treeBlocks {
util.PushEndlessProgress("[" + treeID + "]")
tree, e := LoadTreeByBlockID(treeID)
if nil != e {
util.ClearPushProgress(100)
return e
}
for _, blockID := range blocks {
node := treenode.GetNodeInTree(tree, blockID)
if nil == node {
continue
}
if ast.NodeDocument == node.Type {
if docTagsVal := node.IALAttr("tags"); strings.Contains(docTagsVal, oldLabel) {
docTags := strings.Split(docTagsVal, ",")
var tmp []string
for _, docTag := range docTags {
if strings.HasPrefix(docTag, oldLabel+"/") || docTag == oldLabel {
docTag = strings.Replace(docTag, oldLabel, newLabel, 1)
tmp = append(tmp, docTag)
} else {
tmp = append(tmp, docTag)
}
}
node.SetIALAttr("tags", strings.Join(tmp, ","))
}
continue
}
nodeTags := node.ChildrenByType(ast.NodeTextMark)
for _, nodeTag := range nodeTags {
if nodeTag.IsTextMarkType("tag") {
if strings.HasPrefix(nodeTag.TextMarkTextContent, oldLabel+"/") || nodeTag.TextMarkTextContent == oldLabel {
nodeTag.TextMarkTextContent = strings.Replace(nodeTag.TextMarkTextContent, oldLabel, newLabel, 1)
}
}
}
}
util.PushEndlessProgress(fmt.Sprintf(Conf.Language(111), util.EscapeHTML(tree.Root.IALAttr("title"))))
if err = writeTreeUpsertQueue(tree); err != nil {
util.ClearPushProgress(100)
return
}
util.RandomSleep(50, 150)
}
util.ReloadUI()
return
}
type TagBlocks []*Block
func (s TagBlocks) Len() int { return len(s) }
func (s TagBlocks) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s TagBlocks) Less(i, j int) bool { return s[i].ID < s[j].ID }
type Tag struct {
Name string `json:"name"`
Label string `json:"label"`
Children Tags `json:"children"`
Type string `json:"type"` // "tag"
Depth int `json:"depth"`
Count int `json:"count"`
tags Tags
}
type Tags []*Tag
func BuildTags() (ret *Tags) {
WaitForWritingFiles()
sql.FlushQueue()
ret = &Tags{}
labels := labelTags()
tags := Tags{}
for label := range labels {
tags = buildTags(tags, strings.Split(label, "/"), 0)
}
appendTagChildren(&tags, labels)
sortTags(tags)
var total int
tmp := &Tags{}
for _, tag := range tags {
*tmp = append(*tmp, tag)
countTag(tag, &total)
if Conf.FileTree.MaxListCount < total {
util.PushMsg(fmt.Sprintf(Conf.Language(243), Conf.FileTree.MaxListCount), 7000)
break
}
}
ret = tmp
return
}
func countTag(tag *Tag, total *int) {
*total += 1
for _, child := range tag.tags {
countTag(child, total)
}
}
func sortTags(tags Tags) {
switch Conf.Tag.Sort {
case util.SortModeNameASC:
sort.Slice(tags, func(i, j int) bool {
return util.PinYinCompare(tags[i].Name, tags[j].Name)
})
case util.SortModeNameDESC:
sort.Slice(tags, func(j, i int) bool {
return util.PinYinCompare(tags[i].Name, tags[j].Name)
})
case util.SortModeAlphanumASC:
sort.Slice(tags, func(i, j int) bool {
return util.NaturalCompare((tags)[i].Name, (tags)[j].Name)
})
case util.SortModeAlphanumDESC:
sort.Slice(tags, func(i, j int) bool {
return util.NaturalCompare((tags)[j].Name, (tags)[i].Name)
})
case util.SortModeRefCountASC:
sort.Slice(tags, func(i, j int) bool { return (tags)[i].Count < (tags)[j].Count })
case util.SortModeRefCountDESC:
sort.Slice(tags, func(i, j int) bool { return (tags)[i].Count > (tags)[j].Count })
default:
sort.Slice(tags, func(i, j int) bool {
return util.NaturalCompare((tags)[i].Name, (tags)[j].Name)
})
}
}
func SearchTags(keyword string) (ret []string) {
ret = []string{}
defer logging.Recover() // 定位 无法添加题头图标签 https://github.com/siyuan-note/siyuan/issues/6756
labels := labelBlocksByKeyword(keyword)
for label := range labels {
_, t := search.MarkText(label, keyword, 1024, Conf.Search.CaseSensitive)
ret = append(ret, t)
}
sort.Strings(ret)
return
}
func labelBlocksByKeyword(keyword string) (ret map[string]TagBlocks) {
ret = map[string]TagBlocks{}
tags := sql.QueryTagSpansByKeyword(keyword, Conf.Search.Limit)
set := hashset.New()
for _, tag := range tags {
set.Add(tag.BlockID)
}
var blockIDs []string
for _, v := range set.Values() {
blockIDs = append(blockIDs, v.(string))
}
sort.SliceStable(blockIDs, func(i, j int) bool {
return blockIDs[i] > blockIDs[j]
})
sqlBlocks := sql.GetBlocks(blockIDs)
blockMap := map[string]*sql.Block{}
for _, block := range sqlBlocks {
if nil == block {
continue
}
blockMap[block.ID] = block
}
for _, tag := range tags {
label := tag.Content
parentSQLBlock := blockMap[tag.BlockID]
block := fromSQLBlock(parentSQLBlock, "", 0)
if blocks, ok := ret[label]; ok {
blocks = append(blocks, block)
ret[label] = blocks
} else {
ret[label] = []*Block{block}
}
}
return
}
func labelTags() (ret map[string]Tags) {
ret = map[string]Tags{}
tagSpans := sql.QueryTagSpans("")
for _, tagSpan := range tagSpans {
label := tagSpan.Content
if _, ok := ret[label]; ok {
ret[label] = append(ret[label], &Tag{})
} else {
ret[label] = Tags{}
}
}
return
}
func appendTagChildren(tags *Tags, labels map[string]Tags) {
for _, tag := range *tags {
tag.Label = tag.Name
if _, ok := labels[tag.Label]; ok {
tag.Count = len(labels[tag.Label]) + 1
}
appendChildren0(tag, labels)
sortTags(tag.Children)
}
}
func appendChildren0(tag *Tag, labels map[string]Tags) {
sortTags(tag.tags)
for _, t := range tag.tags {
t.Label = tag.Label + "/" + t.Name
if _, ok := labels[t.Label]; ok {
t.Count = len(labels[t.Label]) + 1
}
tag.Children = append(tag.Children, t)
}
for _, child := range tag.tags {
appendChildren0(child, labels)
}
}
func buildTags(root Tags, labels []string, depth int) Tags {
if 1 > len(labels) {
return root
}
i := 0
for ; i < len(root); i++ {
if (root)[i].Name == labels[0] {
break
}
}
if i == len(root) {
root = append(root, &Tag{Name: util.EscapeHTML(labels[0]), Type: "tag", Depth: depth})
}
depth++
root[i].tags = buildTags(root[i].tags, labels[1:], depth)
return root
}