Skip to content

Commit

Permalink
Plugin system for code formatting #12
Browse files Browse the repository at this point in the history
  • Loading branch information
hukkinj1 committed Sep 17, 2020
1 parent 9169a08 commit d3d7e96
Show file tree
Hide file tree
Showing 7 changed files with 58 additions and 8 deletions.
3 changes: 3 additions & 0 deletions .mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ disallow_untyped_defs = False

[mypy-markdown_it.*]
ignore_missing_imports = True

[mypy-importlib_metadata.*]
ignore_missing_imports = True
15 changes: 10 additions & 5 deletions mdformat/_api.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
from pathlib import Path
from typing import Union
from typing import Iterable, Union

from markdown_it import MarkdownIt

from mdformat._renderer import MDRenderer
import mdformat.plugins


def text(md: str) -> str:
def text(md: str, *, codeformatters: Iterable[str] = ()) -> str:
"""Format a Markdown string."""
return MarkdownIt(renderer_cls=MDRenderer).render(md)
markdown_it = MarkdownIt(renderer_cls=MDRenderer)
markdown_it.options["codeformatters"] = {
lang: mdformat.plugins.CODEFORMATTERS[lang] for lang in codeformatters
}
return markdown_it.render(md)


def file(f: Union[str, Path]) -> None:
def file(f: Union[str, Path], *, codeformatters: Iterable[str] = ()) -> None:
"""Format a Markdown file in place."""
if isinstance(f, str):
f = Path(f)
Expand All @@ -22,5 +27,5 @@ def file(f: Union[str, Path]) -> None:
if not is_file:
raise ValueError(f'Can not format "{f}". It is not a file.')
original_md = f.read_text(encoding="utf-8")
formatted_md = text(original_md)
formatted_md = text(original_md, codeformatters=codeformatters)
f.write_text(formatted_md, encoding="utf-8")
14 changes: 12 additions & 2 deletions mdformat/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import mdformat
from mdformat._util import is_md_equal
import mdformat.plugins


def run(cli_args: Sequence[str]) -> int: # noqa: C901
Expand Down Expand Up @@ -41,6 +42,9 @@ def run(cli_args: Sequence[str]) -> int: # noqa: C901
else:
file_paths.append(path_obj)

# Enable code formatting for all languages that have a plugin installed
enabled_codeformatter_langs = mdformat.plugins.CODEFORMATTERS.keys()

format_errors_found = False
for path in file_paths:
if path:
Expand All @@ -49,14 +53,20 @@ def run(cli_args: Sequence[str]) -> int: # noqa: C901
else:
path_str = "-"
original_str = sys.stdin.read()
formatted_str = mdformat.text(original_str)
formatted_str = mdformat.text(
original_str, codeformatters=enabled_codeformatter_langs
)

if args.check:
if formatted_str != original_str:
format_errors_found = True
sys.stderr.write(f'Error: File "{path_str}" is not formatted.\n')
else:
if not is_md_equal(original_str, formatted_str):
if not is_md_equal(
original_str,
formatted_str,
ignore_codeclasses=enabled_codeformatter_langs,
):
sys.stderr.write(
f'Error: Could not format "{path_str}"\n'
"\n"
Expand Down
10 changes: 10 additions & 0 deletions mdformat/_renderer/token_renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ def fence(tokens: List[Token], idx: int, options: dict, env: dict) -> str:
lang = token.info.strip() if token.info else ""
code_block = token.content

# Format the code block using enabled codeformatter funcs
if lang in options.get("codeformatters", {}):
fmt_func = options["codeformatters"][lang]
try:
code_block = fmt_func(code_block)
except Exception:
# Swallow exceptions so that formatter errors (e.g. due to
# invalid code) do not crash mdformat.
pass

# The code block must not include as long or longer sequence of "~"
# chars as the fence string itself
fence_len = max(3, longest_consecutive_sequence(code_block, "~") + 1)
Expand Down
6 changes: 5 additions & 1 deletion mdformat/_util.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import re
from typing import Iterable

from markdown_it import MarkdownIt


def is_md_equal(md1: str, md2: str) -> bool:
def is_md_equal(md1: str, md2: str, *, ignore_codeclasses: Iterable[str] = ()) -> bool:
"""Check if two Markdown produce the same HTML.
Renders HTML from both Markdown strings, strips whitespace and
Expand All @@ -14,4 +15,7 @@ def is_md_equal(md1: str, md2: str) -> bool:
html2 = MarkdownIt().render(md2)
html1 = re.sub(r"\s+", "", html1)
html2 = re.sub(r"\s+", "", html2)
for codeclass in ignore_codeclasses:
html1 = re.sub(rf'<codeclass="language-{codeclass}">.*</pre>', "", html1)
html2 = re.sub(rf'<codeclass="language-{codeclass}">.*</pre>', "", html2)
return html1 == html2
17 changes: 17 additions & 0 deletions mdformat/plugins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import sys
from typing import Callable, Dict, Mapping

if sys.version_info >= (3, 8):
from importlib import metadata as importlib_metadata
else:
import importlib_metadata


def _load_codeformatters() -> Dict[str, Callable[[str], str]]:
codeformatter_entrypoints = importlib_metadata.entry_points().get(
"mdformat.codeformatter", ()
)
return {ep.name: ep.load() for ep in codeformatter_entrypoints}


CODEFORMATTERS: Mapping[str, Callable[[str], str]] = _load_codeformatters()
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ mdformat = "mdformat.__main__:run"
[tool.poetry.dependencies]
python = "^3.6"
markdown-it-py = ">=0.4.7, <0.6.0"
importlib-metadata = { version = ">=0.12", python = "<3.8" }

[tool.poetry.dev-dependencies]
# Tests
Expand Down

0 comments on commit d3d7e96

Please sign in to comment.