From ffaf5c820d2a26c64647666aff44de0240007bad Mon Sep 17 00:00:00 2001 From: berce <75228263+bercedev@users.noreply.github.com> Date: Mon, 19 Oct 2026 02:10:25 +0300 Subject: [PATCH] fix(markdown): resolve wikilinks that start with the scan root's own folders (#4559) Obsidian writes wikilink paths from the vault root, so scanning a folder below it meets links like [[Corp/wiki/Fox]] from inside Corp/. The vault-wide fallback matched them as suffixes of root-relative paths, which never succeeds, and the references edge was dropped. When nothing matches, drop the longest leading run of the scan root's own folders and match the rest exactly from the root. --- graphify/extractors/markdown.py | 11 +++++++++++ tests/test_languages.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) 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."""