generated from coulomb/repo-seed
extension for ref resolve, explode, implode, weave, tangle
This commit is contained in:
@@ -16,6 +16,10 @@ from markitect_tool.cache import (
|
||||
load_cache,
|
||||
save_cache,
|
||||
)
|
||||
from markitect_tool.content_class import (
|
||||
ContentClassResolutionError,
|
||||
load_content_class_file,
|
||||
)
|
||||
from markitect_tool.core import parse_markdown_file
|
||||
from markitect_tool.contract import (
|
||||
ContractLoaderError,
|
||||
@@ -24,6 +28,11 @@ from markitect_tool.contract import (
|
||||
load_contract_file,
|
||||
validate_contract,
|
||||
)
|
||||
from markitect_tool.explode import (
|
||||
ExplodeError,
|
||||
explode_markdown_file,
|
||||
implode_markdown_directory,
|
||||
)
|
||||
from markitect_tool.generation import (
|
||||
GenerationPlanError,
|
||||
generate_stub_from_contract,
|
||||
@@ -31,8 +40,16 @@ from markitect_tool.generation import (
|
||||
load_generation_plan_file,
|
||||
run_generation_plan,
|
||||
)
|
||||
from markitect_tool.literate import tangle_markdown, weave_markdown, write_tangle_files
|
||||
from markitect_tool.ops import IncludeError, compose_files, resolve_includes, transform_markdown
|
||||
from markitect_tool.processor import ProcessorContext, run_fenced_processors
|
||||
from markitect_tool.query import InvalidQueryError, extract_document, query_document
|
||||
from markitect_tool.reference import (
|
||||
ReferenceContext,
|
||||
ReferenceResolutionError,
|
||||
load_namespaces,
|
||||
resolve_reference,
|
||||
)
|
||||
from markitect_tool.schema import load_schema_file, validate_markdown_file, validate_schema
|
||||
from markitect_tool.template import (
|
||||
MissingTemplateVariable,
|
||||
@@ -296,6 +313,224 @@ def include(
|
||||
_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(
|
||||
"--output-dir",
|
||||
required=True,
|
||||
type=click.Path(file_okay=False, path_type=Path),
|
||||
help="Directory to write exploded Markdown files and manifest into.",
|
||||
)
|
||||
@click.option(
|
||||
"--variant",
|
||||
type=click.Choice(["flat", "hierarchical"], case_sensitive=False),
|
||||
default="flat",
|
||||
show_default=True,
|
||||
)
|
||||
@click.option("--force", is_flag=True, help="Allow writing into a non-empty output directory.")
|
||||
@click.option(
|
||||
"--format",
|
||||
"output_format",
|
||||
type=click.Choice(["json", "yaml", "text"], case_sensitive=False),
|
||||
default="text",
|
||||
show_default=True,
|
||||
)
|
||||
def explode(
|
||||
file: Path,
|
||||
output_dir: Path,
|
||||
variant: str,
|
||||
force: bool,
|
||||
output_format: str,
|
||||
) -> None:
|
||||
"""Explode a Markdown file into reversible section files."""
|
||||
|
||||
try:
|
||||
result = explode_markdown_file(file, output_dir, variant=variant, overwrite=force)
|
||||
except ExplodeError as exc:
|
||||
raise click.ClickException(str(exc)) from exc
|
||||
_emit_explode_result(result.to_dict(), output_format)
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.argument("directory", type=click.Path(exists=True, file_okay=False, path_type=Path))
|
||||
@click.option(
|
||||
"--manifest",
|
||||
"manifest_path",
|
||||
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
||||
help="Manifest path. Defaults to markitect-explode.yaml in the input directory.",
|
||||
)
|
||||
@click.option(
|
||||
"--output",
|
||||
type=click.Path(dir_okay=False, path_type=Path),
|
||||
help="Write imploded Markdown to a file.",
|
||||
)
|
||||
@click.option(
|
||||
"--format",
|
||||
"output_format",
|
||||
type=click.Choice(["markdown", "json", "yaml"], case_sensitive=False),
|
||||
default="markdown",
|
||||
show_default=True,
|
||||
)
|
||||
def implode(
|
||||
directory: Path,
|
||||
manifest_path: Path | None,
|
||||
output: Path | None,
|
||||
output_format: str,
|
||||
) -> None:
|
||||
"""Implode a Markdown directory created by `mkt explode`."""
|
||||
|
||||
try:
|
||||
result = implode_markdown_directory(directory, manifest_path=manifest_path)
|
||||
except ExplodeError as exc:
|
||||
raise click.ClickException(str(exc)) from exc
|
||||
_emit_markdown_result(result.to_dict(), output_format, output)
|
||||
|
||||
|
||||
@main.group("ref")
|
||||
def ref_group() -> None:
|
||||
"""Resolve namespaced Markdown content references."""
|
||||
|
||||
|
||||
@ref_group.command("resolve")
|
||||
@click.argument("context_file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
||||
@click.argument("reference")
|
||||
@click.option(
|
||||
"--root",
|
||||
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
||||
default=Path("."),
|
||||
show_default=True,
|
||||
help="Root that relative paths and namespaces must stay within.",
|
||||
)
|
||||
@click.option(
|
||||
"--format",
|
||||
"output_format",
|
||||
type=click.Choice(["json", "yaml", "text"], case_sensitive=False),
|
||||
default="text",
|
||||
show_default=True,
|
||||
)
|
||||
def ref_resolve(context_file: Path, reference: str, root: Path, output_format: str) -> None:
|
||||
"""Resolve a content reference using a Markdown document as context."""
|
||||
|
||||
context_document = parse_markdown_file(context_file)
|
||||
context = ReferenceContext.from_document(
|
||||
context_document,
|
||||
root=root,
|
||||
current_path=context_file,
|
||||
)
|
||||
try:
|
||||
resolution = resolve_reference(reference, context=context)
|
||||
except ReferenceResolutionError as exc:
|
||||
raise click.ClickException(str(exc)) from exc
|
||||
_emit_reference_result(resolution.to_dict(), output_format)
|
||||
|
||||
|
||||
@main.command("process")
|
||||
@click.argument("file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
||||
@click.option(
|
||||
"--root",
|
||||
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
||||
default=Path("."),
|
||||
show_default=True,
|
||||
help="Root used for relative processor references.",
|
||||
)
|
||||
@click.option(
|
||||
"--format",
|
||||
"output_format",
|
||||
type=click.Choice(["json", "yaml", "text"], case_sensitive=False),
|
||||
default="text",
|
||||
show_default=True,
|
||||
)
|
||||
def process(file: Path, root: Path, output_format: str) -> None:
|
||||
"""Run deterministic fenced-block processors in a Markdown file."""
|
||||
|
||||
document = parse_markdown_file(file)
|
||||
context = ProcessorContext(
|
||||
root=root,
|
||||
current_path=file,
|
||||
namespaces=load_namespaces(document.frontmatter),
|
||||
)
|
||||
result = run_fenced_processors(
|
||||
file.read_text(encoding="utf-8"),
|
||||
context=context,
|
||||
source_path=file,
|
||||
)
|
||||
_emit_processor_run(result.to_dict(), output_format)
|
||||
raise click.exceptions.Exit(0 if result.valid else 1)
|
||||
|
||||
|
||||
@main.group("class")
|
||||
def class_group() -> None:
|
||||
"""Resolve deterministic content classes."""
|
||||
|
||||
|
||||
@class_group.command("resolve")
|
||||
@click.argument("class_file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
||||
@click.argument("class_name")
|
||||
@click.option(
|
||||
"--format",
|
||||
"output_format",
|
||||
type=click.Choice(["json", "yaml", "text"], case_sensitive=False),
|
||||
default="text",
|
||||
show_default=True,
|
||||
)
|
||||
def class_resolve(class_file: Path, class_name: str, output_format: str) -> None:
|
||||
"""Resolve content class inheritance and merged slots."""
|
||||
|
||||
try:
|
||||
registry = load_content_class_file(class_file)
|
||||
result = registry.compose(class_name)
|
||||
except ContentClassResolutionError as exc:
|
||||
raise click.ClickException(str(exc)) from exc
|
||||
_emit_content_class_result(result.to_dict(), output_format)
|
||||
raise click.exceptions.Exit(0 if result.valid else 1)
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.argument("file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
||||
@click.option(
|
||||
"--output-dir",
|
||||
type=click.Path(file_okay=False, path_type=Path),
|
||||
help="Write tangled files under this directory. Omit for dry JSON/YAML/text output.",
|
||||
)
|
||||
@click.option(
|
||||
"--format",
|
||||
"output_format",
|
||||
type=click.Choice(["json", "yaml", "text"], case_sensitive=False),
|
||||
default="text",
|
||||
show_default=True,
|
||||
)
|
||||
def tangle(file: Path, output_dir: Path | None, output_format: str) -> None:
|
||||
"""Tangle named Markdown code chunks into target files."""
|
||||
|
||||
result = tangle_markdown(file.read_text(encoding="utf-8"), source_path=file)
|
||||
data = result.to_dict()
|
||||
if output_dir and result.valid:
|
||||
data["written_files"] = write_tangle_files(result, output_dir)
|
||||
_emit_tangle_result(data, output_format)
|
||||
raise click.exceptions.Exit(0 if result.valid else 1)
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.argument("file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
||||
@click.option(
|
||||
"--output",
|
||||
type=click.Path(dir_okay=False, path_type=Path),
|
||||
help="Write woven Markdown to a file.",
|
||||
)
|
||||
@click.option(
|
||||
"--format",
|
||||
"output_format",
|
||||
type=click.Choice(["markdown", "json", "yaml"], case_sensitive=False),
|
||||
default="markdown",
|
||||
show_default=True,
|
||||
)
|
||||
def weave(file: Path, output: Path | None, output_format: str) -> None:
|
||||
"""Weave Markdown documentation with a deterministic chunk index."""
|
||||
|
||||
result = weave_markdown(file.read_text(encoding="utf-8"), source_path=file)
|
||||
_emit_markdown_result(result.to_dict(), output_format, output)
|
||||
|
||||
|
||||
@main.group()
|
||||
def cache() -> None:
|
||||
"""Fingerprint Markdown files and detect changed inputs."""
|
||||
@@ -788,6 +1023,83 @@ def _emit_cache_data(data: dict, output_format: str) -> None:
|
||||
click.echo(f"written: {data['written']}")
|
||||
|
||||
|
||||
def _emit_reference_result(data: dict, output_format: str) -> None:
|
||||
if output_format == "json":
|
||||
click.echo(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
elif output_format == "yaml":
|
||||
click.echo(yaml.safe_dump(data, sort_keys=False))
|
||||
else:
|
||||
click.echo(f"{data['count']} unit(s)")
|
||||
click.echo(f"target: {data['target_path']}")
|
||||
for unit in data["units"]:
|
||||
span = unit.get("span", {})
|
||||
line = f":{span['line_start']}" if span.get("line_start") else ""
|
||||
click.echo(f"- {unit['kind']} {unit['unit_id']} {unit['source_path']}{line}")
|
||||
if unit.get("name"):
|
||||
click.echo(f" {unit['name']}")
|
||||
|
||||
|
||||
def _emit_explode_result(data: dict, output_format: str) -> None:
|
||||
if output_format == "json":
|
||||
click.echo(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
elif output_format == "yaml":
|
||||
click.echo(yaml.safe_dump(data, sort_keys=False))
|
||||
else:
|
||||
manifest = data["manifest"]
|
||||
click.echo(f"manifest: {data['manifest_path']}")
|
||||
click.echo(f"variant: {manifest['variant']}")
|
||||
click.echo(f"entries: {len(manifest['entries'])}")
|
||||
for entry in manifest["entries"]:
|
||||
click.echo(f"- {entry['kind']} {entry['file']}")
|
||||
|
||||
|
||||
def _emit_processor_run(data: dict, output_format: str) -> None:
|
||||
if output_format == "json":
|
||||
click.echo(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
elif output_format == "yaml":
|
||||
click.echo(yaml.safe_dump(data, sort_keys=False))
|
||||
else:
|
||||
click.echo("valid" if data["valid"] else "invalid")
|
||||
click.echo(f"processors: {data['count']}")
|
||||
for block, result in zip(data["blocks"], data["results"], strict=False):
|
||||
line = f":{block['line_start']}" if block.get("line_start") else ""
|
||||
click.echo(f"- {block['processor']} {block['unit_id']}{line}")
|
||||
if result.get("content"):
|
||||
click.echo(f" content: {result['content'].splitlines()[0]}")
|
||||
for diagnostic in result.get("diagnostics", []):
|
||||
click.echo(f" [{diagnostic['severity']}] {diagnostic['code']}: {diagnostic['message']}")
|
||||
|
||||
|
||||
def _emit_content_class_result(data: dict, output_format: str) -> None:
|
||||
if output_format == "json":
|
||||
click.echo(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
elif output_format == "yaml":
|
||||
click.echo(yaml.safe_dump(data, sort_keys=False))
|
||||
else:
|
||||
click.echo("valid" if data["valid"] else "invalid")
|
||||
click.echo("linearization: " + " -> ".join(data["linearization"]))
|
||||
for slot, value in data.get("slots", {}).items():
|
||||
click.echo(f"- {slot}: {value}")
|
||||
for diagnostic in data.get("diagnostics", []):
|
||||
click.echo(f"! [{diagnostic['severity']}] {diagnostic['code']}: {diagnostic['message']}")
|
||||
|
||||
|
||||
def _emit_tangle_result(data: dict, output_format: str) -> None:
|
||||
if output_format == "json":
|
||||
click.echo(json.dumps(data, indent=2, ensure_ascii=False))
|
||||
elif output_format == "yaml":
|
||||
click.echo(yaml.safe_dump(data, sort_keys=False))
|
||||
else:
|
||||
click.echo("valid" if data["valid"] else "invalid")
|
||||
click.echo(f"files: {len(data['files'])}")
|
||||
for file in data["files"]:
|
||||
click.echo(f"- {file['path']}: {', '.join(file['chunk_ids'])}")
|
||||
for diagnostic in data.get("diagnostics", []):
|
||||
click.echo(f"! [{diagnostic['severity']}] {diagnostic['code']}: {diagnostic['message']}")
|
||||
for written in data.get("written_files", []):
|
||||
click.echo(f"written: {written}")
|
||||
|
||||
|
||||
def _emit_jsonish(data: dict, output_format: str) -> None:
|
||||
if output_format == "yaml":
|
||||
click.echo(yaml.safe_dump(data, sort_keys=False))
|
||||
|
||||
Reference in New Issue
Block a user