diff --git a/great-docs.yml b/great-docs.yml index a83178b..00b43f0 100644 --- a/great-docs.yml +++ b/great-docs.yml @@ -129,7 +129,7 @@ tags: # CLI Documentation # ----------------- -# cli: -# enabled: true # Enable CLI documentation -# module: my_package.cli # Module containing Click commands -# name: cli # Name of the Click command object +cli: + enabled: true + module: multimark._cli + name: main diff --git a/pyproject.toml b/pyproject.toml index 533230c..0c17020 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ description = "Python bindings to cmark: CommonMark parsing and rendering (HTML, readme = "README.md" license = {text = "MIT AND BSD-2-Clause"} requires-python = ">=3.9" -dependencies = ["cffi>=1.15"] +dependencies = ["cffi>=1.15", "click>=8.0"] authors = [{name = "Rich Iannone"}] keywords = ["markdown", "commonmark", "cmark-gfm", "html", "latex", "gfm"] classifiers = [ @@ -27,6 +27,9 @@ classifiers = [ "Topic :: Text Processing :: Markup :: Markdown", ] +[project.scripts] +multimark = "multimark._cli:main" + [project.urls] Homepage = "https://github.com/posit-dev/multimark" Issues = "https://github.com/posit-dev/multimark/issues" diff --git a/src/multimark/__init__.py b/src/multimark/__init__.py index 6be6101..6813e96 100644 --- a/src/multimark/__init__.py +++ b/src/multimark/__init__.py @@ -1,8 +1,3 @@ -"""multimark — Python bindings to the cmark-gfm CommonMark/GFM C library. - -Provides parsing and rendering of CommonMark and GitHub Flavored Markdown -to HTML, LaTeX, groff man, XML, and normalized CommonMark. -""" from enum import IntFlag from importlib.metadata import PackageNotFoundError, version as _get_version diff --git a/src/multimark/_build_cmark.py b/src/multimark/_build_cmark.py index 1d7248e..09618ca 100644 --- a/src/multimark/_build_cmark.py +++ b/src/multimark/_build_cmark.py @@ -1,4 +1,3 @@ -"""CFFI build script — compiles the vendored cmark-gfm C library + extensions.""" import os import glob from cffi import FFI diff --git a/src/multimark/_cli.py b/src/multimark/_cli.py new file mode 100644 index 0000000..34acc4c --- /dev/null +++ b/src/multimark/_cli.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +import click + +from multimark import ( + __version__, + markdown_to_html, + markdown_to_latex, + markdown_to_man, + markdown_to_commonmark, + markdown_to_xml, + VALID_EXTENSIONS, +) + +RENDERERS = { + "html": markdown_to_html, + "latex": markdown_to_latex, + "man": markdown_to_man, + "commonmark": markdown_to_commonmark, + "xml": markdown_to_xml, +} + + +@click.command() +@click.argument("file", type=click.File("r"), default="-") +@click.option( + "-t", + "--to", + "format", + type=click.Choice(list(RENDERERS.keys()), case_sensitive=False), + default="html", + help="Output format.", +) +@click.option( + "-o", + "--output", + type=click.File("w"), + default="-", + help="Output file (stdout if omitted).", +) +@click.option( + "-e", + "--extension", + "extensions", + multiple=True, + type=click.Choice(sorted(VALID_EXTENSIONS), case_sensitive=False), + help="Enable a GFM extension (repeatable).", +) +@click.option("--smart", is_flag=True, help="Use smart punctuation.") +@click.option("--unsafe", is_flag=True, help="Allow raw HTML and dangerous URLs.") +@click.option("--hardbreaks", is_flag=True, help="Render softbreaks as hard line breaks.") +@click.option("--sourcepos", is_flag=True, help="Include source position attributes (html/xml only).") +@click.option("--footnotes", is_flag=True, help="Enable footnote parsing.") +@click.version_option(__version__, prog_name="multimark") +def main( + file, + format: str, + output, + extensions: tuple[str, ...], + smart: bool, + unsafe: bool, + hardbreaks: bool, + sourcepos: bool, + footnotes: bool, +) -> None: + """Convert CommonMark/GFM Markdown to various output formats. + + Reads Markdown from FILE (or stdin if omitted) and writes the converted + output to stdout or the file specified by --output. + """ + text = file.read() + + renderer = RENDERERS[format] + kwargs: dict = dict( + extensions=list(extensions) or None, + smart=smart, + unsafe=unsafe, + hardbreaks=hardbreaks, + footnotes=footnotes, + ) + + # sourcepos is only supported by html and xml renderers + if format in ("html", "xml"): + kwargs["sourcepos"] = sourcepos + + result = renderer(text, **kwargs) + output.write(result) diff --git a/src/multimark/_cmark.py b/src/multimark/_cmark.py index 933849a..d895359 100644 --- a/src/multimark/_cmark.py +++ b/src/multimark/_cmark.py @@ -1,4 +1,3 @@ -"""Low-level CFFI wrapper around the cmark-gfm C library.""" from __future__ import annotations from typing import Sequence diff --git a/tests/spec_parser.py b/tests/spec_parser.py index cd38948..9ec7162 100644 --- a/tests/spec_parser.py +++ b/tests/spec_parser.py @@ -1,16 +1,3 @@ -"""Parser for CommonMark spec-format test fixtures. - -The spec format uses 32 backtick delimiters around examples: - - ```````````````````````````````` example - markdown input - . - expected html output - ```````````````````````````````` - -Section headings (# Heading) provide grouping context. -The → character represents a literal tab. -""" from __future__ import annotations import os diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..22bbbd0 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,140 @@ +import pytest +from click.testing import CliRunner + +from multimark._cli import main +from multimark import __version__ + + +@pytest.fixture +def runner(): + return CliRunner() + + +class TestBasicConversion: + def test_stdin_to_html(self, runner): + result = runner.invoke(main, input="# Hello\n") + assert result.exit_code == 0 + assert "