Coverage for src/lektor_ng/sourcesearch.py: 90%
82 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-05 15:10 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-05 15:10 +0000
1import sqlite3
2from collections.abc import Sized
4from lektor_ng.constants import PRIMARY_ALT
7def _iter_parents(path):
8 path = path.strip("/")
9 if path:
10 pieces = path.split("/")
11 for x in range(len(pieces)):
12 yield "/" + "/".join(pieces[:x])
15def _find_info(infos, alt, lang):
16 for info in infos:
17 if info["alt"] == alt and info["lang"] == lang:
18 return info
19 return None
22def _id_from_path(path):
23 try:
24 return path.strip("/").split("/")[-1]
25 except IndexError:
26 return ""
29def _mapping_from_cursor(cur):
30 rv = {}
31 for path, alt, lang, type, title in cur.fetchall():
32 rv.setdefault(path, []).append(
33 {
34 "id": _id_from_path(path),
35 "path": path,
36 "alt": alt,
37 "type": type,
38 "lang": lang,
39 "title": title,
40 }
41 )
42 return rv
45def _find_best_info(infos, alt, lang):
46 for _alt, _lang in [
47 (alt, lang),
48 (PRIMARY_ALT, lang),
49 (alt, "en"),
50 (PRIMARY_ALT, "en"),
51 ]:
52 rv = _find_info(infos, _alt, _lang)
53 if rv is not None:
54 return rv
55 return None
58def _build_parent_path(path, mapping, alt, lang):
59 rv = []
60 for parent in _iter_parents(path):
61 info = _find_best_info(mapping.get(parent) or [], alt, lang)
62 id = _id_from_path(parent)
63 if info is None:
64 title = id or "(Index)"
65 else:
66 title = info.get("title")
67 rv.append({"id": id, "path": parent, "title": title})
68 return rv
71def _placeholders(values: Sized) -> str:
72 """Return SQL '?' placeholders for an array or set of values."""
73 return ",".join(["?"] * len(values))
76def _process_search_results(builder, cur, alt, lang, limit):
77 mapping = _mapping_from_cursor(cur)
78 rv = []
80 files_needed = set()
82 for path, infos in mapping.items():
83 info = _find_best_info(infos, alt, lang)
84 if info is None:
85 continue
87 for parent in _iter_parents(path):
88 if parent not in mapping:
89 files_needed.add(parent)
91 rv.append(info)
92 if len(rv) == limit:
93 break
95 if files_needed:
96 cur.execute(
97 f"""
98 select path, alt, lang, type, title
99 from source_info
100 where path in ({_placeholders(files_needed)})
101 """,
102 list(files_needed),
103 )
104 mapping.update(_mapping_from_cursor(cur))
106 for info in rv:
107 info["parents"] = _build_parent_path(info["path"], mapping, alt, lang)
109 return rv
112def find_files(builder, query, alt=PRIMARY_ALT, lang=None, limit=50, types=None):
113 if types is None:
114 types = ["page"]
115 else:
116 types = list(types)
117 languages = ["en"]
118 if lang not in ("en", None):
119 languages.append(lang)
120 else:
121 lang = "en"
122 alts = [PRIMARY_ALT]
123 if alt != PRIMARY_ALT:
124 alts.append(alt)
126 query = query.strip()
127 title_like = "%" + query + "%"
128 path_like = "/%" + query.rstrip("/") + "%"
130 con = sqlite3.connect(builder.buildstate_database_filename, timeout=10)
131 try:
132 cur = con.cursor()
133 cur.execute(
134 f"""
135 select path, alt, lang, type, title
136 from source_info
137 where (title like ? or path like ?)
138 and lang in ({_placeholders(languages)})
139 and alt in ({_placeholders(alts)})
140 and type in ({_placeholders(types)})
141 order by title
142 collate nocase
143 limit ?
144 """,
145 [title_like, path_like] + languages + alts + types + [limit * 2],
146 )
147 return _process_search_results(builder, cur, alt, lang, limit)
148 finally:
149 con.close()