mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-26 21:57:24 -05:00
feat: cite sources and files on reference pages
# Conflicts: # wiki/testing-guide.md
This commit is contained in:
commit
51b2976515
12 files changed
+1167
-217
No files matched your search
@@ -41,6 +41,37 @@ def _entry_hashes(entry: dict) -> set[str]:
|
||||
return values
|
||||
|
||||
|
||||
def _declared_hashes(entry: dict) -> dict[str, set[str]]:
|
||||
"""Hash field -> the values a platform entry declares, alt_md5 as md5."""
|
||||
found = {f: set(_hash_values(entry, f)) for f in HASH_FIELDS}
|
||||
found["md5"] |= set(_hash_values(entry, "alt_md5"))
|
||||
return {f: values for f, values in found.items() if values}
|
||||
|
||||
|
||||
def _same(declared: str, accepted: str) -> bool:
|
||||
"""Equal, or a cut md5 that starts the accepted one."""
|
||||
return declared == accepted or (
|
||||
_CUT_MD5 <= len(declared) < _FULL_MD5 and accepted.startswith(declared)
|
||||
)
|
||||
|
||||
|
||||
def _accepts(profile_entry: dict, declared: dict[str, set[str]]) -> bool:
|
||||
"""True when the profile entry, or a member of its archive, accepts a
|
||||
hash the platform entry declares."""
|
||||
accepted = _entry_hashes(profile_entry)
|
||||
return any(_same(d, a) for values in declared.values() for d in values for a in accepted)
|
||||
|
||||
|
||||
def _contradicts(profile_entry: dict, declared: dict[str, set[str]]) -> bool:
|
||||
"""True when both sides state the same kind of hash and no value agrees:
|
||||
the profile entry describes another dump under the same name."""
|
||||
for field_name, values in declared.items():
|
||||
own = set(_hash_values(profile_entry, field_name))
|
||||
if own and not any(_same(d, a) for d in values for a in own):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReadEvidence:
|
||||
"""What the examined profiles open, indexed for one lookup per entry."""
|
||||
@@ -373,20 +404,34 @@ class EntryCategories:
|
||||
)
|
||||
|
||||
def _matching(self, key: str, kind: str, entry: dict) -> list[dict]:
|
||||
"""The profile entries of `key` that name the platform entry."""
|
||||
"""The profile entries of `key` that read the platform entry.
|
||||
|
||||
A platform entry that declares a hash is answered by the profile
|
||||
entries accepting that hash: one name covers several revisions
|
||||
(DuckStation lists scph1001.bin v2.2 at bios.cpp:36 and v2.0 at
|
||||
bios.cpp:42), and a match by name cited the line of another dump.
|
||||
A same-named entry is kept, marked `"by": "name"`, only when no
|
||||
entry accepts the hash and its own hashes do not contradict it.
|
||||
"""
|
||||
names = {
|
||||
str(n).casefold()
|
||||
for n in (entry.get("name"), *(entry.get("aliases") or []))
|
||||
if n
|
||||
}
|
||||
found = []
|
||||
declared = _declared_hashes(entry)
|
||||
by_hash: list[dict] = []
|
||||
by_name: list[dict] = []
|
||||
for f in self._profiles[key].get("files") or []:
|
||||
if not isinstance(f, dict) or (f.get("category") == GAME_DATA) != (kind == GAME_DATA):
|
||||
continue
|
||||
cited = {k: f[k] for k in ("name", "source_ref") if f.get(k)}
|
||||
if declared and _accepts(f, declared):
|
||||
by_hash.append(cited)
|
||||
continue
|
||||
own = {str(n).casefold() for n in (f.get("name"), *(f.get("aliases") or [])) if n}
|
||||
if own & names:
|
||||
found.append({k: f[k] for k in ("name", "source_ref") if f.get(k)})
|
||||
return found
|
||||
if own & names and not (declared and _contradicts(f, declared)):
|
||||
by_name.append({**cited, "by": "name"})
|
||||
return by_hash or by_name
|
||||
|
||||
def explain(self, entry: dict, destination: str, system: str) -> dict:
|
||||
"""The category of an entry and what establishes it."""
|
||||
|
||||
+630
-202
File diff suppressed because it is too large.
Load diff
@@ -7,6 +7,7 @@ from __future__ import annotations
|
||||
|
||||
import re
|
||||
import urllib.parse
|
||||
from dataclasses import dataclass
|
||||
import urllib.request
|
||||
|
||||
import upstream
|
||||
@@ -345,3 +346,98 @@ _PATH_RE = re.compile(r"^[A-Za-z0-9_.@/+~-]+$")
|
||||
def _guarded_spans(line: str) -> list[tuple[int, int]]:
|
||||
"""Code spans and links of a markdown line."""
|
||||
return [m.span() for m in re.finditer(r"`[^`]*`|\[[^\]]*\]\([^)]*\)", line)]
|
||||
|
||||
|
||||
# Where a platform's list was read: the forge, the repository, the revision
|
||||
# and the path inside it, out of the URL its scraper pinned.
|
||||
_LIST_URL_RES = (
|
||||
re.compile(r"^https://raw\.githubusercontent\.com/(?P<owner>[^/]+)/(?P<repo>[^/]+)/"
|
||||
r"(?P<rev>[^/]+)/(?P<path>.+)$"),
|
||||
re.compile(r"^https://github\.com/(?P<owner>[^/]+)/(?P<repo>[^/]+)/(?:blob|tree)/"
|
||||
r"(?P<rev>[^/]+)(?:/(?P<path>.+))?$"),
|
||||
re.compile(r"^https://codeload\.github\.com/(?P<owner>[^/]+)/(?P<repo>[^/]+)/"
|
||||
r"tar\.gz/(?P<rev>[^/]+)$"),
|
||||
re.compile(r"^https://(?P<host>gitlab\.com)/(?P<owner>[^/]+)/(?P<repo>[^/]+)/-/"
|
||||
r"(?:raw|blob)/(?P<rev>[^/]+)/(?P<path>.+)$"),
|
||||
)
|
||||
_BRANCHES = frozenset({"master", "main", "db"})
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ListSource:
|
||||
"""A platform's list as its scraper read it."""
|
||||
|
||||
host: str
|
||||
owner: str
|
||||
repo: str
|
||||
rev: str
|
||||
path: str
|
||||
|
||||
@property
|
||||
def moving(self) -> bool:
|
||||
"""The revision is a branch: what was read is not recorded."""
|
||||
return not self.rev or self.rev in _BRANCHES
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
"""`batocera-systems at tag batocera-43.1`, said in words."""
|
||||
name = self.path.rsplit("/", 1)[-1] if self.path else f"{self.repo} repository"
|
||||
if not self.rev:
|
||||
return f"{name}, default branch"
|
||||
if re.fullmatch(r"[0-9a-f]{40}", self.rev):
|
||||
return f"{name} at commit {self.rev[:12]}"
|
||||
if self.moving:
|
||||
return f"{name} on branch {self.rev}"
|
||||
return f"{name} at tag {self.rev}"
|
||||
|
||||
def browse(self, path: str = "", start: int | None = None,
|
||||
end: int | None = None) -> str:
|
||||
"""The page a reader opens: the file, or the tree, at the revision."""
|
||||
path = path or self.path
|
||||
if self.host == "gitlab.com":
|
||||
url = f"https://gitlab.com/{self.owner}/{self.repo}/-/blob/{self.rev}/{path}"
|
||||
anchor = f"#L{start}" + (f"-{end}" if end and end != start else "")
|
||||
else:
|
||||
base = f"https://github.com/{self.owner}/{self.repo}"
|
||||
if not self.rev:
|
||||
return base
|
||||
kind = "blob" if path else "tree"
|
||||
url = f"{base}/{kind}/{self.rev}" + (f"/{path}" if path else "")
|
||||
anchor = f"#L{start}" + (f"-L{end}" if end and end != start else "")
|
||||
return url + (anchor if start else "")
|
||||
|
||||
|
||||
def list_source(url: str) -> ListSource | None:
|
||||
"""The forge, repository, revision and path of a platform's `source:`."""
|
||||
url = str(url or "").strip()
|
||||
for pattern in _LIST_URL_RES:
|
||||
found = pattern.match(url)
|
||||
if found:
|
||||
parts = found.groupdict()
|
||||
return ListSource(parts.get("host") or "github.com", parts["owner"],
|
||||
parts["repo"], parts["rev"], parts.get("path") or "")
|
||||
bare = re.match(r"^https://github\.com/([^/]+)/([^/]+?)/?$", url)
|
||||
if bare:
|
||||
return ListSource("github.com", bare[1], bare[2], "", "")
|
||||
return None
|
||||
|
||||
|
||||
def launcher_ref_markdown(source: ListSource | None, ref: str) -> str:
|
||||
"""A launcher record's source_ref, each part a pinned link.
|
||||
|
||||
Linked only where the caller knows the launcher code sits in the
|
||||
repository and revision of the platform's list. Otherwise each part is
|
||||
plain code: a link to another repository is a false citation.
|
||||
"""
|
||||
rendered: list[str] = []
|
||||
for part in split_source_ref(ref):
|
||||
display = part.path.rsplit("/", 1)[-1]
|
||||
if part.start is not None:
|
||||
display += f":{part.start}"
|
||||
if part.end is not None and part.end != part.start:
|
||||
display += f"-{part.end}"
|
||||
if source is None or not source.rev or not _PATH_RE.fullmatch(part.path):
|
||||
rendered.append(f"`{display}`")
|
||||
else:
|
||||
rendered.append(f"[`{display}`]({source.browse(part.path, part.start, part.end)})")
|
||||
return ", ".join(rendered)
|
||||
Reference in new issue
Block a user