From d627eac4c246fbb2e7185329d8f50bc9a4e7b6b7 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 4 Jul 2022 00:36:26 +0200 Subject: [PATCH 1/8] =?UTF-8?q?=F0=9F=94=A7=20MAINTAIN:=20Update=20pre-com?= =?UTF-8?q?mit=20(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 12 ++++++------ setup.cfg | 1 + 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cbf596d..aebb4b7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ exclude: > repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.3.0 + rev: v4.3.0 hooks: - id: check-json - id: check-yaml @@ -20,7 +20,7 @@ repos: - id: trailing-whitespace - repo: https://github.com/mgedmin/check-manifest - rev: "0.46" + rev: "0.48" hooks: - id: check-manifest args: [--no-build-isolation] @@ -34,23 +34,23 @@ repos: # - id: setup-cfg-fmt - repo: https://github.com/timothycrosley/isort - rev: 5.8.0 + rev: 5.10.1 hooks: - id: isort - repo: https://github.com/psf/black - rev: 20.8b1 + rev: 22.6.0 hooks: - id: black - repo: https://gitlab.com/pycqa/flake8 - rev: 3.9.1 + rev: 3.9.2 hooks: - id: flake8 additional_dependencies: [flake8-bugbear==21.3.1] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.812 + rev: v0.961 hooks: - id: mypy additional_dependencies: [markdown-it-py~=1.0] diff --git a/setup.cfg b/setup.cfg index 7453899..30a06b8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -48,6 +48,7 @@ testing = pytest-cov pytest-regressions rtd = + attrs myst-parser~=0.16.1 sphinx-book-theme~=0.1.0 From 855067ea167d90d5f66075ad124c206d9a1bf959 Mon Sep 17 00:00:00 2001 From: Taneli Hukkinen <3275109+hukkin@users.noreply.github.com> Date: Thu, 25 Aug 2022 22:20:14 +0300 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=90=9B=20FIX:=20Parsing=20when=20newl?= =?UTF-8?q?ine=20is=20between=20footnote=20ID=20and=20first=20paragraph=20?= =?UTF-8?q?(#49)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mdit_py_plugins/footnote/index.py | 2 +- tests/fixtures/footnote.md | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/mdit_py_plugins/footnote/index.py b/mdit_py_plugins/footnote/index.py index 0ddd314..119fb71 100644 --- a/mdit_py_plugins/footnote/index.py +++ b/mdit_py_plugins/footnote/index.py @@ -80,7 +80,7 @@ def footnote_def(state: StateBlock, startLine: int, endLine: int, silent: bool): if pos == start + 2: # no empty footnote labels return False pos += 1 - if pos + 1 >= maximum or state.srcCharCode[pos] != 0x3A: # /* : */ + if pos >= maximum or state.srcCharCode[pos] != 0x3A: # /* : */ return False if silent: return True diff --git a/tests/fixtures/footnote.md b/tests/fixtures/footnote.md index b59dd4a..b0643e1 100644 --- a/tests/fixtures/footnote.md +++ b/tests/fixtures/footnote.md @@ -326,3 +326,21 @@ Some text . + + +Newline after footnote identifier +. +[^a] + +[^a]: +b +. +

[1]

+

b

+
+
+
    +
  1. <-
  2. +
+
+. From e96d4e47ef0e4a866a2062f7e56bcf49d2ffa736 Mon Sep 17 00:00:00 2001 From: DistractedMOSFET <59909722+distractedmosfet@users.noreply.github.com> Date: Fri, 16 Sep 2022 07:54:28 +1200 Subject: [PATCH 3/8] =?UTF-8?q?=F0=9F=90=9B:=20Anchor=20ids=20in=20separat?= =?UTF-8?q?e=20renders=20should=20not=20affect=20each=20other.=20(#43)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Chris Sewell --- mdit_py_plugins/anchors/index.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mdit_py_plugins/anchors/index.py b/mdit_py_plugins/anchors/index.py index dcbf789..abbd48a 100644 --- a/mdit_py_plugins/anchors/index.py +++ b/mdit_py_plugins/anchors/index.py @@ -65,9 +65,8 @@ def _make_anchors_func( permalinkBefore: bool, permalinkSpace: bool, ): - slugs: Set[str] = set() - def _anchor_func(state: StateCore): + slugs: Set[str] = set() for (idx, token) in enumerate(state.tokens): if token.type != "heading_open": continue From 6fbc43fddd3143c8be13244f5e6f8d083546379b Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Tue, 27 Sep 2022 09:17:47 +0200 Subject: [PATCH 4/8] =?UTF-8?q?=F0=9F=94=A7=20PEP=20621=20package=20build,?= =?UTF-8?q?=20drop=20Python=203.6=20(#51)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/tests.yml | 21 ++++++------ .pre-commit-config.yaml | 20 ++---------- MANIFEST.in | 17 ---------- pyproject.toml | 64 +++++++++++++++++++++++++++++++++++-- setup.cfg | 64 ------------------------------------- setup.py | 6 ---- tests/test_amsmath.py | 2 +- tests/test_anchors.py | 2 +- tests/test_colon_fence.py | 2 +- tests/test_container.py | 2 +- tests/test_deflist.py | 2 +- tests/test_dollarmath.py | 2 +- tests/test_field_list.py | 2 +- tests/test_footnote.py | 2 +- tests/test_front_matter.py | 2 +- tests/test_myst_block.py | 2 +- tests/test_myst_role.py | 2 +- tests/test_substitution.py | 2 +- tests/test_tasklists.py | 2 +- tests/test_texmath.py | 2 +- tests/test_wordcount.py | 2 +- tox.ini | 6 +++- 22 files changed, 96 insertions(+), 132 deletions(-) delete mode 100644 MANIFEST.in delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 93948c4..0e56079 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,8 +28,9 @@ jobs: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - python-version: [pypy3, 3.6, 3.7, 3.8, 3.9] + python-version: ['pypy-3.7', '3.7', '3.8', '3.9', '3.10'] steps: - uses: actions/checkout@v2 @@ -64,13 +65,13 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: 3.8 - - name: Build package + python-version: "3.8" + - name: install flit run: | - pip install build - python -m build - - name: Publish - uses: pypa/gh-action-pypi-publish@v1.1.0 - with: - user: __token__ - password: ${{ secrets.PYPI_KEY }} + pip install flit~=3.4 + - name: Build and publish + run: | + flit publish + env: + FLIT_USERNAME: __token__ + FLIT_PASSWORD: ${{ secrets.PYPI_KEY }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aebb4b7..89d2e52 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,27 +19,13 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - - repo: https://github.com/mgedmin/check-manifest - rev: "0.48" - hooks: - - id: check-manifest - args: [--no-build-isolation] - additional_dependencies: [setuptools>=46.4.0] - - # this is not used for now, - # since it converts mdit-py-plugins to mdit_py_plugins and removes comments - # - repo: https://github.com/asottile/setup-cfg-fmt - # rev: v1.17.0 - # hooks: - # - id: setup-cfg-fmt - - repo: https://github.com/timothycrosley/isort rev: 5.10.1 hooks: - id: isort - repo: https://github.com/psf/black - rev: 22.6.0 + rev: 22.8.0 hooks: - id: black @@ -50,7 +36,7 @@ repos: additional_dependencies: [flake8-bugbear==21.3.1] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.961 + rev: v0.971 hooks: - id: mypy - additional_dependencies: [markdown-it-py~=1.0] + additional_dependencies: [markdown-it-py~=2.0] diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index b04c40b..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,17 +0,0 @@ -exclude docs -recursive-exclude docs * -exclude tests -recursive-exclude tests * - -exclude .pre-commit-config.yaml -exclude .readthedocs.yml -exclude tox.ini -exclude .flake8 -exclude codecov.yml -exclude .mypy.ini - -include LICENSE -include CHANGELOG.md -include mdit_py_plugins/py.typed - -recursive-include mdit_py_plugins port.yaml LICENSE README.md diff --git a/pyproject.toml b/pyproject.toml index 013bf4a..5613bca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,67 @@ [build-system] -requires = ["setuptools>=46.4.0", "wheel"] -build-backend = "setuptools.build_meta" +requires = ["flit_core >=3.4,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "mdit-py-plugins" +dynamic = ["version"] +description = "Collection of plugins for markdown-it-py" +readme = "README.md" +authors = [{name = "Chris Sewell", email = "chrisj_sewell@hotmail.com"}] +license = {file = "LICENSE"} +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Text Processing :: Markup", +] +keywords = ["markdown", "markdown-it", "lexer", "parser", "development"] +requires-python = ">=3.7" +dependencies = ["markdown-it-py>=1.0.0,<3.0.0"] + +[project.urls] +Homepage = "https://github.com/executablebooks/mdit-py-plugins" +Documentation = "https://markdown-it-py.readthedocs.io" + +[project.optional-dependencies] +code_style = ["pre-commit"] +testing = [ + "coverage", + "pytest", + "pytest-cov", + "pytest-regressions", +] +rtd = [ + "attrs", + "myst-parser~=0.16.1", + "sphinx-book-theme~=0.1.0", +] + +[tool.flit.module] +name = "mdit_py_plugins" + +[tool.flit.sdist] +exclude = [ + "docs/", + "tests/", +] [tool.isort] profile = "black" +force_sort_within_sections = true known_first_party = ["mdit_py_plugins", "tests"] + +[tool.mypy] +show_error_codes = true +warn_unused_ignores = true +warn_redundant_casts = true +no_implicit_optional = true +strict_equality = true diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 30a06b8..0000000 --- a/setup.cfg +++ /dev/null @@ -1,64 +0,0 @@ -[metadata] -name = mdit-py-plugins -version = attr: mdit_py_plugins.__version__ -description = Collection of plugins for markdown-it-py -long_description = file: README.md -long_description_content_type = text/markdown -url = https://github.com/executablebooks/mdit-py-plugins -author = Chris Sewell -author_email = chrisj_sewell@hotmail.com -license = MIT -license_file = LICENSE -classifiers = - Development Status :: 4 - Beta - Intended Audience :: Developers - License :: OSI Approved :: MIT License - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: Implementation :: CPython - Programming Language :: Python :: Implementation :: PyPy - Topic :: Software Development :: Libraries :: Python Modules - Topic :: Text Processing :: Markup -keywords = markdown, lexer, parser, development -project_urls = - Documentation=https://markdown-it-py.readthedocs.io - -[options] -packages = find: -install_requires = - markdown-it-py>=1.0.0,<3.0.0 -python_requires = >=3.6 -include_package_data = True -zip_safe = True - -[options.packages.find] -exclude = - test* - benchmarking - -[options.extras_require] -code_style = - pre-commit==2.6 -testing = - coverage - pytest>=3.6,<4 - pytest-cov - pytest-regressions -rtd = - attrs - myst-parser~=0.16.1 - sphinx-book-theme~=0.1.0 - -[mypy] -show_error_codes = True -warn_unused_ignores = True -warn_redundant_casts = True -no_implicit_optional = True -strict_equality = True - -[flake8] -max-line-length = 100 -extend-ignore = E203,E731 diff --git a/setup.py b/setup.py deleted file mode 100644 index 3614126..0000000 --- a/setup.py +++ /dev/null @@ -1,6 +0,0 @@ -# This file is needed for editable installs (`pip install -e .`). -# Can be removed once the following is resolved -# https://github.com/pypa/packaging-problems/issues/256 -from setuptools import setup - -setup() diff --git a/tests/test_amsmath.py b/tests/test_amsmath.py index 7cb2e00..55b4987 100644 --- a/tests/test_amsmath.py +++ b/tests/test_amsmath.py @@ -1,9 +1,9 @@ from pathlib import Path from textwrap import dedent -import pytest from markdown_it import MarkdownIt from markdown_it.utils import read_fixture_file +import pytest from mdit_py_plugins.amsmath import amsmath_plugin diff --git a/tests/test_anchors.py b/tests/test_anchors.py index 3d50a68..0515b92 100644 --- a/tests/test_anchors.py +++ b/tests/test_anchors.py @@ -1,8 +1,8 @@ from pathlib import Path -import pytest from markdown_it import MarkdownIt from markdown_it.utils import read_fixture_file +import pytest from mdit_py_plugins.anchors import anchors_plugin diff --git a/tests/test_colon_fence.py b/tests/test_colon_fence.py index 6cb4d99..d520906 100644 --- a/tests/test_colon_fence.py +++ b/tests/test_colon_fence.py @@ -1,9 +1,9 @@ from pathlib import Path from textwrap import dedent -import pytest from markdown_it import MarkdownIt from markdown_it.utils import read_fixture_file +import pytest from mdit_py_plugins.colon_fence import colon_fence_plugin diff --git a/tests/test_container.py b/tests/test_container.py index 21e3bd1..daf2ce6 100644 --- a/tests/test_container.py +++ b/tests/test_container.py @@ -1,9 +1,9 @@ from pathlib import Path from textwrap import dedent -import pytest from markdown_it import MarkdownIt from markdown_it.utils import read_fixture_file +import pytest from mdit_py_plugins.container import container_plugin diff --git a/tests/test_deflist.py b/tests/test_deflist.py index 0aae38f..d924e93 100644 --- a/tests/test_deflist.py +++ b/tests/test_deflist.py @@ -1,9 +1,9 @@ from pathlib import Path from textwrap import dedent -import pytest from markdown_it import MarkdownIt from markdown_it.utils import read_fixture_file +import pytest from mdit_py_plugins.deflist import deflist_plugin diff --git a/tests/test_dollarmath.py b/tests/test_dollarmath.py index 1cb1b12..9f88726 100644 --- a/tests/test_dollarmath.py +++ b/tests/test_dollarmath.py @@ -1,11 +1,11 @@ from pathlib import Path from textwrap import dedent -import pytest from markdown_it import MarkdownIt from markdown_it.rules_block import StateBlock from markdown_it.rules_inline import StateInline from markdown_it.utils import read_fixture_file +import pytest from mdit_py_plugins.dollarmath import dollarmath_plugin from mdit_py_plugins.dollarmath import index as main diff --git a/tests/test_field_list.py b/tests/test_field_list.py index 568a52a..cf00ae9 100644 --- a/tests/test_field_list.py +++ b/tests/test_field_list.py @@ -1,9 +1,9 @@ from pathlib import Path from textwrap import dedent -import pytest from markdown_it import MarkdownIt from markdown_it.utils import read_fixture_file +import pytest from mdit_py_plugins.field_list import fieldlist_plugin diff --git a/tests/test_footnote.py b/tests/test_footnote.py index ca7a10c..c662cb6 100644 --- a/tests/test_footnote.py +++ b/tests/test_footnote.py @@ -1,12 +1,12 @@ from pathlib import Path from textwrap import dedent -import pytest from markdown_it import MarkdownIt from markdown_it.rules_block import StateBlock from markdown_it.rules_inline import StateInline from markdown_it.token import Token from markdown_it.utils import read_fixture_file +import pytest from mdit_py_plugins.footnote import footnote_plugin, index diff --git a/tests/test_front_matter.py b/tests/test_front_matter.py index 3cf1043..627cb89 100644 --- a/tests/test_front_matter.py +++ b/tests/test_front_matter.py @@ -1,9 +1,9 @@ from pathlib import Path -import pytest from markdown_it import MarkdownIt from markdown_it.token import Token from markdown_it.utils import read_fixture_file +import pytest from mdit_py_plugins.front_matter import front_matter_plugin diff --git a/tests/test_myst_block.py b/tests/test_myst_block.py index 5fb46c7..632ed4b 100644 --- a/tests/test_myst_block.py +++ b/tests/test_myst_block.py @@ -1,9 +1,9 @@ from pathlib import Path -import pytest from markdown_it import MarkdownIt from markdown_it.token import Token from markdown_it.utils import read_fixture_file +import pytest from mdit_py_plugins.myst_blocks import myst_block_plugin diff --git a/tests/test_myst_role.py b/tests/test_myst_role.py index 46a2316..f70e3f8 100644 --- a/tests/test_myst_role.py +++ b/tests/test_myst_role.py @@ -1,9 +1,9 @@ from pathlib import Path -import pytest from markdown_it import MarkdownIt from markdown_it.token import Token from markdown_it.utils import read_fixture_file +import pytest from mdit_py_plugins.myst_role import myst_role_plugin diff --git a/tests/test_substitution.py b/tests/test_substitution.py index aae02df..706c1e2 100644 --- a/tests/test_substitution.py +++ b/tests/test_substitution.py @@ -1,9 +1,9 @@ from pathlib import Path from textwrap import dedent -import pytest from markdown_it import MarkdownIt from markdown_it.utils import read_fixture_file +import pytest # from markdown_it.token import Token from mdit_py_plugins.substitution import substitution_plugin diff --git a/tests/test_tasklists.py b/tests/test_tasklists.py index 82e878f..427c336 100644 --- a/tests/test_tasklists.py +++ b/tests/test_tasklists.py @@ -1,9 +1,9 @@ from pathlib import Path from textwrap import dedent -import pytest from markdown_it import MarkdownIt from markdown_it.utils import read_fixture_file +import pytest from mdit_py_plugins.tasklists import tasklists_plugin diff --git a/tests/test_texmath.py b/tests/test_texmath.py index 70fdd53..80bf67b 100644 --- a/tests/test_texmath.py +++ b/tests/test_texmath.py @@ -1,11 +1,11 @@ from pathlib import Path from textwrap import dedent -import pytest from markdown_it import MarkdownIt from markdown_it.rules_block import StateBlock from markdown_it.rules_inline import StateInline from markdown_it.utils import read_fixture_file +import pytest from mdit_py_plugins.texmath import index as main from mdit_py_plugins.texmath import texmath_plugin diff --git a/tests/test_wordcount.py b/tests/test_wordcount.py index edeb0fc..d7629d9 100644 --- a/tests/test_wordcount.py +++ b/tests/test_wordcount.py @@ -1,9 +1,9 @@ import json from pathlib import Path -import pytest from markdown_it import MarkdownIt from markdown_it.utils import read_fixture_file +import pytest from mdit_py_plugins.wordcount import wordcount_plugin diff --git a/tox.ini b/tox.ini index 21e8e49..163d38b 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,7 @@ envlist = py37 [testenv] usedevelop = true -[testenv:py{36,37,38,39}] +[testenv:py{37,38,39,310}] extras = testing commands = pytest {posargs} @@ -19,3 +19,7 @@ whitelist_externals = rm commands = clean: rm -rf docs/_build sphinx-build -nW --keep-going -b {posargs:html} docs/ docs/_build/{posargs:html} + +[flake8] +max-line-length = 100 +extend-ignore = E203,E731 From f2f854aa3506565761aa8d7b33c371efcac16bb3 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Tue, 27 Sep 2022 09:21:43 +0200 Subject: [PATCH 5/8] =?UTF-8?q?=E2=9C=A8=20NEW:=20Add=20`attrs=5Fplugin`?= =?UTF-8?q?=20(#50)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/index.md | 6 + mdit_py_plugins/attrs/__init__.py | 1 + mdit_py_plugins/attrs/index.py | 50 ++++++ mdit_py_plugins/attrs/parse.py | 265 ++++++++++++++++++++++++++++++ tests/fixtures/attrs.md | 46 ++++++ tests/test_attrs.py | 18 ++ 6 files changed, 386 insertions(+) create mode 100644 mdit_py_plugins/attrs/__init__.py create mode 100644 mdit_py_plugins/attrs/index.py create mode 100644 mdit_py_plugins/attrs/parse.py create mode 100644 tests/fixtures/attrs.md create mode 100644 tests/test_attrs.py diff --git a/docs/index.md b/docs/index.md index c4f2217..8b38d31 100644 --- a/docs/index.md +++ b/docs/index.md @@ -85,6 +85,12 @@ html_string = md.render("some *Markdown*") .. autofunction:: mdit_py_plugins.container.container_plugin ``` +## Inline Attributes + +```{eval-rst} +.. autofunction:: mdit_py_plugins.attrs.attrs_plugin +``` + ## Math ```{eval-rst} diff --git a/mdit_py_plugins/attrs/__init__.py b/mdit_py_plugins/attrs/__init__.py new file mode 100644 index 0000000..9359cf8 --- /dev/null +++ b/mdit_py_plugins/attrs/__init__.py @@ -0,0 +1 @@ +from .index import attrs_plugin # noqa: F401 diff --git a/mdit_py_plugins/attrs/index.py b/mdit_py_plugins/attrs/index.py new file mode 100644 index 0000000..bc3feda --- /dev/null +++ b/mdit_py_plugins/attrs/index.py @@ -0,0 +1,50 @@ +from markdown_it import MarkdownIt +from markdown_it.rules_inline import StateInline + +from .parse import ParseError, parse + + +def attrs_plugin(md: MarkdownIt, *, after=("image", "code_inline")): + """Parse inline attributes that immediately follow certain inline elements:: + + ![alt](https://image.com){#id .a b=c} + + Inside the curly braces, the following syntax is possible: + + - `.foo` specifies foo as a class. + Multiple classes may be given in this way; they will be combined. + - `#foo` specifies foo as an identifier. + An element may have only one identifier; + if multiple identifiers are given, the last one is used. + - `key="value"` or `key=value` specifies a key-value attribute. + Quotes are not needed when the value consists entirely of + ASCII alphanumeric characters or `_` or `:` or `-`. + Backslash escapes may be used inside quoted values. + - `%` begins a comment, which ends with the next `%` or the end of the attribute (`}`). + + **Note:** This plugin is currently limited to "self-closing" elements, + such as images and code spans. It does not work with links or emphasis. + + :param md: The MarkdownIt instance to modify. + :param after: The names of inline elements after which attributes may be specified. + """ + + def attr_rule(state: StateInline, silent: bool): + if state.pending or not state.tokens: + return False + token = state.tokens[-1] + if token.type not in after: + return False + try: + new_pos, attrs = parse(state.src[state.pos :]) + except ParseError: + return False + state.pos += new_pos + 1 + if not silent: + if "class" in attrs and "class" in token.attrs: + attrs["class"] = f"{token.attrs['class']} {attrs['class']}" + token.attrs.update(attrs) + + return True + + md.inline.ruler.push("attr", attr_rule) diff --git a/mdit_py_plugins/attrs/parse.py b/mdit_py_plugins/attrs/parse.py new file mode 100644 index 0000000..4a30353 --- /dev/null +++ b/mdit_py_plugins/attrs/parse.py @@ -0,0 +1,265 @@ +"""Parser for attributes:: + + attributes { id = "foo", class = "bar baz", + key1 = "val1", key2 = "val2" } + +Adapted from: +https://github.com/jgm/djot/blob/fae7364b86bfce69bc6d5b5eede1f5196d845fd6/djot/attributes.lua#L1 + +syntax: + +attributes <- '{' whitespace* attribute (whitespace attribute)* whitespace* '}' +attribute <- identifier | class | keyval +identifier <- '#' name +class <- '.' name +name <- (nonspace, nonpunctuation other than ':', '_', '-')+ +keyval <- key '=' val +key <- (ASCII_ALPHANUM | ':' | '_' | '-')+ +val <- bareval | quotedval +bareval <- (ASCII_ALPHANUM | ':' | '_' | '-')+ +quotedval <- '"' ([^"] | '\"') '"' +""" +from __future__ import annotations + +from enum import Enum +import re +from typing import Callable + + +class State(Enum): + START = 0 + SCANNING = 1 + SCANNING_ID = 2 + SCANNING_CLASS = 3 + SCANNING_KEY = 4 + SCANNING_VALUE = 5 + SCANNING_BARE_VALUE = 6 + SCANNING_QUOTED_VALUE = 7 + SCANNING_COMMENT = 8 + SCANNING_ESCAPED = 9 + DONE = 10 + + +REGEX_SPACE = re.compile(r"\s") +REGEX_SPACE_PUNCTUATION = re.compile(r"[\s!\"#$%&'()*+,./;<=>?@[\]^`{|}~]") +REGEX_KEY_CHARACTERS = re.compile(r"[a-zA-Z\d_:-]") + + +class TokenState: + def __init__(self): + self._tokens = [] + self.start: int = 0 + + def set_start(self, start: int) -> None: + self.start = start + + def append(self, start: int, end: int, ttype: str): + self._tokens.append((start, end, ttype)) + + def compile(self, string: str) -> dict[str, str]: + """compile the tokens into a dictionary""" + attributes = {} + classes = [] + idx = 0 + while idx < len(self._tokens): + start, end, ttype = self._tokens[idx] + if ttype == "id": + attributes["id"] = string[start:end] + elif ttype == "class": + classes.append(string[start:end]) + elif ttype == "key": + key = string[start:end] + if idx + 1 < len(self._tokens): + start, end, ttype = self._tokens[idx + 1] + if ttype == "value": + if key == "class": + classes.append(string[start:end]) + else: + attributes[key] = string[start:end] + idx += 1 + idx += 1 + if classes: + attributes["class"] = " ".join(classes) + return attributes + + def __str__(self) -> str: + return str(self._tokens) + + def __repr__(self) -> str: + return repr(self._tokens) + + +class ParseError(Exception): + def __init__(self, msg: str, pos: int) -> None: + self.pos = pos + super().__init__(msg + f" at position {pos}") + + +def parse(string: str) -> tuple[int, dict[str, str]]: + """Parse attributes from start of string. + + :returns: (length of parsed string, dict of attributes) + """ + pos = 0 + state: State = State.START + tokens = TokenState() + while pos < len(string): + state = HANDLERS[state](string[pos], pos, tokens) + if state == State.DONE: + return pos, tokens.compile(string) + pos = pos + 1 + + return pos, tokens.compile(string) + + +def handle_start(char: str, pos: int, tokens: TokenState) -> State: + + if char == "{": + return State.SCANNING + raise ParseError("Attributes must start with '{'", pos) + + +def handle_scanning(char: str, pos: int, tokens: TokenState) -> State: + + if char == " " or char == "\t" or char == "\n" or char == "\r": + return State.SCANNING + if char == "}": + return State.DONE + if char == "#": + tokens.set_start(pos) + return State.SCANNING_ID + if char == "%": + tokens.set_start(pos) + return State.SCANNING_COMMENT + if char == ".": + tokens.set_start(pos) + return State.SCANNING_CLASS + if REGEX_KEY_CHARACTERS.fullmatch(char): + tokens.set_start(pos) + return State.SCANNING_KEY + + raise ParseError(f"Unexpected character whilst scanning: {char}", pos) + + +def handle_scanning_comment(char: str, pos: int, tokens: TokenState) -> State: + + if char == "%": + return State.SCANNING + + return State.SCANNING_COMMENT + + +def handle_scanning_id(char: str, pos: int, tokens: TokenState) -> State: + + if not REGEX_SPACE_PUNCTUATION.fullmatch(char): + return State.SCANNING_ID + + if char == "}": + if (pos - 1) > tokens.start: + tokens.append(tokens.start + 1, pos, "id") + return State.DONE + + if REGEX_SPACE.fullmatch(char): + if (pos - 1) > tokens.start: + tokens.append(tokens.start + 1, pos, "id") + return State.SCANNING + + raise ParseError(f"Unexpected character whilst scanning id: {char}", pos) + + +def handle_scanning_class(char: str, pos: int, tokens: TokenState) -> State: + + if not REGEX_SPACE_PUNCTUATION.fullmatch(char): + return State.SCANNING_CLASS + + if char == "}": + if (pos - 1) > tokens.start: + tokens.append(tokens.start + 1, pos, "class") + return State.DONE + + if REGEX_SPACE.fullmatch(char): + if (pos - 1) > tokens.start: + tokens.append(tokens.start + 1, pos, "class") + return State.SCANNING + + raise ParseError(f"Unexpected character whilst scanning class: {char}", pos) + + +def handle_scanning_key(char: str, pos: int, tokens: TokenState) -> State: + + if char == "=": + tokens.append(tokens.start, pos, "key") + return State.SCANNING_VALUE + + if REGEX_KEY_CHARACTERS.fullmatch(char): + return State.SCANNING_KEY + + raise ParseError(f"Unexpected character whilst scanning key: {char}", pos) + + +def handle_scanning_value(char: str, pos: int, tokens: TokenState) -> State: + + if char == '"': + tokens.set_start(pos) + return State.SCANNING_QUOTED_VALUE + + if REGEX_KEY_CHARACTERS.fullmatch(char): + tokens.set_start(pos) + return State.SCANNING_BARE_VALUE + + raise ParseError(f"Unexpected character whilst scanning value: {char}", pos) + + +def handle_scanning_bare_value(char: str, pos: int, tokens: TokenState) -> State: + + if REGEX_KEY_CHARACTERS.fullmatch(char): + return State.SCANNING_BARE_VALUE + + if char == "}": + tokens.append(tokens.start, pos, "value") + return State.DONE + + if REGEX_SPACE.fullmatch(char): + tokens.append(tokens.start, pos, "value") + return State.SCANNING + + raise ParseError(f"Unexpected character whilst scanning bare value: {char}", pos) + + +def handle_scanning_escaped(char: str, pos: int, tokens: TokenState) -> State: + return State.SCANNING_QUOTED_VALUE + + +def handle_scanning_quoted_value(char: str, pos: int, tokens: TokenState) -> State: + + if char == '"': + tokens.append(tokens.start + 1, pos, "value") + return State.SCANNING + + if char == "\\": + return State.SCANNING_ESCAPED + + if char == "{" or char == "}": + raise ParseError( + f"Unexpected character whilst scanning quoted value: {char}", pos + ) + + if char == "\n": + tokens.append(tokens.start + 1, pos, "value") + return State.SCANNING_QUOTED_VALUE + + return State.SCANNING_QUOTED_VALUE + + +HANDLERS: dict[State, Callable[[str, int, TokenState], State]] = { + State.START: handle_start, + State.SCANNING: handle_scanning, + State.SCANNING_COMMENT: handle_scanning_comment, + State.SCANNING_ID: handle_scanning_id, + State.SCANNING_CLASS: handle_scanning_class, + State.SCANNING_KEY: handle_scanning_key, + State.SCANNING_VALUE: handle_scanning_value, + State.SCANNING_BARE_VALUE: handle_scanning_bare_value, + State.SCANNING_QUOTED_VALUE: handle_scanning_quoted_value, + State.SCANNING_ESCAPED: handle_scanning_escaped, +} diff --git a/tests/fixtures/attrs.md b/tests/fixtures/attrs.md new file mode 100644 index 0000000..5910f00 --- /dev/null +++ b/tests/fixtures/attrs.md @@ -0,0 +1,46 @@ +simple image +. +![a](b){#id .a b=c} +. +

a

+. + +simple inline code +. +`a`{#id .a b=c} +. +

a

+. + +ignore if space +. +![a](b) {#id key="*"} +. +

a {#id key="*"}

+. + +ignore if text +. +![a](b)b{#id key="*"} +. +

ab{#id key="*"}

+. + +multi-line +. +![a](b){ + #id .a + b=c + } +more +. +

a +more

+. + +combined +. +![a](b){#a .a}{.b class=x other=h}{#x class="x g" other=a} +. +

a

+. diff --git a/tests/test_attrs.py b/tests/test_attrs.py new file mode 100644 index 0000000..729162c --- /dev/null +++ b/tests/test_attrs.py @@ -0,0 +1,18 @@ +from pathlib import Path + +from markdown_it import MarkdownIt +from markdown_it.utils import read_fixture_file +import pytest + +from mdit_py_plugins.attrs import attrs_plugin + +FIXTURE_PATH = Path(__file__).parent.joinpath("fixtures", "attrs.md") + + +@pytest.mark.parametrize("line,title,input,expected", read_fixture_file(FIXTURE_PATH)) +def test_fixture(line, title, input, expected): + md = MarkdownIt("commonmark").use(attrs_plugin) + md.options["xhtmlOut"] = False + text = md.render(input) + print(text) + assert text.rstrip() == expected.rstrip() From c35bf143fb45e8572a1488cd78a96a1bbbb92bfb Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Tue, 27 Sep 2022 09:27:03 +0200 Subject: [PATCH 6/8] =?UTF-8?q?=F0=9F=9A=80=20RELEASE:=20v0.3.1=20(#52)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 8 ++++++++ mdit_py_plugins/__init__.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8188179..d1308b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +## 0.3.1 - 2022-09-27 + +- ⬆️ UPGRADE: Drop Python 3.6, support Python 3.10 +- 🐛 FIX: Parsing when newline is between footnote ID and first paragraph +- 🐛 FIX: Anchor ids in separate renders no longer affect each other. +- ✨ NEW: Add `attrs_plugin` +- 🔧 MAINTAIN: Use flit PEP 621 package build + ## 0.3.0 - 2021-12-03 - ⬆️ UPGRADE: Compatible with markdown-it-py `v2`. diff --git a/mdit_py_plugins/__init__.py b/mdit_py_plugins/__init__.py index 493f741..260c070 100644 --- a/mdit_py_plugins/__init__.py +++ b/mdit_py_plugins/__init__.py @@ -1 +1 @@ -__version__ = "0.3.0" +__version__ = "0.3.1" From 7588de2f8e27743601da98a5948e5638f92ad63c Mon Sep 17 00:00:00 2001 From: Taneli Hukkinen <3275109+hukkin@users.noreply.github.com> Date: Thu, 1 Dec 2022 18:27:06 +0200 Subject: [PATCH 7/8] =?UTF-8?q?=F0=9F=94=A7=20MAINTAIN:=20Fix=20pre-commit?= =?UTF-8?q?=20(#54)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 89d2e52..b357ba0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: hooks: - id: black - - repo: https://gitlab.com/pycqa/flake8 + - repo: https://github.com/pycqa/flake8 rev: 3.9.2 hooks: - id: flake8 From 277229c6ff4c21618dd621ed8ab55e228fd6e7c8 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 5 Dec 2022 10:59:52 +0100 Subject: [PATCH 8/8] =?UTF-8?q?=E2=9C=A8=20NEW:=20Add=20span=20parsing=20t?= =?UTF-8?q?o=20inline=20attributes=20plugin=20(#55)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update `attrs_plugin` to support non-self-closing syntaxes (like spans and links), and add parsing for text enclosed in `[]` as spans, e.g. `[my text]{#a .b}`. --- codecov.yml | 2 +- mdit_py_plugins/attrs/index.py | 84 ++++++++++++++++++++--- tests/fixtures/attrs.md | 118 ++++++++++++++++++++++++++++++++- tests/fixtures/span.md | 0 tests/test_attrs.py | 8 ++- 5 files changed, 199 insertions(+), 13 deletions(-) create mode 100644 tests/fixtures/span.md diff --git a/codecov.yml b/codecov.yml index f4796f9..80dcc51 100644 --- a/codecov.yml +++ b/codecov.yml @@ -2,7 +2,7 @@ coverage: status: project: default: - target: 93% + target: 92% threshold: 0.2% patch: default: diff --git a/mdit_py_plugins/attrs/index.py b/mdit_py_plugins/attrs/index.py index bc3feda..7150e5d 100644 --- a/mdit_py_plugins/attrs/index.py +++ b/mdit_py_plugins/attrs/index.py @@ -1,14 +1,26 @@ +from typing import List, Optional + from markdown_it import MarkdownIt from markdown_it.rules_inline import StateInline +from markdown_it.token import Token from .parse import ParseError, parse -def attrs_plugin(md: MarkdownIt, *, after=("image", "code_inline")): +def attrs_plugin( + md: MarkdownIt, + *, + after=("image", "code_inline", "link_close", "span_close"), + spans=True, +): """Parse inline attributes that immediately follow certain inline elements:: ![alt](https://image.com){#id .a b=c} + This syntax is inspired by + `Djot spans + `_. + Inside the curly braces, the following syntax is possible: - `.foo` specifies foo as a class. @@ -22,14 +34,18 @@ def attrs_plugin(md: MarkdownIt, *, after=("image", "code_inline")): Backslash escapes may be used inside quoted values. - `%` begins a comment, which ends with the next `%` or the end of the attribute (`}`). - **Note:** This plugin is currently limited to "self-closing" elements, - such as images and code spans. It does not work with links or emphasis. + Multiple attribute blocks are merged. :param md: The MarkdownIt instance to modify. :param after: The names of inline elements after which attributes may be specified. + This plugin does not support attributes after emphasis, strikethrough or text elements, + which all require post-parse processing. + :param spans: If True, also parse attributes after spans of text, encapsulated by `[]`. + Note Markdown link references take precedence over this syntax. + """ - def attr_rule(state: StateInline, silent: bool): + def _attr_rule(state: StateInline, silent: bool): if state.pending or not state.tokens: return False token = state.tokens[-1] @@ -39,12 +55,64 @@ def attr_rule(state: StateInline, silent: bool): new_pos, attrs = parse(state.src[state.pos :]) except ParseError: return False + token_index = _find_opening(state.tokens, len(state.tokens) - 1) + if token_index is None: + return False state.pos += new_pos + 1 if not silent: + attr_token = state.tokens[token_index] if "class" in attrs and "class" in token.attrs: - attrs["class"] = f"{token.attrs['class']} {attrs['class']}" - token.attrs.update(attrs) - + attrs["class"] = f"{attr_token.attrs['class']} {attrs['class']}" + attr_token.attrs.update(attrs) return True - md.inline.ruler.push("attr", attr_rule) + if spans: + md.inline.ruler.after("link", "span", _span_rule) + md.inline.ruler.push("attr", _attr_rule) + + +def _find_opening(tokens: List[Token], index: int) -> Optional[int]: + """Find the opening token index, if the token is closing.""" + if tokens[index].nesting != -1: + return index + level = 0 + while index >= 0: + level += tokens[index].nesting + if level == 0: + return index + index -= 1 + return None + + +def _span_rule(state: StateInline, silent: bool): + if state.srcCharCode[state.pos] != 0x5B: # /* [ */ + return False + + maximum = state.posMax + labelStart = state.pos + 1 + labelEnd = state.md.helpers.parseLinkLabel(state, state.pos, False) + + # parser failed to find ']', so it's not a valid span + if labelEnd < 0: + return False + + pos = labelEnd + 1 + + try: + new_pos, attrs = parse(state.src[pos:]) + except ParseError: + return False + + pos += new_pos + 1 + + if not silent: + state.pos = labelStart + state.posMax = labelEnd + token = state.push("span_open", "span", 1) + token.attrs = attrs + state.md.inline.tokenize(state) + token = state.push("span_close", "span", -1) + + state.pos = pos + state.posMax = maximum + return True diff --git a/tests/fixtures/attrs.md b/tests/fixtures/attrs.md index 5910f00..bd21ba8 100644 --- a/tests/fixtures/attrs.md +++ b/tests/fixtures/attrs.md @@ -1,3 +1,19 @@ +simple reference link +. +[text *emphasis*](a){#id .a} +. +

text emphasis

+. + +simple definition link +. +[a][]{#id .b} + +[a]: /url +. +

a

+. + simple image . ![a](b){#id .a b=c} @@ -38,9 +54,109 @@ more more

. -combined +merging attributes . ![a](b){#a .a}{.b class=x other=h}{#x class="x g" other=a} .

a

. + +spans: simple +. +[a]{#id .b}c +. +

ac

+. + +spans: space between brace and attrs +. +[a] {.b} +. +

[a] {.b}

+. + +spans: escaped span start +. +\[a]{.b} +. +

[a]{.b}

+. + +spans: escaped span end +. +[a\]{.b} +. +

[a]{.b}

+. + +spans: escaped span attribute +. +[a]\{.b} +. +

[a]{.b}

+. + +spans: nested text syntax +. +[*a*]{.b}c +. +

ac

+. + +spans: nested span +. +*[a]{.b}c* +. +

ac

+. + +spans: multi-line +. +x [a +b]{#id +b=c} y +. +

x a +b y

+. + +spans: nested spans +. +[[a]{.b}]{.c} +. +

a

+. + +spans: short link takes precedence over span +. +[a]{#id .b} + +[a]: /url +. +

a

+. + +spans: long link takes precedence over span +. +[a][a]{#id .b} + +[a]: /url +. +

a

+. + +spans: link inside span +. +[[a]]{#id .b} + +[a]: /url +. +

a

+. + +spans: merge attributes +. +[a]{#a .a}{#b .a .b other=c}{other=d} +. +

a

+. diff --git a/tests/fixtures/span.md b/tests/fixtures/span.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_attrs.py b/tests/test_attrs.py index 729162c..735374b 100644 --- a/tests/test_attrs.py +++ b/tests/test_attrs.py @@ -6,11 +6,13 @@ from mdit_py_plugins.attrs import attrs_plugin -FIXTURE_PATH = Path(__file__).parent.joinpath("fixtures", "attrs.md") +FIXTURE_PATH = Path(__file__).parent.joinpath("fixtures") -@pytest.mark.parametrize("line,title,input,expected", read_fixture_file(FIXTURE_PATH)) -def test_fixture(line, title, input, expected): +@pytest.mark.parametrize( + "line,title,input,expected", read_fixture_file(FIXTURE_PATH / "attrs.md") +) +def test_attrs(line, title, input, expected): md = MarkdownIt("commonmark").use(attrs_plugin) md.options["xhtmlOut"] = False text = md.render(input)