Skip to content

Commit

Permalink
https://github.com/jackdewinter/pymarkdown/issues/817
Browse files Browse the repository at this point in the history
  • Loading branch information
jackdewinter committed Dec 20, 2023
1 parent 95addd1 commit 2e01ecc
Show file tree
Hide file tree
Showing 10 changed files with 1,348 additions and 47 deletions.
70 changes: 63 additions & 7 deletions docs/rules/rule_md029.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
| `md029` |
| `ol-prefix` |

| Autofix Available |
| --- |
| Yes |

## Summary

Ordered list item prefix.
Expand All @@ -13,19 +17,18 @@ Ordered list item prefix.

### Readability

Writing the Ordered List Item prefix using a consistent
form enhances the readability of the document by supplying
consistency. This consistency can also be set to take advantage of
the strengths and weaknesses of the Markdown parser, to ensure the
Writing the Ordered List Item prefix using a consistent form enhances the readability
of the document by supplying consistency. This consistency can also be set to take
advantage of the strengths and weaknesses of the Markdown parser, to ensure the
correct HTML is output.

## Examples

### Failure Scenarios

This rule triggers when an Ordered List Item element does not
start with the number `1` or if any subsequent List Item elements
are not in a numerically increasing order.
By default, this rule uses the `one_or_ordered` configuration. This triggers when
an Ordered List Item element does not start with the number `0` or `1` or if any
subsequent List Item elements are not in a numerically increasing order.

````Markdown
2. second item
Expand Down Expand Up @@ -65,6 +68,44 @@ Instead of the default `one_or_ordered` configuration, the `one` configuration
will trigger is any Ordered List Item element does not start with `1` and the
`zero` configuration will trigger instead if not a `0`.

### Clarifications

The determination of whether the `one` part or the `ordered` part of the configuration
is used for the `one_or_ordered` configuration is done on a list by list basis.
Therefore, the following Markdown will not cause the rule to be triggered:

````Markdown
1. First Line
1. Second Line

text to break up lists

1. First Item
2. Second Item
3. Third Item
````

as there are two lists, each with their own style.

This is also true of nested lists, but with a twist. Given the following:

```Markdown
2. first
1. first-first
1. first-second
2. first-third
3. second
1. second-first
2. second-second
2. second-third
```

this rule triggers on lines 1, 4, and 8. It triggers on line 1 because the list
starts with `2`, line 4 because that inner list's style is `one` and line 8 because
that inner list's style is `ordered`. Note that as a Commonmark compatible parser,
PyMarkdown will only accept an inner list that starts with a `1` to keep alignment
with Commonmark.

## Configuration

| Prefixes |
Expand Down Expand Up @@ -98,3 +139,18 @@ fires for the first non-matching item. As that first item will most
likely provide the pattern for any other items that follow, it should
be enough to call out the first item and let the user fix the rest of
the list.

## Fix Description

In `zero` or `one` configuration, all list item starts will be set to `0` or `1`
respectively. In `ordered` configuration, if the first item does not start with
`0` or `1` it will be set to `1` with any other list items in that list increasing
from that base item in ordered fashion.

With the `one_or_ordered` style, behavior depends on whether the first item's start
is `1` or another number. If it is not `1`, it is set to `1` and the list's style
is considered to be `ordered`. If it is `1`, the determination of the list's style
is delayed to the next list item, determining whether the `one` or `ordered` style
will be followed. If that second list item is `1`, the `one` style is adopted.
Any other number for the second list item causes the `ordered` style to be adopted,
changing that second item's list start to `2`.
8 changes: 4 additions & 4 deletions publish/coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
"projectName": "pymarkdown",
"reportSource": "pytest",
"branchLevel": {
"totalMeasured": 4479,
"totalCovered": 4479
"totalMeasured": 4485,
"totalCovered": 4485
},
"lineLevel": {
"totalMeasured": 18508,
"totalCovered": 18508
"totalMeasured": 18535,
"totalCovered": 18535
}
}

18 changes: 17 additions & 1 deletion publish/test-results.json
Original file line number Diff line number Diff line change
Expand Up @@ -1364,7 +1364,7 @@
},
{
"name": "test.rules.test_md029",
"totalTests": 30,
"totalTests": 55,
"failedTests": 0,
"errorTests": 0,
"skippedTests": 0,
Expand Down Expand Up @@ -1674,6 +1674,14 @@
"skippedTests": 0,
"elapsedTimeInMilliseconds": 0
},
{
"name": "test.tokens.test_list_start_markdown_token",
"totalTests": 1,
"failedTests": 0,
"errorTests": 0,
"skippedTests": 0,
"elapsedTimeInMilliseconds": 0
},
{
"name": "test.tokens.test_markdown_token",
"totalTests": 3,
Expand All @@ -1682,6 +1690,14 @@
"skippedTests": 0,
"elapsedTimeInMilliseconds": 0
},
{
"name": "test.tokens.test_new_list_item_markdown_token",
"totalTests": 1,
"failedTests": 0,
"errorTests": 0,
"skippedTests": 0,
"elapsedTimeInMilliseconds": 0
},
{
"name": "test.tokens.test_plugin_scan_context",
"totalTests": 3,
Expand Down
4 changes: 4 additions & 0 deletions pymarkdown/file_scan_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,10 @@ def __process_file_fix_lines(

# Due to context required to process the line requirements, we need go
# through all the tokens first, before processing the lines.
#
# Basically, to allow any of the rules to build context applicable to
# the line being scanned, we rescan the tokens to present an updated
# picture of the tokens.
for next_token in actual_tokens:
POGGER.info("Processing token: $", next_token)
self.__plugins.next_token(context, next_token)
Expand Down
89 changes: 58 additions & 31 deletions pymarkdown/plugins/rule_md_029.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"""
from typing import List, Optional, Tuple, cast

from pymarkdown.plugin_manager.plugin_details import PluginDetails
from pymarkdown.plugin_manager.plugin_details import PluginDetailsV2
from pymarkdown.plugin_manager.plugin_scan_context import PluginScanContext
from pymarkdown.plugin_manager.rule_plugin import RulePlugin
from pymarkdown.tokens.list_start_markdown_token import ListStartMarkdownToken
Expand Down Expand Up @@ -35,19 +35,19 @@ def __init__(self) -> None:
self.__list_stack: List[MarkdownToken] = []
self.__ordered_list_stack: List[Tuple[Optional[str], Optional[int]]] = []

def get_details(self) -> PluginDetails:
def get_details(self) -> PluginDetailsV2:
"""
Get the details for the plugin.
"""
return PluginDetails(
return PluginDetailsV2(
plugin_name="ol-prefix",
plugin_id="MD029",
plugin_enabled_by_default=True,
plugin_description="Ordered list item prefix",
plugin_version="0.5.0",
plugin_interface_version=1,
plugin_url="https://github.com/jackdewinter/pymarkdown/blob/main/docs/rules/rule_md029.md",
plugin_configuration="style",
plugin_supports_fix=True,
)

@classmethod
Expand All @@ -72,6 +72,25 @@ def starting_new_file(self) -> None:
self.__list_stack = []
self.__ordered_list_stack = []

def __calculate_match_info(
self, list_style: str, initial: bool, last_known_number: Optional[int]
) -> Tuple[str, int]:
if list_style == RuleMd029.__ordered_style:
style = "1/2/3"
if initial:
expected_number = 1
else:
assert last_known_number is not None
expected_number = last_known_number + 1
elif list_style == RuleMd029.__one_style:
style = "1/1/1"
expected_number = 1
else:
assert list_style == RuleMd029.__zero_style
style = "0/0/0"
expected_number = 0
return style, expected_number

def __match_first_item(
self, context: PluginScanContext, token: MarkdownToken
) -> Tuple[Optional[str], Optional[int]]:
Expand All @@ -91,19 +110,25 @@ def __match_first_item(
is_valid = last_known_number == 0
# print(f"list_style={list_style},last_known_number={last_known_number},is_valid={is_valid}")
if not is_valid:
if list_style == RuleMd029.__ordered_style:
style = "1/2/3"
elif list_style == RuleMd029.__one_style:
style = "1/1/1"
else:
assert list_style == RuleMd029.__zero_style
style = "0/0/0"
expected_number = 0 if list_style == RuleMd029.__zero_style else 1
extra_error_information = f"Expected: {expected_number}; Actual: {last_known_number}; Style: {style}"
self.report_next_token_error(
context, token, extra_error_information=extra_error_information
assert list_style is not None
style, expected_number = self.__calculate_match_info(
list_style, True, last_known_number
)
list_style, last_known_number = (None, None)
extra_error_information = f"Expected: {expected_number}; Actual: {last_known_number}; Style: {style}"
if context.in_fix_mode:
self.register_fix_token_request(
context,
token,
"next_token",
"list_start_content",
str(expected_number),
)
last_known_number = expected_number
else:
self.report_next_token_error(
context, token, extra_error_information=extra_error_information
)
list_style, last_known_number = (None, None)
return list_style, last_known_number

def __match_non_first_items(
Expand Down Expand Up @@ -134,24 +159,26 @@ def __match_non_first_items(
assert last_known_number is not None
is_valid = new_number == last_known_number + 1
if not is_valid:
if list_style == RuleMd029.__ordered_style:
style = "1/2/3"
assert last_known_number is not None
expected_number = last_known_number + 1
elif list_style == RuleMd029.__one_style:
style = "1/1/1"
expected_number = 1
else:
assert list_style == RuleMd029.__zero_style
style = "0/0/0"
expected_number = 0
style, expected_number = self.__calculate_match_info(
list_style, False, last_known_number
)
extra_error_information = (
f"Expected: {expected_number}; Actual: {new_number}; Style: {style}"
)
self.report_next_token_error(
context, token, extra_error_information=extra_error_information
)
list_style, new_number = (None, None)
if context.in_fix_mode:
self.register_fix_token_request(
context,
token,
"next_token",
"list_start_content",
str(expected_number),
)
new_number = expected_number
else:
self.report_next_token_error(
context, token, extra_error_information=extra_error_information
)
list_style, new_number = (None, None)
last_known_number = new_number
return list_style, last_known_number

Expand Down
12 changes: 11 additions & 1 deletion pymarkdown/tokens/list_start_markdown_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

# pylint: disable=too-many-instance-attributes
import logging
from typing import Optional
from typing import Optional, Union

from typing_extensions import override

from pymarkdown.general.parser_helper import ParserHelper
from pymarkdown.general.parser_logger import ParserLogger
Expand Down Expand Up @@ -209,5 +211,13 @@ def set_extracted_whitespace(self, new_whitespace: str) -> None:
self.__extracted_whitespace = new_whitespace
self.__compose_extra_data_field()

@override
def _modify_token(self, field_name: str, field_value: Union[str, int]) -> bool:
if field_name == "list_start_content" and isinstance(field_value, str):
self.__list_start_content = field_value
self.__compose_extra_data_field()
return True
return False


# pylint: enable=too-many-instance-attributes
31 changes: 28 additions & 3 deletions pymarkdown/tokens/new_list_item_markdown_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
Module to provide for an encapsulation of the new list item element.
"""

from typing import Union

from typing_extensions import override

from pymarkdown.general.parser_helper import ParserHelper
from pymarkdown.general.position_marker import PositionMarker
from pymarkdown.tokens.container_markdown_token import ContainerMarkdownToken
Expand Down Expand Up @@ -33,9 +37,7 @@ def __init__(
ContainerMarkdownToken.__init__(
self,
MarkdownToken._token_new_list_item,
MarkdownToken.extra_data_separator.join(
[str(indent_level), extracted_whitespace, list_start_content]
),
self.__compose_extra_data_field(),
position_marker=position_marker,
)

Expand Down Expand Up @@ -70,6 +72,29 @@ def list_start_content(self) -> str:
"""
return self.__list_start_content

@override
def _modify_token(self, field_name: str, field_value: Union[str, int]) -> bool:
if field_name == "list_start_content" and isinstance(field_value, str):
self.__list_start_content = field_value
self.__compose_extra_data_field()
return True
return False

def __compose_extra_data_field(self) -> str:
"""
Compose the object's self.extra_data field from the local object's variables.
"""

new_field = MarkdownToken.extra_data_separator.join(
[
str(self.__indent_level),
self.__extracted_whitespace,
self.__list_start_content,
]
)
self._set_extra_data(new_field)
return new_field

@staticmethod
def register_for_html_transform(
register_handlers: RegisterHtmlTransformHandlersProtocol,
Expand Down
Loading

0 comments on commit 2e01ecc

Please sign in to comment.