diff --git a/graphify/extractors/markdown.py b/graphify/extractors/markdown.py index 2a389be9f..d0ea4d626 100644 --- a/graphify/extractors/markdown.py +++ b/graphify/extractors/markdown.py @@ -211,6 +211,17 @@ def _vault_lookup(target: str, root: Path) -> "Path | None": c for c in candidates if c[1] == suffix or c[1].endswith("/" + suffix) ] + if not matches: + # Obsidian writes vault-relative paths, so scanning a folder below the + # vault root meets links that start with that folder's own path + # ([[Corp/wiki/Fox]] from inside Corp/, #4559). Drop the longest such + # leading run and match the rest from the scan root. + root_parts = [_nfc(p) for p in Path(root).parts] + for k in range(len(parts) - 1, 0, -1): + if parts[:k] == root_parts[-k:]: + rest = "/".join(parts[k:]) + matches = [c for c in candidates if c[1] == rest] + break if not matches: return None return min(matches)[2] diff --git a/tests/test_languages.py b/tests/test_languages.py index 0f80e46a9..f1d981995 100644 --- a/tests/test_languages.py +++ b/tests/test_languages.py @@ -3772,6 +3772,35 @@ def test_markdown_wikilink_fallback_path_qualified(tmp_path): for e in refs), f"path-qualified vault link lost: {refs}" +def test_markdown_wikilink_vault_relative_to_a_scan_root_below_the_vault(tmp_path): + """Obsidian writes vault-relative paths, so scanning a folder below the + vault meets links that start with that folder's own path (#4559).""" + corp = tmp_path / "Vault" / "Projects" / "Corp" + (corp / "wiki").mkdir(parents=True) + for name in ("Charlie", "Delta", "Fox"): + (corp / "wiki" / f"{name}.md").write_text(f"# {name}\n") + (corp / "Alpha.md").write_text( + "[[wiki/Charlie]] [[Corp/wiki/Delta|alias]] [[Projects/Corp/wiki/Fox]]\n") + pages = [corp / "Alpha.md", *(corp / "wiki").iterdir()] + _, refs, page_id = _vault_extract(corp, pages) + alpha = page_id(corp / "Alpha.md") + assert {e["target"] for e in refs if e["source"] == alpha} == { + page_id(corp / "wiki" / f"{name}.md") for name in ("Charlie", "Delta", "Fox")} + + +def test_markdown_wikilink_scan_root_prefix_still_needs_the_exact_path(tmp_path): + """Once the scan root's folder is dropped the rest is a path from the + root, not a suffix: [[Corp/Fox]] does not reach Corp/wiki/Fox.md.""" + corp = tmp_path / "Corp" + (corp / "wiki").mkdir(parents=True) + (corp / "wiki" / "Fox.md").write_text("# Fox\n") + (corp / "Alpha.md").write_text("[[Corp/Fox]]\n") + _, refs, page_id = _vault_extract(corp, [corp / "Alpha.md", corp / "wiki" / "Fox.md"]) + alpha = page_id(corp / "Alpha.md") + fox = page_id(corp / "wiki" / "Fox.md") + assert fox not in {e["target"] for e in refs if e["source"] == alpha} + + def test_markdown_wikilink_fallback_root_wins(tmp_path): """On a bare-name collision the shallowest match wins — Obsidian resolves a bare wikilink to the root-level file over a same-named subfolder file."""