Skip to content

Commit

Permalink
Simplify argument API and update tests and docs
Browse files Browse the repository at this point in the history
  • Loading branch information
facelessuser committed Feb 22, 2023
1 parent cfef8d7 commit 2d4ec56
Show file tree
Hide file tree
Showing 15 changed files with 262 additions and 574 deletions.
2 changes: 2 additions & 0 deletions docs/src/markdown/about/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
- **NEW**: Block attribute specifier is renamed from `attributes` to `attrs`.
- **NEW**: Remove `colon_syntax` option and cement that we are using `///` format moving forward.
- **NEW**: Revise available validators for Block options. Remove unnecessary validators and replace some with new ones.
- **NEW**: Simplify argument API.
- **NEW**: Block extensions can now be registered directly as normal Python Markdown extensions.

## 9.10a3

Expand Down
39 changes: 16 additions & 23 deletions docs/src/markdown/extensions/blocks/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The various different block types are created via the `Block` class. The `Block`
and handling of the various block parts. The basic structure of a block is shown below:

```
/// name | argument(s)
/// name | argument
options: per block options
Markdown content.
Expand Down Expand Up @@ -78,8 +78,8 @@ print(markdown.markdown(MD, extensions=[MyBlockExtension()]))

## The Block Object

The block object allows us to define the name of a block, what arguments and options it accepts, and how we generally
handle the content.
The block object allows us to define the name of a block, whether an argument and/or options are allowed, and how we
generally handle the content.

## Global Options

Expand Down Expand Up @@ -115,33 +115,25 @@ The tracker is accessed by a given `Block` extension via the `self.tracker` attr
dictionary where various keys and values can be stored. This can be used to count blocks on a page or anything else you
can think of.

## Arguments
## Argument

Arguments are used to declare common block specific input for a particular block type. These are often, but not
exclusively, used for things like titles. They are specified on the same line as the initial block deceleration.
The argument is used to declare a common block specific input for a particular block type. This is often, but not
exclusively, used for things like titles. It is specified on the same line as the initial block deceleration.

Blocks are not required to use arguments and are not included by default and must be declared in the class. Arguments
can be `optional`, `required`, or a combination of both.
Blocks are not required to use an argument and it is not required by default and must be declared as either optional or
required in order for the block to accept an argument. An argument is declared by setting `ARGUMENT` to `#!py True` if
it is required, `#!py None` if it is optionally allowed, or `#!py False` if it is not allowed.

The arguments are always parsed as a single string, but if a user declares that multiple arguments are accepted, the
result will be broken by a specified delimiter, the default being a space.

Arguments will be positionally assigned as a list to the instance attribute `self.args` and can be accessed anywhere
after initialization.
The argument is always parsed as a single string, if it is desired to validate the format of the argument or even to
process it as multiple arguments, this can be done in the [`on_parse` event](#on_parse-event).

```python
class MyBlock(Block):
# Name used for the block
NAME = 'my-block'
ARGUMENTS = {'required': 1, 'optional': 1, 'delimiter': ','}
ARGUMENT = True
```

/// tip | Validating Arguments
If you'd like to validate arguments, you can utilize the [`on_parse` event](#on_parse-event). It is also acceptable
manually parse arguments in the `on_parse` event as well. This may be useful if the arguments cannot be parsed with
a simple delimiter.
///

## Options

Options is how a block specifies any per block features via an indented YAML block immediately after the block
Expand Down Expand Up @@ -193,9 +185,10 @@ def on_parse(self) -> bool:
...
```

Executed right after per block argument and option parsing occurs. Arguments and options are accessible via `self.args`
and `self.options`. This gives the extension a chance to perform additional validation or adjustments of the arguments
and options. This also gives the opportunity to initialize class variables based on the per block arguments and options.
Executed right after the per block argument and option parsing occurs. The argument and options are accessible via
`self.argument` and `self.options`. This gives the extension a chance to perform additional validation or adjustments of
the argument and options. This also gives the opportunity to initialize class variables based on the per block argument
and options.

If validation fails, `#!py3 False` should be returned and the block will not be parsed as a generic block.

Expand Down
4 changes: 2 additions & 2 deletions docs/src/markdown/extensions/blocks/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ implemented. Simple paragraphs should not require an empty new line before them,
statement about all blocks. If in doubt, use an empty line before the first content block.
///

Some blocks may implement a special argument in the header for things such as, but not limited to, titles. These
arguments can be optional or sometimes enforced as a requirement. This is up to the given Blocks extension to decide.
Some blocks may implement a special argument in the header for things such as, but not limited to, titles. This
argument can be optional or sometimes enforced as a requirement. This is up to the given Blocks extension to decide.

```
/// note | Did you know?
Expand Down
11 changes: 1 addition & 10 deletions docs/src/markdown/extensions/blocks/plugins/admonition.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,7 @@ meta-plugins get loaded, you can do so by doing the following:

```py3
import markdown
from pymdownx.blocks.admonition import Admonition
md = markdown.Markdown(
extensions=['pymdownx.blocks']
extension_configs={
'pymdownx.blocks': {
# Use {Admonition: {'option1': 'value', ...}} to set block specific global settings
'pymdownx.blocks': {Admonition: None}
}
}
)
md = markdown.Markdown(extensions=['pymdownx.blocks.admonition'])
```

## Usage
Expand Down
8 changes: 1 addition & 7 deletions docs/src/markdown/extensions/blocks/plugins/definition.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,7 @@ meta-plugins get loaded, you can do so by doing the following:

```py3
import markdown
from pymdownx.blocks.definition import Definition
md = markdown.Markdown(
extensions=['pymdownx.blocks']
extension_configs={
'pymdownx.blocks': {Definition: None}
}
)
md = markdown.Markdown(extensions=['pymdownx.blocks.definition'])
```

## Usage
Expand Down
11 changes: 1 addition & 10 deletions docs/src/markdown/extensions/blocks/plugins/details.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,7 @@ meta-plugins get loaded, you can do so by doing the following:

```py3
import markdown
from pymdownx.blocks.details import Details
md = markdown.Markdown(
extensions=['pymdownx.blocks']
extension_configs={
'pymdownx.blocks': {
# Use {Details: {'option1': 'value', ...}} to set block specific global settings
'pymdownx.blocks': {Details: None}
}
}
)
md = markdown.Markdown(extensions=['pymdownx.blocks.details'])
```

## Usage
Expand Down
9 changes: 1 addition & 8 deletions docs/src/markdown/extensions/blocks/plugins/html.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,7 @@ meta-plugins get loaded, you can do so by doing the following:
```py3
import markdown
from pymdownx.blocks.html import HTML
md = markdown.Markdown(
extensions=['pymdownx.blocks']
extension_configs={
'pymdownx.blocks': {
'pymdownx.blocks': {HTML: None}
}
}
)
md = markdown.Markdown(extensions=['pymdownx.blocks.html'])
```

## Usage
Expand Down
9 changes: 1 addition & 8 deletions docs/src/markdown/extensions/blocks/plugins/tab.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,7 @@ meta-plugins get loaded, you can do so by doing the following:

```py3
import markdown
from pymdownx.blocks.tab import Tab
md = markdown.Markdown(
extensions=['pymdownx.blocks']
extension_configs={
# Use {Tab: {'option1': 'value', ...}} to set block specific global settings
'pymdownx.blocks': {Tab: None}
}
)
md = markdown.Markdown(extensions=['pymdownx.blocks.tab'])
```

## Usage
Expand Down
6 changes: 3 additions & 3 deletions pymdownx/blocks/admonition.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class Admonition(Block):
"""

NAME = 'admonition'
ARGUMENTS = {'optional': 1}
ARGUMENT = None
OPTIONS = {
'type': ['', type_html_identifier],
}
Expand All @@ -49,13 +49,13 @@ def on_create(self, parent):
el = etree.SubElement(parent, 'div', {'class': ' '.join(classes)})

# Create the title
if not self.args:
if not self.argument:
if not atype:
title = None
else:
title = atype.capitalize()
else:
title = self.args[0]
title = self.argument

if title is not None:
ad_title = etree.SubElement(el, 'p', {'class': 'admonition-title'})
Expand Down
53 changes: 8 additions & 45 deletions pymdownx/blocks/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ class Block(metaclass=ABCMeta):
NAME = ''

# Instance arguments and options
ARGUMENTS = {}
ARGUMENT = False
OPTIONS = {}

# Extension config
Expand All @@ -195,16 +195,16 @@ def __init__(self, length, tracker, md, config):

# Setup up the argument and options spec
# Note that `attributes` is handled special and we always override it
self.arg_spec = copy.deepcopy(self.ARGUMENTS)
self.arg_spec = self.ARGUMENT
self.option_spec = copy.deepcopy(self.OPTIONS)
if 'attrs' in self.option_spec:
if 'attrs' in self.option_spec: # pragma: no cover
raise ValueError("'attrs' is a reserved option name and cannot be overriden")
self.option_spec['attrs'] = [{}, type_html_attribute_dict]

self.length = length
self.tracker = tracker
self.md = md
self.args = []
self.arguments = []
self.options = {}
self.config = config
self.on_init()
Expand All @@ -217,51 +217,14 @@ def on_markdown(self):

return "auto"

def parse_config(self, args, **options):
def parse_config(self, arg, **options):
"""Parse configuration."""

spec = self.arg_spec
required = spec.get('required', 0)
optional = spec.get('optional', 0)
delim = spec.get('delimiter', ' ')
parsers = spec.get('parsers', [None])
total = required + optional

# If we have arguments but allow none,
# or have no arguments but require at least 1,
# then quit
if (args and total == 0) or (not args and required >= 1):
# Check argument
if (self.arg_spec is not None and ((arg and not self.arg_spec) or (not arg and self.arg_spec))):
return False

# Split arguments if we can have more than 1
if args is not None:
if total > 1:
arguments = type_string_delimiter(delim)(args)
else:
arguments = [args]
else:
arguments = []

length = len(arguments)

# If total number of arguments exceed what is allowed, quit
if length > total or length < required:
return False

# Parse each argument with the appropriate parser, if we run out,
# it is assumed that the last is meant to represent the rest.
# A `None` for a parser means accept as is.
total_parsers = len(parsers)
for e, a in enumerate(arguments):
parser = parsers[e] if e < total_parsers else parsers[-1]
if parser is not None:
try:
a = parser(a)
except Exception:
return False
arguments[e] = a

self.args = arguments
self.argument = arg

# Fill in defaults options
spec = self.option_spec
Expand Down
6 changes: 3 additions & 3 deletions pymdownx/blocks/details.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class Details(Block):

NAME = 'details'

ARGUMENTS = {'optional': 1}
ARGUMENT = None
OPTIONS = {
'open': [False, type_boolean],
'type': ['', type_html_identifier]
Expand Down Expand Up @@ -60,13 +60,13 @@ def on_create(self, parent):
el = etree.SubElement(parent, 'details', attributes)

# Create the summary
if not self.args:
if not self.argument:
if not dtype:
summary = None
else:
summary = dtype.capitalize()
else:
summary = self.args[0]
summary = self.argument

# Create the summary
if summary is not None:
Expand Down
4 changes: 2 additions & 2 deletions pymdownx/blocks/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ class HTML(Block):
"""

NAME = 'html'
ARGUMENTS = {'required': 1}
ARGUMENT = True
OPTIONS = {
'markdown': ['auto', type_string_in(['auto', 'span', 'block', 'raw'])]
}
Expand All @@ -138,7 +138,7 @@ def on_parse(self):
"""Handle argument parsing."""

try:
self.tag, self.attr = parse_selectors(self.args[0])
self.tag, self.attr = parse_selectors(self.argument)
except ValueError:
return False

Expand Down
12 changes: 2 additions & 10 deletions pymdownx/blocks/tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class Tab(Block):

NAME = 'tab'

ARGUMENTS = {'required': 1}
ARGUMENT = True
OPTIONS = {
'new': [False, type_boolean],
'select': [False, type_boolean]
Expand All @@ -96,14 +96,6 @@ def on_init(self):

self.tab_content = None

@classmethod
def on_register(cls, blocks_extension, md, config):
"""Handle registration event."""

if callable(config['slugify']):
slugs = TabbedTreeprocessor(md, config)
md.treeprocessors.register(slugs, 'tab_slugs', 4)

def last_child(self, parent):
"""Return the last child of an `etree` element."""

Expand Down Expand Up @@ -132,7 +124,7 @@ def on_create(self, parent):

new_group = self.options['new']
select = self.options['select']
title = self.args[0] if self.args and self.args[0] else ''
title = self.argument
sibling = self.last_child(parent)
tabbed_set = 'tabbed-set' if not self.alternate_style else 'tabbed-set tabbed-alternate'
index = 0
Expand Down
Loading

0 comments on commit 2d4ec56

Please sign in to comment.