From 10a1f475b015029ec6dfe5c8714d38e36d6b307e Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 20 Oct 2026 05:44:08 +0200 Subject: [PATCH] feat: prose citations link to the pinned source Notes, file notes and platform details carried file:line citations as plain text although profile_sync keeps each on the profile's pin. A citation now links when the profile reads one repository and the path is rooted or already used by a structured source_ref. Platform detail source_refs go through the same renderer as file cards. --- scripts/generate_site.py | 18 ++++++++++--- scripts/sitesources.py | 55 +++++++++++++++++++++++++++++++++++++- tests/test_site_exports.py | 26 ++++++++++++++++++ 3 files changed, 94 insertions(+), 5 deletions(-) diff --git a/scripts/generate_site.py b/scripts/generate_site.py index d2252d92..4c240595 100644 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -2793,7 +2793,7 @@ def _render_emulator_file( for d in details: lines.append(f"- {d}") if fnote: - lines.append(f"- {_escape_tags(str(fnote))}") + lines.append(f"- {_escape_tags(link_prose_citations(profile, str(fnote)))}") if not in_repo and f.get("unsourceable"): # The badge alone sent readers after a dump that cannot exist, and the # reason was only on the gaps page, which the card did not link. @@ -2876,14 +2876,23 @@ def _render_platform_details(profile: dict) -> list[str]: if isinstance(pv, dict): lines.append(f" **{pk}:**") for sk, sv in pv.items(): - lines.append(f" - {sk}: {_plain_value(sv)}") + lines.append(f" - {sk}: {_detail_value(profile, sk, sv)}") else: - lines.append(f" **{pk}:** {_plain_value(pv)}") + lines.append(f" **{pk}:** {_detail_value(profile, pk, pv)}") lines.append("") return lines +def _detail_value(profile: dict, key: object, value: object) -> str: + """A platform detail, its citations linked at the profile's pin.""" + if key == "source_ref" and isinstance(value, (str, dict)): + return _source_ref_markdown(profile, value) + if isinstance(value, str): + return _escape_tags(link_prose_citations(profile, value)) + return _plain_value(value) + + def _plain_value(value: object) -> str: """A scalar or a list of scalars as a reader writes it, not as Python does.""" if isinstance(value, bool): @@ -3176,7 +3185,7 @@ def generate_emulator_page( # Notes if notes: - indented = _admonition_body(notes) + indented = _admonition_body(link_prose_citations(profile, notes)) lines.extend(['???+ note "Technical notes"', f" {indented}", ""]) if not files: @@ -4859,6 +4868,7 @@ from sitewrite import ( # noqa: E402,F401 _sweep_generated, ) from sitesources import ( # noqa: E402,F401 + link_prose_citations, revision_markdown, _repo_pins, _forge_sources, diff --git a/scripts/sitesources.py b/scripts/sitesources.py index 1b4dd1ea..3ca75ddb 100644 --- a/scripts/sitesources.py +++ b/scripts/sitesources.py @@ -162,4 +162,57 @@ def _source_ref_markdown(profile: dict, value: str | dict | None) -> str: if label: text = f"**{label}:** {text}" rendered_groups.append(text) - return "; ".join(rendered_groups) + return ", ".join(rendered_groups) + + +def link_prose_citations(profile: dict, text: str) -> str: + """Prose file:line citations as pinned links, where the target is certain. + + profile_sync keeps these citations on their pin, and the page printed + 4,328 of them as plain text. A citation links only when the profile reads + one repository and the path is rooted (it holds a directory) or is one a + structured source_ref of the profile already uses: a bare name could be + anywhere in the tree, and a link to the wrong file is a false citation. + Text already in backticks or in a link is left alone. + """ + from profile_sync import _prose_runs + + candidates = _forge_sources(profile) + if len(candidates) != 1 or ":" not in text: + return text + repo, pin = candidates[0] + structured = { + part.path + for entry in profile.get("files") or [] + if isinstance(entry, dict) + for _label, refs in source_ref_values(entry.get("source_ref")) + for part in split_source_ref(refs) + } + out = [] + for line in text.split("\n"): + guarded = _guarded_spans(line) + edits = [] + for _n, path_span, spans, parts, _run, wrapped in _prose_runs(line): + path = parts[0].path + if wrapped or " " in path or not _PATH_RE.fullmatch(path): + continue + if "/" not in path and path not in structured: + continue + if any(lo <= path_span[0] < hi for lo, hi in guarded): + continue + for index, ((lo, hi), part) in enumerate(zip(spans, parts)): + start = path_span[0] if index == 0 else lo + url = _source_permalink(repo, pin, part.path, part.start, part.end) + edits.append((start, hi, f"[`{line[start:hi]}`]({url})")) + for start, end, link in sorted(edits, reverse=True): + line = line[:start] + link + line[end:] + out.append(line) + return "\n".join(out) + + +_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)] diff --git a/tests/test_site_exports.py b/tests/test_site_exports.py index ab331ff1..c149f0b6 100644 --- a/tests/test_site_exports.py +++ b/tests/test_site_exports.py @@ -702,5 +702,31 @@ class ACardNamesThePacksThatInstallIt(unittest.TestCase): self.assertEqual(_installed_links("a" * 40, consumers, {"retrobat"}), []) +class ProseCitationsLinkToThePin(unittest.TestCase): + """4,328 file:line citations in notes and platform details were plain + text, while profile_sync keeps each one on the profile's pin.""" + + PROFILE = {"source": "https://github.com/libretro/FreeChaF", + "source_commit": "76c7a84f1f7e80f3e6f2bba96fe100cb24e99124", + "files": [{"name": "x", "source_ref": "libretro.c:10"}]} + + def test_a_rooted_or_structured_path_is_linked(self): + from sitesources import link_prose_citations + + text = link_prose_citations( + self.PROFILE, "BIOS loading is in src/libretro.c:186-208 and libretro.c:12.") + self.assertIn("[`src/libretro.c:186-208`](https://github.com/libretro/FreeChaF/blob/" + "76c7a84f1f7e80f3e6f2bba96fe100cb24e99124/src/libretro.c#L186-L208)", text) + self.assertIn("[`libretro.c:12`](", text) + + def test_what_cannot_be_placed_stays_text(self): + from sitesources import link_prose_citations + + self.assertEqual(link_prose_citations(self.PROFILE, "see memory.c:5"), "see memory.c:5") + self.assertEqual(link_prose_citations(self.PROFILE, "in `src/a.c:5`"), "in `src/a.c:5`") + two = {**self.PROFILE, "upstream": "https://github.com/x/y", "upstream_commit": "a" * 40} + self.assertEqual(link_prose_citations(two, "src/a.c:5"), "src/a.c:5") + + if __name__ == "__main__": unittest.main()