From ed2715f0bc30eda7cb7a1410dcf297ff14904ad0 Mon Sep 17 00:00:00 2001 From: Kyle King Date: Sun, 27 Oct 2024 07:24:35 -0600 Subject: [PATCH] feat(#40): support listed code blocks (#41) --- mdformat_mkdocs/_helpers.py | 2 +- mdformat_mkdocs/_normalize_list.py | 83 +- .../mdit_plugins/_pymd_abbreviations.py | 2 +- .../__snapshots__/test_parsed_result.ambr | 739 +++++++++++++++++- tests/format/fixtures/parsed_result.md | 30 + tests/format/fixtures/text.md | 33 + tests/helpers.py | 2 +- 7 files changed, 858 insertions(+), 33 deletions(-) diff --git a/mdformat_mkdocs/_helpers.py b/mdformat_mkdocs/_helpers.py index e9e86d2..eb9b8e1 100644 --- a/mdformat_mkdocs/_helpers.py +++ b/mdformat_mkdocs/_helpers.py @@ -40,5 +40,5 @@ def separate_indent(line: str) -> tuple[str, str]: """ re_indent = re.compile(r"(?P\s*)(?P[^\s]?.*)") match = re_indent.match(line) - assert match is not None # for pyright + assert match # for pyright return (match["indent"], match["content"]) diff --git a/mdformat_mkdocs/_normalize_list.py b/mdformat_mkdocs/_normalize_list.py index 35ffc16..7aa9fdd 100644 --- a/mdformat_mkdocs/_normalize_list.py +++ b/mdformat_mkdocs/_normalize_list.py @@ -39,11 +39,10 @@ def map_lookback( """ results = [initial] - if len(items) > 1: - for item in items[1:]: - result = func(results[-1], item) - results.append(result) - return results + for item in items: + result = func(results[-1], item) + results.append(result) + return results[1:] # ====================================================================================== @@ -59,6 +58,8 @@ def map_lookback( class Syntax(Enum): """Non-standard line types.""" + CODE_BULLETED = "CODE_BULLETED" + CODE_NUMBERED = "CODE_NUMBERED" LIST_BULLETED = "LIST_BULLETED" LIST_NUMBERED = "LIST_NUMBERED" START_MARKED = "START_MARKED" @@ -74,11 +75,10 @@ def from_content(cls, content: str) -> Syntax | None: """ if match := RE_LIST_ITEM.fullmatch(content): - return ( - cls.LIST_NUMBERED - if match["bullet"] not in {"-", "*"} - else cls.LIST_BULLETED - ) + is_numbered = match["bullet"] not in {"-", "*"} + if match["item"].startswith("```"): + return cls.CODE_NUMBERED if is_numbered else cls.CODE_BULLETED + return cls.LIST_NUMBERED if is_numbered else cls.LIST_BULLETED if any(content.startswith(f"{marker} ") for marker in MARKERS): return cls.START_MARKED if content.startswith("```"): @@ -88,6 +88,10 @@ def from_content(cls, content: str) -> Syntax | None: return None +SYNTAX_CODE_LIST = {Syntax.CODE_BULLETED, Syntax.CODE_NUMBERED} +"""The start of a code block, which is also the start of a list.""" + + class ParsedLine(NamedTuple): """Parsed Line of text.""" @@ -114,7 +118,11 @@ def _is_parent_line(prev_line: LineResult, parsed: ParsedLine) -> bool: def _is_peer_list_line(prev_line: LineResult, parsed: ParsedLine) -> bool: """Return True if two list items share the same scope and level.""" - list_types = {Syntax.LIST_BULLETED, Syntax.LIST_NUMBERED} + list_types = { + *SYNTAX_CODE_LIST, + Syntax.LIST_BULLETED, + Syntax.LIST_NUMBERED, + } return ( parsed.syntax in list_types and prev_line.parsed.syntax in list_types @@ -204,7 +212,10 @@ class BlockIndent(NamedTuple): def _parse_code_block(last: BlockIndent | None, line: LineResult) -> BlockIndent | None: """Identify fenced or indented sections internally referred to as 'code blocks'.""" result = last - if line.parsed.syntax == Syntax.EDGE_CODE: + if line.parsed.syntax in { + *SYNTAX_CODE_LIST, + Syntax.EDGE_CODE, + }: # On first edge, start tracking a code block # on the second edge, stop tracking result = ( @@ -260,7 +271,11 @@ def _parse_semantic_indent( # PLANNED: This works, but is very confusing line, code_indent = tin - if not line.parsed.content or code_indent is not None: + if ( + not line.parsed.content + or code_indent is not None + or line.parsed.syntax in SYNTAX_CODE_LIST + ): result = SemanticIndent.EMPTY elif line.parsed.syntax == Syntax.LIST_BULLETED: @@ -305,6 +320,12 @@ def _format_new_indent(line: LineResult, block_indent: BlockIndent | None) -> st line_indent=line.parsed.indent, ) result = DEFAULT_INDENT * depth + extra_indent + elif line.parents and line.parents[-1].syntax in SYNTAX_CODE_LIST: + depth = len(line.parents) - 1 + match = RE_LIST_ITEM.fullmatch(line.parents[-1].content) + assert match # for pyright + extra_indent = " " * (len(match["bullet"]) + 1) + result = DEFAULT_INDENT * depth + extra_indent else: result = DEFAULT_INDENT * len(line.parents) return result @@ -313,9 +334,9 @@ def _format_new_indent(line: LineResult, block_indent: BlockIndent | None) -> st class ParsedText(NamedTuple): """Intermediary result of parsing the text.""" - lines: list[LineResult] new_lines: list[tuple[str, str]] # Used only for debugging purposes + debug_original_lines: list[LineResult] debug_block_indents: list[BlockIndent | None] @@ -327,7 +348,7 @@ def _format_new_content(line: LineResult, inc_numbers: bool, is_code: bool) -> s Syntax.LIST_NUMBERED, }: list_match = RE_LIST_ITEM.fullmatch(line.parsed.content) - assert list_match is not None # for pyright + assert list_match # for pyright new_bullet = "-" if line.parsed.syntax == Syntax.LIST_NUMBERED: first_peer = ( @@ -341,6 +362,25 @@ def _format_new_content(line: LineResult, inc_numbers: bool, is_code: bool) -> s return new_content +def _insert_newlines( + parsed_lines: list[LineResult], + zipped_lines: list[tuple[str, str]], +) -> list[tuple[str, str]]: + """Extend zipped_lines with newlines if necessary.""" + newline = ("", "") + new_lines: list[tuple[str, str]] = [] + for line, zip_line in zip_equal(parsed_lines, zipped_lines): + new_lines.append(zip_line) + if ( + line.parsed.syntax == Syntax.EDGE_CODE + and line.parents + and line.parents[-1].syntax in SYNTAX_CODE_LIST + ): + new_lines.append(newline) + + return new_lines + + def parse_text(*, text: str, inc_numbers: bool, use_sem_break: bool) -> ParsedText: """Post-processor to normalize lists. @@ -354,7 +394,7 @@ def parse_text(*, text: str, inc_numbers: bool, use_sem_break: bool) -> ParsedTe code_indents = map_lookback(_parse_code_block, lines, None) html_indents = [ # Any indents initiated from within a `code_block_indents` should be ignored - indent if indent and code_indents[indent.start_line] is None else None + indent if (indent and code_indents[indent.start_line] is None) else None for indent in map_lookback(_parse_html_line, lines, None) ] # When both, code_indents take precedence @@ -379,9 +419,10 @@ def parse_text(*, text: str, inc_numbers: bool, use_sem_break: bool) -> ParsedTe ), ] + new_lines = _insert_newlines(lines, [*zip_equal(new_indents, new_contents)]) return ParsedText( - lines=lines, - new_lines=[*zip_equal(new_indents, new_contents)], + new_lines=new_lines, + debug_original_lines=lines, debug_block_indents=block_indents, ) @@ -390,9 +431,9 @@ def parse_text(*, text: str, inc_numbers: bool, use_sem_break: bool) -> ParsedTe # Outputs string result -def _join(parsed_text: ParsedText) -> str: +def _join(*, new_lines: list[tuple[str, str]]) -> str: """Join ParsedText into a single string representation.""" - new_indents, new_contents = unzip(parsed_text.new_lines) + new_indents, new_contents = unzip(new_lines) new_indents_iter = new_indents @@ -434,4 +475,4 @@ def normalize_list( inc_numbers=inc_numbers, use_sem_break=check_if_align_semantic_breaks_in_lists(), ) - return _join(parsed_text=parsed_text) + return _join(new_lines=parsed_text.new_lines) diff --git a/mdformat_mkdocs/mdit_plugins/_pymd_abbreviations.py b/mdformat_mkdocs/mdit_plugins/_pymd_abbreviations.py index 8f9f602..2bc532f 100644 --- a/mdformat_mkdocs/mdit_plugins/_pymd_abbreviations.py +++ b/mdformat_mkdocs/mdit_plugins/_pymd_abbreviations.py @@ -69,7 +69,7 @@ def _pymd_abbreviations( matches = [match] max_line = start_line - while match is not None: + while match: if max_line == end_line: break if match := _new_match(state, max_line + 1): diff --git a/tests/format/__snapshots__/test_parsed_result.ambr b/tests/format/__snapshots__/test_parsed_result.ambr index 2541fa4..eca47b2 100644 --- a/tests/format/__snapshots__/test_parsed_result.ambr +++ b/tests/format/__snapshots__/test_parsed_result.ambr @@ -6,7 +6,7 @@ None, None, ]), - lines=list([ + debug_original_lines=list([ LineResult( parents=list([ ]), @@ -86,7 +86,7 @@ None, None, ]), - lines=list([ + debug_original_lines=list([ LineResult( parents=list([ ]), @@ -201,7 +201,7 @@ None, None, ]), - lines=list([ + debug_original_lines=list([ LineResult( parents=list([ ]), @@ -323,7 +323,7 @@ None, None, ]), - lines=list([ + debug_original_lines=list([ LineResult( parents=list([ ]), @@ -398,7 +398,7 @@ start_line=3, ), ]), - lines=list([ + debug_original_lines=list([ LineResult( parents=list([ ]), @@ -630,7 +630,7 @@ ), None, ]), - lines=list([ + debug_original_lines=list([ LineResult( parents=list([ ]), @@ -1032,7 +1032,7 @@ None, None, ]), - lines=list([ + debug_original_lines=list([ LineResult( parents=list([ ]), @@ -1317,7 +1317,7 @@ ), None, ]), - lines=list([ + debug_original_lines=list([ LineResult( parents=list([ ]), @@ -1537,7 +1537,7 @@ None, None, ]), - lines=list([ + debug_original_lines=list([ LineResult( parents=list([ ]), @@ -1699,3 +1699,724 @@ ]), ) # --- +# name: test_parsed_result[Support inline bulleted code (https://github.com/KyleKing/mdformat-mkdocs/issues/40)] + ParsedText( + debug_block_indents=list([ + BlockIndent( + indent_depth=0, + kind='code', + raw_indent='', + start_line=0, + ), + BlockIndent( + indent_depth=0, + kind='code', + raw_indent='', + start_line=0, + ), + BlockIndent( + indent_depth=0, + kind='code', + raw_indent='', + start_line=0, + ), + None, + None, + BlockIndent( + indent_depth=1, + kind='code', + raw_indent=' ', + start_line=5, + ), + BlockIndent( + indent_depth=1, + kind='code', + raw_indent=' ', + start_line=5, + ), + BlockIndent( + indent_depth=1, + kind='code', + raw_indent=' ', + start_line=5, + ), + BlockIndent( + indent_depth=1, + kind='code', + raw_indent=' ', + start_line=5, + ), + None, + None, + BlockIndent( + indent_depth=2, + kind='code', + raw_indent=' ', + start_line=11, + ), + BlockIndent( + indent_depth=2, + kind='code', + raw_indent=' ', + start_line=11, + ), + None, + None, + BlockIndent( + indent_depth=1, + kind='code', + raw_indent=' ', + start_line=15, + ), + BlockIndent( + indent_depth=1, + kind='code', + raw_indent=' ', + start_line=15, + ), + BlockIndent( + indent_depth=1, + kind='code', + raw_indent=' ', + start_line=15, + ), + None, + None, + BlockIndent( + indent_depth=0, + kind='code', + raw_indent='', + start_line=20, + ), + BlockIndent( + indent_depth=0, + kind='code', + raw_indent='', + start_line=20, + ), + BlockIndent( + indent_depth=0, + kind='code', + raw_indent='', + start_line=20, + ), + BlockIndent( + indent_depth=0, + kind='code', + raw_indent='', + start_line=20, + ), + None, + ]), + debug_original_lines=list([ + LineResult( + parents=list([ + ]), + parsed=ParsedLine( + content='- ```python', + indent='', + line_num=0, + syntax=, + ), + prev_list_peers=list([ + ]), + ), + LineResult( + parents=list([ + ParsedLine( + content='- ```python', + indent='', + line_num=0, + syntax=, + ), + ]), + parsed=ParsedLine( + content='for idx in range(10):', + indent=' ', + line_num=1, + syntax=None, + ), + prev_list_peers=list([ + ]), + ), + LineResult( + parents=list([ + ParsedLine( + content='- ```python', + indent='', + line_num=0, + syntax=, + ), + ParsedLine( + content='for idx in range(10):', + indent=' ', + line_num=1, + syntax=None, + ), + ]), + parsed=ParsedLine( + content='print(idx)', + indent=' ', + line_num=2, + syntax=None, + ), + prev_list_peers=list([ + ]), + ), + LineResult( + parents=list([ + ParsedLine( + content='- ```python', + indent='', + line_num=0, + syntax=, + ), + ]), + parsed=ParsedLine( + content='```', + indent=' ', + line_num=3, + syntax=, + ), + prev_list_peers=list([ + ]), + ), + LineResult( + parents=list([ + ]), + parsed=ParsedLine( + content='', + indent='', + line_num=4, + syntax=None, + ), + prev_list_peers=list([ + ]), + ), + LineResult( + parents=list([ + ParsedLine( + content='- ```python', + indent='', + line_num=0, + syntax=, + ), + ]), + parsed=ParsedLine( + content='1. ```bash', + indent=' ', + line_num=5, + syntax=, + ), + prev_list_peers=list([ + ]), + ), + LineResult( + parents=list([ + ParsedLine( + content='- ```python', + indent='', + line_num=0, + syntax=, + ), + ParsedLine( + content='1. ```bash', + indent=' ', + line_num=5, + syntax=, + ), + ]), + parsed=ParsedLine( + content='for match in %(ls);', + indent=' ', + line_num=6, + syntax=None, + ), + prev_list_peers=list([ + ]), + ), + LineResult( + parents=list([ + ParsedLine( + content='- ```python', + indent='', + line_num=0, + syntax=, + ), + ParsedLine( + content='1. ```bash', + indent=' ', + line_num=5, + syntax=, + ), + ParsedLine( + content='for match in %(ls);', + indent=' ', + line_num=6, + syntax=None, + ), + ]), + parsed=ParsedLine( + content='do echo match;', + indent=' ', + line_num=7, + syntax=None, + ), + prev_list_peers=list([ + ]), + ), + LineResult( + parents=list([ + ParsedLine( + content='- ```python', + indent='', + line_num=0, + syntax=, + ), + ParsedLine( + content='1. ```bash', + indent=' ', + line_num=5, + syntax=, + ), + ]), + parsed=ParsedLine( + content='done', + indent=' ', + line_num=8, + syntax=None, + ), + prev_list_peers=list([ + ]), + ), + LineResult( + parents=list([ + ParsedLine( + content='- ```python', + indent='', + line_num=0, + syntax=, + ), + ParsedLine( + content='1. ```bash', + indent=' ', + line_num=5, + syntax=, + ), + ]), + parsed=ParsedLine( + content='```', + indent=' ', + line_num=9, + syntax=, + ), + prev_list_peers=list([ + ]), + ), + LineResult( + parents=list([ + ]), + parsed=ParsedLine( + content='', + indent='', + line_num=10, + syntax=None, + ), + prev_list_peers=list([ + ]), + ), + LineResult( + parents=list([ + ParsedLine( + content='- ```python', + indent='', + line_num=0, + syntax=, + ), + ParsedLine( + content='1. ```bash', + indent=' ', + line_num=5, + syntax=, + ), + ]), + parsed=ParsedLine( + content='- ```powershell', + indent=' ', + line_num=11, + syntax=, + ), + prev_list_peers=list([ + ]), + ), + LineResult( + parents=list([ + ParsedLine( + content='- ```python', + indent='', + line_num=0, + syntax=, + ), + ParsedLine( + content='1. ```bash', + indent=' ', + line_num=5, + syntax=, + ), + ParsedLine( + content='- ```powershell', + indent=' ', + line_num=11, + syntax=, + ), + ]), + parsed=ParsedLine( + content="iex (new-object net.webclient).DownloadString('https://get.scoop.sh')", + indent=' ', + line_num=12, + syntax=None, + ), + prev_list_peers=list([ + ]), + ), + LineResult( + parents=list([ + ParsedLine( + content='- ```python', + indent='', + line_num=0, + syntax=, + ), + ParsedLine( + content='1. ```bash', + indent=' ', + line_num=5, + syntax=, + ), + ParsedLine( + content='- ```powershell', + indent=' ', + line_num=11, + syntax=, + ), + ]), + parsed=ParsedLine( + content='```', + indent=' ', + line_num=13, + syntax=, + ), + prev_list_peers=list([ + ]), + ), + LineResult( + parents=list([ + ]), + parsed=ParsedLine( + content='', + indent='', + line_num=14, + syntax=None, + ), + prev_list_peers=list([ + ]), + ), + LineResult( + parents=list([ + ParsedLine( + content='- ```python', + indent='', + line_num=0, + syntax=, + ), + ]), + parsed=ParsedLine( + content='```txt', + indent=' ', + line_num=15, + syntax=, + ), + prev_list_peers=list([ + ]), + ), + LineResult( + parents=list([ + ParsedLine( + content='- ```python', + indent='', + line_num=0, + syntax=, + ), + ]), + parsed=ParsedLine( + content='- First Line', + indent=' ', + line_num=16, + syntax=, + ), + prev_list_peers=list([ + ParsedLine( + content='1. ```bash', + indent=' ', + line_num=5, + syntax=, + ), + ]), + ), + LineResult( + parents=list([ + ParsedLine( + content='- ```python', + indent='', + line_num=0, + syntax=, + ), + ParsedLine( + content='- First Line', + indent=' ', + line_num=16, + syntax=, + ), + ]), + parsed=ParsedLine( + content='Second Line', + indent=' ', + line_num=17, + syntax=None, + ), + prev_list_peers=list([ + ]), + ), + LineResult( + parents=list([ + ParsedLine( + content='- ```python', + indent='', + line_num=0, + syntax=, + ), + ]), + parsed=ParsedLine( + content='```', + indent=' ', + line_num=18, + syntax=, + ), + prev_list_peers=list([ + ]), + ), + LineResult( + parents=list([ + ]), + parsed=ParsedLine( + content='', + indent='', + line_num=19, + syntax=None, + ), + prev_list_peers=list([ + ]), + ), + LineResult( + parents=list([ + ]), + parsed=ParsedLine( + content='```yaml', + indent='', + line_num=20, + syntax=, + ), + prev_list_peers=list([ + ]), + ), + LineResult( + parents=list([ + ]), + parsed=ParsedLine( + content='repos:', + indent='', + line_num=21, + syntax=None, + ), + prev_list_peers=list([ + ]), + ), + LineResult( + parents=list([ + ParsedLine( + content='repos:', + indent='', + line_num=21, + syntax=None, + ), + ]), + parsed=ParsedLine( + content='- repo: https://github.com/psf/black', + indent=' ', + line_num=22, + syntax=, + ), + prev_list_peers=list([ + ]), + ), + LineResult( + parents=list([ + ParsedLine( + content='repos:', + indent='', + line_num=21, + syntax=None, + ), + ParsedLine( + content='- repo: https://github.com/psf/black', + indent=' ', + line_num=22, + syntax=, + ), + ]), + parsed=ParsedLine( + content='rev: v24.4', + indent=' ', + line_num=23, + syntax=None, + ), + prev_list_peers=list([ + ]), + ), + LineResult( + parents=list([ + ]), + parsed=ParsedLine( + content='```', + indent='', + line_num=24, + syntax=, + ), + prev_list_peers=list([ + ]), + ), + ]), + new_lines=list([ + tuple( + '', + '- ```python', + ), + tuple( + ' ', + 'for idx in range(10):', + ), + tuple( + ' ', + 'print(idx)', + ), + tuple( + ' ', + '```', + ), + tuple( + '', + '', + ), + tuple( + '', + '', + ), + tuple( + ' ', + '1. ```bash', + ), + tuple( + ' ', + 'for match in %(ls);', + ), + tuple( + ' ', + 'do echo match;', + ), + tuple( + ' ', + 'done', + ), + tuple( + ' ', + '```', + ), + tuple( + '', + '', + ), + tuple( + '', + '', + ), + tuple( + ' ', + '- ```powershell', + ), + tuple( + ' ', + "iex (new-object net.webclient).DownloadString('https://get.scoop.sh')", + ), + tuple( + ' ', + '```', + ), + tuple( + '', + '', + ), + tuple( + '', + '', + ), + tuple( + ' ', + '```txt', + ), + tuple( + '', + '', + ), + tuple( + ' ', + '- First Line', + ), + tuple( + ' ', + 'Second Line', + ), + tuple( + ' ', + '```', + ), + tuple( + '', + '', + ), + tuple( + '', + '', + ), + tuple( + '', + '```yaml', + ), + tuple( + '', + 'repos:', + ), + tuple( + ' ', + '- repo: https://github.com/psf/black', + ), + tuple( + ' ', + 'rev: v24.4', + ), + tuple( + '', + '```', + ), + ]), + ) +# --- diff --git a/tests/format/fixtures/parsed_result.md b/tests/format/fixtures/parsed_result.md index 2efb38b..49903fa 100644 --- a/tests/format/fixtures/parsed_result.md +++ b/tests/format/fixtures/parsed_result.md @@ -110,3 +110,33 @@ Do not format code (https://github.com/KyleKing/mdformat-mkdocs/issues/36). Also ``` . . + +Support inline bulleted code (https://github.com/KyleKing/mdformat-mkdocs/issues/40) +. +- ```python + for idx in range(10): + print(idx) + ``` + + 1. ```bash + for match in %(ls); + do echo match; + done + ``` + + - ```powershell + iex (new-object net.webclient).DownloadString('https://get.scoop.sh') + ``` + + ```txt + - First Line + Second Line + ``` + +```yaml +repos: + - repo: https://github.com/psf/black + rev: v24.4 +``` +. +. diff --git a/tests/format/fixtures/text.md b/tests/format/fixtures/text.md index 89b6dd0..4b57a2d 100644 --- a/tests/format/fixtures/text.md +++ b/tests/format/fixtures/text.md @@ -1617,3 +1617,36 @@ Broken formatting (https://github.com/KyleKing/mdformat-mkdocs/issues/35) - id: black ``` . + +Support inline bulleted code (https://github.com/KyleKing/mdformat-mkdocs/issues/40) +. +- ```python + for idx in range(10): + print(idx) + ``` + + 1. ```bash + for match in %(ls); + do echo match; + done + ``` + + - ```powershell + iex (new-object net.webclient).DownloadString('https://get.scoop.sh') + ``` +. +- ```python + for idx in range(10): + print(idx) + ``` + + 1. ```bash + for match in %(ls); + do echo match; + done + ``` + + - ```powershell + iex (new-object net.webclient).DownloadString('https://get.scoop.sh') + ``` +. diff --git a/tests/helpers.py b/tests/helpers.py index ea29807..3897751 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -16,7 +16,7 @@ def separate_indent(line: str) -> tuple[str, str]: """ re_indent = re.compile(r"(?P\s*)(?P[^\s]?.*)") match = re_indent.match(line) - assert match is not None # for pyright + assert match # for pyright return (match["indent"], match["content"])