Skip to content

Configuration

obsidian_export.config

Configuration module for obsidian-export pipeline.

Re-exports all public names so that from obsidian_export.config import X continues to work.

build_config

build_config(raw: dict[str, Any], config_dir: Path | None) -> ConvertConfig

Build ConvertConfig from a raw dict. Resolve relative paths if config_dir given.

Source code in obsidian_export/config/loader.py
def build_config(raw: dict[str, Any], config_dir: Path | None) -> ConvertConfig:
    """Build ConvertConfig from a raw dict. Resolve relative paths if config_dir given."""
    if config_dir is not None and not config_dir.is_absolute():
        config_dir = config_dir.resolve()

    from_format = raw["pandoc"]["from_format"]
    validate_from_format(from_format)

    style = build_style_config(raw["style"], config_dir)
    validate_pandoc_variable("geometry", style.geometry)
    validate_pandoc_variable("fontsize", style.fontsize)
    validate_pandoc_variable("linkcolor", style.linkcolor)
    validate_pandoc_variable("urlcolor", style.urlcolor)
    validate_pandoc_variable("code_fontsize", style.code_fontsize)
    validate_pandoc_variable("table_fontsize", style.table_fontsize)

    validate_url_strategy(raw["obsidian"]["url_strategy"])

    return ConvertConfig(
        mermaid=build_mermaid_config(raw["mermaid"], config_dir),
        obsidian=ObsidianConfig(
            url_strategy=raw["obsidian"]["url_strategy"],
            url_length_threshold=raw["obsidian"]["url_length_threshold"],
            max_embed_depth=int(raw["obsidian"]["max_embed_depth"]),
        ),
        pandoc=PandocConfig(
            from_format=from_format,
        ),
        style=style,
    )

deep_merge

deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]

Recursively merge override into base. override wins on conflicts.

Source code in obsidian_export/config/loader.py
def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
    """Recursively merge override into base. override wins on conflicts."""
    merged = dict(base)
    for key, value in override.items():
        if key in merged and isinstance(merged[key], dict) and isinstance(value, dict):
            merged[key] = deep_merge(merged[key], value)
        else:
            merged[key] = value
    return merged

default_config

default_config() -> ConvertConfig

Return ConvertConfig with all defaults from bundled default.yaml.

Source code in obsidian_export/config/loader.py
def default_config() -> ConvertConfig:
    """Return ConvertConfig with all defaults from bundled default.yaml."""
    return build_config(load_default_yaml(), config_dir=None)

load_config

load_config(path: Path) -> ConvertConfig

Load config from YAML file, merging on top of bundled defaults.

Users can write minimal YAML with only overrides. Relative paths in config are resolved relative to the config file's directory.

Source code in obsidian_export/config/loader.py
def load_config(path: Path) -> ConvertConfig:
    """Load config from YAML file, merging on top of bundled defaults.

    Users can write minimal YAML with only overrides. Relative paths in
    config are resolved relative to the config file's directory.
    """
    user_raw = yaml.safe_load(path.read_text(encoding="utf-8"))
    if not user_raw:
        user_raw = {}
    base = load_default_yaml()
    merged = deep_merge(base, user_raw)
    return build_config(merged, config_dir=path.parent)

load_default_yaml

load_default_yaml() -> dict[str, Any]

Load the bundled default.yaml.

Source code in obsidian_export/config/loader.py
def load_default_yaml() -> dict[str, Any]:
    """Load the bundled default.yaml."""
    ref = resources.files("obsidian_export") / "defaults" / "default.yaml"
    return yaml.safe_load(ref.read_text(encoding="utf-8"))

parse_brand_colors

parse_brand_colors(raw: dict[str, Any]) -> tuple[tuple[str, int, int, int], ...]

Parse brand_colors dict {name: [r,g,b]} into tuple of (name, r, g, b).

Source code in obsidian_export/config/loader.py
def parse_brand_colors(raw: dict[str, Any]) -> tuple[tuple[str, int, int, int], ...]:
    """Parse brand_colors dict {name: [r,g,b]} into tuple of (name, r, g, b)."""
    return tuple((name, int(rgb[0]), int(rgb[1]), int(rgb[2])) for name, rgb in raw.items())

parse_heading_styles

parse_heading_styles(raw: list[dict[str, Any]]) -> tuple[HeadingStyle, ...]

Parse list of heading style dicts into tuple of HeadingStyle.

Source code in obsidian_export/config/loader.py
def parse_heading_styles(raw: list[dict[str, Any]]) -> tuple[HeadingStyle, ...]:
    """Parse list of heading style dicts into tuple of HeadingStyle."""
    return tuple(
        HeadingStyle(
            level=h["level"],
            size=h["size"],
            bold=bool(h.get("bold", False)),
            sans=bool(h.get("sans", False)),
            color=h.get("color", ""),
            uppercase=bool(h.get("uppercase", False)),
        )
        for h in raw
    )

parse_title_style

parse_title_style(raw: dict[str, Any] | None) -> TitleStyle | None

Parse title style dict into TitleStyle, or None if absent.

Source code in obsidian_export/config/loader.py
def parse_title_style(raw: dict[str, Any] | None) -> TitleStyle | None:
    """Parse title style dict into TitleStyle, or None if absent."""
    if not raw:
        return None
    return TitleStyle(
        size=raw["size"],
        bold=bool(raw.get("bold", False)),
        sans=bool(raw.get("sans", False)),
        color=raw.get("color", ""),
        date_visible=bool(raw.get("date_visible", True)),
        vskip_after=raw.get("vskip_after", ""),
    )

parse_unicode_chars

parse_unicode_chars(raw: dict[str, str]) -> tuple[tuple[str, str], ...]

Parse unicode_chars dict {char: latex} into tuple of (char, latex).

Source code in obsidian_export/config/loader.py
def parse_unicode_chars(raw: dict[str, str]) -> tuple[tuple[str, str], ...]:
    """Parse unicode_chars dict {char: latex} into tuple of (char, latex)."""
    return tuple((char, latex) for char, latex in raw.items())

resolve_path

resolve_path(raw_path: str, config_dir: Path | None) -> str

Resolve a relative path string against config_dir. Return as string.

Source code in obsidian_export/config/loader.py
def resolve_path(raw_path: str, config_dir: Path | None) -> str:
    """Resolve a relative path string against config_dir. Return as string."""
    if raw_path and config_dir and not Path(raw_path).is_absolute():
        return str((config_dir / raw_path).resolve())
    return raw_path

validate_from_format

validate_from_format(value: str) -> None

Validate pandoc from_format against safe base formats and extensions.

Raises ConfigValueError if the base format is not allowlisted, any extension name is malformed, or a dangerous extension is enabled.

Source code in obsidian_export/config/validators.py
def validate_from_format(value: str) -> None:
    """Validate pandoc from_format against safe base formats and extensions.

    Raises ConfigValueError if the base format is not allowlisted,
    any extension name is malformed, or a dangerous extension is enabled.
    """
    parts = re.split(r"(?=[+-])", value, maxsplit=1)
    base_format = parts[0]
    if base_format not in _SAFE_PANDOC_FORMATS:
        raise ConfigValueError(
            f"Unsupported pandoc base format: {base_format!r}. Allowed: {sorted(_SAFE_PANDOC_FORMATS)}"
        )

    if len(parts) < 2:
        return

    ext_string = parts[1]
    for match in re.finditer(r"([+-])([^+-]+)", ext_string):
        sign, ext_name = match.group(1), match.group(2)
        if not _PANDOC_EXTENSION_RE.match(ext_name):
            raise ConfigValueError(f"Malformed pandoc extension name: {ext_name!r} in from_format {value!r}")
        if sign == "+" and ext_name in _DANGEROUS_EXTENSIONS:
            raise ConfigValueError(
                f"Dangerous pandoc extension enabled: +{ext_name} in from_format {value!r}. "
                f"Blocked extensions: {sorted(_DANGEROUS_EXTENSIONS)}"
            )

validate_pandoc_variable

validate_pandoc_variable(name: str, value: str) -> None

Validate a string that will be passed as a pandoc --variable value.

Allows alphanumeric characters and limited punctuation (commas, equals, dots, hyphens, underscores, spaces). Raises ConfigValueError on mismatch.

Source code in obsidian_export/config/validators.py
def validate_pandoc_variable(name: str, value: str) -> None:
    """Validate a string that will be passed as a pandoc --variable value.

    Allows alphanumeric characters and limited punctuation (commas, equals,
    dots, hyphens, underscores, spaces). Raises ConfigValueError on mismatch.
    """
    if not value:
        return
    if not _PANDOC_VARIABLE_RE.match(value):
        raise ConfigValueError(
            f"Invalid characters in style config {name!r}: {value!r}. "
            f"Only alphanumeric characters and ,=._- are allowed."
        )

validate_url_strategy

validate_url_strategy(value: str) -> None

Validate url_strategy against the allowlist of supported strategies.

Raises ConfigValueError if the value is not recognised.

Source code in obsidian_export/config/validators.py
def validate_url_strategy(value: str) -> None:
    """Validate url_strategy against the allowlist of supported strategies.

    Raises ConfigValueError if the value is not recognised.
    """
    if value not in _VALID_URL_STRATEGIES:
        raise ConfigValueError(f"Unknown url_strategy: {value!r}. Allowed: {sorted(_VALID_URL_STRATEGIES)}")