"""``md2star templates {list,path}`` — inspect bundled / cached / per-project templates.
Module summary
--------------
A diagnostic CLI verb that mirrors the resolution order of
``md2star.cli._resolve_reference_doc`` without actually converting
anything. Two sub-verbs:
* ``md2star templates list`` — prints a table of every template
resolution candidate (per-project / cached / bundled) and
whether it exists. Distinguishes ``docx`` from ``pptx`` rows so
you can see at a glance which format is missing its template.
* ``md2star templates path [--fmt {docx,pptx}] [INPUT]`` — prints
the absolute filesystem path of the template that md2star WOULD
use for the given input (default: current directory + ``docx``).
One line, parseable by shell scripts.
The motivation is the recurring "why isn't my branding applied?"
debug loop. Before this command, users had to read the
``_resolve_reference_doc`` docstring and re-derive the priority
order in their head. Now they ``md2star templates path file.md``
and md2star tells them.
Usage
-----
>>> # From the shell:
>>> # md2star templates list
>>> # md2star templates path report.md --fmt docx
>>> # gives you the per-format resolution table and the resolved
>>> # path that `md2docx report.md` would pick up.
Author
------
[Warith HARCHAOUI](https://linkedin.com/in/warith-harchaoui/)
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from .cache import cache_dir
from .logging import get_logger
# Module logger — child of the root "md2star" logger (configured by the CLI).
logger = get_logger(__name__)
# ─────────────────────────────────────────────────────────────────────
# Candidate enumeration — mirrors `_resolve_reference_doc` priority
# ─────────────────────────────────────────────────────────────────────
def _bundled_template_path(fmt: str) -> Path:
"""Return the absolute path to the bundled ``template.<fmt>``.
Duplicates :func:`md2star.cli._bundled_template` so this module
does not have to import from :mod:`md2star.cli` (which would
pull in argparse + every preprocessor phase just to print a
table).
"""
from importlib import resources
# joinpath one component at a time: Python 3.10's MultiplexedPath only
# accepts a single arg per call, so the multi-arg form would break there.
ref = resources.files("md2star.data")
for part in (f"template.{fmt}",):
ref = ref.joinpath(part)
return Path(str(ref))
def _candidates(input_dir: Path, fmt: str) -> list[tuple[str, Path]]:
"""Return ``[(label, candidate_path), ...]`` in resolution order.
Mirrors the resolution chain in :func:`md2star.cli._resolve_reference_doc`
so the user-visible "which template wins" answer stays in sync
with the actual conversion path.
"""
# Highest priority first: a template committed next to the source, then the
# legacy dotfile name, then the XDG cache, then the always-present bundled
# copy. _first_existing() walks this list top-down to pick the winner.
#
# Note: between "cached" and "bundled" the real resolver
# (`_resolve_reference_doc`) fetches https://deraison.ai/template.<fmt> by
# default (v2.5.0+) and writes it into the "cached" slot. This diagnostic is
# a filesystem-only view, so it cannot show that network step directly — it
# surfaces as a populated "cached" row after the first online run.
return [
("per-project (template)", input_dir / f"template.{fmt}"),
("per-project (legacy)", input_dir / f".pandoc-reference.{fmt}"),
("cached", cache_dir("templates") / f"template.{fmt}"),
("bundled", _bundled_template_path(fmt)),
]
def _first_existing(candidates: list[tuple[str, Path]]) -> tuple[str, Path] | None:
"""Return the first ``(label, path)`` whose path exists, or ``None``."""
# Order is priority (see _candidates): the first hit is the active template.
for label, path in candidates:
if path.exists():
return label, path
return None
# ─────────────────────────────────────────────────────────────────────
# `md2star templates list` — table of every candidate
# ─────────────────────────────────────────────────────────────────────
[docs]
def cmd_list(args: argparse.Namespace) -> int:
"""Print a per-format resolution table to stdout.
Walks the same candidate chain as the conversion path, marks
each existing file with ``✓``, marks the first existing one
as the active winner with ``→``. No conversion, no network.
"""
# Resolve relative to --dir if given, else the current directory — the same
# anchor the real conversion uses for per-project templates.
input_dir = Path(args.dir).expanduser().resolve() if args.dir else Path.cwd()
# This is program OUTPUT (a report the user reads), so it goes to stdout via
# print, not the logging surface.
print(f"md2star templates — resolution from {input_dir}\n")
for fmt in ("docx", "pptx"):
print(f" [{fmt}]")
candidates = _candidates(input_dir, fmt)
# The winner is the first existing candidate; mark it distinctly below.
winner = _first_existing(candidates)
winner_path = winner[1] if winner else None
for label, path in candidates:
# → active winner, ✓ present-but-shadowed, space = absent.
mark = "→" if path == winner_path else ("✓" if path.exists() else " ")
size_note = ""
if path.exists():
size = path.stat().st_size
size_note = f" ({size:,} bytes)"
print(f" {mark} {label:<25} {path}{size_note}")
print()
return 0
# ─────────────────────────────────────────────────────────────────────
# `md2star templates path` — script-friendly resolved path
# ─────────────────────────────────────────────────────────────────────
[docs]
def cmd_path(args: argparse.Namespace) -> int:
"""Print the absolute path of the active template for ``fmt``.
Useful for shell scripts that want to operate on the template
md2star is currently using (e.g. opening it in Word for
branding). Exits with code 2 + a stderr message if no template
exists at any of the candidate locations (an install bug).
"""
input_dir = (
Path(args.input).expanduser().resolve().parent
if args.input
else Path.cwd()
)
winner = _first_existing(_candidates(input_dir, args.fmt))
if winner is None:
# No template anywhere is an install bug — diagnostic to stderr.
logger.error(
f"md2star: no template.{args.fmt} found at any candidate path "
f"(per-project / cached / bundled). Reinstall md2star."
)
return 2
# The resolved path itself is program OUTPUT (scripts capture it), so it
# stays on stdout via print — not the logging surface.
print(winner[1])
return 0
# ─────────────────────────────────────────────────────────────────────
# Entry point — wired into `md2star.cli.main` as the `templates`
# subcommand dispatcher.
# ─────────────────────────────────────────────────────────────────────
[docs]
def main(argv: list[str] | None = None) -> int:
"""Console entry point: ``md2star templates {list,path}``."""
parser = argparse.ArgumentParser(
prog="md2star templates",
description=(
"Inspect bundled / cached / per-project templates without "
"running a conversion. Mirrors the resolution priority "
"used by md2docx / md2pptx / md2pdf."
),
)
subparsers = parser.add_subparsers(dest="action", required=True)
p_list = subparsers.add_parser(
"list",
help="Show every resolution candidate (per-project / cached / bundled).",
)
p_list.add_argument(
"--dir",
help=(
"Treat this directory as the per-project root (i.e. the "
"directory that would contain `template.docx`). Defaults "
"to the current working directory."
),
)
p_path = subparsers.add_parser(
"path",
help="Print the absolute path of the active template for a given format.",
)
p_path.add_argument(
"--fmt",
choices=("docx", "pptx"),
default="docx",
help="Output format whose template to resolve (default: docx).",
)
p_path.add_argument(
"input",
nargs="?",
help=(
"Optional input markdown file — its containing directory "
"becomes the per-project root. Without it, the current "
"working directory is used."
),
)
args = parser.parse_args(argv)
if args.action == "list":
return cmd_list(args)
if args.action == "path":
return cmd_path(args)
parser.error(f"unknown action: {args.action!r}")
return 2 # pragma: no cover — argparse.error never returns
if __name__ == "__main__":
sys.exit(main())