.pragma library const STOPWORDS = [ "a", "an", "and", "are", "for", "in", "not", "notification", "of", "on", "or", "out", "the", "to", ]; const FIELD_WEIGHT = { title: 1.0, keywords: 0.4, }; const SKIP_LABELS = ["Muted", "None"]; function cleanLabel(text) { return String(text ?? "") .replace(/\s*\(?%\d+\)?/g, "") .trim(); } const PAGE_NAME_RE = /^\s*name:\s*qsTr\("([^"]+)"\)/; const ROW_RE = /^\s*(ToggleRow|SliderRow|SelectRow|SpinRow|NavRow|RowButton|InfoRow|PopupRow|DefaultRow|TextFieldRow|IconTextButton|TimeDialogSelect)\s*\{/; const NAV_ROW_TYPES = ["NavRow", "IconTextButton"]; const LABEL_RE = /^\s*(?:label|text):\s*qsTr\("([^"]+)"\)/; const ANCHOR_RE = /^\s*(?:property\s+string\s+)?settingAnchor:\s*"([^"]+)"/; const CHECKED_RE = /^\s*checked:\s*(?:GlobalConfig|Config)\.([\w.]+)\s*$/; const ONTOGGLED_RE = /^\s*onToggled:\s*(?:GlobalConfig|Config)\.([\w.]+)\s*=\s*checked\s*$/; const ICON_RE = /^\s*icon:\s*"([^"]+)"/; const SUBTEXT_RE = /^\s*(?:subtext|status):\s*qsTr\("([^"]+)"\)/; const SECTION_RE = /^\s*SectionHeader\s*\{/; function tokenize(text) { const toks = []; for (const word of text.toLowerCase().split(/\s+/)) { if (!word) continue; const parts = word.split(/[^a-z0-9]+/).filter((p) => p); for (const p of parts) { if (!STOPWORDS.includes(p) && !toks.includes(p)) toks.push(p); } if (parts.length > 1) { const joined = parts.join(""); if (!toks.includes(joined)) toks.push(joined); } } return toks; } function discoverFiles(settingsDir, listFiles) { const files = {}; for (const p of listFiles(`${settingsDir}/Pages`, ".qml")) { const name = p.slice(p.lastIndexOf("/") + 1).replace(/\.qml$/, ""); files[name] = p; } return files; } function parsePageRegistry(settingsDir, readLines) { const lines = readLines(`${settingsDir}/PageRegistry.qml`); const out = []; let inArray = false; let depth = 0; let label = null; let icon = null; for (const line of lines) { const s = line.trim(); if (s.includes("pages:") && s.includes("[")) { inArray = true; continue; } if (!inArray) continue; if (s.startsWith("//")) continue; if (s.startsWith("]")) break; if (s.startsWith("{")) { depth++; label = icon = null; continue; } if (s.startsWith("}")) { if (label !== null) out.push([icon || "tune", label]); depth--; continue; } if (depth >= 1) { const m = PAGE_NAME_RE.exec(line); if (m && label === null) label = m[1]; const mi = ICON_RE.exec(line); if (mi && icon === null) icon = mi[1]; } } return out; } function parsePageComps(settingsDir, readFile) { const text = readFile(`${settingsDir}/PageCompRegistry.qml`); const start = text.indexOf("pageComps:"); if (start === -1) return []; const comps = []; let current = null; let depth = 0; for (const raw of text.slice(start).split("\n")) { const line = raw.split("//")[0]; const s = line.trim(); if (s.startsWith("]")) break; const atTop = depth === 0; depth += (line.match(/\{/g) ?? []).length - (line.match(/\}/g) ?? []).length; if (atTop && /^Component\s*\{/.test(s)) { current = []; comps.push(current); continue; } if (current !== null) { const m = /(^|\s)([A-Z][A-Za-z0-9]*)\s*\{\s*\}/.exec(s); if (m) current.push(m[2]); } } return comps; } function dedupCrumbs(labels, icons) { const outLabels = []; const outIcons = []; for (let i = 0; i < labels.length; i++) { if ( outLabels.length > 0 && outLabels[outLabels.length - 1] === labels[i] ) continue; outLabels.push(labels[i]); outIcons.push(icons[i]); } return [outLabels, outIcons]; } function buildNavMap(settingsDir, files, readFile, readLines) { const comps = parsePageComps(settingsDir, readFile); const registry = parsePageRegistry(settingsDir, readLines); const navChildren = {}; for (const names of comps) { for (const name of names) { const pf = files[name]; if (!pf) continue; let pendingIcon = null; let pendingLabel = null; let section = ""; let expectSection = false; for (const ln of readLines(pf)) { if (SECTION_RE.test(ln)) { expectSection = true; continue; } const ml = LABEL_RE.exec(ln); if (ml) { if (expectSection) { section = ml[1]; expectSection = false; } else { pendingLabel = ml[1]; } continue; } const mi = ICON_RE.exec(ln); if (mi) pendingIcon = mi[1]; const mo = /openSubPage\((\d+)\)/.exec(ln); if (mo) { const pos = parseInt(mo[1], 10); if (!navChildren[name]) navChildren[name] = {}; navChildren[name][pos] = [ pendingIcon || "tune", pendingLabel || "", section, ]; pendingIcon = pendingLabel = null; } } } } const nav = {}; for (let topIdx = 0; topIdx < comps.length; topIdx++) { const names = comps[topIdx]; if (names.length === 0) continue; const main = names[0]; const [mainIcon, mainLabel] = registry[topIdx] ?? ["tune", main]; nav[main] = { pageIdx: topIdx, subPath: [], crumbIcons: [mainIcon], crumbLabels: [mainLabel], }; const children = Object.assign({}, navChildren[main] ?? {}); const openedViaSubpage = []; for (const owner in navChildren) { const ownerGroup = comps.find((ns) => ns.includes(owner)); if (!ownerGroup) continue; for (const kpos in navChildren[owner]) { const k = parseInt(kpos, 10); if (k < ownerGroup.length) openedViaSubpage.push(ownerGroup[k]); } } for (let pos = 1; pos < names.length; pos++) { if (!(pos in children) && !openedViaSubpage.includes(names[pos])) { let label = names[pos].replace(/(Detail)?Page$/, ""); label = label.replace(/(?= names.length) continue; const [icon, label, section] = children[posKey]; const child = names[pos]; let labels = [mainLabel] .concat(section ? [section] : []) .concat([label]); let icons = [mainIcon].concat(section ? [icon] : []).concat([icon]); [labels, icons] = dedupCrumbs(labels, icons); nav[child] = { pageIdx: topIdx, subPath: [pos], crumbIcons: icons, crumbLabels: labels, }; const grandChildren = navChildren[child] ?? {}; for (const gposKey in grandChildren) { const gpos = parseInt(gposKey, 10); if (gpos >= names.length) continue; const [gicon, glabel, gsection] = grandChildren[gposKey]; let glabels = labels .concat(gsection ? [gsection] : []) .concat([glabel]); let gicons = icons .concat(gsection ? [gicon] : []) .concat([gicon]); [glabels, gicons] = dedupCrumbs(glabels, gicons); nav[names[gpos]] = { pageIdx: topIdx, subPath: [pos, gpos], crumbIcons: gicons, crumbLabels: glabels, }; } } } return nav; } function findBlockEnd(lines, start) { let depth = 0; for (let i = start; i < lines.length; i++) { const line = lines[i].split("//")[0]; depth += (line.match(/\{/g) ?? []).length - (line.match(/\}/g) ?? []).length; if (i > start && depth <= 0) return i; } return lines.length; } function extractSettings(files, nav, comps, readLines) { const entries = []; for (const comp in nav) { const meta = nav[comp]; const pf = files[comp]; if (!pf) continue; const lines = readLines(pf); let section = ""; for (let i = 0; i < lines.length; i++) { if (SECTION_RE.test(lines[i])) { for (let j = i + 1; j < Math.min(i + 4, lines.length); j++) { const m = LABEL_RE.exec(lines[j]); if (m) { section = m[1]; break; } } } const rowMatch = ROW_RE.exec(lines[i]); if (!rowMatch) continue; const rowType = rowMatch[1]; let label = null; let anchor = null; let subtext = null; let checkedPath = null; let toggledPath = null; let targetPos = null; const rowEnd = findBlockEnd(lines, i); for (let j = i + 1; j < rowEnd; j++) { if (label === null) { const m = LABEL_RE.exec(lines[j]); if (m) label = m[1]; } if (anchor === null) { const a = ANCHOR_RE.exec(lines[j]); if (a) anchor = a[1]; } if (subtext === null) { const st = SUBTEXT_RE.exec(lines[j]); if (st) subtext = st[1]; } if (checkedPath === null) { const ch = CHECKED_RE.exec(lines[j]); if (ch) checkedPath = ch[1]; } if (toggledPath === null) { const tg = ONTOGGLED_RE.exec(lines[j]); if (tg) toggledPath = tg[1]; } if (NAV_ROW_TYPES.includes(rowType) && targetPos === null) { const sp = /openSubPage\((\d+)\)/.exec(lines[j]); if (sp) targetPos = parseInt(sp[1], 10); } } const togglePath = rowType === "ToggleRow" && checkedPath && checkedPath === toggledPath ? checkedPath : ""; if (label && !SKIP_LABELS.includes(label) && anchor) { const extra = meta.crumbLabels.join(" ") + " " + section + " " + (subtext && !/%\d/.test(subtext) ? subtext : ""); const group = comps[meta.pageIdx] ?? []; const targetSubPath = targetPos !== null && targetPos < group.length ? meta.subPath.concat([targetPos]) : []; entries.push({ rowType: rowType, pageIdx: meta.pageIdx, subPath: meta.subPath, targetSubPath: targetSubPath, crumbIcons: meta.crumbIcons, crumbLabels: meta.crumbLabels, trailKey: meta.crumbLabels.join("/"), title: cleanLabel(label), anchor: anchor, section: section, subtext: subtext && !/%\d/.test(subtext) ? subtext : "", togglePath: togglePath, }); } } } return mergeInfoRows(entries); } function mergeInfoRows(entries) { const out = []; const merged = {}; for (const entry of entries) { const isInfo = entry.rowType === "InfoRow"; delete entry.rowType; if (!isInfo || !entry.section) { out.push(entry); continue; } const key = entry.anchor.split("-")[0] + "/" + entry.trailKey + "/" + entry.section; const existing = merged[key]; if (existing === undefined) { entry.keywords = entry.title; entry.title = entry.section; merged[key] = entry; out.push(entry); } else { existing.keywords += " " + entry.title; } } return out; } function buildInvertedAndRanking(entries) { const inverted = {}; const ranking = {}; for (let idx = 0; idx < entries.length; idx++) { const e = entries[idx]; const extra = e.crumbLabels.join(" ") + " " + e.section + " " + e.subtext + " " + (e.keywords ?? ""); const fields = { title: e.title, keywords: tokenize(e.title + " " + extra) .sort() .join(" "), }; for (const field in fields) { const weight = FIELD_WEIGHT[field] ?? 0.2; for (const tok of tokenize(fields[field])) { if (!inverted[tok]) inverted[tok] = []; if (!inverted[tok].includes(idx)) inverted[tok].push(idx); if (!ranking[tok]) ranking[tok] = {}; ranking[tok][idx] = Math.max(ranking[tok][idx] ?? 0.0, weight); } } } for (const tok in inverted) inverted[tok].sort((a, b) => ranking[tok][b] - ranking[tok][a]); return [inverted, ranking]; } function buildIndex(settingsDir, readFile, listFiles) { const lineCache = {}; const readLines = (path) => { if (!(path in lineCache)) lineCache[path] = readFile(path).split("\n"); return lineCache[path]; }; const files = discoverFiles(settingsDir, listFiles); const nav = buildNavMap(settingsDir, files, readFile, readLines); const comps = parsePageComps(settingsDir, readFile); const entries = extractSettings(files, nav, comps, readLines); const [inverted, ranking] = buildInvertedAndRanking(entries); return { version: 3, entries: entries, inverted: inverted, ranking: ranking, }; }