From f6e97203fba872e3753199bdcaa67c68072432da Mon Sep 17 00:00:00 2001 From: jf nz Date: Sun, 6 Sep 2026 08:57:34 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20NEW:=20Add=20--enable-tables=20?= =?UTF-8?q?to=20the=20CLI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + README.md | 22 ++++++++++++++++------ markdown_it/cli/parse.py | 29 ++++++++++++++++++----------- tests/test_cli.py | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 033cf4fe..807e4ac9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +* ✨ Add `--enable-tables` to the CLI for file, standard input and interactive parsing. * 📚 Document the Python renderer constructor contract. ## 4.2.0 - 2026-05-07 diff --git a/README.md b/README.md index ce27254f..55f04ad9 100644 --- a/README.md +++ b/README.md @@ -101,17 +101,18 @@ Render markdown to HTML with markdown-it-py from the command-line: ```console -usage: markdown-it [-h] [-v] [--stdin|filenames [filenames ...]] +usage: markdown-it [-h] [-v] [--stdin] [--enable-tables] [filenames ...] Parse one or more markdown files, convert each to HTML, and print to stdout positional arguments: - --stdin read source Markdown file from standard input - filenames specify an optional list of files to convert + filenames specify an optional list of files to convert -optional arguments: - -h, --help show this help message and exit - -v, --version show program's version number and exit +options: + -h, --help show this help message and exit + -v, --version show program's version number and exit + --stdin read Markdown from standard input + --enable-tables enable table parsing Interactive: @@ -132,6 +133,15 @@ Batch: ``` +Tables are disabled by default, as in CommonMark. +Use `--enable-tables` with any input mode to enable them: + +```bash +markdown-it --enable-tables README.md > index.html +markdown-it --enable-tables --stdin < README.md > index.html +markdown-it --enable-tables +``` + ## References / Thanks Big thanks to the authors of [markdown-it]: diff --git a/markdown_it/cli/parse.py b/markdown_it/cli/parse.py index 16e7ac4c..7ebda8a8 100644 --- a/markdown_it/cli/parse.py +++ b/markdown_it/cli/parse.py @@ -18,50 +18,54 @@ def main(args: Sequence[str] | None = None) -> int: namespace = parse_args(args) + md = MarkdownIt() + if namespace.enable_tables: + md.enable("table") if namespace.filenames: - convert(namespace.filenames) + convert(namespace.filenames, md) elif namespace.stdin: - convert_stdin() + convert_stdin(md) else: - interactive() + interactive(md) return 0 -def convert(filenames: Iterable[str]) -> None: +def convert(filenames: Iterable[str], md: MarkdownIt | None = None) -> None: for filename in filenames: - convert_file(filename) + convert_file(filename, md) -def convert_stdin() -> None: +def convert_stdin(md: MarkdownIt | None = None) -> None: """ Parse a Markdown file and dump the output to stdout. """ try: - rendered = MarkdownIt().render(sys.stdin.read()) + rendered = (md or MarkdownIt()).render(sys.stdin.read()) print(rendered, end="") except OSError: sys.stderr.write("Cannot parse Markdown from the standard input.\n") sys.exit(1) -def convert_file(filename: str) -> None: +def convert_file(filename: str, md: MarkdownIt | None = None) -> None: """ Parse a Markdown file and dump the output to stdout. """ try: with open(filename, encoding="utf8", errors="ignore") as fin: - rendered = MarkdownIt().render(fin.read()) + rendered = (md or MarkdownIt()).render(fin.read()) print(rendered, end="") except OSError: sys.stderr.write(f'Cannot open file "{filename}".\n') sys.exit(1) -def interactive() -> None: +def interactive(md: MarkdownIt | None = None) -> None: """ Parse user input, dump to stdout, rinse and repeat. Python REPL style. """ + md = md or MarkdownIt() print_heading() contents = [] more = False @@ -70,7 +74,7 @@ def interactive() -> None: prompt, more = ("... ", True) if more else (">>> ", True) contents.append(input(prompt) + "\n") except EOFError: - print("\n" + MarkdownIt().render("\n".join(contents)), end="") + print("\n" + md.render("".join(contents)), end="") more = False contents = [] except KeyboardInterrupt: @@ -110,6 +114,9 @@ def parse_args(args: Sequence[str] | None) -> argparse.Namespace: parser.add_argument( "--stdin", action="store_true", help="read Markdown from standard input" ) + parser.add_argument( + "--enable-tables", action="store_true", help="enable table parsing" + ) parser.add_argument( "filenames", nargs="*", help="specify an optional list of files to convert" ) diff --git a/tests/test_cli.py b/tests/test_cli.py index a2fe51d0..88afb62f 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -95,3 +95,37 @@ def test_interactive_render(): # The rendered output is prefixed by a newline assert "\n

hello

\n" in output assert "Exiting" in output + + +@pytest.mark.parametrize("route", ["files", "stdin", "interactive"]) +@pytest.mark.parametrize("enable_tables", [False, True]) +def test_tables(route, enable_tables, tmp_path, capsys): + """Table parsing is opt-in on every CLI route, preserving HTML settings.""" + source = "a | b\n--- | ---\n1 | 2\n\nraw ~~plain~~\n" + expected = ( + "\n\n\n\n\n\n\n" + "\n\n\n\n\n\n
ab
12
\n" + if enable_tables + else "

a | b\n--- | ---\n1 | 2

\n" + ) + "

raw ~~plain~~

\n" + args = ["--enable-tables"] if enable_tables else [] + if route == "files": + paths = [tmp_path / "first.md", tmp_path / "second.md"] + for path in paths: + path.write_text(source, encoding="utf8") + assert parse.main([*args, *map(str, paths)]) == 0 + expected *= 2 + elif route == "stdin": + with patch("sys.stdin", io.StringIO(source)): + assert parse.main([*args, "--stdin"]) == 0 + else: + inputs = [*source.splitlines(), EOFError] * 2 + [KeyboardInterrupt] + with patch("builtins.input", side_effect=inputs): + assert parse.main(args) == 0 + expected = ( + f"{parse.version_str} (interactive)\n" + "Type Ctrl-D to complete input, or Ctrl-C to exit.\n" + + ("\n" + expected) * 2 + + "\nExiting.\n" + ) + assert capsys.readouterr().out == expected From 0061613813b0e6407f3bc500d5de91a925d4b47f Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 9 Sep 2026 12:00:40 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=90=9B=20FIX:=20Note=20and=20test=20i?= =?UTF-8?q?nteractive-mode=20line=20joining=20fix=20(#172)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Joining interactive input with "".join, rather than "\n".join, also fixes issue #172: every input line already ends in a newline, so the old join doubled them, splitting each line into its own paragraph and breaking hard line breaks. Record this in the changelog and add a regression test. --- CHANGELOG.md | 3 ++- tests/test_cli.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 807e4ac9..8fe152fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,8 @@ ## Unreleased -* ✨ Add `--enable-tables` to the CLI for file, standard input and interactive parsing. +* ✨ Add `--enable-tables` to the CLI for file, standard input and interactive parsing in [#422](https://github.com/executablebooks/markdown-it-py/pull/422) +* 🐛 Fix CLI interactive mode joining input lines with an extra newline, which split every line into its own paragraph and broke hard line breaks, in [#172](https://github.com/executablebooks/markdown-it-py/issues/172) * 📚 Document the Python renderer constructor contract. ## 4.2.0 - 2026-05-07 diff --git a/tests/test_cli.py b/tests/test_cli.py index 88afb62f..4dbf0b8f 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -97,6 +97,20 @@ def test_interactive_render(): assert "Exiting" in output +def test_interactive_hard_line_break(): + """Interactive input lines are joined as typed, see issue #172.""" + # Simulate user typing 'foo\\' then 'bar', Ctrl-D (renders), then Ctrl-C (exits) + mock_input = patch( + "builtins.input", side_effect=["foo\\", "bar", EOFError, KeyboardInterrupt] + ) + string_io = io.StringIO() + with redirect_stdout(string_io), mock_input: + parse.interactive() + + # a single paragraph with a hard break, not two paragraphs + assert "\n

foo
\nbar

\n" in string_io.getvalue() + + @pytest.mark.parametrize("route", ["files", "stdin", "interactive"]) @pytest.mark.parametrize("enable_tables", [False, True]) def test_tables(route, enable_tables, tmp_path, capsys):