From 5dc5c577a353729d3bb9cf3be20b6c460989ff11 Mon Sep 17 00:00:00 2001 From: Scott Shawcroft Date: Mon, 3 Jan 2022 18:26:13 -0800 Subject: [PATCH] Add fancy list plugin. This is ported from https://github.com/Moxio/markdown-it-fancy-lists https://github.com/Moxio/markdown-it-fancy-lists/commit/f47a6c2b63d9ce4b82882a59302b24abc0328032 Fixes #34 --- mdit_py_plugins/fancy_list/__init__.py | 552 +++++++++++++++++++ setup.cfg | 2 + tests/fixtures/fancy_list.md | 553 ++++++++++++++++++++ tests/test_fancy_list.py | 34 ++ tests/test_fancy_list/test_plugin_parse.yml | 84 +++ 5 files changed, 1225 insertions(+) create mode 100644 mdit_py_plugins/fancy_list/__init__.py create mode 100644 tests/fixtures/fancy_list.md create mode 100644 tests/test_fancy_list.py create mode 100644 tests/test_fancy_list/test_plugin_parse.yml diff --git a/mdit_py_plugins/fancy_list/__init__.py b/mdit_py_plugins/fancy_list/__init__.py new file mode 100644 index 0000000..049d189 --- /dev/null +++ b/mdit_py_plugins/fancy_list/__init__.py @@ -0,0 +1,552 @@ +"""Fancy list plugin""" +import functools +import re +from typing import Dict, Literal, Tuple, Union + +from markdown_it import MarkdownIt +from markdown_it.common.utils import isSpace +from markdown_it.rules_block import StateBlock +from markdown_it.token import Token +from roman import InvalidRomanNumeralError, fromRoman + +# Ported from https://github.com/Moxio/markdown-it-fancy-lists + +MarkerType = Literal["0", "a", "A", "i", "I", "#", "*", "-", "+"] + + +class Marker: + isOrdered: bool + isRoman: bool + isAlpha: bool + listType: MarkerType + bulletChar: str + hasOrdinalIndicator: bool + delimiter: Literal[")", "."] + start: int + posAfterMarker: int + original: str + + def __init__(self, **kwargs): + for k in kwargs: + setattr(self, k, kwargs[k]) + + def __str__(self): + return self.original + + +# Search `[-+*][\n ]`, returns next pos after marker on success +# or -1 on fail. +def parseUnorderedListMarker(state: StateBlock, startLine: int): + pos = state.bMarks[startLine] + state.tShift[startLine] + maximum = state.eMarks[startLine] + + marker = state.src[pos] + pos += 1 + + # Check bullet + if marker not in "*-+": + return None + + if pos < maximum: + ch = state.srcCharCode[pos] + + if not isSpace(ch): + # " -test " - is not a list item + return None + + return {"listType": state.src[pos - 1], "posAfterMarker": pos, "original": marker} + + +# Search `^(\d{1,9}|[a-z]|[A-Z]|[ivxlcdm]+|[IVXLCDM]+|#)([\u00BA\u00B0\u02DA\u1D52]?)([.)])`, +# returns next pos after marker on success or -1 on fail. +def parseOrderedListMarker(state: StateBlock, startLine: int): + start = state.bMarks[startLine] + state.tShift[startLine] + maximum = state.eMarks[startLine] + + # List marker should have at least 2 chars (digit + dot) + if start + 1 >= maximum: + return None + + stringContainingNumberAndMarker = state.src[start : min(maximum, start + 10)] + + match = re.match( + "^(\\d{1,9}|[a-z]|[A-Z]|[ivxlcdm]+|[IVXLCDM]+|#)([\u00BA\u00B0\u02DA\u1D52]?)([.)])", + stringContainingNumberAndMarker, + ) + if not match: + return None + + markerPos = start + len(match[1]) + markerChar = state.src[markerPos] + + finalPos = start + len(match[0]) + finalChar = state.srcCharCode[finalPos] + + # requires once space after marker + if not isSpace(finalChar): + return None + + # requires two spaces after a capital letter and a period + if ( + isCharCodeUppercaseAlpha(ord(match[1][0])) + and len(match[1]) == 1 + and markerChar == "." + ): + finalPos += 1 # consume another space + finalChar = state.srcCharCode[finalPos] + if not isSpace(finalChar): + return None + + return { + "bulletChar": match[1], + "hasOrdinalIndicator": match[2] != "", + "delimiter": match[3], + "posAfterMarker": finalPos, + "original": match[0], + } + + +def markTightParagraphs(state: StateBlock, idx: int): + i: int = idx + 2 + l: int = len(state.tokens) - 2 + level = state.level + 2 + + while i < l: + if state.tokens[i].level == level and state.tokens[i].type == "paragraph_open": + state.tokens[i + 2].hidden = True + state.tokens[i].hidden = True + i += 2 + i += 1 + + +def isCharCodeDigit(charChode: int): + return ord("0") <= charChode <= ord("9") + + +def isCharCodeLowercaseAlpha(charChode: int): + return ord("a") <= charChode <= ord("z") + + +def isCharCodeUppercaseAlpha(charChode: int): + return ord("A") <= charChode <= ord("Z") + + +def analyzeRoman(romanNumeralString: str): + parsedRomanNumber: int = 1 + isValidRoman = True + try: + # This parser only works on uppercase letters (unlike the JS library.) + parsedRomanNumber = fromRoman(romanNumeralString.upper()) + except InvalidRomanNumeralError: + isValidRoman = False + + return ( + parsedRomanNumber, + isValidRoman, + ) + + +def analyseMarker( + state: StateBlock, startLine: int, endLine: int, previousMarker: Marker | None +): + orderedListMarker = parseOrderedListMarker(state, startLine) + if orderedListMarker is not None: + bulletChar = orderedListMarker["bulletChar"] + charCode = ord(orderedListMarker["bulletChar"][0]) + + if isCharCodeDigit(charCode): + return Marker( + isOrdered=True, + isRoman=False, + isAlpha=False, + listType="0", + start=int(bulletChar), + **orderedListMarker, + ) + elif isCharCodeLowercaseAlpha(charCode): + isValidAlpha = len(bulletChar) == 1 + preferRoman = (previousMarker is not None and previousMarker.isRoman) or ( + (previousMarker is None or not previousMarker.isAlpha) + and bulletChar == "i" + ) + parsedRomanNumber, isValidRoman = analyzeRoman(bulletChar) + + if isValidRoman and (not isValidAlpha or preferRoman): + return Marker( + isOrdered=True, + isRoman=True, + isAlpha=False, + listType="i", + start=parsedRomanNumber, + **orderedListMarker, + ) + elif isValidAlpha: + return Marker( + isOrdered=True, + isRoman=False, + isAlpha=True, + listType="a", + start=ord(bulletChar) - ord("a") + 1, + **orderedListMarker, + ) + return None + elif isCharCodeUppercaseAlpha(charCode): + isValidAlpha = len(bulletChar) == 1 + preferRoman = (previousMarker is not None and previousMarker.isRoman) or ( + (previousMarker is None or not previousMarker.isAlpha) + and bulletChar == "I" + ) + parsedRomanNumber, isValidRoman = analyzeRoman(bulletChar) + + if isValidRoman and (not isValidAlpha or preferRoman): + return Marker( + isOrdered=True, + isRoman=True, + isAlpha=False, + listType="I", + start=parsedRomanNumber, + **orderedListMarker, + ) + elif isValidAlpha: + return Marker( + isOrdered=True, + isRoman=False, + isAlpha=True, + listType="A", + start=ord(bulletChar) - ord("A") + 1, + **orderedListMarker, + ) + return None + else: + return Marker( + isOrdered=True, + isRoman=False, + isAlpha=False, + listType="#", + start=1, + **orderedListMarker, + ) + unorderedListMarker = parseUnorderedListMarker(state, startLine) + if unorderedListMarker: + return Marker( + isOrdered=False, + isRoman=False, + isAlpha=False, + bulletChar="", + hasOrdinalIndicator=False, + delimiter=")", + start=1, + **unorderedListMarker, + ) + return None + + +def areMarkersCompatible(previousMarker: Marker, currentMarker: Marker): + return ( + previousMarker.isOrdered == currentMarker.isOrdered + and ( + previousMarker.listType == currentMarker.listType + or currentMarker.listType == "#" + ) + and previousMarker.delimiter == currentMarker.delimiter + and previousMarker.hasOrdinalIndicator == currentMarker.hasOrdinalIndicator + ) + + +def fancy_list_plugin(md: MarkdownIt, allow_ordinal: bool = False): + """Fancy lists use the Pandoc rules to specify the style of ordered lists. + + .. code-block:: md + + 1. One + a. One + b. Two + i. One + A. One + B. Two + I. One + II. Two + C. Three + ii. Two + c. Three + 2. Two + + """ + md.block.ruler.at( + "list", + functools.partial(_fancylist_rule, allow_ordinal), + {"alt": ["paragraph", "reference", "blockquote"]}, + ) + + +def parseNameMarker(state: StateBlock, startLine: int) -> Tuple[int, str]: + """Parse field name: `:name:` + + :returns: position after name marker, name text + """ + start = state.bMarks[startLine] + state.tShift[startLine] + pos = start + maximum = state.eMarks[startLine] + + # marker should have at least 3 chars (colon + character + colon) + if pos + 2 >= maximum: + return -1, "" + + # first character should be ':' + if state.src[pos] != ":": + return -1, "" + + # scan name length + name_length = 1 + found_close = False + for ch in state.src[pos + 1 :]: + if ch == "\n": + break + if ch == ":": + # TODO backslash escapes + found_close = True + break + name_length += 1 + + if not found_close: + return -1, "" + + # get name + name_text = state.src[pos + 1 : pos + name_length] + + # name should contain at least one character + if not name_text.strip(): + return -1, "" + + return pos + name_length + 1, name_text + + +def _fancylist_rule( + allowOrdinal: bool, state: StateBlock, startLine: int, endLine: int, silent: bool +): + # if it's indented more than 3 spaces, it should be a code block + if state.sCount[startLine] - state.blkIndent >= 4: + return False + + # Special case: + # - item 1 + # - item 2 + # - item 3 + # - item 4 + # - this one is a paragraph continuation + if ( + state.listIndent >= 0 + and state.sCount[startLine] - state.listIndent >= 4 + and state.sCount[startLine] < state.blkIndent + ): + return False + + isTerminatingParagraph = False + # limit conditions when list can interrupt + # a paragraph (validation mode only) + if silent and state.parentType == "paragraph": + # Next list item should still terminate previous list item; + # + # This code can fail if plugins use blkIndent as well as lists, + # but I hope the spec gets fixed long before that happens. + # + if state.tShift[startLine] >= state.blkIndent: + isTerminatingParagraph = True + + marker: Marker = analyseMarker(state, startLine, endLine, None) + if marker is None: + return False + + if marker.hasOrdinalIndicator and not allowOrdinal: + return False + + # do not allow subsequent numbers to interrupt paragraphs in non-nested lists + isNestedList = state.listIndent != -1 + if isTerminatingParagraph and marker.start != 1 and not isNestedList: + return False + + # If we're starting a new unordered list right after + # a paragraph, first line should not be empty. + if isTerminatingParagraph: + if state.skipSpaces(marker.posAfterMarker) >= state.eMarks[startLine]: + return False + + # We should terminate list on style change. Remember first one to compare. + markerCharCode = ord(state.src[marker.posAfterMarker - 1]) + + # For validation mode we can terminate immediately + if silent: + return True + + # Start list + listTokIdx = len(state.tokens) + + token: Token + if marker.isOrdered: + token = state.push("ordered_list_open", "ol", 1) + attrs: Dict[str, Union[str, int, float]] = {} + if marker.listType != "0" and marker.listType != "#": + attrs["type"] = str(marker.listType) + if marker.start != 1: + attrs["start"] = str(marker.start) + if marker.hasOrdinalIndicator: + attrs["class"] = "ordinal" + token.attrs = attrs + else: + token = state.push("bullet_list_open", "ul", 1) + + listLines = [startLine, 0] + token.map = listLines + token.markup = chr(markerCharCode) + + # + # Iterate list items + # + + nextLine = startLine + prevEmptyEnd = False + terminatorRules = state.md.block.ruler.getRules("list") + + oldParentType = state.parentType + state.parentType = "list" + + tight = True + while nextLine < endLine: + nextMarker = analyseMarker(state, nextLine, endLine, marker) + if nextMarker is None or not areMarkersCompatible(marker, nextMarker): + break + pos: int = nextMarker.posAfterMarker + maximum = state.eMarks[nextLine] + + initial = offset = ( + state.sCount[nextLine] + + pos + - (state.bMarks[startLine] + state.tShift[startLine]) + ) + + while pos < maximum: + ch = state.srcCharCode[pos] + + if ch == 0x09: + offset += 4 - (offset + state.bsCount[nextLine]) % 4 + elif ch == 0x20: + offset += 1 + else: + break + + pos += 1 + + contentStart = pos + + indentAfterMarker: int = 1 + if contentStart >= maximum: + # trimming space in "- \n 3" case, indent is 1 here + indentAfterMarker = 1 + else: + indentAfterMarker = offset - initial + + # If we have more than 4 spaces, the indent is 1 + # (the rest is just indented code block) + if indentAfterMarker > 4: + indentAfterMarker = 1 + + # " - test" + # ^^^^^ - calculating total length of this thing + indent = initial + indentAfterMarker + + # Run subparser & write tokens + token = state.push("list_item_open", "li", 1) + token.markup = chr(markerCharCode) + token.map = itemLines = [int(startLine), 0] + + # change current state, then restore it after parser subcall + oldTight = state.tight + oldTShift = state.tShift[startLine] + oldSCount = state.sCount[startLine] + + # - example list + # ^ listIndent position will be here + # ^ blkIndent position will be here + # + oldListIndent = state.listIndent + state.listIndent = state.blkIndent + state.blkIndent = indent + + state.tight = True + state.tShift[startLine] = contentStart - state.bMarks[startLine] + state.sCount[startLine] = offset + + if contentStart >= maximum and state.isEmpty(startLine + 1): + # workaround for this case + # (list item is empty, list terminates before "foo"): + # ~~~~~~~~ + # - + # + # foo + # ~~~~~~~~ + state.line = min(state.line + 2, endLine) + else: + state.md.block.tokenize(state, startLine, endLine) + + # If any of list item is tight, mark list as tight + if not state.tight or prevEmptyEnd: + tight = False + # Item become loose if finish with empty line, + # but we should filter last element, because it means list finish + prevEmptyEnd = (state.line - startLine) > 1 and state.isEmpty(state.line - 1) + + state.blkIndent = state.listIndent + state.listIndent = oldListIndent + state.tShift[startLine] = oldTShift + state.sCount[startLine] = oldSCount + state.tight = oldTight + + token = state.push("list_item_close", "li", -1) + token.markup = chr(markerCharCode) + + nextLine = startLine = state.line + itemLines[1] = nextLine + contentStart = state.bMarks[startLine] + + if nextLine >= endLine: + break + + # + # Try to check if list is terminated or continued. + # + if state.sCount[nextLine] < state.blkIndent: + break + + # if it's indented more than 3 spaces, it should be a code block + if state.sCount[startLine] - state.blkIndent >= 4: + break + + # fail if terminating block found + terminate = False + for rule in terminatorRules: + if rule(state, nextLine, endLine, True): + terminate = True + break + if terminate: + break + + marker = nextMarker + + # Finalize list + if marker.isOrdered: + token = state.push("ordered_list_close", "ol", -1) + else: + token = state.push("bullet_list_close", "ul", -1) + + token.markup = chr(markerCharCode) + + listLines[1] = nextLine + state.line = nextLine + + state.parentType = oldParentType + + # mark paragraphs tight if needed + if tight: + markTightParagraphs(state, listTokIdx) + + return True diff --git a/setup.cfg b/setup.cfg index 843ad94..0d6db9f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -50,6 +50,8 @@ testing = rtd = myst-parser~=0.14.0 sphinx-book-theme~=0.1.0 +fancylist = + roman [mypy] show_error_codes = True diff --git a/tests/fixtures/fancy_list.md b/tests/fixtures/fancy_list.md new file mode 100644 index 0000000..f00d3cc --- /dev/null +++ b/tests/fixtures/fancy_list.md @@ -0,0 +1,553 @@ +does not alter ordinary unordered list syntax: +. +* foo +* bar +- baz +. + + +. + +does not alter ordinary ordered list syntax: +. +1. foo +2. bar +3) baz +. +
    +
  1. foo
  2. +
  3. bar
  4. +
+
    +
  1. baz
  2. +
+. + +does not alter ordinary nested ordered list syntax: +. +1. foo + 1. first + 2. second +2. bar +3) baz +. +
    +
  1. foo +
      +
    1. first
    2. +
    3. second
    4. +
    +
  2. +
  3. bar
  4. +
+
    +
  1. baz
  2. +
+. + +supports lowercase alphabetical numbering: +. +a. foo +b. bar +c. baz +. +
    +
  1. foo
  2. +
  3. bar
  4. +
  5. baz
  6. +
+. + +supports offsets for lowercase alphabetical numbering: +. +b. foo +c. bar +d. baz +. +
    +
  1. foo
  2. +
  3. bar
  4. +
  5. baz
  6. +
+. + +supports uppercase alphabetical numbering: +. +A) foo +B) bar +C) baz +. +
    +
  1. foo
  2. +
  3. bar
  4. +
  5. baz
  6. +
+. + +supports offsets for uppercase alphabetical numbering: +. +B) foo +C) bar +D) baz +. +
    +
  1. foo
  2. +
  3. bar
  4. +
  5. baz
  6. +
+. + +test supports lowercase roman numbering: +. +i. foo +ii. bar +iii. baz +. +
    +
  1. foo
  2. +
  3. bar
  4. +
  5. baz
  6. +
+. + +supports offsets for lowercase roman numbering: +. +iv. foo +v. bar +vi. baz +. +
    +
  1. foo
  2. +
  3. bar
  4. +
  5. baz
  6. +
+. + +supports uppercase roman numbering: +. +I) foo +II) bar +III) baz +. +
    +
  1. foo
  2. +
  3. bar
  4. +
  5. baz
  6. +
+. + +supports offsets for uppercase roman numbering: +. +XII. foo +XIII. bar +XIV. baz +. +
    +
  1. foo
  2. +
  3. bar
  4. +
  5. baz
  6. +
+. + +ignores invalid roman numerals as list marker: +. +VV. foo +VVI. bar +VVII. baz +. +

VV. foo +VVI. bar +VVII. baz

+. + +supports hash as list marker for subsequent items: +. +1. foo +#. bar +#. baz +. +
    +
  1. foo
  2. +
  3. bar
  4. +
  5. baz
  6. +
+. + +supports hash as list marker for subsequent roman numeric marker: +. +i. foo +#. bar +#. baz +. +
    +
  1. foo
  2. +
  3. bar
  4. +
  5. baz
  6. +
+. + +supports hash as list marker for subsequent alphanumeric marker: +. +a. foo +#. bar +#. baz +. +
    +
  1. foo
  2. +
  3. bar
  4. +
  5. baz
  6. +
+. + +supports hash as list marker for initial item: +. +#. foo +#. bar +#. baz +. +
    +
  1. foo
  2. +
  3. bar
  4. +
  5. baz
  6. +
+. + +allows first numbers to interrupt paragraphs: +. +I need to buy +a. new shoes +b. a coat +c. a plane ticket + +I also need to buy +i. new shoes +ii. a coat +iii. a plane ticket +. +

I need to buy

+
    +
  1. new shoes
  2. +
  3. a coat
  4. +
  5. a plane ticket
  6. +
+

I also need to buy

+
    +
  1. new shoes
  2. +
  3. a coat
  4. +
  5. a plane ticket
  6. +
+. + +does not allow subsequent numbers to interrupt paragraphs: +. +I need to buy +b. new shoes +c. a coat +d. a plane ticket + +I also need to buy +ii. new shoes +iii. a coat +iv. a plane ticket +. +

I need to buy +b. new shoes +c. a coat +d. a plane ticket

+

I also need to buy +ii. new shoes +iii. a coat +iv. a plane ticket

+. + +supports nested lists: +. + 9) Ninth +10) Tenth +11) Eleventh + i. subone + ii. subtwo + iii. subthree +. +
    +
  1. Ninth
  2. +
  3. Tenth
  4. +
  5. Eleventh +
      +
    1. subone
    2. +
    3. subtwo
    4. +
    5. subthree
    6. +
    +
  6. +
+. + +supports nested lists with start: +. +c. charlie +#. delta + iv) subfour + #) subfive + #) subsix +#. echo +. +
    +
  1. charlie
  2. +
  3. delta +
      +
    1. subfour
    2. +
    3. subfive
    4. +
    5. subsix
    6. +
    +
  4. +
  5. echo
  6. +
+. + +supports nested lists with extra newline: +. +c. charlie +#. delta + + sigma + iv) subfour + #) subfive + #) subsix +#. echo +. +
    +
  1. +

    charlie

    +
  2. +
  3. +

    delta

    +

    sigma

    +
      +
    1. subfour
    2. +
    3. subfive
    4. +
    5. subsix
    6. +
    +
  4. +
  5. +

    echo

    +
  6. +
+. + +starts a new list when a different type of numbering is used: +. +1) First +A) First again +i) Another first +ii) Second +. +
    +
  1. First
  2. +
+
    +
  1. First again
  2. +
+
    +
  1. Another first
  2. +
  3. Second
  4. +
+. + +starts a new list when a sequence of letters is not a valid roman numeral: +. +I) First +A) First again +. +
    +
  1. First
  2. +
+
    +
  1. First again
  2. +
+. + +Nested capital inside lower roman: +. +i. First + A. First again +ii. Second + A. First again again + I. First +. +
    +
  1. First +
      +
    1. First again
    2. +
    +
  2. +
  3. Second +
      +
    1. First again again +
        +
      1. First
      2. +
      +
    2. +
    +
  4. +
+. + +marker is considered to be alphabetical when part of an alphabetical list: +. +A) First +I) Second +II) First of new list + +a) First +i) Second +ii) First of new list +. +
    +
  1. First
  2. +
  3. Second
  4. +
+
    +
  1. First of new list
  2. +
+
    +
  1. First
  2. +
  3. Second
  4. +
+
    +
  1. First of new list
  2. +
+. + +single letter roman numerals other than I are considered alphabetical without context: +. +v. foo + +X) foo + +l. foo + +C) foo + +d. foo + +M) foo +. +
    +
  1. foo
  2. +
+
    +
  1. foo
  2. +
+
    +
  1. foo
  2. +
+
    +
  1. foo
  2. +
+
    +
  1. foo
  2. +
+
    +
  1. foo
  2. +
+. + +requires two spaces after a capital letter and a period: +. +B. Russell was an English philosopher. + +I. Elba is an English actor. + +I. foo +II. bar + +B. foo +C. bar +. +

B. Russell was an English philosopher.

+

I. Elba is an English actor.

+
    +
  1. foo
  2. +
  3. bar
  4. +
+
    +
  1. foo
  2. +
  3. bar
  4. +
+. + +does not support an ordinal indicator by default: +. +1º. foo +2º. bar +3º. baz +. +

1º. foo +2º. bar +3º. baz

+. + +supports an ordinal indicator if enabled in options (ordinal): +. +1º. foo +2º. bar +3º. baz +. +
    +
  1. foo
  2. +
  3. bar
  4. +
  5. baz
  6. +
+. + +allows ordinal indicators with Roman numerals (ordinal): +. +IIº. foo +IIIº. bar +IVº. baz +. +
    +
  1. foo
  2. +
  3. bar
  4. +
  5. baz
  6. +
+. + +starts a new list when ordinal indicators are introduced or omitted (ordinal): +. +1) First +1º) First again +2º) Second +1) Another first +. +
    +
  1. First
  2. +
+
    +
  1. First again
  2. +
  3. Second
  4. +
+
    +
  1. Another first
  2. +
+. + +tolerates characters commonly mistaken for ordinal indicators (ordinal): +. +1°. degree sign +2˚. ring above +3ᵒ. modifier letter small o +4º. ordinal indicator +. +
    +
  1. degree sign
  2. +
  3. ring above
  4. +
  5. modifier letter small o
  6. +
  7. ordinal indicator
  8. +
+. diff --git a/tests/test_fancy_list.py b/tests/test_fancy_list.py new file mode 100644 index 0000000..ca24101 --- /dev/null +++ b/tests/test_fancy_list.py @@ -0,0 +1,34 @@ +from pathlib import Path +from textwrap import dedent + +import pytest +from markdown_it import MarkdownIt +from markdown_it.utils import read_fixture_file + +from mdit_py_plugins.fancy_list import fancy_list_plugin + +FIXTURE_PATH = Path(__file__).parent.joinpath("fixtures", "fancy_list.md") + + +def test_plugin_parse(data_regression): + md = MarkdownIt().use(fancy_list_plugin) + tokens = md.parse( + dedent( + """\ + :abc: Content + :def: Content + """ + ) + ) + data_regression.check([t.as_dict() for t in tokens]) + + +@pytest.mark.parametrize("line,title,input,expected", read_fixture_file(FIXTURE_PATH)) +def test_all(line, title, input, expected): + md = MarkdownIt("commonmark").use( + fancy_list_plugin, allow_ordinal="(ordinal)" in title + ) + md.options["xhtmlOut"] = False + text = md.render(input) + print(text) + assert text.rstrip() == expected.rstrip() diff --git a/tests/test_fancy_list/test_plugin_parse.yml b/tests/test_fancy_list/test_plugin_parse.yml new file mode 100644 index 0000000..fcdbc4c --- /dev/null +++ b/tests/test_fancy_list/test_plugin_parse.yml @@ -0,0 +1,84 @@ +- attrs: null + block: true + children: null + content: '' + hidden: false + info: '' + level: 0 + map: + - 0 + - 2 + markup: '' + meta: {} + nesting: 1 + tag: p + type: paragraph_open +- attrs: null + block: true + children: + - attrs: null + block: false + children: null + content: ':abc: Content' + hidden: false + info: '' + level: 0 + map: null + markup: '' + meta: {} + nesting: 0 + tag: '' + type: text + - attrs: null + block: false + children: null + content: '' + hidden: false + info: '' + level: 0 + map: null + markup: '' + meta: {} + nesting: 0 + tag: br + type: softbreak + - attrs: null + block: false + children: null + content: ':def: Content' + hidden: false + info: '' + level: 0 + map: null + markup: '' + meta: {} + nesting: 0 + tag: '' + type: text + content: ':abc: Content + + :def: Content' + hidden: false + info: '' + level: 1 + map: + - 0 + - 2 + markup: '' + meta: {} + nesting: 0 + tag: '' + type: inline +- attrs: null + block: true + children: null + content: '' + hidden: false + info: '' + level: 0 + map: null + markup: '' + meta: {} + nesting: -1 + tag: p + type: paragraph_close