diff --git a/mdformat_rustfmt/__init__.py b/mdformat_rustfmt/__init__.py index f4aa609..983e15a 100644 --- a/mdformat_rustfmt/__init__.py +++ b/mdformat_rustfmt/__init__.py @@ -1,12 +1,27 @@ __version__ = "0.0.3" # DO NOT EDIT THIS LINE MANUALLY. LET bump2version UTILITY DO IT +from collections.abc import Iterable import re import subprocess from typing import Callable +in_commented = False + + +def flatten(deep_list): + for el in deep_list: + if isinstance(el, Iterable) and not isinstance(el, (str, bytes)): + yield from flatten(el) + else: + yield el + def format_rust(unformatted: str, _info_str: str) -> str: + global in_commented + global remove_newlines + unformatted = _for_each_line(unformatted, _hide_sharp) + unformatted_bytes = unformatted.encode("utf-8") result = subprocess.run( ["rustfmt"], @@ -14,32 +29,108 @@ def format_rust(unformatted: str, _info_str: str) -> str: stderr=subprocess.DEVNULL, input=unformatted_bytes, ) - if result.returncode: - raise Exception("Failed to format Rust code") + formatted = result.stdout.decode("utf-8") - formatted = _for_each_line(formatted, _unhide_sharp) - return formatted + + if result.returncode: + raise Exception("Failed to format Rust code\n" + formatted) + + in_commented = False + remove_newlines = False + + return _for_each_line(formatted, _unhide_sharp).replace("\r", "") def _for_each_line(string: str, action: Callable[[str], str]) -> str: lines = string.split("\n") - lines = (action(line) for line in lines) + + lines = [action(line) for line in lines] + + lines = list(flatten(lines)) + + lines = [x for x in lines if x is not None] return "\n".join(lines) -_RUSTFMT_CUSTOM_COMMENT_PREFIX = "//#### " +_RUSTFMT_CUSTOM_COMMENT_BLOCK_BEGIN = "//__MDFORMAT_RUSTFMT_COMMENT_BEGIN__" +_RUSTFMT_CUSTOM_COMMENT_BLOCK_END = "//__MDFORMAT_RUSTFMT_COMMENT_END__" +_RUSTFMT_CUSTOM_COMMENT_ESCAPE = "//__MDFORMAT_RUSTFMT_COMMENT_ESCAPE__" +_RUSTFMT_CUSTOM_COMMENT_BLANK_LINE = "//__MDFORMAT_RUSTFMT_COMMENT_BLANK_LINE__" -def _hide_sharp(line: str) -> str: +def _hide_sharp(line: str): + global in_commented stripped = line.strip() - if stripped.startswith("# ") or stripped == "#": - return _RUSTFMT_CUSTOM_COMMENT_PREFIX + line - return line + if stripped.startswith("# ") or stripped.startswith("##") or stripped == "#": + tokens = [] + + if not in_commented: + in_commented = True + tokens.append(_RUSTFMT_CUSTOM_COMMENT_BLOCK_BEGIN) + + if stripped.startswith("##"): + tokens.append(_RUSTFMT_CUSTOM_COMMENT_ESCAPE) + + # if stripped == "#": + # tokens.append(_RUSTFMT_CUSTOM_COMMENT_BLANK_LINE) + + tokens.append(stripped[1:]) + + return tokens + + if in_commented: + in_commented = False + return [_RUSTFMT_CUSTOM_COMMENT_BLOCK_END, stripped] + + return stripped + + +next_line_escape = False +remove_newlines = False + + +def _unhide_sharp(line: str): + global in_commented + global next_line_escape + global remove_newlines + + if _RUSTFMT_CUSTOM_COMMENT_BLOCK_BEGIN in line: + remove_newlines = True + in_commented = True + line = re.sub( + re.escape(_RUSTFMT_CUSTOM_COMMENT_BLOCK_BEGIN), "", line, 1 + ).rstrip() + return line or None + + if _RUSTFMT_CUSTOM_COMMENT_BLOCK_END in line: + in_commented = False + line = re.sub( + re.escape(_RUSTFMT_CUSTOM_COMMENT_BLOCK_END), "", line, 1 + ).rstrip() + return line or None + + if _RUSTFMT_CUSTOM_COMMENT_ESCAPE in line: + next_line_escape = True + line = re.sub(re.escape(_RUSTFMT_CUSTOM_COMMENT_ESCAPE), "", line, 1).rstrip() + return line or None + + if _RUSTFMT_CUSTOM_COMMENT_BLANK_LINE in line: + return "#" + + if in_commented: + if line == "" and remove_newlines: + return None + + remove_newlines = False + + if line.startswith("#") and next_line_escape: + next_line_escape = False + return "#" + line + + if line.startswith(" "): + return "#" + line[1:] + + return "# " + line -def _unhide_sharp(line: str) -> str: - if re.match(r"\s*" + re.escape(_RUSTFMT_CUSTOM_COMMENT_PREFIX), line): - # Remove the first "rustfmt custom comment prefix" and any leading - # whitespace the prefixed line originally had. - return re.sub(re.escape(_RUSTFMT_CUSTOM_COMMENT_PREFIX) + r"\s*", "", line, 1) return line diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..1b4670e --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +blank_lines_lower_bound = 1 diff --git a/tests/data/fixtures.md b/tests/data/fixtures.md index 5f86195..4b47921 100644 --- a/tests/data/fixtures.md +++ b/tests/data/fixtures.md @@ -23,10 +23,131 @@ fn main() { . ```rust fn main() { - # let x=a();b();c(); +# let x = a(); +# b(); +# c(); let y = d(); e(); f(); } ``` . + +Format hidden lines +. +~~~rust +fn main() { + # let x=a();b();c(); +} +~~~ +. +```rust +fn main() { +# let x = a(); +# b(); +# c(); +} +``` +. + +Handle empty comment lines +. +~~~rust +fn main() { + # +# + # // comment +let s = "asdf +## literal hash"; +let x = 5; +let y = 6; +} +~~~ +. +```rust +fn main() { +# // comment + let s = "asdf +## literal hash"; + let x = 5; + let y = 6; +} +``` +. + +Handle hidden derive and attr statements +. +~~~rust +# #[derive(Debug)] +struct MyStruct {} +~~~ +. +```rust +# #[derive(Debug)] +struct MyStruct {} +``` +. +Handle derive and attr statements +. +~~~rust +#[derive(Debug)] +struct MyStruct {} +~~~ +. +```rust +#[derive(Debug)] +struct MyStruct {} +``` +. +[NIGHTLY] Preserve blank lines +. +~~~rust +# struct Something {} +# +# +# fn main() {} +# +# +trait SomeTrait { fn nothing() {} } + struct Another; + + impl Another {} +~~~ +. +```rust +# struct Something {} +# +# fn main() {} +# +trait SomeTrait { + fn nothing() {} +} + +struct Another; + +impl Another {} +``` +. +Comment collapse does not delete lines +. +~~~rust +# fn main() -> Result<(), amethyst::Error> { +# let game_data = DispatcherBuilder::default().with_bundle( + // inside your rendering bundle setup + RenderingBundle::::new() + .with_plugin(RenderFlat2D::default()) +# )?; +# Ok(()) +# } +~~~ +. +```rust +# fn main() -> Result<(), amethyst::Error> { +# let game_data = DispatcherBuilder::default().with_bundle( + // inside your rendering bundle setup + RenderingBundle::::new().with_plugin(RenderFlat2D::default()), +# )?; +# Ok(()) +# } +``` +. \ No newline at end of file diff --git a/tests/test_mdformat_rustfmt.py b/tests/test_mdformat_rustfmt.py index 425902c..617a4e1 100644 --- a/tests/test_mdformat_rustfmt.py +++ b/tests/test_mdformat_rustfmt.py @@ -12,6 +12,8 @@ ) def test_fixtures(line, title, text, expected): """Test fixtures in tests/data/fixtures.md.""" + if "NIGHTLY" in title: + pytest.skip("nightly test not supported on stable") md_new = mdformat.text(text, codeformatters={"rust"}) if md_new != expected: print("Formatted (unexpected) Markdown below:")