From 3a5bdcc98e67de9df26ebb8bc7cd0221a0d6b51b Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 14 Dec 2020 16:36:52 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20NEW:=20Add=20colon=5Ffence=20plugin?= =?UTF-8?q?=20(#91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- markdown_it/extensions/colon_fence.py | 132 ++++++ tests/test_plugins/fixtures/colon_fence.md | 423 ++++++++++++++++++ tests/test_plugins/test_colon_fence.py | 36 ++ .../test_colon_fence/test_plugin_parse.yml | 17 + 4 files changed, 608 insertions(+) create mode 100644 markdown_it/extensions/colon_fence.py create mode 100644 tests/test_plugins/fixtures/colon_fence.md create mode 100644 tests/test_plugins/test_colon_fence.py create mode 100644 tests/test_plugins/test_colon_fence/test_plugin_parse.yml diff --git a/markdown_it/extensions/colon_fence.py b/markdown_it/extensions/colon_fence.py new file mode 100644 index 00000000..7235c3a2 --- /dev/null +++ b/markdown_it/extensions/colon_fence.py @@ -0,0 +1,132 @@ +from markdown_it import MarkdownIt +from markdown_it.common.utils import unescapeAll, escapeHtml, stripEscape +from markdown_it.rules_block import StateBlock + + +def colon_fence_plugin(md: MarkdownIt): + """This plugin directly mimics regular fences, but with `:` colons. + + Example:: + + :::name + contained text + ::: + + """ + + md.block.ruler.before( + "fence", + "colon_fence", + _rule, + {"alt": ["paragraph", "reference", "blockquote", "list", "footnote_def"]}, + ) + md.add_render_rule("colon_fence", _render) + + +def _rule(state: StateBlock, startLine: int, endLine: int, silent: bool): + + haveEndMarker = False + pos = state.bMarks[startLine] + state.tShift[startLine] + maximum = state.eMarks[startLine] + + # if it's indented more than 3 spaces, it should be a code block + if state.sCount[startLine] - state.blkIndent >= 4: + return False + + if pos + 3 > maximum: + return False + + marker = state.srcCharCode[pos] + + # /* : */ + if marker != 0x3A: + return False + + # scan marker length + mem = pos + pos = state.skipChars(pos, marker) + + length = pos - mem + + if length < 3: + return False + + markup = state.src[mem:pos] + params = state.src[pos:maximum] + + # Since start is found, we can report success here in validation mode + if silent: + return True + + # search end of block + nextLine = startLine + + while True: + nextLine += 1 + if nextLine >= endLine: + # unclosed block should be autoclosed by end of document. + # also block seems to be autoclosed by end of parent + break + + pos = mem = state.bMarks[nextLine] + state.tShift[nextLine] + maximum = state.eMarks[nextLine] + + if pos < maximum and state.sCount[nextLine] < state.blkIndent: + # non-empty line with negative indent should stop the list: + # - ``` + # test + break + + if state.srcCharCode[pos] != marker: + continue + + if state.sCount[nextLine] - state.blkIndent >= 4: + # closing fence should be indented less than 4 spaces + continue + + pos = state.skipChars(pos, marker) + + # closing code fence must be at least as long as the opening one + if pos - mem < length: + continue + + # make sure tail has spaces only + pos = state.skipSpaces(pos) + + if pos < maximum: + continue + + haveEndMarker = True + # found! + break + + # If a fence has heading spaces, they should be removed from its inner block + length = state.sCount[startLine] + + state.line = nextLine + (1 if haveEndMarker else 0) + + token = state.push("colon_fence", "code", 0) + token.info = stripEscape(params) + token.content = state.getLines(startLine + 1, nextLine, length, True) + token.markup = markup + token.map = [startLine, state.line] + + return True + + +def _render(self, tokens, idx, options, env): + token = tokens[idx] + info = unescapeAll(token.info).strip() if token.info else "" + content = escapeHtml(token.content) + block_name = "" + + if info: + block_name = info.split()[0] + + return ( + "
"
+        + content
+        + "
\n" + ) diff --git a/tests/test_plugins/fixtures/colon_fence.md b/tests/test_plugins/fixtures/colon_fence.md new file mode 100644 index 00000000..b04e0e25 --- /dev/null +++ b/tests/test_plugins/fixtures/colon_fence.md @@ -0,0 +1,423 @@ +# The initial tests are adapted from the test for normal code fences in tests/test_port/fixtures/commonmark_spec.md + +src line: 1638 + +. +::: +< + > +::: +. +
<
+ >
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1665 + +. +:: +foo +:: +. +

:: +foo +::

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1676 + +. +::: +aaa +~~~ +::: +. +
aaa
+~~~
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1688 + +. +::: +aaa +``` +::: +. +
aaa
+```
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1702 + +. +:::: +aaa +::: +:::::: +. +
aaa
+:::
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1729 + +. +::: +. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1736 + +. +::::: + +::: +aaa +. +

+:::
+aaa
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1749 + +. +> ::: +> aaa + +bbb +. +
+
aaa
+
+
+

bbb

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1765 + +. +::: + + +::: +. +

+  
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1779 + +. +::: +::: +. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1791 + +. + ::: + aaa +aaa +::: +. +
aaa
+aaa
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1803 + +. + ::: +aaa + aaa +aaa + ::: +. +
aaa
+aaa
+aaa
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1817 + +. + ::: + aaa + aaa + aaa + ::: +. +
aaa
+ aaa
+aaa
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1833 + +. + ::: + aaa + ::: +. +
:::
+aaa
+:::
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1848 + +. +::: +aaa + ::: +. +
aaa
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1858 + +. + ::: +aaa + ::: +. +
aaa
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1870 + +. +::: +aaa + ::: +. +
aaa
+    :::
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1884 + +. +::: ::: +aaa +. +
aaa
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1907 + +. +foo +::: +bar +::: +baz +. +

foo

+
bar
+
+

baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1946 + +. +:::ruby +def foo(x) + return 3 +end +::: +. +
def foo(x)
+  return 3
+end
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1974 + +. +::::; +:::: +. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1984 + +. +::: aa ::: +foo +. +
foo
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2007 + +. +::: +::: aaa +::: +. +
::: aaa
+
+. + + + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2007 + +. +::: +::: aaa +::: +. +
::: aaa
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Ending marker could be longer +. +::::: name ::::: + hello world +:::::::::::::::: +. +
  hello world
+
+. + +Nested blocks +. +::::: name +:::: name +xxx +:::: +::::: +. +
:::: name
+xxx
+::::
+
+. + +Name could be adjacent to marker +. +:::name +xxx +::: +. +
xxx
+
+. + +They should terminate paragraphs +. +blah blah +::: name +content +::: +. +

blah blah

+
content
+
+. + +They could be nested in lists +. + - ::: name + - xxx + ::: +. + +. + +Or in blockquotes +. +> ::: name +> xxx +>> yyy +> zzz +> ::: +. +
+
xxx
+> yyy
+zzz
+
+
+. + +List indentation quirks +. + - ::: name + xxx + yyy + ::: + + - ::: name + xxx + yyy + ::: +. + +

+-  ::: name
+ xxx
+yyy
+
+. diff --git a/tests/test_plugins/test_colon_fence.py b/tests/test_plugins/test_colon_fence.py new file mode 100644 index 00000000..15df2cef --- /dev/null +++ b/tests/test_plugins/test_colon_fence.py @@ -0,0 +1,36 @@ +from pathlib import Path +from textwrap import dedent + +import pytest + +from markdown_it import MarkdownIt +from markdown_it.utils import read_fixture_file +from markdown_it.extensions.colon_fence import colon_fence_plugin + +FIXTURE_PATH = Path(__file__).parent.joinpath("fixtures", "colon_fence.md") + + +@pytest.mark.parametrize("line,title,input,expected", read_fixture_file(FIXTURE_PATH)) +def test_fixtures(line, title, input, expected): + md = MarkdownIt("commonmark").use(colon_fence_plugin) + md.options["xhtmlOut"] = False + text = md.render(input) + try: + assert text.rstrip() == expected.rstrip() + except AssertionError: + print(text) + raise + + +def test_plugin_parse(data_regression): + md = MarkdownIt().use(colon_fence_plugin) + tokens = md.parse( + dedent( + """\ + ::: name + *content* + ::: + """ + ) + ) + data_regression.check([t.as_dict() for t in tokens]) diff --git a/tests/test_plugins/test_colon_fence/test_plugin_parse.yml b/tests/test_plugins/test_colon_fence/test_plugin_parse.yml new file mode 100644 index 00000000..16c73034 --- /dev/null +++ b/tests/test_plugins/test_colon_fence/test_plugin_parse.yml @@ -0,0 +1,17 @@ +- attrs: null + block: true + children: null + content: '*content* + + ' + hidden: false + info: ' name' + level: 0 + map: + - 0 + - 3 + markup: ':::' + meta: {} + nesting: 0 + tag: code + type: colon_fence