generated from coulomb/repo-seed
Deterministic ops layer and cli
This commit is contained in:
@@ -16,6 +16,7 @@ from markitect_tool.contract import (
|
||||
load_contract_file,
|
||||
validate_contract,
|
||||
)
|
||||
from markitect_tool.ops import IncludeError, compose_files, resolve_includes, transform_markdown
|
||||
from markitect_tool.query import InvalidQueryError, extract_document, query_document
|
||||
from markitect_tool.schema import load_schema_file, validate_markdown_file, validate_schema
|
||||
|
||||
@@ -120,6 +121,160 @@ def extract(file: Path, selector: str, output_format: str) -> None:
|
||||
_emit_extract(data, output_format)
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.argument("file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
||||
@click.option("--strip-frontmatter", is_flag=True, help="Remove YAML frontmatter.")
|
||||
@click.option(
|
||||
"--set",
|
||||
"set_values",
|
||||
multiple=True,
|
||||
metavar="KEY=VALUE",
|
||||
help="Set a frontmatter value. Dot paths create nested mappings.",
|
||||
)
|
||||
@click.option(
|
||||
"--heading-delta",
|
||||
type=int,
|
||||
default=0,
|
||||
show_default=True,
|
||||
help="Shift ATX heading levels, clamped to 1..6.",
|
||||
)
|
||||
@click.option("--extract", "extract_selector", help="Replace content with selector output.")
|
||||
@click.option(
|
||||
"--output",
|
||||
type=click.Path(dir_okay=False, path_type=Path),
|
||||
help="Write transformed Markdown to a file.",
|
||||
)
|
||||
@click.option(
|
||||
"--format",
|
||||
"output_format",
|
||||
type=click.Choice(["markdown", "json", "yaml"], case_sensitive=False),
|
||||
default="markdown",
|
||||
show_default=True,
|
||||
)
|
||||
def transform(
|
||||
file: Path,
|
||||
strip_frontmatter: bool,
|
||||
set_values: tuple[str, ...],
|
||||
heading_delta: int,
|
||||
extract_selector: str | None,
|
||||
output: Path | None,
|
||||
output_format: str,
|
||||
) -> None:
|
||||
"""Apply deterministic transforms to a Markdown file."""
|
||||
|
||||
try:
|
||||
frontmatter_updates = _parse_key_value_options(set_values)
|
||||
result = transform_markdown(
|
||||
file.read_text(encoding="utf-8"),
|
||||
strip_frontmatter=strip_frontmatter,
|
||||
set_frontmatter=frontmatter_updates,
|
||||
heading_delta=heading_delta,
|
||||
extract_selector=extract_selector,
|
||||
source_path=str(file),
|
||||
)
|
||||
except (InvalidQueryError, ValueError) as exc:
|
||||
raise click.ClickException(str(exc)) from exc
|
||||
_emit_markdown_result(result.to_dict(), output_format, output)
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.argument(
|
||||
"files",
|
||||
nargs=-1,
|
||||
required=True,
|
||||
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
||||
)
|
||||
@click.option("--title", help="Add a top-level title before composed files.")
|
||||
@click.option(
|
||||
"--heading-delta",
|
||||
type=int,
|
||||
default=0,
|
||||
show_default=True,
|
||||
help="Shift heading levels in each input before composing.",
|
||||
)
|
||||
@click.option(
|
||||
"--include-frontmatter",
|
||||
is_flag=True,
|
||||
help="Keep each input file's frontmatter in the composed body.",
|
||||
)
|
||||
@click.option(
|
||||
"--output",
|
||||
type=click.Path(dir_okay=False, path_type=Path),
|
||||
help="Write composed Markdown to a file.",
|
||||
)
|
||||
@click.option(
|
||||
"--format",
|
||||
"output_format",
|
||||
type=click.Choice(["markdown", "json", "yaml"], case_sensitive=False),
|
||||
default="markdown",
|
||||
show_default=True,
|
||||
)
|
||||
def compose(
|
||||
files: tuple[Path, ...],
|
||||
title: str | None,
|
||||
heading_delta: int,
|
||||
include_frontmatter: bool,
|
||||
output: Path | None,
|
||||
output_format: str,
|
||||
) -> None:
|
||||
"""Compose multiple Markdown files into one document."""
|
||||
|
||||
result = compose_files(
|
||||
list(files),
|
||||
title=title,
|
||||
heading_delta=heading_delta,
|
||||
include_frontmatter=include_frontmatter,
|
||||
)
|
||||
_emit_markdown_result(result.to_dict(), output_format, output)
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.argument("file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
||||
@click.option(
|
||||
"--base-dir",
|
||||
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
||||
help="Directory includes must stay within. Defaults to the input file directory.",
|
||||
)
|
||||
@click.option(
|
||||
"--max-depth",
|
||||
type=int,
|
||||
default=10,
|
||||
show_default=True,
|
||||
help="Maximum recursive include depth.",
|
||||
)
|
||||
@click.option(
|
||||
"--output",
|
||||
type=click.Path(dir_okay=False, path_type=Path),
|
||||
help="Write resolved Markdown to a file.",
|
||||
)
|
||||
@click.option(
|
||||
"--format",
|
||||
"output_format",
|
||||
type=click.Choice(["markdown", "json", "yaml"], case_sensitive=False),
|
||||
default="markdown",
|
||||
show_default=True,
|
||||
)
|
||||
def include(
|
||||
file: Path,
|
||||
base_dir: Path | None,
|
||||
max_depth: int,
|
||||
output: Path | None,
|
||||
output_format: str,
|
||||
) -> None:
|
||||
"""Resolve Markdown include markers in a document."""
|
||||
|
||||
try:
|
||||
result = resolve_includes(
|
||||
file.read_text(encoding="utf-8"),
|
||||
base_dir=base_dir or file.parent,
|
||||
current_path=file,
|
||||
max_depth=max_depth,
|
||||
)
|
||||
except IncludeError as exc:
|
||||
raise click.ClickException(str(exc)) from exc
|
||||
_emit_markdown_result(result.to_dict(), output_format, output)
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.argument("file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
||||
@click.option(
|
||||
@@ -292,5 +447,42 @@ def _emit_extract(data: dict, output_format: str) -> None:
|
||||
click.echo("\n\n".join(data["items"]))
|
||||
|
||||
|
||||
def _emit_markdown_result(data: dict, output_format: str, output: Path | None) -> None:
|
||||
if output_format == "json":
|
||||
click.echo(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
return
|
||||
if output_format == "yaml":
|
||||
click.echo(yaml.safe_dump(data, sort_keys=False))
|
||||
return
|
||||
markdown = data["markdown"]
|
||||
if output:
|
||||
output.write_text(markdown, encoding="utf-8")
|
||||
else:
|
||||
click.echo(markdown, nl=False)
|
||||
|
||||
|
||||
def _parse_key_value_options(items: tuple[str, ...]) -> dict[str, object]:
|
||||
values: dict[str, object] = {}
|
||||
for item in items:
|
||||
if "=" not in item:
|
||||
raise ValueError(f"Expected KEY=VALUE, got `{item}`")
|
||||
key, raw_value = item.split("=", 1)
|
||||
key = key.strip()
|
||||
if not key:
|
||||
raise ValueError(f"Expected non-empty key in `{item}`")
|
||||
_set_path(values, key.split("."), yaml.safe_load(raw_value))
|
||||
return values
|
||||
|
||||
|
||||
def _set_path(mapping: dict[str, object], path: list[str], value: object) -> None:
|
||||
current = mapping
|
||||
for part in path[:-1]:
|
||||
next_value = current.setdefault(part, {})
|
||||
if not isinstance(next_value, dict):
|
||||
raise ValueError(f"Cannot set nested frontmatter path through scalar `{part}`")
|
||||
current = next_value
|
||||
current[path[-1]] = value
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user