A privacy-first, self-hosted, fully open source personal knowledge management software, written in typescript and golang. (PERSONAL FORK)
at lambda-fork/main 382 lines 9.7 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 "github.com/88250/gulu" 21 "github.com/88250/lute/ast" 22 "github.com/88250/lute/html" 23 "github.com/88250/lute/parse" 24 "github.com/emirpasic/gods/stacks/linkedliststack" 25 "github.com/siyuan-note/siyuan/kernel/treenode" 26 "github.com/siyuan-note/siyuan/kernel/util" 27) 28 29func (tx *Transaction) doMoveOutlineHeading(operation *Operation) (ret *TxErr) { 30 headingID := operation.ID 31 previousID := operation.PreviousID 32 parentID := operation.ParentID 33 34 tree, err := tx.loadTree(headingID) 35 if err != nil { 36 return &TxErr{code: TxErrCodeBlockNotFound, id: headingID} 37 } 38 operation.RetData = tree.Root.ID 39 40 if headingID == parentID || headingID == previousID { 41 return 42 } 43 44 heading := treenode.GetNodeInTree(tree, headingID) 45 if nil == heading { 46 return &TxErr{code: TxErrCodeBlockNotFound, id: headingID} 47 } 48 49 if ast.NodeDocument != heading.Parent.Type { 50 // 仅支持文档根节点下第一层标题,不支持容器块内标题 51 util.PushMsg(Conf.language(240), 5000) 52 return 53 } 54 55 headings := []*ast.Node{} 56 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { 57 if entering && ast.NodeHeading == n.Type && !n.ParentIs(ast.NodeBlockquote) { 58 headings = append(headings, n) 59 } 60 return ast.WalkContinue 61 }) 62 63 headingChildren := treenode.HeadingChildren(heading) 64 65 if "" != previousID { 66 previousHeading := treenode.GetNodeInTree(tree, previousID) 67 if nil == previousHeading { 68 return &TxErr{code: TxErrCodeBlockNotFound, id: previousID} 69 } 70 71 if ast.NodeDocument != previousHeading.Parent.Type { 72 // 仅支持文档根节点下第一层标题,不支持容器块内标题 73 util.PushMsg(Conf.language(248), 5000) 74 return 75 } 76 77 for _, h := range headingChildren { 78 if h.ID == previousID { 79 // 不能移动到自己的子标题下 80 util.PushMsg(Conf.language(241), 5000) 81 return 82 } 83 } 84 85 generateOpTypeHistory(tree, HistoryOpOutline) 86 87 targetNode := previousHeading 88 previousHeadingChildren := treenode.HeadingChildren(previousHeading) 89 if 0 < len(previousHeadingChildren) { 90 targetNode = previousHeadingChildren[len(previousHeadingChildren)-1] 91 } 92 93 for _, h := range headingChildren { 94 if h.ID == targetNode.ID { // 目标节点是当前标题的子节点 95 targetNode = heading.Previous 96 } 97 } 98 if targetNode.ID == heading.ID { 99 targetNode = heading.Previous 100 } 101 102 diffLevel := heading.HeadingLevel - previousHeading.HeadingLevel 103 heading.HeadingLevel = previousHeading.HeadingLevel 104 105 for i := len(headingChildren) - 1; i >= 0; i-- { 106 child := headingChildren[i] 107 if ast.NodeHeading == child.Type { 108 child.HeadingLevel -= diffLevel 109 if 6 < child.HeadingLevel { 110 child.HeadingLevel = 6 111 } 112 } 113 targetNode.InsertAfter(child) 114 } 115 targetNode.InsertAfter(heading) 116 } else if "" != parentID { 117 parentHeading := treenode.GetNodeInTree(tree, parentID) 118 if nil == parentHeading { 119 return &TxErr{code: TxErrCodeBlockNotFound, id: parentID} 120 } 121 122 if ast.NodeDocument != parentHeading.Parent.Type { 123 // 仅支持文档根节点下第一层标题,不支持容器块内标题 124 util.PushMsg(Conf.language(248), 5000) 125 return 126 } 127 128 for _, h := range headingChildren { 129 if h.ID == parentID { 130 // 不能移动到自己的子标题下 131 util.PushMsg(Conf.language(241), 5000) 132 return 133 } 134 } 135 136 generateOpTypeHistory(tree, HistoryOpOutline) 137 138 targetNode := parentHeading 139 parentHeadingChildren := treenode.HeadingChildren(parentHeading) 140 // 找到下方第一个非标题节点 141 var tmp []*ast.Node 142 for _, child := range parentHeadingChildren { 143 if ast.NodeHeading == child.Type { 144 break 145 } 146 tmp = append(tmp, child) 147 } 148 parentHeadingChildren = tmp 149 if 0 < len(parentHeadingChildren) { 150 for _, child := range parentHeadingChildren { 151 if child.ID == headingID { 152 break 153 } 154 targetNode = child 155 } 156 } 157 158 diffLevel := heading.HeadingLevel - parentHeading.HeadingLevel - 1 159 heading.HeadingLevel = parentHeading.HeadingLevel + 1 160 if 6 < heading.HeadingLevel { 161 heading.HeadingLevel = 6 162 } 163 164 for i := len(headingChildren) - 1; i >= 0; i-- { 165 child := headingChildren[i] 166 if ast.NodeHeading == child.Type { 167 child.HeadingLevel -= diffLevel 168 if 6 < child.HeadingLevel { 169 child.HeadingLevel = 6 170 } 171 } 172 targetNode.InsertAfter(child) 173 } 174 targetNode.InsertAfter(heading) 175 } else { 176 generateOpTypeHistory(tree, HistoryOpOutline) 177 178 // 移到第一个标题前 179 var firstHeading *ast.Node 180 for n := tree.Root.FirstChild; nil != n; n = n.Next { 181 if ast.NodeHeading == n.Type { 182 firstHeading = n 183 break 184 } 185 } 186 if nil == firstHeading || firstHeading.ID == heading.ID { 187 return 188 } 189 190 diffLevel := heading.HeadingLevel - firstHeading.HeadingLevel 191 heading.HeadingLevel = firstHeading.HeadingLevel 192 193 firstHeading.InsertBefore(heading) 194 for i := 0; i < len(headingChildren); i++ { 195 child := headingChildren[i] 196 if ast.NodeHeading == child.Type { 197 child.HeadingLevel -= diffLevel 198 if 6 < child.HeadingLevel { 199 child.HeadingLevel = 6 200 } 201 } 202 firstHeading.InsertBefore(child) 203 } 204 } 205 206 if err = tx.writeTree(tree); err != nil { 207 return 208 } 209 return 210} 211 212func Outline(rootID string, preview bool) (ret []*Path, err error) { 213 FlushTxQueue() 214 215 ret = []*Path{} 216 tree, _ := LoadTreeByBlockID(rootID) 217 if nil == tree { 218 return 219 } 220 221 if preview && Conf.Export.AddTitle { 222 if root, _ := getBlock(tree.ID, tree); nil != root { 223 root.IAL["type"] = "doc" 224 title := &ast.Node{ID: root.ID, Type: ast.NodeHeading, HeadingLevel: 1} 225 for k, v := range root.IAL { 226 if "type" == k { 227 continue 228 } 229 title.SetIALAttr(k, v) 230 } 231 title.InsertAfter(&ast.Node{Type: ast.NodeKramdownBlockIAL, Tokens: parse.IAL2Tokens(title.KramdownIAL)}) 232 233 content := html.UnescapeString(root.Content) 234 title.AppendChild(&ast.Node{Type: ast.NodeText, Tokens: []byte(content)}) 235 tree.Root.PrependChild(title) 236 } 237 } 238 239 ret = outline(tree) 240 241 storage, _ := GetOutlineStorage(rootID) 242 if nil == storage || 0 == len(storage) { 243 // 默认全部展开 244 for _, p := range ret { 245 p.Folded = false 246 for _, b := range p.Blocks { 247 b.Folded = false 248 for _, c := range b.Children { 249 walkChildren(c, []string{"expandAll"}) 250 } 251 } 252 } 253 } 254 255 if nil != storage["expandIds"] { 256 // 先全部折叠,后面再根据展开 ID 列表展开对应标题 257 for _, p := range ret { 258 p.Folded = true 259 for _, b := range p.Blocks { 260 b.Folded = true 261 for _, c := range b.Children { 262 walkChildren(c, []string{"expandNone"}) 263 } 264 } 265 } 266 267 expandIDsArg := storage["expandIds"].([]interface{}) 268 var expandIDs []string 269 for _, id := range expandIDsArg { 270 expandIDs = append(expandIDs, id.(string)) 271 } 272 273 for _, p := range ret { 274 p.Folded = !gulu.Str.Contains(p.ID, expandIDs) 275 for _, b := range p.Blocks { 276 b.Folded = !gulu.Str.Contains(b.ID, expandIDs) 277 for _, c := range b.Children { 278 walkChildren(c, expandIDs) 279 } 280 } 281 } 282 } 283 return 284} 285 286func walkChildren(b *Block, expandIDs []string) { 287 if 1 == len(expandIDs) { 288 if "expandAll" == expandIDs[0] { 289 b.Folded = false 290 } else if "expandNone" == expandIDs[0] { 291 b.Folded = true 292 } else { 293 b.Folded = !gulu.Str.Contains(b.ID, expandIDs) 294 } 295 } else { 296 b.Folded = !gulu.Str.Contains(b.ID, expandIDs) 297 } 298 299 for _, c := range b.Children { 300 walkChildren(c, expandIDs) 301 } 302} 303 304func outline(tree *parse.Tree) (ret []*Path) { 305 luteEngine := NewLute() 306 var headings []*Block 307 ast.Walk(tree.Root, func(n *ast.Node, entering bool) ast.WalkStatus { 308 if entering && ast.NodeHeading == n.Type && !n.ParentIs(ast.NodeBlockquote) { 309 n.Box, n.Path = tree.Box, tree.Path 310 block := &Block{ 311 RootID: tree.Root.ID, 312 Depth: n.HeadingLevel, 313 Box: n.Box, 314 Path: n.Path, 315 ID: n.ID, 316 Content: renderOutline(n, luteEngine), 317 Type: n.Type.String(), 318 SubType: treenode.SubTypeAbbr(n), 319 Folded: true, 320 } 321 headings = append(headings, block) 322 return ast.WalkSkipChildren 323 } 324 return ast.WalkContinue 325 }) 326 327 if 1 > len(headings) { 328 return 329 } 330 331 var blocks []*Block 332 stack := linkedliststack.New() 333 for _, h := range headings { 334 L: 335 for ; ; stack.Pop() { 336 cur, ok := stack.Peek() 337 if !ok { 338 blocks = append(blocks, h) 339 stack.Push(h) 340 break L 341 } 342 343 tip := cur.(*Block) 344 if tip.Depth < h.Depth { 345 tip.Children = append(tip.Children, h) 346 stack.Push(h) 347 break L 348 } 349 tip.Count = len(tip.Children) 350 } 351 } 352 353 ret = toFlatTree(blocks, 0, "outline", tree) 354 if 0 < len(ret) { 355 children := ret[0].Blocks 356 ret = nil 357 for _, b := range children { 358 resetDepth(b, 0) 359 ret = append(ret, &Path{ 360 ID: b.ID, 361 Box: b.Box, 362 Name: b.Content, 363 NodeType: b.Type, 364 Type: "outline", 365 SubType: b.SubType, 366 Blocks: b.Children, 367 Depth: 0, 368 Count: b.Count, 369 Folded: true, 370 }) 371 } 372 } 373 return 374} 375 376func resetDepth(b *Block, depth int) { 377 b.Depth = depth 378 b.Count = len(b.Children) 379 for _, c := range b.Children { 380 resetDepth(c, depth+1) 381 } 382}