chore: split cmake into multiple files and fix settings searching regex + add enabled option to llm
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
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
This commit is contained in:
@@ -1,452 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from functools import cache
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@cache
|
||||
def read_lines(path: Path) -> tuple[str, ...]:
|
||||
return tuple(path.read_text().splitlines())
|
||||
|
||||
|
||||
ROW_RE = re.compile(
|
||||
r"^\s*(ToggleRow|SliderRow|SelectRow|SpinRow|NavRow|InfoRow|PopupRow|OverlayRow|DefaultRow)\s*\{"
|
||||
)
|
||||
LABEL_RE = re.compile(r'^\s*(?:label|text):\s*qsTr\("([^"]+)"\)')
|
||||
ANCHOR_RE = re.compile(r'^\s*settingAnchor:\s*"([^"]+)"')
|
||||
CHECKED_RE = re.compile(r"^\s*checked:\s*(?:Config)\.([\w.]+)\s*$")
|
||||
ONTOGGLED_RE = re.compile(
|
||||
r"^\s*onToggled:\s*(?:Config)\.([\w.]+)\s*=\s*checked\s*$"
|
||||
)
|
||||
ICON_RE = re.compile(r'^\s*icon:\s*"([^"]+)"')
|
||||
SKIP_LABELS = {"Muted", "None"}
|
||||
FIELD_WEIGHT = {"title": 1.0, "keywords": 0.4}
|
||||
STOPWORDS = {"the", "a", "an", "of", "and", "or", "to", "on", "in", "for"}
|
||||
|
||||
|
||||
def find_pages_dir(settings: Path) -> Path:
|
||||
return settings / "Pages"
|
||||
|
||||
|
||||
def discover_files(settings: Path) -> dict[str, Path]:
|
||||
files: dict[str, Path] = {}
|
||||
for p in find_pages_dir(settings).rglob("*.qml"):
|
||||
files[p.stem] = p
|
||||
return files
|
||||
|
||||
|
||||
PAGE_NAME_RE = re.compile(r'^\s*name:\s*qsTr\("([^"]+)"\)')
|
||||
PAGE_ICON_RE = re.compile(r'^\s*icon:\s*"([^"]+)"')
|
||||
|
||||
|
||||
def parse_page_registry(settings: Path) -> list[tuple[str, str]]:
|
||||
text = (settings / "PageRegistry.qml").read_text().splitlines()
|
||||
|
||||
start = next(
|
||||
i for i, line in enumerate(text) if re.search(r"\bpages\s*:\s*\[", line)
|
||||
)
|
||||
|
||||
out: list[tuple[str, str]] = []
|
||||
i = start + 1
|
||||
|
||||
while i < len(text):
|
||||
line = text[i].strip()
|
||||
|
||||
if line.startswith("]"):
|
||||
break
|
||||
|
||||
if line.startswith("//") or not line:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if line.startswith("{"):
|
||||
name = None
|
||||
icon = None
|
||||
i += 1
|
||||
|
||||
while i < len(text):
|
||||
s = text[i].strip()
|
||||
|
||||
if s.startswith("}"):
|
||||
if name is not None:
|
||||
out.append((icon or "tune", name))
|
||||
break
|
||||
|
||||
if name is None:
|
||||
m = PAGE_NAME_RE.match(text[i])
|
||||
if m:
|
||||
name = m.group(1)
|
||||
|
||||
if icon is None:
|
||||
mi = PAGE_ICON_RE.match(text[i])
|
||||
if mi:
|
||||
icon = mi.group(1)
|
||||
|
||||
i += 1
|
||||
|
||||
i += 1
|
||||
|
||||
return out
|
||||
|
||||
|
||||
BLOCK_RE = re.compile(r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*\{\s*$")
|
||||
|
||||
|
||||
def _strip_comment(line: str) -> str:
|
||||
return line.split("//", 1)[0].rstrip()
|
||||
|
||||
|
||||
def parse_block(
|
||||
lines: list[str], i: int
|
||||
) -> tuple[str, list[tuple[str, list]], int]:
|
||||
line = _strip_comment(lines[i]).strip()
|
||||
m = BLOCK_RE.match(line)
|
||||
if not m:
|
||||
raise ValueError(f"Expected block start at line {i + 1}: {lines[i]!r}")
|
||||
|
||||
name = m.group(1)
|
||||
i += 1
|
||||
children: list[tuple[str, list]] = []
|
||||
|
||||
while i < len(lines):
|
||||
s = _strip_comment(lines[i]).strip()
|
||||
if not s:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if s.startswith("}"):
|
||||
return name, children, i + 1
|
||||
|
||||
if BLOCK_RE.match(s):
|
||||
child_name, child_children, i = parse_block(lines, i)
|
||||
children.append((child_name, child_children))
|
||||
continue
|
||||
|
||||
i += 1
|
||||
|
||||
raise ValueError(f"Unterminated block: {name}")
|
||||
|
||||
|
||||
def collect_page_names(block: tuple[str, list[tuple[str, list]]]) -> list[str]:
|
||||
name, children = block
|
||||
|
||||
if name != "Component":
|
||||
return [name]
|
||||
|
||||
for child_name, child_children in children:
|
||||
if child_name == "StackPage":
|
||||
out: list[str] = []
|
||||
for grand_name, grand_children in child_children:
|
||||
if grand_name == "Component":
|
||||
out.extend(collect_page_names((grand_name, grand_children)))
|
||||
return out
|
||||
|
||||
if child_name != "Component":
|
||||
return [child_name]
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def parse_page_comps(settings: Path) -> list[list[str]]:
|
||||
text = (settings / "PageCompRegistry.qml").read_text().splitlines()
|
||||
|
||||
start = next(
|
||||
i
|
||||
for i, line in enumerate(text)
|
||||
if re.search(r"\bpageComps\s*:\s*\[", _strip_comment(line))
|
||||
)
|
||||
|
||||
comps: list[list[str]] = []
|
||||
i = start + 1
|
||||
|
||||
while i < len(text):
|
||||
s = _strip_comment(text[i]).strip()
|
||||
if not s:
|
||||
i += 1
|
||||
continue
|
||||
if s.startswith("]"):
|
||||
break
|
||||
|
||||
if BLOCK_RE.match(s) and BLOCK_RE.match(s).group(1) == "Component":
|
||||
block = parse_block(text, i)
|
||||
names = collect_page_names((block[0], block[1]))
|
||||
if names:
|
||||
comps.append(names)
|
||||
i = block[2]
|
||||
continue
|
||||
|
||||
i += 1
|
||||
|
||||
return comps
|
||||
|
||||
|
||||
def dedup_crumbs(
|
||||
labels: list[str], icons: list[str]
|
||||
) -> tuple[list[str], list[str]]:
|
||||
out_labels: list[str] = []
|
||||
out_icons: list[str] = []
|
||||
for lbl, ico in zip(labels, icons, strict=False):
|
||||
if out_labels and out_labels[-1] == lbl:
|
||||
continue
|
||||
out_labels.append(lbl)
|
||||
out_icons.append(ico)
|
||||
return out_labels, out_icons
|
||||
|
||||
|
||||
def build_nav_map(settings: Path, files: dict[str, Path]) -> dict[str, dict]:
|
||||
comps = parse_page_comps(settings)
|
||||
registry = parse_page_registry(settings)
|
||||
|
||||
top_meta: dict[int, tuple[str, str]] = {}
|
||||
for i, (icon, label) in enumerate(registry):
|
||||
top_meta[i] = (icon, label)
|
||||
|
||||
nav_children: dict[str, dict[int, tuple[str, str, str]]] = {}
|
||||
for names in comps:
|
||||
for name in names:
|
||||
pf = files.get(name)
|
||||
if not pf:
|
||||
continue
|
||||
pending_icon = pending_label = None
|
||||
section = ""
|
||||
expect_section = False
|
||||
for ln in read_lines(pf):
|
||||
if SECTION_RE.match(ln):
|
||||
expect_section = True
|
||||
continue
|
||||
ml = LABEL_RE.match(ln)
|
||||
if ml:
|
||||
if expect_section:
|
||||
section = ml.group(1)
|
||||
expect_section = False
|
||||
else:
|
||||
pending_label = ml.group(1)
|
||||
continue
|
||||
mi = ICON_RE.match(ln)
|
||||
if mi:
|
||||
pending_icon = mi.group(1)
|
||||
mo = re.search(r"openSubPage\((\d+)\)", ln)
|
||||
if mo:
|
||||
pos = int(mo.group(1))
|
||||
nav_children.setdefault(name, {})[pos] = (
|
||||
pending_icon or "tune",
|
||||
pending_label or "",
|
||||
section,
|
||||
)
|
||||
pending_icon = pending_label = None
|
||||
|
||||
nav: dict[str, dict] = {}
|
||||
for top_idx, names in enumerate(comps):
|
||||
if not names:
|
||||
continue
|
||||
main = names[0]
|
||||
main_icon, main_label = top_meta.get(top_idx, ("tune", main))
|
||||
nav[main] = {
|
||||
"pageIdx": top_idx,
|
||||
"subPath": [],
|
||||
"crumbIcons": [main_icon],
|
||||
"crumbLabels": [main_label],
|
||||
}
|
||||
children = dict(nav_children.get(main, {}))
|
||||
opened_via_subpage = set()
|
||||
for owner, kids in nav_children.items():
|
||||
owner_group = next((ns for ns in comps if owner in ns), None)
|
||||
if not owner_group:
|
||||
continue
|
||||
for kpos in kids:
|
||||
if kpos < len(owner_group):
|
||||
opened_via_subpage.add(owner_group[kpos])
|
||||
for pos in range(1, len(names)):
|
||||
if pos not in children and names[pos] not in opened_via_subpage:
|
||||
label = re.sub(r"(Detail)?Page$", "", names[pos])
|
||||
label = re.sub(r"(?<!^)(?=[A-Z])", " ", label)
|
||||
children[pos] = (main_icon, label, "")
|
||||
for pos, (icon, label, section) in children.items():
|
||||
if pos >= len(names):
|
||||
continue
|
||||
child = names[pos]
|
||||
labels = [main_label] + ([section] if section else []) + [label]
|
||||
icons = [main_icon] + ([icon] if section else []) + [icon]
|
||||
labels, icons = dedup_crumbs(labels, icons)
|
||||
nav[child] = {
|
||||
"pageIdx": top_idx,
|
||||
"subPath": [pos],
|
||||
"crumbIcons": icons,
|
||||
"crumbLabels": labels,
|
||||
}
|
||||
for gpos, (gicon, glabel, gsection) in nav_children.get(
|
||||
child, {}
|
||||
).items():
|
||||
if gpos >= len(names):
|
||||
continue
|
||||
glabels = labels + ([gsection] if gsection else []) + [glabel]
|
||||
gicons = icons + ([gicon] if gsection else []) + [gicon]
|
||||
glabels, gicons = dedup_crumbs(glabels, gicons)
|
||||
nav[names[gpos]] = {
|
||||
"pageIdx": top_idx,
|
||||
"subPath": [pos, gpos],
|
||||
"crumbIcons": gicons,
|
||||
"crumbLabels": glabels,
|
||||
}
|
||||
return nav
|
||||
|
||||
|
||||
def tokenize(text: str) -> list[str]:
|
||||
toks: list[str] = []
|
||||
for word in text.lower().split():
|
||||
parts = [p for p in re.split(r"[^a-z0-9]+", word) if p]
|
||||
for p in parts:
|
||||
if p not in STOPWORDS and p not in toks:
|
||||
toks.append(p)
|
||||
if len(parts) > 1:
|
||||
joined = "".join(parts)
|
||||
if joined not in toks:
|
||||
toks.append(joined)
|
||||
return toks
|
||||
|
||||
|
||||
SUBTEXT_RE = re.compile(r'^\s*(?:subtext|status):\s*qsTr\("([^"]+)"\)')
|
||||
SECTION_RE = re.compile(r"^\s*SectionHeader\s*\{")
|
||||
|
||||
|
||||
def extract_settings(
|
||||
files: dict[str, Path], nav: dict[str, dict]
|
||||
) -> list[dict]:
|
||||
entries: list[dict] = []
|
||||
for comp, meta in nav.items():
|
||||
pf = files.get(comp)
|
||||
if not pf:
|
||||
continue
|
||||
lines = read_lines(pf)
|
||||
section = ""
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
if SECTION_RE.match(lines[i]):
|
||||
for j in range(i + 1, min(i + 4, len(lines))):
|
||||
m = LABEL_RE.match(lines[j])
|
||||
if m:
|
||||
section = m.group(1)
|
||||
break
|
||||
row_match = ROW_RE.match(lines[i])
|
||||
if row_match:
|
||||
row_type = row_match.group(1)
|
||||
label = anchor = subtext = None
|
||||
checked_path = toggled_path = None
|
||||
for j in range(i + 1, min(i + 12, len(lines))):
|
||||
if label is None:
|
||||
m = LABEL_RE.match(lines[j])
|
||||
if m:
|
||||
label = m.group(1)
|
||||
if anchor is None:
|
||||
a = ANCHOR_RE.match(lines[j])
|
||||
if a:
|
||||
anchor = a.group(1)
|
||||
if subtext is None:
|
||||
st = SUBTEXT_RE.match(lines[j])
|
||||
if st:
|
||||
subtext = st.group(1)
|
||||
if checked_path is None:
|
||||
ch = CHECKED_RE.match(lines[j])
|
||||
if ch:
|
||||
checked_path = ch.group(1)
|
||||
if toggled_path is None:
|
||||
tg = ONTOGGLED_RE.match(lines[j])
|
||||
if tg:
|
||||
toggled_path = tg.group(1)
|
||||
toggle_path = (
|
||||
checked_path
|
||||
if row_type == "ToggleRow"
|
||||
and checked_path
|
||||
and checked_path == toggled_path
|
||||
else ""
|
||||
)
|
||||
if label and label not in SKIP_LABELS and anchor:
|
||||
extra = (
|
||||
" ".join(meta["crumbLabels"])
|
||||
+ " "
|
||||
+ section
|
||||
+ " "
|
||||
+ (subtext or "")
|
||||
)
|
||||
entries.append(
|
||||
{
|
||||
"pageIdx": meta["pageIdx"],
|
||||
"subPath": meta["subPath"],
|
||||
"crumbIcons": meta["crumbIcons"],
|
||||
"crumbLabels": meta["crumbLabels"],
|
||||
"title": label,
|
||||
"anchor": anchor,
|
||||
"section": section,
|
||||
"subtext": subtext or "",
|
||||
"togglePath": toggle_path,
|
||||
"keywords": " ".join(
|
||||
sorted(set(tokenize(label + " " + extra)))
|
||||
),
|
||||
}
|
||||
)
|
||||
i += 1
|
||||
return entries
|
||||
|
||||
|
||||
def build_inverted_and_ranking(entries: list[dict]):
|
||||
inverted: dict[str, list[int]] = defaultdict(list)
|
||||
ranking: dict[str, dict[int, float]] = defaultdict(dict)
|
||||
for idx, e in enumerate(entries):
|
||||
fields = {"title": e["title"], "keywords": e["keywords"]}
|
||||
seen: set[str] = set()
|
||||
for field, text in fields.items():
|
||||
weight = FIELD_WEIGHT.get(field, 0.2)
|
||||
for tok in tokenize(text):
|
||||
if idx not in inverted[tok]:
|
||||
inverted[tok].append(idx)
|
||||
ranking[tok][idx] = max(ranking[tok].get(idx, 0.0), weight)
|
||||
seen.add(tok)
|
||||
for tok, ids in inverted.items():
|
||||
ids.sort(key=lambda i: ranking[tok][i], reverse=True)
|
||||
return inverted, {
|
||||
t: {str(k): v for k, v in d.items()} for t, d in ranking.items()
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) != 3:
|
||||
print(__doc__)
|
||||
return 1
|
||||
settings = Path(sys.argv[1])
|
||||
out = Path(sys.argv[2])
|
||||
files = discover_files(settings)
|
||||
nav = build_nav_map(settings, files)
|
||||
entries = extract_settings(files, nav)
|
||||
inverted, ranking = build_inverted_and_ranking(entries)
|
||||
for e in entries:
|
||||
e.pop("keywords", None)
|
||||
out.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"version": 2,
|
||||
"entries": entries,
|
||||
"inverted": inverted,
|
||||
"ranking": ranking,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
print(
|
||||
f"settings index: {len(entries)} entries, "
|
||||
f"{len(inverted)} tokens -> {out}"
|
||||
)
|
||||
print("files:", len(files))
|
||||
print("comps:", len(parse_page_comps(settings)))
|
||||
print("registry:", len(parse_page_registry(settings)))
|
||||
print("nav:", len(nav))
|
||||
print("entries:", len(entries))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,450 @@
|
||||
.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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user