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