"""`mkt` command entry point.""" from __future__ import annotations import json from pathlib import Path import click import yaml from markitect_tool.core import parse_markdown_file from markitect_tool.contract import ( ContractLoaderError, check_markdown_file, collect_metrics, load_contract_file, validate_contract, ) from markitect_tool.query import InvalidQueryError, extract_document, query_document from markitect_tool.schema import load_schema_file, validate_markdown_file, validate_schema @click.group() @click.version_option() def main() -> None: """Markdown-native toolkit for structured knowledge artifacts.""" @main.command() @click.argument("file", type=click.Path(exists=True, dir_okay=False, path_type=Path)) @click.option( "--format", "output_format", type=click.Choice(["json", "yaml", "tree"], case_sensitive=False), default="json", show_default=True, ) def parse(file: Path, output_format: str) -> None: """Parse a Markdown file into a structured representation.""" document = parse_markdown_file(file) data = document.to_dict() if output_format == "yaml": click.echo(yaml.safe_dump(data, sort_keys=False)) elif output_format == "tree": for heading in document.headings: click.echo(f"{'#' * heading.level} {heading.text}") else: click.echo(json.dumps(data, indent=2, ensure_ascii=False)) @main.command() @click.argument("file", type=click.Path(exists=True, dir_okay=False, path_type=Path)) @click.option( "--format", "output_format", type=click.Choice(["json", "yaml", "text"], case_sensitive=False), default="text", show_default=True, ) def metrics(file: Path, output_format: str) -> None: """Report practical size and complexity metrics for a Markdown file.""" document = parse_markdown_file(file) data = collect_metrics(document).to_dict() | {"document_path": str(file)} _emit_metrics(data, output_format) @main.command() @click.argument("file", type=click.Path(exists=True, dir_okay=False, path_type=Path)) @click.argument("selector") @click.option( "--format", "output_format", type=click.Choice(["json", "yaml", "text"], case_sensitive=False), default="json", show_default=True, ) def query(file: Path, selector: str, output_format: str) -> None: """Query structured Markdown content with a small selector.""" document = parse_markdown_file(file) try: matches = query_document(document, selector) except InvalidQueryError as exc: raise click.ClickException(str(exc)) from exc data = { "selector": selector, "document_path": str(file), "count": len(matches), "matches": [match.to_dict() for match in matches], } _emit_query(data, output_format) @main.command() @click.argument("file", type=click.Path(exists=True, dir_okay=False, path_type=Path)) @click.argument("selector") @click.option( "--format", "output_format", type=click.Choice(["text", "json", "yaml"], case_sensitive=False), default="text", show_default=True, ) def extract(file: Path, selector: str, output_format: str) -> None: """Extract text or Markdown content from structured Markdown.""" document = parse_markdown_file(file) try: items = extract_document(document, selector) except InvalidQueryError as exc: raise click.ClickException(str(exc)) from exc data = { "selector": selector, "document_path": str(file), "count": len(items), "items": items, } _emit_extract(data, output_format) @main.command() @click.argument("file", type=click.Path(exists=True, dir_okay=False, path_type=Path)) @click.option( "--schema", "schema_file", required=True, type=click.Path(exists=True, dir_okay=False, path_type=Path), ) @click.option( "--format", "output_format", type=click.Choice(["json", "yaml", "text"], case_sensitive=False), default="text", show_default=True, ) def validate(file: Path, schema_file: Path, output_format: str) -> None: """Validate a Markdown file against a Markdown schema file.""" result = validate_markdown_file(file, schema_file) _emit_result(result.to_dict(), output_format) raise click.exceptions.Exit(0 if result.valid else 1) @main.group() def schema() -> None: """Work with Markdown schema files.""" @schema.command("validate") @click.argument("schema_file", type=click.Path(exists=True, dir_okay=False, path_type=Path)) @click.option( "--format", "output_format", type=click.Choice(["json", "yaml", "text"], case_sensitive=False), default="text", show_default=True, ) def schema_validate(schema_file: Path, output_format: str) -> None: """Validate that a Markdown schema contains a well-formed JSON Schema.""" loaded = load_schema_file(schema_file) result = validate_schema(loaded.schema) data = result.to_dict() | {"schema_path": str(schema_file)} _emit_result(data, output_format) raise click.exceptions.Exit(0 if result.valid else 1) @main.group() def contract() -> None: """Work with Markdown document contracts.""" @contract.command("validate") @click.argument("contract_file", type=click.Path(exists=True, dir_okay=False, path_type=Path)) @click.option( "--format", "output_format", type=click.Choice(["json", "yaml", "text"], case_sensitive=False), default="text", show_default=True, ) def contract_validate(contract_file: Path, output_format: str) -> None: """Validate that a Markdown contract file is well formed.""" result = validate_contract(load_contract_file(contract_file)) _emit_diagnostic_result(result.to_dict(), output_format) raise click.exceptions.Exit(0 if result.valid else 1) @contract.command("check") @click.argument("file", type=click.Path(exists=True, dir_okay=False, path_type=Path)) @click.option( "--contract", "contract_file", required=True, type=click.Path(exists=True, dir_okay=False, path_type=Path), ) @click.option( "--format", "output_format", type=click.Choice(["json", "yaml", "text"], case_sensitive=False), default="text", show_default=True, ) def contract_check(file: Path, contract_file: Path, output_format: str) -> None: """Check a Markdown file against a Markdown document contract.""" try: result = check_markdown_file(file, contract_file) except ContractLoaderError as exc: raise click.ClickException(str(exc)) from exc _emit_diagnostic_result(result.to_dict(), output_format) raise click.exceptions.Exit(0 if result.valid else 1) def _emit_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: if data.get("valid"): click.echo("valid") else: click.echo("invalid") for violation in data.get("violations", []): click.echo(f"- {violation['path']}: {violation['message']}") def _emit_diagnostic_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.get("valid") else "invalid") for diagnostic in data.get("diagnostics", []): click.echo( f"- [{diagnostic['severity']}] {diagnostic['code']}: " f"{diagnostic['message']}" ) if diagnostic.get("source"): source = diagnostic["source"] suffix = f":{source['line']}" if source.get("line") else "" click.echo(f" source: {source.get('path', '')}{suffix}") if diagnostic.get("guidance"): click.echo(f" guidance: {diagnostic['guidance']}") def _emit_metrics(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: doc = data["document"] click.echo("document") for metric, value in doc.items(): click.echo(f"- {metric}: {value}") sections = data.get("sections", []) if sections: click.echo("sections") for section in sections: click.echo( f"- {section['heading']}: words={section['words']}, " f"paragraphs={section['paragraphs']}, line={section['line']}" ) def _emit_query(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']} match(es)") for match in data["matches"]: location = f":{match['line']}" if match.get("line") else "" click.echo(f"- {match['kind']} {match['path']}{location}") if match.get("text"): click.echo(f" {match['text'].splitlines()[0]}") def _emit_extract(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("\n\n".join(data["items"])) if __name__ == "__main__": main()