Files
arthur-os/shell/plugins/menu/MenuModel.js
T

294 lines
8.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}