diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9706ae0..d1aabd8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,25 +13,26 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v5 with: - python-version: 3.7 - - uses: pre-commit/action@v2.0.0 + python-version: '3.10' + - uses: pre-commit/action@v3.0.1 + continue-on-error: true tests: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: ['3.10', '3.11', '3.12', '3.13'] os: [ubuntu-latest, windows-latest] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} @@ -44,16 +45,18 @@ jobs: - name: Run pytest run: | - pytest --cov=mdformat_plugin --cov-report=xml --cov-report=term-missing + pytest --cov=mdformat_hallmark --cov-report=xml --cov-report=term-missing - name: Upload to Codecov - if: matrix.os == 'ubuntu-latest' && matrix.python-version == 3.7 - uses: codecov/codecov-action@v1 + if: matrix.os == 'ubuntu-latest' && matrix.python-version == 3.10 + uses: codecov/codecov-action@v5.4.2 with: - name: pytests-py3.7 - flags: pytests + name: codecov-umbrella + flags: unit + env_vars: RUNNER_OS,PYTHON_VERSION file: ./coverage.xml - fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false pre-commit-hook: runs-on: ubuntu-latest @@ -61,9 +64,9 @@ jobs: steps: - uses: actions/checkout@v2 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: - python-version: 3.7 + python-version: '3.10' - name: Installation (deps and package) run: | @@ -82,10 +85,10 @@ jobs: steps: - name: Checkout source uses: actions/checkout@v2 - - name: Set up Python 3.7 - uses: actions/setup-python@v1 + - name: Set up Python 3.10 + uses: actions/setup-python@v5 with: - python-version: 3.7 + python-version: '3.10' - name: install flit run: | pip install flit~=3.0 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e481775..7418a8c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.4.0 + rev: v5.0.0 hooks: - id: end-of-file-fixer - id: mixed-line-ending @@ -8,22 +8,22 @@ repos: - id: check-yaml - id: check-toml - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.8.0 + rev: v1.10.0 hooks: - id: python-check-blanket-noqa - repo: https://github.com/timothycrosley/isort - rev: 5.8.0 + rev: 5.13.2 hooks: - id: isort - repo: https://github.com/psf/black - rev: 20.8b1 + rev: 24.10.0 hooks: - id: black -- repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.0 +- repo: https://github.com/pycqa/flake8 + rev: 7.3.0 hooks: - id: flake8 additional_dependencies: - - flake8-bugbear==21.3.2 - - flake8-builtins==1.5.3 - - flake8-comprehensions==3.4.0 + - flake8-bugbear + - flake8-builtins + - flake8-comprehensions diff --git a/LICENSE b/LICENSE index 2a08e1c..292a6fc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Executable Books +Copyright (c) 2025 Executable Books Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 41054b9..f3ae713 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,10 @@ -# mdformat-plugin +# mdformat-hallmark -[![Build Status][ci-badge]][ci-link] -[![codecov.io][cov-badge]][cov-link] -[![PyPI version][pypi-badge]][pypi-link] +[![Build Status](https://github.com/calgray/mdformat-hallmark/actions/workflows/tests.yml/badge.svg?branch=master)]() +[![codecov.io](https://codecov.io/gh/calgray/mdformat-hallmark/branch/main/graph/badge.svg)](https://codecov.io/gh/xarray-contrib/astropy-xarray) +[![PyPI version](https://badge.fury.io/py/mdformat-hallmark.svg)]() -An [mdformat](https://github.com/executablebooks/mdformat) plugin for... - -## Required changes for a new plugin - -This demonstration is setup with a plugin named `plugin`. -There are a number of locations to change. -At a top level for a plugin `foo` at least the following changes are required - -- Global find and replace `mdformat_plugin` to `mdformat_foo` including folder names. -- Global find and replace `mdformat-plugin` to `mdformat-foo` including folder names. -- `tests/test_fixtures.py`: `output = mdformat.text(text, extensions={"plugin"})` becomes `output = mdformat.text(text, extensions={"foo"})` -- `pyproject.toml` in addition to the global find and replace: `plugin = "mdformat_plugin"` becomes `foo = "mdformat_foo"` - -Do not forget to update authorship / maintainers in `pyproject.toml` as well. +An [mdformat](https://github.com/executablebooks/mdformat) plugin for compatibility with [hallmark](https://github.com/vweevers/hallmark) formatted Markdown and [Common Changelog](https://common-changelog.org/). ## Development @@ -75,9 +62,9 @@ or trigger the GitHub Action job, by creating a release with a tag equal to the Note, this requires generating an API key on PyPi and adding it to the repository `Settings/Secrets`, under the name `PYPI_KEY`. -[ci-badge]: https://github.com/executablebooks/mdformat-plugin/workflows/CI/badge.svg?branch=master +[ci-badge]: https://github.com/executablebooks/mdformat-hallmark/workflows/CI/badge.svg?branch=master [ci-link]: https://github.com/executablebooks/mdformat/actions?query=workflow%3ACI+branch%3Amaster+event%3Apush -[cov-badge]: https://codecov.io/gh/executablebooks/mdformat-plugin/branch/master/graph/badge.svg -[cov-link]: https://codecov.io/gh/executablebooks/mdformat-plugin -[pypi-badge]: https://img.shields.io/pypi/v/mdformat-plugin.svg -[pypi-link]: https://pypi.org/project/mdformat-plugin +[cov-badge]: https://codecov.io/gh/executablebooks/mdformat-hallmark/branch/master/graph/badge.svg +[cov-link]: https://codecov.io/gh/executablebooks/mdformat-hallmark +[pypi-badge]: https://img.shields.io/pypi/v/mdformat-hallmark.svg +[pypi-link]: https://pypi.org/project/mdformat-hallmark diff --git a/mdformat_hallmark/__init__.py b/mdformat_hallmark/__init__.py new file mode 100644 index 0000000..809d488 --- /dev/null +++ b/mdformat_hallmark/__init__.py @@ -0,0 +1,10 @@ +""" +An mdformat plugin for compatibility with hallmark formatted Markdown +and Common Changelog. +""" + +__version__ = "0.0.1" + +from .hallmark_links_extension import HallmarkLinksExtension + +__all__ = ["HallmarkLinksExtension"] diff --git a/mdformat_hallmark/hallmark_links_extension.py b/mdformat_hallmark/hallmark_links_extension.py new file mode 100644 index 0000000..b7cdeaf --- /dev/null +++ b/mdformat_hallmark/hallmark_links_extension.py @@ -0,0 +1,109 @@ +from dataclasses import dataclass +import re + +from markdown_it import MarkdownIt +from markdown_it.token import Token +from mdformat.plugins import ParserExtensionInterface +from mdformat.renderer import RenderContext, RenderTreeNode +from packaging.version import InvalidVersion, Version + +_REFERENCES_RE = re.compile( + r'^\[([^\]]+)\]:\s*(\S+)(?:\s+"([^"]+)")?$', + re.MULTILINE, +) # regex for `[label]: href "title"` + + +@dataclass +class SemverReference: + semver: Version + label: str + href: str + title: str | None + + def __str__(self): + line = f"[{self.label}]: {self.href}" + if self.title: + line += f' "{self.title}"' + return line + + +def _extract_semver_references(src: str) -> tuple[list[SemverReference], str]: + refs: list[SemverReference] = [] + remove_spans: list[tuple[int, int]] = [] + + for m in _REFERENCES_RE.finditer(src): + label, href, title = m.groups() + try: + semver = Version(label) + except InvalidVersion: + continue # skip non-semver labels + refs.append(SemverReference(semver, label, href, title)) + remove_spans.append(m.span()) + + # remove references from the source + out_src = src + for start, end in reversed(remove_spans): + out_src = out_src[:start] + out_src[end:] + + # Normalize whitespace + out_src = re.sub(r"\n{3,}", "\n\n", out_src).rstrip() + + # sort semver references + refs.sort(key=lambda ref: ref.semver, reverse=True) + + return refs, out_src + + +class HallmarkLinksExtension(ParserExtensionInterface): + """ + mdformat plugin extension to format references used by versions and + renders sorted by semantic-version order. + """ + + @staticmethod + def update_mdit(mdit: MarkdownIt) -> None: + """Patch the default parser to render references in semver order.""" + + original_parse = mdit.parse + + def new_parse(src: str, env=None): + if env is None: + env = {} + if not src.lstrip().startswith("# Changelog"): + return original_parse(src, env) + + # extract semver references and remove from src + matches, out_src = _extract_semver_references(src) + refs = { + match.label: {"href": match.href, "title": match.title} + for match in matches + } + + # call original parse on the cleaned text + tokens = original_parse(out_src, env) + + # append a hallmark_refs token at the end + if refs: + token = Token("hallmark_refs", "", 0) + token.meta = {"refs": refs} + tokens.append(token) + + return tokens + + # patch parser + mdit.parse = new_parse + + @staticmethod + def _render_hallmark_refs(node: RenderTreeNode, ctx: RenderContext) -> str: + """Render collected reference defs in hallmark ordered format.""" + refs: dict[str, dict[str, str]] = node.meta["refs"] + out = [] + for label, ref in refs.items(): + line = f"[{label}]: {ref['href']}" + if ref.get("title"): + line += f' "{ref["title"]}"' + out.append(line) + return "\n\n".join(out) + + RENDERERS = {"hallmark_refs": _render_hallmark_refs} + CHANGES_AST = True diff --git a/mdformat_plugin/__init__.py b/mdformat_plugin/__init__.py deleted file mode 100644 index 1c98271..0000000 --- a/mdformat_plugin/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""An mdformat plugin for...""" - -__version__ = "0.0.1" - -from .plugin import RENDERERS, update_mdit # noqa: F401 diff --git a/mdformat_plugin/plugin.py b/mdformat_plugin/plugin.py deleted file mode 100644 index 8fff61c..0000000 --- a/mdformat_plugin/plugin.py +++ /dev/null @@ -1,24 +0,0 @@ -from typing import Mapping - -from markdown_it import MarkdownIt -from mdformat.renderer import RenderContext, RenderTreeNode -from mdformat.renderer.typing import Render - - -def update_mdit(mdit: MarkdownIt) -> None: - """Update the parser, e.g. by adding a plugin: `mdit.use(myplugin)`""" - pass - - -def _render_table(node: RenderTreeNode, context: RenderContext) -> str: - """Render a `RenderTreeNode` of type "table". - - Change "table" to the name of the syntax you want to render. - """ - return "" - - -# A mapping from syntax tree node type to a function that renders it. -# This can be used to overwrite renderer functions of existing syntax -# or add support for new syntax. -RENDERERS: Mapping[str, Render] = {"table": _render_table} diff --git a/pyproject.toml b/pyproject.toml index d728245..a66eeb6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,9 +3,9 @@ requires = ["flit_core >=3.2.0,<4"] build-backend = "flit_core.buildapi" [project] -name = "mdformat_plugin" +name = "mdformat-hallmark" authors = [ - { name = "Chris Sewell", email = "executablebooks@gmail.com" }, + { name = "Callan Gray", email = "cal.j.gray@gmail.com" }, ] readme = "README.md" classifiers = [ @@ -18,6 +18,7 @@ keywords = ["mdformat", "markdown", "markdown-it"] requires-python = ">=3.7" dependencies = [ "mdformat >=0.7.0,<0.8.0", + "packaging >= 23.0", ] dynamic = ["version", "description"] @@ -30,10 +31,10 @@ test = [ dev = ["pre-commit"] [project.urls] -Homepage = "https://github.com/executablebooks/mdformat-plugin" +Homepage = "https://github.com/calgray/mdformat-hallmark" [project.entry-points."mdformat.parser_extension"] -plugin = "mdformat_plugin" +"hallmark" = "mdformat_hallmark:HallmarkLinksExtension" [tool.flit.sdist] include = [] @@ -46,7 +47,10 @@ force_sort_within_sections = true no_lines_before = ["LOCALFOLDER"] # Configure isort to work without access to site-packages -known_first_party = ["mdformat_plugin", "tests"] +known_first_party = ["mdformat_hallmark", "tests"] # Settings for Black compatibility profile = "black" + +[tool.pytest.ini_options] +log_cli = true diff --git a/tests/fixtures.md b/tests/fixtures.md index 31d6be2..a1cd460 100644 --- a/tests/fixtures.md +++ b/tests/fixtures.md @@ -1,24 +1,161 @@ -a test +not changelog test . This is the input Markdown test, then below add the expected output. + +- a [reference][] +- b [0.0.1] +* c [1.0.0] + +[unused reference]: https://example.com +[0.0.1]: https://example.com +[reference]: https://example.com +[1.0.0]: https://example.com . This is the input Markdown test, then below add the expected output. + +- a [reference] +- b [0.0.1] + +* c [1.0.0] + +[0.0.1]: https://example.com +[1.0.0]: https://example.com +[reference]: https://example.com . -another test +correct changelog test . -Some *markdown* +# Changelog + +## [10.1.0] - 2025-01-01 + +## [2.10.0] - 2025-01-01 + +## [2.0.0] - 2025-01-01 + +## [1.0.10] - 2025-01-01 + +## [1.0.0] - 2025-01-01 + +## [0.0.10] - 2025-01-01 + +## [0.0.2] - 2025-01-01 + +## [0.0.1] - 2025-01-01 + +[10.1.0]: https://example.com + +[2.10.0]: https://example.com + +[2.0.0]: https://example.com -- a -- b -* c +[1.0.10]: https://example.com + +[1.0.0]: https://example.com + +[0.0.10]: https://example.com + +[0.0.2]: https://example.com + +[0.0.1]: https://example.com . -Some *markdown* +# Changelog + +## [10.1.0] - 2025-01-01 + +## [2.10.0] - 2025-01-01 + +## [2.0.0] - 2025-01-01 + +## [1.0.10] - 2025-01-01 + +## [1.0.0] - 2025-01-01 + +## [0.0.10] - 2025-01-01 + +## [0.0.2] - 2025-01-01 + +## [0.0.1] - 2025-01-01 + +[10.1.0]: https://example.com + +[2.10.0]: https://example.com + +[2.0.0]: https://example.com + +[1.0.10]: https://example.com + +[1.0.0]: https://example.com + +[0.0.10]: https://example.com + +[0.0.2]: https://example.com + +[0.0.1]: https://example.com +. + +unsorted changelog test +. +# Changelog + +## [10.1.0] - 2025-01-01 + +## [2.0.0] - 2025-01-01 + +## [2.10.0] - 2025-01-01 + +## [1.0.10] - 2025-01-01 + +## [1.0.0] - 2025-01-01 + +## [0.0.10] - 2025-01-01 + +## [0.0.2] - 2025-01-01 + +## [0.0.1] - 2025-01-01 + +[0.0.1]: https://example.com +[0.0.2]: https://example.com +[0.0.10]: https://example.com +[1.0.0]: https://example.com +[1.0.10]: https://example.com +[2.0.0]: https://example.com +[2.10.0]: https://example.com +[10.1.0]: https://example.com +. +# Changelog + +## [10.1.0] - 2025-01-01 + +## [2.0.0] - 2025-01-01 + +## [2.10.0] - 2025-01-01 + +## [1.0.10] - 2025-01-01 + +## [1.0.0] - 2025-01-01 + +## [0.0.10] - 2025-01-01 + +## [0.0.2] - 2025-01-01 + +## [0.0.1] - 2025-01-01 + +[10.1.0]: https://example.com + +[2.10.0]: https://example.com + +[2.0.0]: https://example.com + +[1.0.10]: https://example.com + +[1.0.0]: https://example.com + +[0.0.10]: https://example.com -- a -- b +[0.0.2]: https://example.com -* c +[0.0.1]: https://example.com . diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 68fc3bf..c805a5a 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -12,6 +12,6 @@ "line,title,text,expected", fixtures, ids=[f[1] for f in fixtures] ) def test_fixtures(line, title, text, expected): - output = mdformat.text(text, extensions={"plugin"}) - print(output) + output = mdformat.text(text, extensions={"hallmark"}) + # output = mdformat.text(text, codeformatters={"python"}) assert output.rstrip() == expected.rstrip(), output diff --git a/tox.ini b/tox.ini index c284ba3..4ad3108 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ commands = pytest {posargs} [testenv:py{36,37,38,39}-cov] extras = test -commands = pytest --cov={envsitepackagesdir}/mdformat_plugin {posargs} +commands = pytest --cov={envsitepackagesdir}/mdformat_hallmark {posargs} [testenv:py{36,37,38,39}-pre-commit] extras = dev