Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions myst_parser/mdit_to_docutils/sphinx_.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,25 @@ def _handle_relative_docs(self, destination: str) -> str:
)
return destination

def _link_target_path(self, path: str) -> str:
"""Return the absolute path of a file that a link points to.

Like ``BuildEnvironment.relfn2path``, a path starting with ``/`` is
relative to the source directory, any other is relative to the
directory of the current document.
Unlike ``relfn2path``, symlinks are not followed:
a document is registered under the path of the symlink,
so the path of its target would not be found as a document.
"""
env = self.sphinx_env
file_path = Path(path)
if file_path.parts[:1] in {("/",), ("\\",)}:
base = env.srcdir.joinpath(*file_path.parts[1:])
else:
doc_dir = env.doc2path(env.docname, base=False).parent
base = env.srcdir.joinpath(doc_dir, file_path)
return os.path.normpath(base)

def render_link_project(self, token: SyntaxTreeNode) -> None:
destination = cast(str, token.attrGet("href") or "")
destination = destination.removeprefix("project:")
Expand All @@ -90,7 +109,7 @@ def render_link_project(self, token: SyntaxTreeNode) -> None:
path_dest, *_path_ids = destination.split("#", maxsplit=1)
path_id = _path_ids[0] if _path_ids else None
explicit = (token.info != "auto") and (len(token.children or []) > 0)
_, abs_path = self.sphinx_env.relfn2path(path_dest, self.sphinx_env.docname)
abs_path = self._link_target_path(path_dest)
docname = self.sphinx_env.path2doc(abs_path)
if not docname:
self.create_warning(
Expand Down Expand Up @@ -144,8 +163,7 @@ def render_link_unknown(self, token: SyntaxTreeNode) -> None:

potential_path: Path | None = None
if self.sphinx_env.srcdir: # not set in some test situations
_, path_str = self.sphinx_env.relfn2path(path_dest, self.sphinx_env.docname)
potential_path = Path(path_str)
potential_path = Path(self._link_target_path(path_dest))

if potential_path and potential_path.is_file():
docname = self.sphinx_env.path2doc(str(potential_path))
Expand Down
62 changes: 62 additions & 0 deletions tests/test_sphinx/test_symlinked_docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Links to a document that is a symlink to a file outside the source directory."""

from __future__ import annotations

import os
from io import StringIO
from pathlib import Path

import pytest
from sphinx.application import Sphinx


def _build(srcdir: Path, outdir: Path) -> tuple[str, str]:
"""Build the project with the html builder, return (warnings, index.html)."""
status, warning = StringIO(), StringIO()
app = Sphinx(
str(srcdir),
str(srcdir),
str(outdir),
str(outdir / ".doctrees"),
"html",
status=status,
warning=warning,
freshenv=True,
)
app.build()
return warning.getvalue(), (outdir / "index.html").read_text(encoding="utf-8")


@pytest.mark.parametrize(
"link",
[
pytest.param("linked.md#target-section", id="relative-path"),
pytest.param("project:linked.md#target-section", id="project-scheme"),
],
)
def test_link_to_symlinked_doc_outside_srcdir(tmp_path: Path, link: str):
"""A link to a document that is a symlink to a file outside the source
directory resolves to the document, rather than to the symlink target.
"""
docs = tmp_path / "project" / "docs"
docs.mkdir(parents=True)
(tmp_path / "project" / "README.md").write_text(
"# Readme\n\n## Target section\n\ntext\n", encoding="utf-8"
)
try:
os.symlink(os.path.join("..", "README.md"), docs / "linked.md")
except (OSError, NotImplementedError): # pragma: no cover
pytest.skip("symlinks are not available")
(docs / "conf.py").write_text(
'extensions = ["myst_parser"]\nmyst_heading_anchors = 3\n', encoding="utf-8"
)
(docs / "index.md").write_text(
f"# Index\n\n[Section title]({link})\n\n```{{toctree}}\nlinked\n```\n",
encoding="utf-8",
)

warnings, index_html = _build(docs, tmp_path / "_build")

assert "Unknown source document" not in warnings
assert "Could not find document" not in warnings
assert 'href="linked.html#target-section"' in index_html