Repository navigation
Expand file tree
/
Copy pathlinearizeTree.ts
More file actions
205 lines (188 loc) · 10 KB
/
Copy pathlinearizeTree.ts
File metadata and controls
205 lines (188 loc) · 10 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
import { pick } from 'lodash'
import React from 'react'
import LazyEnv from '../@types/LazyEnv'
import Path from '../@types/Path'
import SimplePath from '../@types/SimplePath'
import State from '../@types/State'
import ThoughtId from '../@types/ThoughtId'
import TreeThought from '../@types/TreeThought'
import { HOME_PATH } from '../constants'
import calculateAutofocus from '../selectors/calculateAutofocus'
import findDescendant from '../selectors/findDescendant'
import getChildren, { childrenFilterPredicate, getChildrenRanked, hasChildren } from '../selectors/getChildren'
import getContextsSortedAndRanked from '../selectors/getContextsSortedAndRanked'
import getStyle from '../selectors/getStyle'
import getThoughtById from '../selectors/getThoughtById'
import isContextViewActive from '../selectors/isContextViewActive'
import rootedParentOf from '../selectors/rootedParentOf'
import simplifyPath from '../selectors/simplifyPath'
import thoughtToPath from '../selectors/thoughtToPath'
import { appendToPathMemo } from '../util/appendToPath'
import equalPath from '../util/equalPath'
import hashPath from '../util/hashPath'
import head from '../util/head'
import isAttribute from '../util/isAttribute'
import isRoot from '../util/isRoot'
import parentOf from '../util/parentOf'
import parseLet from '../util/parseLet'
import safeRefMerge from '../util/safeRefMerge'
import attributeEquals from './attributeEquals'
// style properties that accumulate down the hierarchy.
// We need to accmulate positioning like marginLeft so that all descendants' positions are indented with the thought.
const ACCUM_STYLE_PROPERTIES = ['marginLeft', 'paddingLeft']
/** Generates a VirtualThought key that is unique across context views. */
// include the head of each context view in the path in the key, otherwise there will be duplicate keys when the same thought is visible in normal view and context view
const crossContextualKey = (contextChain: Path[] | undefined, id: ThoughtId) =>
`${(contextChain || []).map(head).join('')}|${id}`
/** Recursiveley calculates the tree of visible thoughts, in order, represented as a flat list of thoughts with tree layout information. */
const linearizeTree = (
state: State,
{
// Base path to start the traversal. Defaults to HOME_PATH.
basePath,
/** Used to set belowCursor in recursive calls. Once true, all remaining thoughts will have belowCursor: true. See: TreeThought.belowCursor. */
belowCursor,
// The id of a specific context within the context view.
// This allows the contexts to render the children of their Lexeme instance rather than their own children.
// i.e. a/~m/b should render b/m's children rather than rendering b's children. Notice that the Path a/~m/b contains a different m than b/m, so we need to pass the id of b/m to the next level to render the correct children.
// If we rendered the children as usual, the Lexeme would be repeated in each context, i.e. a/~m/a/m/x and a/~m/b/m/y. There is no need to render m a second time since we know the context view is activated on m.
contextId,
// accumulate the context chain in order to provide a unique key for rendering the same thought in normal view and context view
contextChain,
depth,
env,
indexDescendant,
// ancestor styles that accmulate such as marginLeft are applied, merged, and passed to descendants
styleAccum,
// =grandparent styles must be passed separately since they skip a level
styleFromGrandparent,
}: {
basePath?: Path
belowCursor?: boolean
contextId?: ThoughtId
contextChain?: SimplePath[]
depth: number
env?: LazyEnv
indexDescendant: number
styleAccum?: React.CSSProperties | null
styleFromGrandparent?: React.CSSProperties | null
} = {
depth: 0,
indexDescendant: 0,
},
getAutofocus = calculateAutofocus(state),
): TreeThought[] => {
const path = basePath || HOME_PATH
const hashedPath = hashPath(path)
if (!isRoot(path) && !state.expanded[hashedPath] && !equalPath(state.expandHoverDownPath, path)) return []
const thoughtId = head(path)
const thought = getThoughtById(state, thoughtId)
const simplePath = simplifyPath(state, path)
const contextViewActive = isContextViewActive(state, path)
const contextChainNew = contextViewActive ? [...(contextChain || []), simplePath] : contextChain
const children = contextViewActive
? thought
? getContextsSortedAndRanked(state, thought.value)
: []
: // context children should render the children of a specific Lexeme instance to avoid repeating the Lexeme.
// See: contextId (above)
getChildrenRanked(state, contextId || thoughtId)
const filteredChildren = children.filter(childrenFilterPredicate(state, simplePath))
// short circuit if the context view only has one context and the NoOtherContexts component will be displayed
if (contextViewActive && filteredChildren.length === 1) return []
const childrenAttributeId = findDescendant(state, thoughtId, '=children')
const grandchildrenAttributeId = findDescendant(state, thoughtId, '=grandchildren')
const styleChildren = getStyle(state, childrenAttributeId)
const style = safeRefMerge(styleAccum, styleChildren, styleFromGrandparent)
// =let definitions on this thought are added to the env inherited from ancestors and passed to all descendants.
// If there are no =let definitions, the inherited env is passed through unchanged to preserve its object reference, otherwise a new reference on every render would defeat the memoization of TreeNode and its descendants.
const envParsed = parseLet(state, path)
const envNew = Object.keys(envParsed).length > 0 ? { ...env, ...envParsed } : env
// 0-based ordinal of each visible non-attribute child, used to number =bullet/Ordered lists without re-sorting siblings in each Bullet.
// Attributes (e.g. =children when showHiddenThoughts is enabled) occupy a slot in filteredChildren but are skipped in the numbering (-1).
let ordinal = 0
const childIndexNonAttribute = filteredChildren.map(child => (isAttribute(child.value) ? -1 : ordinal++))
const thoughts = filteredChildren.reduce<TreeThought[]>((accum, filteredChild, i) => {
// If the context view is active, render the context's parent instead of the context itself.
// This allows the path to be accumulated correctly across the context view.
// e.g. a/m~/b should render the children of b/m, not a/m
const child = contextViewActive ? getThoughtById(state, filteredChild.parentId) : filteredChild
// Context thought may still be pending
if (!child) return accum
const childPath = appendToPathMemo(path, child.id)
const lastVirtualIndex = accum.length > 0 ? accum[accum.length - 1].indexDescendant : 0
const virtualIndexNew = indexDescendant + lastVirtualIndex + (depth === 0 && i === 0 ? 0 : 1)
// As soon as the cursor is found, set belowCursor to true. It will be propagated to every subsequent thought.
// See: TreeThought.belowCursor
const isCursor = !belowCursor && equalPath(childPath, state.cursor)
if (isCursor || !state.cursor) {
belowCursor = true
}
const isEmpty = child.value === ''
const isTable = attributeEquals(state, child.id, '=view', 'Table')
const isTableCol1 = attributeEquals(state, head(simplePath), '=view', 'Table')
const isInSortedContext = attributeEquals(state, head(simplePath), '=sort', 'Alphabetical')
const isTableCol2 = attributeEquals(state, head(rootedParentOf(state, simplePath)), '=view', 'Table')
const isTableCol2Child = attributeEquals(state, head(rootedParentOf(state, parentOf(simplePath))), '=view', 'Table')
const autofocus = getAutofocus(childPath)
const node: TreeThought = {
belowCursor: !!belowCursor,
depth,
env: envNew,
indexChild: i,
childIndexNonAttribute: childIndexNonAttribute[i],
indexDescendant: virtualIndexNew,
isCursor,
isEmpty,
isInSortedContext,
isTableCol1,
isTableCol2,
isTableCol2Child,
autofocus,
// In the context view, use filteredChild.id (the context) rather than child.id (the context parent), otherwise duplicate thoughts in the same context will have the same key.
// For example, a/~m/cat and a/~m/cats need to use the ids of cat/cats rather than m.
// filteredChild === child in normal view, so it does not matter in that case.
key: crossContextualKey(contextChainNew, filteredChild.id),
// must filteredChild.id to work for both normal view and context view
leaf: !hasChildren(state, filteredChild.id),
path: childPath,
prevChild: filteredChildren[i - 1],
rank: child.rank,
showContexts: contextViewActive,
simplePath: contextViewActive ? thoughtToPath(state, child.id) : appendToPathMemo(simplePath, child.id),
style,
thoughtId: child.id,
...(isTable
? { visibleChildrenKeys: getChildren(state, child.id).map(child => crossContextualKey(contextChain, child.id)) }
: null),
}
// RECURSION
const descendants = linearizeTree(
state,
{
basePath: childPath,
belowCursor,
contextId: contextViewActive ? filteredChild.id : undefined,
contextChain: contextChainNew,
depth: depth + 1,
env: envNew,
indexDescendant: virtualIndexNew,
// merge styleGrandchildren so it gets applied to this child's children
styleAccum: safeRefMerge(
styleAccum,
pick(styleChildren, ACCUM_STYLE_PROPERTIES),
pick(getStyle(state, grandchildrenAttributeId), ACCUM_STYLE_PROPERTIES),
),
styleFromGrandparent: getStyle(state, grandchildrenAttributeId),
},
getAutofocus,
)
// In order to mark every thought after the cursor as belowCursor, we need to update belowCursor before the next sibling is processed. Otherwise, the recursive belowCursor will not be propagated up the call stack and will still be undefined on the next uncle.
if (!belowCursor && descendants[descendants.length - 1]?.belowCursor) {
belowCursor = true
}
return [...accum, node, ...descendants]
}, [])
return thoughts
}
export default linearizeTree