C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Failing after 17s
JS/TS / lint (pull_request) Successful in 19s
Python / static (pull_request) Successful in 1m11s
Rust / fmt (pull_request) Successful in 1m3s
Rust / build (pull_request) Successful in 2m26s
Rust / clippy (pull_request) Failing after 16m40s
Python / verify (pull_request) Failing after 17m52s
C++ / clang-tidy (pull_request) Failing after 18m0s
C++ / build (pull_request) Failing after 18m2s
451 lines
15 KiB
JavaScript
451 lines
15 KiB
JavaScript
.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(/(?<!^)(?=[A-Z])/g, " ");
|
|
children[pos] = [mainIcon, label, ""];
|
|
}
|
|
}
|
|
for (const posKey in children) {
|
|
const pos = parseInt(posKey, 10);
|
|
if (pos >= 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,
|
|
};
|
|
}
|