function stripJsonc(raw) { return String(raw || "") .replace(/^\s*\/\/[^\n]*(\n|$)/gm, "") .replace(/,(\s*[}\]])/g, "$1") } function normalizeAliases(value) { if (Array.isArray(value)) return value.filter(function(v) { return v }) if (typeof value === "string" && value) return [value] return [] } function normalizeKeywords(id, aliases, raw) { var parts = String(raw || "").split(/\s+/) var seen = {} var out = [] for (var i = 0; i < parts.length; i++) { var p = parts[i] if (!p || seen[p]) continue seen[p] = true out.push(p) } return out.join(" ") } function normalizeItem(id, raw) { var value = raw || {} var aliases = normalizeAliases(value.aliases) var parent = value.parent if (parent === undefined) parent = id.indexOf(".") >= 0 ? id.split(".").slice(0, -1).join(".") : "root" if (id === "root") parent = "" var kind = value.action ? "action" : (value.target ? "link" : "menu") return { id: id, parent: parent, kind: kind, icon: value.icon || "", label: value.label || id, target: value.target || "", keywords: normalizeKeywords(id, aliases, value.keywords), description: value.description || "", action: value.action || "", provider: value.provider || "", aliases: aliases, when: value.when || "", checked: value.checked || "" } } function parseMenuJsonc(raw) { var stripped = stripJsonc(raw) if (!stripped.trim()) return [] var parsed try { parsed = JSON.parse(stripped) } catch (e) { return [] } if (typeof parsed !== "object" || parsed === null) return [] var source = (parsed.items && typeof parsed.items === "object" && !Array.isArray(parsed.items)) ? parsed.items : parsed var out = [] for (var id in source) { var entry = source[id] if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue out.push(normalizeItem(id, entry)) } return out } function mergeMenuSources(defaultItems, userItems) { var nextItems = ({}) var nextOrder = [] var sources = [defaultItems || [], userItems || []] for (var s = 0; s < sources.length; s++) { var src = sources[s] for (var i = 0; i < src.length; i++) { var entry = src[i] if (!entry || !entry.id) continue if (!nextItems[entry.id]) nextOrder.push(entry.id) var prior = nextItems[entry.id] || {} var merged = {} for (var k in prior) merged[k] = prior[k] for (var k2 in entry) merged[k2] = entry[k2] merged.id = entry.id nextItems[entry.id] = merged } } if (!nextItems.root) { nextItems.root = { id: "root", parent: "", kind: "menu", icon: "", label: "Go", target: "", keywords: "", description: "", aliases: [], when: "", checked: "", action: "", provider: "" } nextOrder.unshift("root") } for (var k3 = 0; k3 < nextOrder.length; k3++) nextItems[nextOrder[k3]].order = k3 return { items: nextItems, itemOrder: nextOrder } } function item(items, id) { return items && items[id] ? items[id] : null } function slugify(value) { return String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "item" } function depthFor(items, id) { var depth = 0 var current = item(items, id) var guard = 0 while (current && current.parent && current.parent !== "root" && guard < 32) { depth += 1 current = item(items, current.parent) guard += 1 } return depth } function pathFor(items, id) { var labels = [] var current = item(items, id) var guard = 0 while (current && current.id !== "root" && guard < 32) { labels.unshift(current.label) current = item(items, current.parent) guard += 1 } return labels.join(" › ") } function parentPathFor(items, id) { var entry = item(items, id) if (!entry || !entry.parent || entry.parent === "root") return "" return pathFor(items, entry.parent) } function isDescendantOf(items, id, ancestorId) { if (ancestorId === "root") return id !== "root" var current = item(items, id) var guard = 0 while (current && current.parent && guard < 32) { if (current.parent === ancestorId) return true current = item(items, current.parent) guard += 1 } return false } function childCount(items, itemOrder, id) { var count = 0 var order = Array.isArray(itemOrder) ? itemOrder : [] for (var i = 0; i < order.length; i++) { var entry = item(items, order[i]) if (entry && entry.parent === id) count += 1 } return count } function labelFor(entry, checkedResults) { if (!entry) return "" if (entry.checked && checkedResults && checkedResults[entry.id]) return entry.label + " ✓" return entry.label } function searchableToken(value) { return String(value || "").replace(/[._-]+/g, " ") } function leafIdFor(id) { var parts = String(id || "").split(".") return parts.length > 0 ? parts[parts.length - 1] : id } function nameSearchText(entry) { if (!entry) return "" var aliases = [] var values = Array.isArray(entry.aliases) ? entry.aliases : [] for (var i = 0; i < values.length; i++) aliases.push(searchableToken(values[i])) return [entry.label, searchableToken(leafIdFor(entry.id)), aliases.join(" ")].join(" ").toLowerCase() } function termInSearchWords(term, text) { var words = String(text || "").toLowerCase().split(/\s+/) for (var i = 0; i < words.length; i++) { if (words[i] === term) return true } return false } function keywordTextMatches(query, text) { var terms = String(query || "").toLowerCase().trim().split(/\s+/) for (var i = 0; i < terms.length; i++) { if (terms[i] && !termInSearchWords(terms[i], text)) return false } return true } function matchesQuery(entry, query, visible) { if (!entry || entry.id === "root") return false if (!visible) return false var nameText = nameSearchText(entry) var keywordText = (entry.keywords + " " + entry.description).toLowerCase() var terms = String(query || "").toLowerCase().trim().split(/\s+/) for (var i = 0; i < terms.length; i++) { if (!terms[i]) continue if (nameText.indexOf(terms[i]) >= 0) continue if (termInSearchWords(terms[i], keywordText)) continue return false } return true } function searchScore(items, entry, query) { var needle = String(query || "").toLowerCase().trim() var label = entry.label.toLowerCase() var nameText = nameSearchText(entry) var keywordText = (entry.keywords + " " + entry.description).toLowerCase() var score = 80 if (label === needle) score = entry.parent === "root" ? 2 : 0 else if (label.indexOf(needle) === 0) score = 10 else if (label.indexOf(needle) >= 0) score = 30 else if (nameText.indexOf(needle) >= 0) score = 40 else if (keywordTextMatches(needle, keywordText)) score = 60 if (entry.kind === "menu" || entry.kind === "link") score -= 2 return score * 1000 + depthFor(items, entry.id) * 25 + entry.order } function displayRow(items, itemOrder, checkedResults, entry, detail, score, section) { var target = entry.kind === "link" ? entry.target : entry.id return { itemId: entry.id, kind: entry.kind, icon: entry.icon, label: labelFor(entry, checkedResults), target: target, detail: detail || "", path: pathFor(items, entry.id), childCount: (entry.kind === "menu" || entry.kind === "link") ? childCount(items, itemOrder, target) : 0, action: entry.action || "", provider: entry.provider || "", score: score || 0, section: section || "" } } if (typeof module !== "undefined") { module.exports = { stripJsonc: stripJsonc, normalizeAliases: normalizeAliases, normalizeKeywords: normalizeKeywords, normalizeItem: normalizeItem, parseMenuJsonc: parseMenuJsonc, mergeMenuSources: mergeMenuSources, item: item, slugify: slugify, depthFor: depthFor, pathFor: pathFor, parentPathFor: parentPathFor, isDescendantOf: isDescendantOf, childCount: childCount, labelFor: labelFor, searchableToken: searchableToken, leafIdFor: leafIdFor, nameSearchText: nameSearchText, termInSearchWords: termInSearchWords, keywordTextMatches: keywordTextMatches, matchesQuery: matchesQuery, searchScore: searchScore, displayRow: displayRow } }