diff --git a/docs/examples/example.html b/docs/examples/example.html
new file mode 100644
index 0000000..e6f4a7e
--- /dev/null
+++ b/docs/examples/example.html
@@ -0,0 +1,11 @@
+
+
+
+
+Example Extra Page
+
+
+Example Extra Page
+This standalone HTML file is served via html_extra_path.
+
+
diff --git a/pyproject.toml b/pyproject.toml
index 1fda771..0b4dfd3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -192,6 +192,7 @@ pages = [
"docs/src/api.md",
]
use-autoapi = false
+html_extra_path = ["docs/examples"]
[tool.yardang.breathe]
projects = { calculator = "examples/cpp/xml" }
diff --git a/yardang/build.py b/yardang/build.py
index 14e40c4..be3ca1a 100644
--- a/yardang/build.py
+++ b/yardang/build.py
@@ -8,7 +8,7 @@
from jinja2 import Environment, FileSystemLoader
-from .utils import get_config
+from .utils import get_config, get_config_flex
__all__ = ("generate_docs_configuration", "run_doxygen_if_needed", "generate_wiki_configuration")
@@ -239,16 +239,18 @@ def customize(args):
root = root if root is not None else get_config(section="root", base=config_base)
cname = cname if cname is not None else get_config(section="cname", base=config_base)
pages = pages if pages is not None else get_config(section="pages", base=config_base) or []
- use_autoapi = use_autoapi if use_autoapi is not None else get_config(section="use-autoapi", base=config_base)
- autoapi_ignore = autoapi_ignore if autoapi_ignore is not None else get_config(section="docs.autoapi-ignore")
+ use_autoapi = use_autoapi if use_autoapi is not None else get_config_flex(section="use-autoapi", base=config_base)
+ autoapi_ignore = autoapi_ignore if autoapi_ignore is not None else get_config_flex(section="autoapi-ignore", base=config_base)
custom_css = (
custom_css
if custom_css is not None
- else Path(get_config(section="custom-css", base=config_base) or (Path(__file__).parent / "custom.css"))
+ else Path(get_config_flex(section="custom-css", base=config_base) or (Path(__file__).parent / "custom.css"))
)
custom_js = (
- custom_js if custom_js is not None else Path(get_config(section="custom-js", base=config_base) or (Path(__file__).parent / "custom.js"))
+ custom_js
+ if custom_js is not None
+ else Path(get_config_flex(section="custom-js", base=config_base) or (Path(__file__).parent / "custom.js"))
)
# if custom_css and custom_js are strings and they exist as paths, read them as Paths
@@ -280,6 +282,7 @@ def customize(args):
# sphinx generic
"html_theme_options": {},
"html_static_path": [],
+ "html_extra_path": [],
"html_css_files": [],
"html_js_files": [],
"source_suffix": [],
@@ -357,7 +360,9 @@ def customize(args):
# sphinx-reredirects
"redirects": {},
}.items():
- configuration_args[config_option] = get_config(section=config_option, base=config_base) or default
+ configuration_args[config_option] = get_config_flex(section=config_option, base=config_base)
+ if configuration_args[config_option] is None:
+ configuration_args[config_option] = default
# Load breathe/doxygen configuration from tool.yardang.breathe
breathe_config_base = f"{config_base}.breathe"
@@ -397,6 +402,13 @@ def customize(args):
if use_breathe and auto_run_doxygen and breathe_args["breathe_projects"]:
run_doxygen_if_needed(breathe_args["breathe_projects"])
+ # Convert relative paths in html_extra_path to absolute paths
+ # This is needed because the conf.py is generated in a temp directory
+ if configuration_args["html_extra_path"]:
+ configuration_args["html_extra_path"] = [
+ str(Path(path).resolve()) if not Path(path).is_absolute() else path for path in configuration_args["html_extra_path"]
+ ]
+
# Convert relative paths in breathe_projects to absolute paths
# This is needed because the conf.py is generated in a temp directory
if breathe_args["breathe_projects"]:
diff --git a/yardang/conf.py.j2 b/yardang/conf.py.j2
index 322a418..6b6cfd6 100644
--- a/yardang/conf.py.j2
+++ b/yardang/conf.py.j2
@@ -113,6 +113,7 @@ os.environ["SPHINX_BUILDING"] = "1"
html_theme = "{{theme}}"
html_theme_options = {{html_theme_options}}
html_static_path = {{html_static_path}}
+html_extra_path = {{html_extra_path}}
html_css_files = [
"styles/custom.css",
*{{html_css_files}},
diff --git a/yardang/tests/test_all.py b/yardang/tests/test_all.py
index 0b3edce..5660a31 100644
--- a/yardang/tests/test_all.py
+++ b/yardang/tests/test_all.py
@@ -3,6 +3,7 @@
from yardang.build import generate_docs_configuration
from yardang.cli import build, debug
+from yardang.utils import get_config_flex
def test_build():
@@ -82,3 +83,188 @@ def test_use_autoapi_none_falls_back_to_config(self, tmp_path):
assert "use_autoapi = True" in conf_content
finally:
os.chdir(original_cwd)
+
+
+class TestGetConfigFlex:
+ """Tests for get_config_flex accepting both hyphens and underscores."""
+
+ def test_hyphen_key_found(self, tmp_path):
+ """Test that hyphenated keys are found."""
+ pyproject_content = """
+[project]
+name = "test-project"
+version = "1.0.0"
+
+[tool.yardang]
+html-extra-path = ["docs/extra"]
+"""
+ (tmp_path / "pyproject.toml").write_text(pyproject_content)
+ original_cwd = os.getcwd()
+ try:
+ os.chdir(tmp_path)
+ result = get_config_flex(section="html-extra-path", base="tool.yardang")
+ assert result == ["docs/extra"]
+ finally:
+ os.chdir(original_cwd)
+
+ def test_underscore_key_found(self, tmp_path):
+ """Test that underscored keys are found."""
+ pyproject_content = """
+[project]
+name = "test-project"
+version = "1.0.0"
+
+[tool.yardang]
+html_extra_path = ["docs/extra"]
+"""
+ (tmp_path / "pyproject.toml").write_text(pyproject_content)
+ original_cwd = os.getcwd()
+ try:
+ os.chdir(tmp_path)
+ result = get_config_flex(section="html_extra_path", base="tool.yardang")
+ assert result == ["docs/extra"]
+ finally:
+ os.chdir(original_cwd)
+
+ def test_hyphen_key_searched_when_underscore_queried(self, tmp_path):
+ """Test that querying with underscores finds hyphenated keys."""
+ pyproject_content = """
+[project]
+name = "test-project"
+version = "1.0.0"
+
+[tool.yardang]
+html-extra-path = ["docs/extra"]
+"""
+ (tmp_path / "pyproject.toml").write_text(pyproject_content)
+ original_cwd = os.getcwd()
+ try:
+ os.chdir(tmp_path)
+ # Query with underscores, but TOML uses hyphens
+ result = get_config_flex(section="html_extra_path", base="tool.yardang")
+ assert result == ["docs/extra"]
+ finally:
+ os.chdir(original_cwd)
+
+ def test_underscore_key_searched_when_hyphen_queried(self, tmp_path):
+ """Test that querying with hyphens finds underscored keys."""
+ pyproject_content = """
+[project]
+name = "test-project"
+version = "1.0.0"
+
+[tool.yardang]
+html_extra_path = ["docs/extra"]
+"""
+ (tmp_path / "pyproject.toml").write_text(pyproject_content)
+ original_cwd = os.getcwd()
+ try:
+ os.chdir(tmp_path)
+ # Query with hyphens, but TOML uses underscores
+ result = get_config_flex(section="html-extra-path", base="tool.yardang")
+ assert result == ["docs/extra"]
+ finally:
+ os.chdir(original_cwd)
+
+ def test_hyphen_takes_precedence(self, tmp_path):
+ """Test that hyphenated key takes precedence when both exist."""
+ pyproject_content = """
+[project]
+name = "test-project"
+version = "1.0.0"
+
+[tool.yardang]
+html-extra-path = ["from-hyphens"]
+html_extra_path = ["from-underscores"]
+"""
+ (tmp_path / "pyproject.toml").write_text(pyproject_content)
+ original_cwd = os.getcwd()
+ try:
+ os.chdir(tmp_path)
+ result = get_config_flex(section="html_extra_path", base="tool.yardang")
+ assert result == ["from-hyphens"]
+ finally:
+ os.chdir(original_cwd)
+
+ def test_missing_key_returns_none(self, tmp_path):
+ """Test that missing keys return None."""
+ pyproject_content = """
+[project]
+name = "test-project"
+version = "1.0.0"
+
+[tool.yardang]
+title = "Test"
+"""
+ (tmp_path / "pyproject.toml").write_text(pyproject_content)
+ original_cwd = os.getcwd()
+ try:
+ os.chdir(tmp_path)
+ result = get_config_flex(section="html_extra_path", base="tool.yardang")
+ assert result is None
+ finally:
+ os.chdir(original_cwd)
+
+
+class TestHtmlExtraPath:
+ """Tests for html_extra_path in generated conf.py."""
+
+ def test_html_extra_path_with_hyphens(self, tmp_path):
+ """Test that html-extra-path (hyphens) is picked up in generated conf.py."""
+ extra_dir = tmp_path / "docs" / "extra"
+ extra_dir.mkdir(parents=True)
+ (extra_dir / "page.html").write_text("test")
+
+ pyproject_content = """
+[project]
+name = "test-project"
+version = "1.0.0"
+
+[tool.yardang]
+title = "Test Project"
+root = "README.md"
+use-autoapi = false
+html-extra-path = ["docs/extra"]
+"""
+ (tmp_path / "pyproject.toml").write_text(pyproject_content)
+ (tmp_path / "README.md").write_text("# Test\n\nContent.")
+
+ original_cwd = os.getcwd()
+ try:
+ os.chdir(tmp_path)
+ with generate_docs_configuration() as conf_dir:
+ conf_content = (Path(conf_dir) / "conf.py").read_text()
+ assert "html_extra_path" in conf_content
+ assert "docs/extra" in conf_content or "docs\\\\extra" in conf_content or str(extra_dir) in conf_content
+ finally:
+ os.chdir(original_cwd)
+
+ def test_html_extra_path_with_underscores(self, tmp_path):
+ """Test that html_extra_path (underscores) is also accepted."""
+ extra_dir = tmp_path / "docs" / "extra"
+ extra_dir.mkdir(parents=True)
+ (extra_dir / "page.html").write_text("test")
+
+ pyproject_content = """
+[project]
+name = "test-project"
+version = "1.0.0"
+
+[tool.yardang]
+title = "Test Project"
+root = "README.md"
+use-autoapi = false
+html_extra_path = ["docs/extra"]
+"""
+ (tmp_path / "pyproject.toml").write_text(pyproject_content)
+ (tmp_path / "README.md").write_text("# Test\n\nContent.")
+
+ original_cwd = os.getcwd()
+ try:
+ os.chdir(tmp_path)
+ with generate_docs_configuration() as conf_dir:
+ conf_content = (Path(conf_dir) / "conf.py").read_text()
+ assert "html_extra_path" in conf_content
+ assert "docs/extra" in conf_content or "docs\\\\extra" in conf_content or str(extra_dir) in conf_content
+ finally:
+ os.chdir(original_cwd)
diff --git a/yardang/utils.py b/yardang/utils.py
index 94ca6f8..e6dd8d3 100644
--- a/yardang/utils.py
+++ b/yardang/utils.py
@@ -3,7 +3,7 @@
import toml
-__all__ = ("get_config",)
+__all__ = ("get_config", "get_config_flex")
def get_pyproject_toml():
@@ -22,3 +22,26 @@ def get_config(section="", base="tool.yardang"):
if config is None:
return None
return config
+
+
+def get_config_flex(section="", base="tool.yardang"):
+ """Look up a config key, accepting both hyphens and underscores.
+
+ Tries the hyphenated form first (TOML convention), then the
+ underscored form (Sphinx convention). For example, looking up
+ ``html_extra_path`` will try ``html-extra-path`` first, then
+ ``html_extra_path``.
+ """
+ hyphen_key = section.replace("_", "-")
+ underscore_key = section.replace("-", "_")
+
+ # Prefer hyphens (TOML convention)
+ result = get_config(section=hyphen_key, base=base)
+ if result is not None:
+ return result
+
+ # Fall back to underscores (Sphinx convention)
+ if underscore_key != hyphen_key:
+ return get_config(section=underscore_key, base=base)
+
+ return None