Skip to content

Commit 394b654

Browse files
committed
Add --stdin option to CLI for reading Markdown from standard input
- Add --stdin flag to parse_args for reading from stdin - Add convert_stdin() function to handle stdin parsing - Update main() to call convert_stdin() when --stdin flag is used - Add comprehensive tests for stdin functionality and CLI behavior Signed-off-by: Matěj Cepl <[email protected]>
1 parent c62983f commit 394b654

File tree

2 files changed

+72
-0
lines changed

2 files changed

+72
-0
lines changed

markdown_it/cli/parse.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ def main(args: Sequence[str] | None = None) -> int:
2121
namespace = parse_args(args)
2222
if namespace.filenames:
2323
convert(namespace.filenames)
24+
elif namespace.stdin:
25+
convert_stdin()
2426
else:
2527
interactive()
2628
return 0
@@ -31,6 +33,18 @@ def convert(filenames: Iterable[str]) -> None:
3133
convert_file(filename)
3234

3335

36+
def convert_stdin() -> None:
37+
"""
38+
Parse a Markdown file and dump the output to stdout.
39+
"""
40+
try:
41+
rendered = MarkdownIt().render(sys.stdin.read())
42+
print(rendered, end="")
43+
except OSError:
44+
sys.stderr.write("Cannot parse Markdown from the standard input.\n")
45+
sys.exit(1)
46+
47+
3448
def convert_file(filename: str) -> None:
3549
"""
3650
Parse a Markdown file and dump the output to stdout.
@@ -94,6 +108,9 @@ def parse_args(args: Sequence[str] | None) -> argparse.Namespace:
94108
formatter_class=argparse.RawDescriptionHelpFormatter,
95109
)
96110
parser.add_argument("-v", "--version", action="version", version=version_str)
111+
parser.add_argument(
112+
"--stdin", action="store_true", help="read Markdown from standard input"
113+
)
97114
parser.add_argument(
98115
"filenames", nargs="*", help="specify an optional list of files to convert"
99116
)

tests/test_cli.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from contextlib import redirect_stdout
2+
import io
13
import pathlib
24
import tempfile
35
from unittest.mock import patch
@@ -40,3 +42,56 @@ def mock_input(prompt):
4042
with patch("builtins.print") as patched, patch("builtins.input", mock_input):
4143
parse.interactive()
4244
patched.assert_called()
45+
46+
47+
def test_main_no_args_is_interactive():
48+
with patch("markdown_it.cli.parse.interactive") as mock_interactive:
49+
assert parse.main([]) == 0
50+
mock_interactive.assert_called_once()
51+
52+
53+
def test_parse_output():
54+
with tempfile.TemporaryDirectory() as tempdir:
55+
path = pathlib.Path(tempdir).joinpath("test.md")
56+
path.write_text("# a b c")
57+
string_io = io.StringIO()
58+
with redirect_stdout(string_io):
59+
assert parse.main([str(path)]) == 0
60+
assert string_io.getvalue() == "<h1>a b c</h1>\n"
61+
62+
63+
def test_stdin():
64+
with patch("sys.stdin", io.StringIO("# a b c")):
65+
string_io = io.StringIO()
66+
with redirect_stdout(string_io):
67+
assert parse.main(["--stdin"]) == 0
68+
assert string_io.getvalue() == "<h1>a b c</h1>\n"
69+
70+
71+
def test_multiple_files():
72+
with tempfile.TemporaryDirectory() as tempdir:
73+
path1 = pathlib.Path(tempdir).joinpath("test1.md")
74+
path1.write_text("# file 1")
75+
path2 = pathlib.Path(tempdir).joinpath("test2.md")
76+
path2.write_text("* file 2")
77+
string_io = io.StringIO()
78+
with redirect_stdout(string_io):
79+
assert parse.main([str(path1), str(path2)]) == 0
80+
assert string_io.getvalue() == "<h1>file 1</h1>\n<ul>\n<li>file 2</li>\n</ul>\n"
81+
82+
83+
def test_interactive_render():
84+
# Simulate user typing '# hello', pressing Ctrl-D (renders), then Ctrl-C (exits)
85+
# This is needed to break the infinite loop in interactive mode on EOF.
86+
mock_input = patch(
87+
"builtins.input", side_effect=["# hello", EOFError, KeyboardInterrupt]
88+
)
89+
string_io = io.StringIO()
90+
with redirect_stdout(string_io), mock_input:
91+
parse.interactive()
92+
93+
output = string_io.getvalue()
94+
assert "markdown-it-py" in output # from print_heading
95+
# The rendered output is prefixed by a newline
96+
assert "\n<h1>hello</h1>\n" in output
97+
assert "Exiting" in output

0 commit comments

Comments
 (0)