From b26608681d764e781a3c21fc8700b25fd831965a Mon Sep 17 00:00:00 2001 From: Raph471 Date: Tue, 11 Mar 2025 13:31:31 +0100 Subject: [PATCH] Add pymarkdown for Markdown linting (#4906) Add support for pymarkdown Closes #4785 --- ale_linters/markdown/pymarkdown.vim | 73 +++++++++++++++++++++++ doc/ale-markdown.txt | 59 +++++++++++++++++- doc/ale-supported-languages-and-tools.txt | 1 + doc/ale.txt | 1 + supported-tools.md | 1 + test/linter/test_pymarkdown.vader | 50 ++++++++++++++++ test/linter/test_pymarkdown_handler.vader | 52 ++++++++++++++++ 7 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 ale_linters/markdown/pymarkdown.vim create mode 100644 test/linter/test_pymarkdown.vader create mode 100644 test/linter/test_pymarkdown_handler.vader diff --git a/ale_linters/markdown/pymarkdown.vim b/ale_linters/markdown/pymarkdown.vim new file mode 100644 index 00000000..7700974b --- /dev/null +++ b/ale_linters/markdown/pymarkdown.vim @@ -0,0 +1,73 @@ + +call ale#Set('markdown_pymarkdown_executable', 'pymarkdown') +call ale#Set('markdown_pymarkdown_options', '') +call ale#Set('markdown_pymarkdown_use_global', get(g:, 'ale_use_global_executables', 0)) +call ale#Set('markdown_pymarkdown_auto_pipenv', 0) +call ale#Set('markdown_pymarkdown_auto_poetry', 0) +call ale#Set('markdown_pymarkdown_auto_uv', 0) + +function! ale_linters#markdown#pymarkdown#GetExecutable(buffer) abort + if (ale#Var(a:buffer, 'python_auto_pipenv') || ale#Var(a:buffer, 'markdown_pymarkdown_auto_pipenv')) + \ && ale#python#PipenvPresent(a:buffer) + return 'pipenv' + endif + + if (ale#Var(a:buffer, 'python_auto_poetry') || ale#Var(a:buffer, 'markdown_pymarkdown_auto_poetry')) + \ && ale#python#PoetryPresent(a:buffer) + return 'poetry' + endif + + if (ale#Var(a:buffer, 'python_auto_uv') || ale#Var(a:buffer, 'markdown_pymarkdown_auto_uv')) + \ && ale#python#UvPresent(a:buffer) + return 'uv' + endif + + return ale#python#FindExecutable(a:buffer, 'markdown_pymarkdown', ['pymarkdown']) +endfunction + +function! ale_linters#markdown#pymarkdown#GetCommand(buffer) abort + let l:executable = ale_linters#markdown#pymarkdown#GetExecutable(a:buffer) + + let l:exec_args = l:executable =~? 'pipenv\|poetry\|uv$' + \ ? ' run pymarkdown' + \ : '' + + return ale#Escape(l:executable) . l:exec_args + \ . ' ' + \ . ale#Var(a:buffer, 'markdown_pymarkdown_options') + \ . 'scan-stdin' +endfunction + +function! ale_linters#markdown#pymarkdown#Handle(buffer, lines) abort + let l:pattern = '\v^(\S*):(\d+):(\d+): ([A-Z]+\d+): (.*)$' + let l:output = [] + " lines are formatted as follows: + " sample.md:1:1: MD022: Headings should be surrounded by blank lines. [Expected: 1; Actual: 0; Below] (blanks-around-headings,blanks-around-headers) + + for l:match in ale#util#GetMatches(a:lines, l:pattern) + if(l:match[4] is# 'MD009') + \&& !ale#Var(a:buffer, 'warn_about_trailing_whitespace') + " Skip warnings for trailing whitespace if the option is off. + continue + endif + + let l:item = { + \ 'lnum': l:match[2] + 0, + \ 'col': l:match[3] + 0, + \ 'type': l:match[4][0], + \ 'text': l:match[5], + \ 'code': l:match[4], + \} + + call add(l:output, l:item) + endfor + + return l:output +endfunction + +call ale#linter#Define('markdown', { +\ 'name': 'pymarkdown', +\ 'executable': function('ale_linters#markdown#pymarkdown#GetExecutable'), +\ 'command': function('ale_linters#markdown#pymarkdown#GetCommand'), +\ 'callback': 'ale_linters#markdown#pymarkdown#Handle', +\}) diff --git a/doc/ale-markdown.txt b/doc/ale-markdown.txt index 693c8a44..0325c911 100644 --- a/doc/ale-markdown.txt +++ b/doc/ale-markdown.txt @@ -83,13 +83,70 @@ g:ale_markdown_pandoc_options *g:ale_markdown_pandoc_options* This variable can be set to change the default options passed to pandoc - =============================================================================== prettier *ale-markdown-prettier* See |ale-javascript-prettier| for information about the available options. +=============================================================================== +pymarkdown *ale-markdown-pymarkdown* + +g:ale_markdown_pymarkdown_executable *g:ale_markdown_pymarkdown_executable* + *b:ale_markdown_pymarkdown_executable* + Type: |String| + Default: `'pymarkdown'` + + See |ale-integrations-local-executables| + + Set this to `'pipenv'` to invoke `'pipenv` `run` `pymarkdown'`. + Set this to `'poetry'` to invoke `'poetry` `run` `pymarkdown'`. + + +g:ale_markdown_pymarkdown_options *g:ale_markdown_pymarkdown_options* + *b:ale_markdown_pymarkdown_options* + Type: |String| + Default: `''` + + This variable can be changed to add command-line arguments to the + pymarkdown invocation. + + +g:ale_markdown_pymarkdown_use_global *g:ale_markdown_pymarkdown_use_global* + *b:ale_markdown_pymarkdown_use_global* + Type: |Number| + Default: `get(g:, 'ale_use_global_executables', 0)` + + See |ale-integrations-local-executables| + + +g:ale_markdown_pymarkdown_auto_pipenv *g:ale_markdown_pymarkdown_auto_pipenv* + *b:ale_markdown_pymarkdown_auto_pipenv* + Type: |Number| + Default: `0` + + Detect whether the file is inside a pipenv, and set the executable to `pipenv` + if true. This is overridden by a manually-set executable. + + +g:ale_markdown_pymarkdown_auto_poetry *g:ale_markdown_pymarkdown_auto_poetry* + *b:ale_markdown_pymarkdown_auto_poetry* + Type: |Number| + Default: `0` + + Detect whether the file is inside a poetry, and set the executable to `poetry` + if true. This is overridden by a manually-set executable. + + +g:ale_markdown_pymarkdown_auto_uv *g:ale_markdown_pymarkdown_auto_uv* + *b:ale_markdown_pymarkdown_auto_uv* + Type: |Number| + Default: `0` + + Set the executable to `uv` if true. This is overridden by a manually-set + executable. + + =============================================================================== remark-lint *ale-markdown-remark-lint* diff --git a/doc/ale-supported-languages-and-tools.txt b/doc/ale-supported-languages-and-tools.txt index fb6219a6..826bc412 100644 --- a/doc/ale-supported-languages-and-tools.txt +++ b/doc/ale-supported-languages-and-tools.txt @@ -391,6 +391,7 @@ Notes: * `pandoc` * `prettier` * `proselint` + * `pymarkdown` * `redpen` * `remark-lint` * `textlint` diff --git a/doc/ale.txt b/doc/ale.txt index 302fbfa2..18a07777 100644 --- a/doc/ale.txt +++ b/doc/ale.txt @@ -3209,6 +3209,7 @@ documented in additional help files. mdl...................................|ale-markdown-mdl| pandoc................................|ale-markdown-pandoc| prettier..............................|ale-markdown-prettier| + pymarkdown............................|ale-markdown-pymarkdown| remark-lint...........................|ale-markdown-remark-lint| textlint..............................|ale-markdown-textlint| write-good............................|ale-markdown-write-good| diff --git a/supported-tools.md b/supported-tools.md index d6477687..d9b3ad8e 100644 --- a/supported-tools.md +++ b/supported-tools.md @@ -400,6 +400,7 @@ formatting. * [pandoc](https://pandoc.org) * [prettier](https://github.com/prettier/prettier) * [proselint](http://proselint.com/) + * [pymarkdown](https://github.com/jackdewinter/pymarkdown) * [redpen](http://redpen.cc/) * [remark-lint](https://github.com/wooorm/remark-lint) * [textlint](https://textlint.github.io/) diff --git a/test/linter/test_pymarkdown.vader b/test/linter/test_pymarkdown.vader new file mode 100644 index 00000000..2bfb2387 --- /dev/null +++ b/test/linter/test_pymarkdown.vader @@ -0,0 +1,50 @@ +Before: + call ale#assert#SetUpLinterTest('markdown', 'pymarkdown') + +After: + call ale#assert#TearDownLinterTest() + +Execute(The pymarkdown command callback should return default string): + AssertLinter 'pymarkdown', ale#Escape('pymarkdown') . ' scan-stdin' + +Execute(The pycodestyle command callback should allow options): + let g:markdown_pymarkdown_options = '--exclude=test*.py' + +Execute(The pymarkdown executable should be configurable): + let g:ale_markdown_pymarkdown_executable = '~/.local/bin/pymarkdown' + + AssertLinter '~/.local/bin/pymarkdown', + \ ale#Escape('~/.local/bin/pymarkdown') . ' scan-stdin' + +Execute(Setting executable to 'pipenv' appends 'run pymarkdown'): + let g:ale_markdown_pymarkdown_executable = 'path/to/pipenv' + + AssertLinter 'path/to/pipenv', + \ ale#Escape('path/to/pipenv') . ' run pymarkdown scan-stdin' + +Execute(Pipenv is detected when markdown_pymarkdown_auto_pipenv is set): + let g:ale_markdown_pymarkdown_auto_pipenv = 1 + call ale#test#SetFilename('../test-files/python/pipenv/whatever.py') + + AssertLinter 'pipenv', + \ ale#Escape('pipenv') . ' run pymarkdown scan-stdin' + +Execute(Setting executable to 'poetry' appends 'run pymarkdown'): + let g:ale_markdown_pymarkdown_executable = 'path/to/poetry' + + AssertLinter 'path/to/poetry', + \ ale#Escape('path/to/poetry') . ' run pymarkdown scan-stdin' + +Execute(Poetry is detected when markdown_pymarkdown_auto_poetry is set): + let g:ale_markdown_pymarkdown_auto_poetry = 1 + call ale#test#SetFilename('../test-files/python/poetry/whatever.py') + + AssertLinter 'poetry', + \ ale#Escape('poetry') . ' run pymarkdown scan-stdin' + +Execute(uv is detected when markdown_pymarkdown_auto_uv is set): + let g:ale_markdown_pymarkdown_auto_uv = 1 + call ale#test#SetFilename('../test-files/python/uv/whatever.py') + + AssertLinter 'uv', + \ ale#Escape('uv') . ' run pymarkdown scan-stdin' diff --git a/test/linter/test_pymarkdown_handler.vader b/test/linter/test_pymarkdown_handler.vader new file mode 100644 index 00000000..13acdd7d --- /dev/null +++ b/test/linter/test_pymarkdown_handler.vader @@ -0,0 +1,52 @@ +Before: + Save g:ale_warn_about_trailing_whitespace + + let g:ale_warn_about_trailing_whitespace = 1 + + runtime ale_linters/markdown/pymarkdown.vim + +After: + Restore + unlet! b:ale_warn_about_trailing_whitespace + + call ale#linter#Reset() + +Execute (Should parse error correctly): + AssertEqual + \ [ + \ { + \ 'lnum': 1, + \ 'col': 1, + \ 'type': 'M', + \ 'text': 'Headings should be surrounded by blank lines', + \ 'code': 'MD022', + \ } + \ ], + \ ale_linters#markdown#pymarkdown#Handle(bufnr(''), [ + \ 'foo.md:1:1: MD022: Headings should be surrounded by blank lines', + \ ]) + +Execute(Warnings about trailing whitespace should be reported by default): + AssertEqual + \ [ + \ { + \ 'lnum': 1, + \ 'col': 1, + \ 'type': 'M', + \ 'text': 'who cares', + \ 'code': 'MD009', + \ }, + \ ], + \ ale_linters#markdown#pymarkdown#Handle(bufnr(''), [ + \ 'foo.md:1:1: MD009: who cares', + \ ]) + +Execute(Disabling trailing whitespace warnings should work): + let b:ale_warn_about_trailing_whitespace = 0 + + AssertEqual + \ [ + \ ], + \ ale_linters#markdown#pymarkdown#Handle(bufnr(''), [ + \ 'foo.md:1:1: MD009: who cares', + \ ]) \ No newline at end of file