• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mobalazs / rotor-framework / 20966940173

13 Jan 2026 05:52PM UTC coverage: 86.321% (-0.04%) from 86.364%
20966940173

push

github

mobalazs
chore: bump version to 0.7.4

2032 of 2354 relevant lines covered (86.32%)

1.19 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.67
/src/source/engine/builder/TreeBase.bs
1
namespace Rotor.ViewBuilder
2

3
    ' =====================================================================
4
    ' WidgetTreeBase - Base class for widget tree management with HID generation and search capabilities
5
    '
6
    ' Provides core functionality for hierarchical widget organization,
7
    ' lookup operations, and glob pattern matching.
8
    ' =====================================================================
9
    class WidgetTreeBase
10

11
        tree = new TreeRoot()
12
        tree_HIDHash = new Rotor.BaseStack()
13

14
        ' sub new()
15
        '     super()
16
        ' end sub
17

18
        ' ---------------------------------------------------------------------
19
        ' generateHID - Generates a unique Hierarchical ID for a widget under a parent
20
        '
21
        ' @param {object} parent - Parent widget object
22
        ' @returns {string} Unique HID string
23
        '
24
        function generateHID(parent) as string
25
            tryCounter = 32
1✔
26
            newHID = ""
1✔
27
            ' Performance way
28
            while tryCounter > 0 and newHID = ""
1✔
29
                newHID = parent.HID + Rotor.Utils.getUUIDHex(3)
1✔
30
                if parent.childrenHIDhash.DoesExist(newHID) = true
2✔
31
                    newHID = ""
×
32
                end if
33
                tryCounter--
1✔
34
            end while
35
            ' Ok, then try another way (very rare scenario)
36
            if newHID = ""
2✔
37
                for decValue = 0 to 4095
×
38
                    hexValue = stri(decValue, 16)
×
39
                    newHID = parent.HID + hexValue
×
40
                    if parent.childrenHIDhash.DoesExist(newHID) = true
×
41
                        newHID = ""
×
42
                        exit for
43
                    end if
44
                end for
45
            end if
46
            return newHID
1✔
47
        end function
48

49
        ' ---------------------------------------------------------------------
50
        ' get - Gets a single widget by search pattern
51
        '
52
        ' @param {string} searchPattern - HID or glob pattern to search for
53
        ' @param {string} HID - Starting context HID (default: "0")
54
        ' @returns {object} First matching widget or invalid if not found
55
        '
56
        function get(searchPattern as string, HID = "0" as string) as object
57
            untilFirstItem = true
1✔
58
            results = m.find(searchPattern, HID, untilFirstItem)
1✔
59
            if results = invalid
2✔
60
                return invalid
1✔
61
            else
3✔
62
                return results.shift()
1✔
63
            end if
64
        end function
65

66
        ' ---------------------------------------------------------------------
67
        ' getByHID - Gets a widget directly by its HID
68
        '
69
        ' @param {string} HID - Hierarchical ID to look up
70
        ' @returns {object} Widget at the specified HID or invalid if not found
71
        '
72
        function getByHID(HID as string) as object
73
            return HID = "0" ? m.tree : m.tree_HIDHash.get(HID)
1✔
74
        end function
75

76
        ' ---------------------------------------------------------------------
77
        ' hasByHID - Checks if a widget exists at the specified HID
78
        '
79
        ' @param {string} HID - Hierarchical ID to check
80
        ' @returns {boolean} True if widget exists, false otherwise
81
        '
82
        function hasByHID(HID as string) as boolean
83
            return m.tree_HIDHash.has(HID)
1✔
84
        end function
85

86
        ' ---------------------------------------------------------------------
87
        ' find - Searches the widget tree using a path-like glob pattern or direct HID
88
        '
89
        ' Supports:
90
        ' - Direct lookup by HID
91
        ' - Relative path resolution (./ and .. operators)
92
        ' - Glob patterns: *, ** and normalization (e.g. multiple slashes, stars)
93
        '
94
        ' @param {string} searchPattern - The lookup expression
95
        ' @param {string} HID - Optional starting node (default: root)
96
        ' @param {boolean} untilFirstItem - If true, stops after first match (default: false)
97
        ' @returns {object} Array of matching widgets, or invalid if none found
98
        '
99
        function find(searchPattern as string, HID = "0" as string, untilFirstItem = false as boolean) as object
100
            ' TODO: In the future, this could use a well-designed ID-based cache to improve performance
101

102
            ' Direct lookup: if the search pattern is an HID
103
            if m.hasByHID(searchPattern) = true
2✔
104
                return [m.getByHID(searchPattern)]
1✔
105
            end if
106

107
            ' Check if the pattern begins with "./" (relative to root)
108
            prefix = Left(searchPattern, 2)
1✔
109
            if prefix = "./"
2✔
110
                searchPattern = Right(searchPattern, Len(searchPattern) - 2)
1✔
111
                HID = "0" ' Reset to root if explicitly referencing from top
1✔
112
            end if
113

114
            ' Normalize search pattern
115
            searchPattern = /^\/*/.ReplaceAll(searchPattern, "") ' Remove leading slashes
1✔
116
            searchPattern = /\/{2,}/.ReplaceAll(searchPattern, "/") ' Replace multiple slashes with one
1✔
117
            searchPattern = /\*{3,}/.ReplaceAll(searchPattern, "**") ' Replace 3+ stars with **
1✔
118
            searchPattern = /\*\*$/.ReplaceAll(searchPattern, "*") ' Convert trailing ** to *
1✔
119
            searchPattern = /(\*\*\/){2,}/.ReplaceAll(searchPattern, "**/")' Collapse multiple **/** into one
1✔
120

121
            searchPattern = LCase(searchPattern)
1✔
122
            parts = /\//.Split(searchPattern)
1✔
123

3✔
124
            startNode = HID = "0" ? m.tree : m.getByHID(HID)
1✔
125

126
            ' Resolve ".." (parent) operators
127
            while parts[0] = ".." and startNode.HID <> "0"
1✔
128
                parts.shift()
1✔
129
                startNode = m.getByHID(startNode.parentHID)
1✔
130
            end while
131

132
            results = []
1✔
133

134
            ' If parts is empty after resolving "..", return the startNode itself
135
            if parts.Count() = 0
2✔
136
                return [startNode]
1✔
137
            end if
138

139
            ' Add leading "**" if pattern doesn't start with ** or *
140
            ' This enables deep search when user provides just an ID like "menuItem2"
141
            if parts[0] <> "**" and parts[0] <> "*"
3✔
142
                parts.unshift("**")
1✔
143
            end if
144

145
            lastPart = parts[parts.Count() - 1]
1✔
146
            if startNode.HID <> "0" and LCase(startNode.id) = lastPart
2✔
147
                results.unshift(startNode)
×
148
            end if
149

150
            ' Begin recursive search
151
            m.recursionFind(results, parts, untilFirstItem, startNode.children)
1✔
152

153
            if results.Count() = 0 then return invalid
2✔
154

155
            return results
1✔
156
        end function
157

158
        ' ---------------------------------------------------------------------
159
        ' matchesPattern - Check if a string matches a wildcard pattern
160
        '
161
        ' Supports:
162
        ' - "*" matches everything
163
        ' - "prefix*" matches strings starting with prefix
164
        ' - "*suffix" matches strings ending with suffix
165
        ' - "prefix*suffix" matches strings starting with prefix and ending with suffix
166
        '
167
        ' @param {string} str - The string to check
168
        ' @param {string} pattern - The pattern with optional wildcards
169
        ' @returns {boolean} True if matches, false otherwise
170
        '
171
        function matchesPattern(str as string, pattern as string) as boolean
172
            if pattern = "*" then return true
2✔
173
            if pattern.Instr("*") < 0 then return str = pattern ' No wildcard, exact match
1✔
174

175
            ' Split pattern by *
176
            parts = pattern.Split("*")
1✔
177
            partsCount = parts.Count()
1✔
178

179
            ' Check prefix (before first *)
180
            if parts[0] <> "" and str.Left(parts[0].Len()) <> parts[0]
3✔
181
                return false
1✔
182
            end if
183

184
            ' Check suffix (after last *)
185
            if parts[partsCount - 1] <> "" and str.Right(parts[partsCount - 1].Len()) <> parts[partsCount - 1]
2✔
186
                return false
1✔
187
            end if
188

189
            ' For patterns like "prefix*suffix", check both prefix and suffix
190
            if partsCount = 2 and parts[0] <> "" and parts[1] <> ""
2✔
191
                ' Check if string is long enough
192
                if str.Len() < parts[0].Len() + parts[1].Len()
×
193
                    return false
×
194
                end if
195
            end if
196

197
            return true
1✔
198
        end function
199

200
        ' ---------------------------------------------------------------------
201
        ' recursionFind - Recursively finds widgets in a tree structure based on glob-like patterns
202
        '
203
        ' Supports:
204
        ' - "*" to match any key at one level
205
        ' - "**" to match any number of nested levels
206
        '
207
        ' @param {object} results - Array to collect matching widgets
208
        ' @param {object} parts - Pattern parts split by "/"
209
        ' @param {boolean} untilFirstItem - If true, stops after the first match
210
        ' @param {object} children - The widget tree branch to search (default: {})
211
        ' @param {integer} index - Current position in the pattern (default: 0)
212
        ' @returns {object} Array of matching widgets
213
        '
214
        function recursionFind(results as object, parts as object, untilFirstItem as boolean, children = {} as object, index = 0 as integer) as object
215
            currentPart = parts[index]
1✔
216
            partsCount = parts.Count()
1✔
217
            isLastPart = (partsCount - 1 = index)
1✔
218
            nextPart = isLastPart ? "" : parts[index + 1]
1✔
219

220
            if children.Count() > 0
3✔
221
                for each key in children
1✔
222
                    widget = children[key]
1✔
223
                    ' Note: key is already lowercase because widgets are stored with LCase(id) as key in tree.bs:85
224

225
                    if isLastPart = true
3✔
226
                        ' Check if current part matches the key (supports wildcards like "menuItem*")
227
                        if m.matchesPattern(key, currentPart)
3✔
228
                            results.push(widget)
1✔
229
                            if untilFirstItem = true then return results
2✔
230
                        end if
231
                    else
3✔
232
                        nextChildren = {}
1✔
233
                        matchedParent = false
1✔
234

235
                        if currentPart = "**"
3✔
236
                            ' Deep search: check if next part matches current key
237
                            if m.matchesPattern(key, nextPart)
3✔
238
                                matchedParent = true
1✔
239
                                nextChildren = children ' Stay at same level, exit ** mode
1✔
240
                            else
3✔
241
                                nextChildren = widget.children
1✔
242
                            end if
243
                        else if m.matchesPattern(key, currentPart)
2✔
244
                            ' Matched current level, move to next level
245
                            matchedParent = true
1✔
246
                            nextChildren = widget.children
1✔
247
                        end if
248

3✔
249
                        newIndex = matchedParent = true ? index + 1 : index
1✔
250

251
                        if nextChildren.Count() > 0 and newIndex < partsCount
3✔
252
                            m.recursionFind(results, parts, untilFirstItem, nextChildren, newIndex)
1✔
253
                        end if
254
                    end if
255
                end for
256
            end if
257

258
            return results
1✔
259
        end function
260

261
        ' ---------------------------------------------------------------------
262
        ' getSubtreeClone - Creates a cloned subtree from a widget by search pattern
263
        '
264
        ' @param {string} searchPattern - Pattern to find the root widget
265
        ' @param {object} keyPathList - List of key paths to include in clone (default: [])
266
        ' @param {string} parentHID - Parent HID context (default: "0")
267
        ' @returns {object} Cloned subtree structure or invalid if not found
268
        '
269
        function getSubtreeClone(searchPattern as string, configIncludeFilter = [] as object, parentHID = "0" as string) as object
270
            subTree = m.get(searchPattern, parentHID)
1✔
271
            if subTree = invalid then return invalid
2✔
272

273
            configIncludeFilter.push("HID")
1✔
274

275
            subTreeClone = {}
1✔
276
            m.recursion_getSubtreeClone(subTree, subTreeClone, configIncludeFilter)
1✔
277

278
            return subTreeClone
1✔
279
        end function
280

281
        ' ---------------------------------------------------------------------
282
        ' recursion_getSubtreeClone - Recursively clones a widget subtree by specified key paths
283
        '
284
        ' Note: Tree uses AA for children but clone uses Array for updates.
285
        '
286
        ' @param {object} subTree - Source subtree to clone
287
        ' @param {object} subTreeClone - Target clone object to populate
288
        ' @param {object} configIncludeFilter - List of key paths to include in clone
289
        '
290
        sub recursion_getSubtreeClone(subTree as object, subTreeClone as object, configIncludeFilter as object)
291
            ' Note that
292
            for each keyPath in configIncludeFilter
1✔
293
                clonedValue = Rotor.Utils.getCloneByKeyPath(subTree, keyPath)
1✔
294
                if clonedValue <> invalid
3✔
295
                    Rotor.Utils.deepExtendAA(subTreeClone, clonedValue)
1✔
296
                end if
297
            end for
298
            if subTree.children <> invalid and subTree.children.Count() > 0
2✔
299
                subTreeClone.children = []
1✔
300
                childIndex = 0
1✔
301
                for each id in subTree.children
1✔
302
                    subTreeClone.children.push({ id: id })
1✔
303
                    m.recursion_getSubtreeClone(subTree.children[id], subTreeClone.children[childIndex], configIncludeFilter)
1✔
304
                    childIndex++
1✔
305
                end for
306
            end if
307
        end sub
308

309
    end class
310

311
end namespace
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc