cppmanlite 0.1.6__tar.gz → 0.1.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cppmanlite-0.1.6 → cppmanlite-0.1.7}/PKG-INFO +1 -1
- {cppmanlite-0.1.6 → cppmanlite-0.1.7}/cppmanlite/core.py +148 -20
- cppmanlite-0.1.7/cppmanlite/data/index.json +1 -0
- {cppmanlite-0.1.6 → cppmanlite-0.1.7}/cppmanlite.egg-info/PKG-INFO +1 -1
- {cppmanlite-0.1.6 → cppmanlite-0.1.7}/pyproject.toml +1 -1
- cppmanlite-0.1.6/cppmanlite/data/index.json +0 -1
- {cppmanlite-0.1.6 → cppmanlite-0.1.7}/LICENSE +0 -0
- {cppmanlite-0.1.6 → cppmanlite-0.1.7}/README.md +0 -0
- {cppmanlite-0.1.6 → cppmanlite-0.1.7}/cppmanlite/__init__.py +0 -0
- {cppmanlite-0.1.6 → cppmanlite-0.1.7}/cppmanlite.egg-info/SOURCES.txt +0 -0
- {cppmanlite-0.1.6 → cppmanlite-0.1.7}/cppmanlite.egg-info/dependency_links.txt +0 -0
- {cppmanlite-0.1.6 → cppmanlite-0.1.7}/cppmanlite.egg-info/requires.txt +0 -0
- {cppmanlite-0.1.6 → cppmanlite-0.1.7}/cppmanlite.egg-info/top_level.txt +0 -0
- {cppmanlite-0.1.6 → cppmanlite-0.1.7}/setup.cfg +0 -0
|
@@ -11,6 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import html
|
|
13
13
|
import json
|
|
14
|
+
import math
|
|
14
15
|
import re
|
|
15
16
|
import textwrap
|
|
16
17
|
from pathlib import Path
|
|
@@ -101,13 +102,127 @@ def _fetch_url_sync(url: str) -> str:
|
|
|
101
102
|
# --------------------------------------------------------------------------- #
|
|
102
103
|
# Search
|
|
103
104
|
# --------------------------------------------------------------------------- #
|
|
105
|
+
#
|
|
106
|
+
# Deterministic scorer that mirrors site/app.js exactly (same tokenisation,
|
|
107
|
+
# alias table, field weights, AND penalty and sort). Replacing the old naive
|
|
108
|
+
# substring scorer so the Python package and the static site rank identically.
|
|
109
|
+
# Why not BM25/lunr here? There is no lunr dependency in the package (it must
|
|
110
|
+
# stay pure-stdlib / Pyodide-safe), and a hand-rolled scorer puts exact-title
|
|
111
|
+
# hits first in a fully predictable way.
|
|
112
|
+
|
|
113
|
+
_SEARCH_TOKEN_RE = re.compile(r"[a-z_][a-z0-9_]*|[0-9]+")
|
|
114
|
+
|
|
115
|
+
# cppreference-style aliases: map a common name to the canonical identifier
|
|
116
|
+
# used in page titles. (std::string == std::basic_string, ...)
|
|
117
|
+
_SEARCH_ALIAS = {
|
|
118
|
+
"string": "basic_string", "wstring": "basic_wstring",
|
|
119
|
+
"u8string": "basic_u8string", "u16string": "basic_u16string",
|
|
120
|
+
"u32string": "basic_u32string", "str": "basic_string", "wstr": "basic_wstring",
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# field weights: (whole-token match, substring-inside-larger-token)
|
|
124
|
+
_W = {"title": (100, 40), "terms": (80, 32), "url": (40, 16), "snippet": (8, 3)}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _tok_set(s: str) -> set:
|
|
128
|
+
return set(_SEARCH_TOKEN_RE.findall(s.lower()))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _strip_ns(s: str) -> str:
|
|
132
|
+
return re.sub(r"^(std::)+", "", s)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _strip_args(s: str) -> str:
|
|
136
|
+
return re.sub(r"\(.*\)\s*$", "", s).strip()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _alias_expand(s: str) -> str:
|
|
140
|
+
parts = [p for p in re.split(r"[\s_]+", s) if p]
|
|
141
|
+
return "_".join(_SEARCH_ALIAS.get(p, p) for p in parts)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _match_tok(tok: str, s_low: str, tokset: set, full: int, sub: int) -> int:
|
|
145
|
+
if tok in tokset:
|
|
146
|
+
return full
|
|
147
|
+
if tok in s_low:
|
|
148
|
+
return sub
|
|
149
|
+
return 0
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _term_score(tok: str, term_freq: dict, terms_low: str) -> float:
|
|
153
|
+
# base "terms" weight, scaled up (log) by how often the identifier appears
|
|
154
|
+
# in the page body → the defining page outranks a page that just mentions it.
|
|
155
|
+
n = term_freq.get(tok, 0)
|
|
156
|
+
if n:
|
|
157
|
+
return _W["terms"][0] * (1 + math.log2(n))
|
|
158
|
+
if tok in terms_low:
|
|
159
|
+
return _W["terms"][1]
|
|
160
|
+
return 0
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _doc_score(meta: dict, search_toks: list, whole_cands: list) -> float:
|
|
164
|
+
score = 0
|
|
165
|
+
matched = 0
|
|
166
|
+
# Whole-title relations: EXACT always; PREFIX only for specific/compound queries.
|
|
167
|
+
for wc in whole_cands:
|
|
168
|
+
if not wc or len(wc) < 2:
|
|
169
|
+
continue
|
|
170
|
+
d_title = _strip_ns(meta["t_low"])
|
|
171
|
+
if d_title == wc:
|
|
172
|
+
score += 1000
|
|
173
|
+
else:
|
|
174
|
+
specific = len(search_toks) >= 2 or "_" in wc or len(wc) >= 8
|
|
175
|
+
if specific and d_title.startswith(wc + " "):
|
|
176
|
+
score += 400
|
|
177
|
+
for tok in search_toks:
|
|
178
|
+
s = max(
|
|
179
|
+
_match_tok(tok, meta["t_low"], meta["t_tok"], _W["title"][0], _W["title"][1]),
|
|
180
|
+
_term_score(tok, meta["term_freq"], meta["terms_low"]),
|
|
181
|
+
_match_tok(tok, meta["u_low"], meta["u_tok"], _W["url"][0], _W["url"][1]),
|
|
182
|
+
_match_tok(tok, meta["s_low"], meta["s_tok"], _W["snippet"][0], _W["snippet"][1]),
|
|
183
|
+
)
|
|
184
|
+
if s > 0:
|
|
185
|
+
score += s
|
|
186
|
+
matched += 1
|
|
187
|
+
# AND semantics: penalise if not every query token matched somewhere.
|
|
188
|
+
if len(search_toks) > 1 and matched < len(search_toks):
|
|
189
|
+
score *= matched / len(search_toks)
|
|
190
|
+
return score
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _build_meta(entry: dict) -> dict:
|
|
194
|
+
t = entry.get("title", "")
|
|
195
|
+
u = entry.get("url", "")
|
|
196
|
+
s = entry.get("snippet", "")
|
|
197
|
+
# terms is a {identifier: count} map; tolerate an older list format.
|
|
198
|
+
terms = entry.get("terms") or {}
|
|
199
|
+
if isinstance(terms, list):
|
|
200
|
+
term_freq = {t: 1 for t in terms}
|
|
201
|
+
else:
|
|
202
|
+
term_freq = terms
|
|
203
|
+
t_low = t.lower()
|
|
204
|
+
return {
|
|
205
|
+
"t_low": t_low,
|
|
206
|
+
"u_low": u.lower(),
|
|
207
|
+
"s_low": s.lower(),
|
|
208
|
+
"t_tok": _tok_set(t),
|
|
209
|
+
"u_tok": _tok_set(u),
|
|
210
|
+
"s_tok": _tok_set(s),
|
|
211
|
+
"term_freq": term_freq,
|
|
212
|
+
"terms_low": " ".join(term_freq.keys()),
|
|
213
|
+
"title_len": len(t_low),
|
|
214
|
+
}
|
|
104
215
|
|
|
105
216
|
|
|
106
217
|
def search(query: str, limit: int = 20) -> list[dict[str, str]]:
|
|
107
218
|
"""Search C++ documentation pages.
|
|
108
219
|
|
|
220
|
+
Deterministic scorer (mirrors the static site): exact-title matches rank
|
|
221
|
+
first, then alias titles, then identifiers found in a page's body (e.g.
|
|
222
|
+
``int64_t`` → Fixed width integer types), then URL/snippet substring hits.
|
|
223
|
+
|
|
109
224
|
Args:
|
|
110
|
-
query: Search term (e.g. "vector", "std::
|
|
225
|
+
query: Search term (e.g. "vector", "std::max", "int64_t", "shared_ptr").
|
|
111
226
|
limit: Maximum number of results.
|
|
112
227
|
|
|
113
228
|
Returns:
|
|
@@ -116,27 +231,40 @@ def search(query: str, limit: int = 20) -> list[dict[str, str]]:
|
|
|
116
231
|
idx = _load_index()
|
|
117
232
|
if not idx:
|
|
118
233
|
return []
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
234
|
+
q_norm = query.lower().strip()
|
|
235
|
+
if not q_norm:
|
|
236
|
+
return []
|
|
237
|
+
|
|
238
|
+
q_toks = _SEARCH_TOKEN_RE.findall(q_norm)
|
|
239
|
+
# drop a leading "std" namespace token (std::vector → ["std","vector"])
|
|
240
|
+
if q_toks and q_toks[0] == "std" and q_norm.startswith("std::"):
|
|
241
|
+
q_toks = q_toks[1:]
|
|
242
|
+
if not q_toks:
|
|
243
|
+
return []
|
|
244
|
+
|
|
245
|
+
# expand each token through the alias table, dedup, preserve order
|
|
246
|
+
seen = set()
|
|
247
|
+
search_toks = []
|
|
248
|
+
for t in q_toks:
|
|
249
|
+
for cand in (t, _SEARCH_ALIAS.get(t, "")):
|
|
250
|
+
if cand and cand not in seen:
|
|
251
|
+
seen.add(cand)
|
|
252
|
+
search_toks.append(cand)
|
|
253
|
+
|
|
254
|
+
q_title_whole = _strip_args(_strip_ns(q_norm))
|
|
255
|
+
whole_cands = []
|
|
256
|
+
for c in (q_title_whole, _alias_expand(q_title_whole)):
|
|
257
|
+
if len(c) >= 2 and c not in whole_cands:
|
|
258
|
+
whole_cands.append(c)
|
|
259
|
+
|
|
260
|
+
scored = []
|
|
123
261
|
for entry in idx:
|
|
124
|
-
|
|
125
|
-
url = entry.get("url", "").lower()
|
|
126
|
-
# Score: exact match > starts with > contains in title > contains in URL
|
|
127
|
-
score = 0
|
|
128
|
-
if title == q or title == q_norm:
|
|
129
|
-
score = 100
|
|
130
|
-
elif title.startswith(q) or title.startswith(q_norm):
|
|
131
|
-
score = 80
|
|
132
|
-
elif q in title or q_norm in title:
|
|
133
|
-
score = 60
|
|
134
|
-
elif q in url or q_norm in url:
|
|
135
|
-
score = 40
|
|
262
|
+
score = _doc_score(_build_meta(entry), search_toks, whole_cands)
|
|
136
263
|
if score > 0:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
264
|
+
scored.append((score, entry))
|
|
265
|
+
|
|
266
|
+
scored.sort(key=lambda x: (-x[0], len(x[1].get("title", "").lower()), x[1].get("url", "")))
|
|
267
|
+
return [entry for _, entry in scored[:limit]]
|
|
140
268
|
|
|
141
269
|
|
|
142
270
|
def list_pages(limit: int = 0) -> list[dict[str, str]]:
|