A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 424 lines 12 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 "os" 22 "path" 23 "path/filepath" 24 "sort" 25 "strings" 26 27 "github.com/88250/lute/ast" 28 "github.com/88250/lute/parse" 29 "github.com/siyuan-note/logging" 30 "github.com/siyuan-note/siyuan/kernel/search" 31 "github.com/siyuan-note/siyuan/kernel/sql" 32 "github.com/siyuan-note/siyuan/kernel/treenode" 33 "github.com/siyuan-note/siyuan/kernel/util" 34) 35 36func createDocsByHPath(boxID, hPath, content, parentID, id string) (retID string, err error) { 37 if "" == id { 38 id = ast.NewNodeID() 39 } 40 retID = id 41 42 hPath = strings.TrimSuffix(hPath, ".sy") 43 hPath = util.TrimSpaceInPath(hPath) 44 if "" != parentID { 45 // The save path is incorrect when creating a sub-doc by ref in a doc with the same name https://github.com/siyuan-note/siyuan/issues/8138 46 // 在指定了父文档 ID 的情况下优先查找父文档 47 parentHPath, name := path.Split(hPath) 48 parentHPath = strings.TrimSuffix(parentHPath, "/") 49 preferredParent := treenode.GetBlockTreeByHPathPreferredParentID(boxID, parentHPath, parentID) 50 if nil != preferredParent && preferredParent.RootID == parentID { 51 // 如果父文档存在且 ID 一致,则直接在父文档下创建 52 p := strings.TrimSuffix(preferredParent.Path, ".sy") + "/" + id + ".sy" 53 if _, err = createDoc(boxID, p, name, content); err != nil { 54 logging.LogErrorf("create doc [%s] failed: %s", p, err) 55 } 56 return 57 } 58 } 59 60 root := treenode.GetBlockTreeRootByPath(boxID, hPath) 61 if nil != root { 62 retID = root.ID 63 return 64 } 65 66 hPathBuilder := bytes.Buffer{} 67 hpathBtMap := map[string]*treenode.BlockTree{} 68 parts := strings.Split(hPath, "/")[1:] 69 // The subdoc creation path is unstable when a parent doc with the same name exists https://github.com/siyuan-note/siyuan/issues/9322 70 // 存在同名父文档时子文档创建路径不稳定,这里需要按照完整的 hpath 映射,不能在下面的循环中边构建 hpath 边构建 path,否则虽然 hpath 相同,但是会导致 path 组装错位 71 for i, part := range parts { 72 if i == len(parts)-1 { 73 break 74 } 75 76 hPathBuilder.WriteString("/") 77 hPathBuilder.WriteString(part) 78 hp := hPathBuilder.String() 79 root = treenode.GetBlockTreeRootByHPath(boxID, hp) 80 if nil == root { 81 break 82 } 83 84 hpathBtMap[hp] = root 85 } 86 87 pathBuilder := bytes.Buffer{} 88 pathBuilder.WriteString("/") 89 hPathBuilder = bytes.Buffer{} 90 hPathBuilder.WriteString("/") 91 for i, part := range parts { 92 hPathBuilder.WriteString(part) 93 hp := hPathBuilder.String() 94 root = hpathBtMap[hp] 95 isNotLast := i < len(parts)-1 96 if nil == root { 97 rootID := ast.NewNodeID() 98 if i == len(parts)-1 { 99 rootID = retID 100 } 101 102 pathBuilder.WriteString(rootID) 103 docP := pathBuilder.String() + ".sy" 104 if isNotLast { 105 if _, err = createDoc(boxID, docP, part, ""); err != nil { 106 return 107 } 108 } else { 109 if _, err = createDoc(boxID, docP, part, content); err != nil { 110 return 111 } 112 } 113 114 if isNotLast { 115 dirPath := filepath.Join(util.DataDir, boxID, pathBuilder.String()) 116 if err = os.MkdirAll(dirPath, 0755); err != nil { 117 logging.LogErrorf("mkdir [%s] failed: %s", dirPath, err) 118 return 119 } 120 } 121 } else { 122 pathBuilder.WriteString(root.ID) 123 if !isNotLast { 124 pathBuilder.WriteString(".sy") 125 } 126 } 127 128 if isNotLast { 129 pathBuilder.WriteString("/") 130 hPathBuilder.WriteString("/") 131 } 132 } 133 return 134} 135 136func toFlatTree(blocks []*Block, baseDepth int, typ string, tree *parse.Tree) (ret []*Path) { 137 var blockRoots []*Block 138 for _, block := range blocks { 139 root := getBlockIn(blockRoots, block.RootID) 140 if nil == root { 141 root, _ = getBlock(block.RootID, tree) 142 blockRoots = append(blockRoots, root) 143 } 144 if nil == root { 145 return 146 } 147 block.Depth = baseDepth + 1 148 block.Count = len(block.Children) 149 root.Children = append(root.Children, block) 150 } 151 152 folded := false 153 if "outline" == typ { 154 folded = true 155 } 156 157 for _, root := range blockRoots { 158 treeNode := &Path{ 159 ID: root.ID, 160 Box: root.Box, 161 Name: path.Base(root.HPath), 162 NodeType: root.Type, 163 Type: typ, 164 SubType: root.SubType, 165 Depth: baseDepth, 166 Count: len(root.Children), 167 Folded: folded, 168 169 Updated: root.IAL["updated"], 170 Created: root.ID[:14], 171 } 172 for _, c := range root.Children { 173 treeNode.Blocks = append(treeNode.Blocks, c) 174 } 175 ret = append(ret, treeNode) 176 177 if "backlink" == typ { 178 treeNode.HPath = root.HPath 179 } 180 } 181 182 sort.Slice(ret, func(i, j int) bool { 183 return ret[i].ID > ret[j].ID 184 }) 185 return 186} 187 188func toSubTree(blocks []*Block, keyword string) (ret []*Path) { 189 keyword = strings.TrimSpace(keyword) 190 var blockRoots []*Block 191 for _, block := range blocks { 192 root := getBlockIn(blockRoots, block.RootID) 193 if nil == root { 194 root, _ = getBlock(block.RootID, nil) 195 blockRoots = append(blockRoots, root) 196 } 197 block.Depth = 1 198 block.Count = len(block.Children) 199 root.Children = append(root.Children, block) 200 } 201 202 for _, root := range blockRoots { 203 treeNode := &Path{ 204 ID: root.ID, 205 Box: root.Box, 206 Name: path.Base(root.HPath), 207 Type: "backlink", 208 NodeType: "NodeDocument", 209 SubType: root.SubType, 210 Depth: 0, 211 Count: len(root.Children), 212 } 213 for _, c := range root.Children { 214 if "NodeListItem" == c.Type { 215 tree, _ := LoadTreeByBlockID(c.RootID) 216 li := treenode.GetNodeInTree(tree, c.ID) 217 if nil == li || nil == li.FirstChild { 218 // 反链面板拖拽到文档以后可能会出现这种情况 https://github.com/siyuan-note/siyuan/issues/5363 219 continue 220 } 221 222 var first *sql.Block 223 if 3 != li.ListData.Typ { 224 first = sql.GetBlock(li.FirstChild.ID) 225 } else { 226 first = sql.GetBlock(li.FirstChild.Next.ID) 227 } 228 name := first.Content 229 parentPos := 0 230 if "" != keyword { 231 parentPos, name = search.MarkText(name, keyword, 12, Conf.Search.CaseSensitive) 232 } 233 subRoot := &Path{ 234 ID: li.ID, 235 Box: li.Box, 236 Name: name, 237 Type: "backlink", 238 NodeType: li.Type.String(), 239 SubType: c.SubType, 240 Depth: 1, 241 Count: 1, 242 } 243 244 unfold := true 245 for liFirstBlockSpan := li.FirstChild.FirstChild; nil != liFirstBlockSpan; liFirstBlockSpan = liFirstBlockSpan.Next { 246 if treenode.IsBlockRef(liFirstBlockSpan) { 247 continue 248 } 249 if "" != strings.TrimSpace(liFirstBlockSpan.Text()) { 250 unfold = false 251 break 252 } 253 } 254 for next := li.FirstChild.Next; nil != next; next = next.Next { 255 subBlock, _ := getBlock(next.ID, tree) 256 if unfold { 257 if ast.NodeList == next.Type { 258 for subLi := next.FirstChild; nil != subLi; subLi = subLi.Next { 259 subLiBlock, _ := getBlock(subLi.ID, tree) 260 var subFirst *sql.Block 261 if 3 != subLi.ListData.Typ { 262 subFirst = sql.GetBlock(subLi.FirstChild.ID) 263 } else { 264 subFirst = sql.GetBlock(subLi.FirstChild.Next.ID) 265 } 266 subPos := 0 267 content := subFirst.Content 268 if "" != keyword { 269 subPos, content = search.MarkText(subFirst.Content, keyword, 12, Conf.Search.CaseSensitive) 270 } 271 if -1 < subPos { 272 parentPos = 0 // 需要显示父级 273 } 274 subLiBlock.Content = content 275 subLiBlock.Depth = 2 276 subRoot.Blocks = append(subRoot.Blocks, subLiBlock) 277 } 278 } else if ast.NodeHeading == next.Type { 279 subBlock.Depth = 2 280 subRoot.Blocks = append(subRoot.Blocks, subBlock) 281 headingChildren := treenode.HeadingChildren(next) 282 var breakSub bool 283 for _, n := range headingChildren { 284 block, _ := getBlock(n.ID, tree) 285 subPos := 0 286 content := block.Content 287 if "" != keyword { 288 subPos, content = search.MarkText(block.Content, keyword, 12, Conf.Search.CaseSensitive) 289 } 290 if -1 < subPos { 291 parentPos = 0 292 } 293 block.Content = content 294 block.Depth = 3 295 subRoot.Blocks = append(subRoot.Blocks, block) 296 if ast.NodeHeading == n.Type { 297 // 跳过子标题下面的块 298 breakSub = true 299 break 300 } 301 } 302 if breakSub { 303 break 304 } 305 } else { 306 if nil == treenode.HeadingParent(next) { 307 subBlock.Depth = 2 308 subRoot.Blocks = append(subRoot.Blocks, subBlock) 309 } 310 } 311 } 312 } 313 if -1 < parentPos { 314 treeNode.Children = append(treeNode.Children, subRoot) 315 } 316 } else if "NodeHeading" == c.Type { 317 tree, _ := LoadTreeByBlockID(c.RootID) 318 h := treenode.GetNodeInTree(tree, c.ID) 319 if nil == h { 320 continue 321 } 322 323 name := sql.GetBlock(h.ID).Content 324 parentPos := 0 325 if "" != keyword { 326 parentPos, name = search.MarkText(name, keyword, 12, Conf.Search.CaseSensitive) 327 } 328 subRoot := &Path{ 329 ID: h.ID, 330 Box: h.Box, 331 Name: name, 332 Type: "backlink", 333 NodeType: h.Type.String(), 334 SubType: c.SubType, 335 Depth: 1, 336 Count: 1, 337 } 338 339 unfold := true 340 for headingFirstSpan := h.FirstChild; nil != headingFirstSpan; headingFirstSpan = headingFirstSpan.Next { 341 if treenode.IsBlockRef(headingFirstSpan) { 342 continue 343 } 344 if "" != strings.TrimSpace(headingFirstSpan.Text()) { 345 unfold = false 346 break 347 } 348 } 349 350 if unfold { 351 headingChildren := treenode.HeadingChildren(h) 352 for _, headingChild := range headingChildren { 353 if ast.NodeList == headingChild.Type { 354 for subLi := headingChild.FirstChild; nil != subLi; subLi = subLi.Next { 355 subLiBlock, _ := getBlock(subLi.ID, tree) 356 var subFirst *sql.Block 357 if 3 != subLi.ListData.Typ { 358 subFirst = sql.GetBlock(subLi.FirstChild.ID) 359 } else { 360 subFirst = sql.GetBlock(subLi.FirstChild.Next.ID) 361 } 362 subPos := 0 363 content := subFirst.Content 364 if "" != keyword { 365 subPos, content = search.MarkText(content, keyword, 12, Conf.Search.CaseSensitive) 366 } 367 if -1 < subPos { 368 parentPos = 0 369 } 370 subLiBlock.Content = subFirst.Content 371 subLiBlock.Depth = 2 372 subRoot.Blocks = append(subRoot.Blocks, subLiBlock) 373 } 374 } else { 375 subBlock, _ := getBlock(headingChild.ID, tree) 376 subBlock.Depth = 2 377 subRoot.Blocks = append(subRoot.Blocks, subBlock) 378 } 379 } 380 } 381 382 if -1 < parentPos { 383 treeNode.Children = append(treeNode.Children, subRoot) 384 } 385 } else { 386 pos := 0 387 content := c.Content 388 if "" != keyword { 389 pos, content = search.MarkText(content, keyword, 12, Conf.Search.CaseSensitive) 390 } 391 if -1 < pos { 392 treeNode.Blocks = append(treeNode.Blocks, c) 393 } 394 } 395 } 396 397 rootPos := -1 398 var rootContent string 399 if "" != keyword { 400 rootPos, rootContent = search.MarkText(treeNode.Name, keyword, 12, Conf.Search.CaseSensitive) 401 treeNode.Name = rootContent 402 } 403 if 0 < len(treeNode.Children) || 0 < len(treeNode.Blocks) || (-1 < rootPos && "" != keyword) { 404 ret = append(ret, treeNode) 405 } 406 } 407 408 sort.Slice(ret, func(i, j int) bool { 409 return ret[i].ID > ret[j].ID 410 }) 411 return 412} 413 414func getBlockIn(blocks []*Block, id string) *Block { 415 if "" == id { 416 return nil 417 } 418 for _, block := range blocks { 419 if block.ID == id { 420 return block 421 } 422 } 423 return nil 424}