Skip to content

Commit

Permalink
Update blocks and add type_any
Browse files Browse the repository at this point in the history
  • Loading branch information
facelessuser committed Feb 22, 2023
1 parent b95f2b6 commit cfef8d7
Show file tree
Hide file tree
Showing 12 changed files with 132 additions and 173 deletions.
180 changes: 90 additions & 90 deletions docs/src/markdown/extensions/blocks/api.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Blocks Plugin API
# Blocks Extension API

## Block Structure

The various different block types are created via `Block` plugins. The basic structure of a block is shown below:
The various different block types are created via the `Block` class. The `Block` class can define all the various parts
and handling of the various block parts. The basic structure of a block is shown below:

```
/// name | argument(s)
Expand All @@ -12,67 +13,107 @@ Markdown content.
///
```

In addition, there are sometimes global options for a specific block type are supplied at registration time. Such
options would control features for the specific block type they are defined for and would affect all the blocks of that
type globally.
## Block Extension Anatomy

## Creating a Plugin
Normally with Python Markdown, you'd create a processor derived from the various processor types available in the
library. You'd then derive an extension from `markdown.Extension` that would register the the processor. Block
extensions are very similar.

To create a new Blocks plugin, you must derive a new class from the `Block` class.
A Block extension is comprised of two parts: the `Block` object and the `BlocksExtension`. It should be noted that we do
not use `markdown.Extension`, but `BlocksExtension` which is derived from it. This is done so we can abstract away the
management of all the various registered `Block` extensions. It is important to note though that when using the
`BlocksExtension` that we do not override the `extendMarkdown` method, but instead override `extendMarkdownBlocks`. In
all other respects, `BlocksExtension` is just like `markdown.Extension` and you can register traditional processors via
the `md` object or register `Block` objects via the `block_mgr` object.

```py
from pymdownx.blocks.block import Block

class MyBlock(Block):
...
```

You will need to set up related class attributes. At a minimum, `NAME` must be specified with a unique name, and you
must define the `on_create` method which should return the element under which all the content in that block will live.

Once created, we can include the `pymdownx.blocks` extension and specify our plugin with any others that we want to
have enabled. Below, we only specify our new block.
Below we have the very bare minimum required to create an extension.

```py
import markdown
import xml.etree.ElementTree as etree
from pymdownx.blocks import BlocksExtension
from pymdownx.blocks.block import Block
import xml.etree.ElementTree as etree

class MyBlock(Block):
# Name used for the block
NAME = 'my-block'

def on_create(self, parent):
"""Create the needed element and return it."""

return etree.SubElement(parent, 'div')


class MyBlockExtension(BlocksExtension):

def extendMarkdownBlocks(self, md, block_mgr):

block_mgr.register(MyBlock, self.getConfigs())


def makeExtension(*args, **kwargs):
"""Return extension."""

return MyBlockExtension(*args, **kwargs)
```

Then we can register and run it:

```py
import markdown

MD = """
/// my-block
content
///
"""

print(markdown.markdown(
MD,
extensions=['pymdownx.blocks'],
extension_configs={
'pymdownx.blocks': {
'blocks': {MyBlock: None}
}
}
))
print(markdown.markdown(MD, extensions=[MyBlockExtension()]))
```

<div class="result" markdown>

/// html | div.result
```html
<div>
<p>content</p>
</div>
```
///

</div>
## 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.

## Global Options

Global options are often set in the `BlocksExtension` object like traditional extensions. They are meant to globally
control the behavior of a specific block type:

```python
class AdmonitionExtension(BlocksExtension):
"""Admonition Blocks Extension."""

def __init__(self, *args, **kwargs):
"""Initialize."""

self.config = {
"types": [
['note', 'attention', 'caution', 'danger', 'error', 'tip', 'hint', 'warning'],
"Generate Admonition block extensions for the given types."
]
}

super().__init__(*args, **kwargs)
```

These options are available in the instantiated `Block` object via the `self.config` attribute.

## Tracking Data Across Blocks

There are times when it can be useful to store data across multiple blocks. Each block instance has access to a tracker
that is specific to a specific block type and persists across all blocks. It is only cleared when `reset` is called on
the `Markdown` object.

The tracker is accessed by a given `Block` extension via the `self.tracker` attribute. The attribute contains a
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

Expand Down Expand Up @@ -128,57 +169,6 @@ class MyBlock(Block):
}
```

## Global Config

It may be desirable to have global options. These kind of options affect all blocks and are not configured per block.

Configs are created using the `CONFIG` attribute and should contain a dictionary with default values. It is up to the
plugin creator to validate options as no mechanism is provided at this time to validate global configs. It is recommend
that global options be processed in the [`on_init` event](#on_init-event).

```py
class MyBlock(Block):
# Name used for the block
NAME = 'my-block'
CONFIG = {
'enable_feature': False
}
```

## Tracking Data Across Blocks

There are times when it can be useful to store data across multiple blocks. Each block instance has access to a tracker
that is specific to a specific block type and persists across all blocks. It is only cleared when `reset` is called
on the block manager and will reset trackers of all registered blocks.

The tracker is accessed by a given `Block` plugin via the `self.tracker` attribute. The attribute contains a 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.

## `on_register` Event

```py
@classmethod
def on_register(
cls,
blocks_processor: BlocksProcessor,
md: Markdown,
config: dict[str, Any]
) -> None:
...
```

When a Block plugin is registered, `on_register` is executed.

Parameter | Description
------------------ | -----------
`blocks_processor` | The parent block processor of the meta plugin.
`md` | The Markdown object.
`config` | Global config for the meta plugin.

This is useful if you need to register support extensions (Treeprocessor, Postprocessor, etc.) through the `md` object
if required.

## `on_init` Event

```py
Expand All @@ -204,8 +194,8 @@ 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 plugin 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.
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.

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

Expand Down Expand Up @@ -257,15 +247,25 @@ def on_end(self, block: Element) -> None:
...
```

When a block is parsed to completion, the `on_end` event is executed. This allows a plugin to perform any post
When a block is parsed to completion, the `on_end` event is executed. This allows an extension to perform any post
processing on the elements. You could save the data as raw text and then parse it special at the end or you could walk
the HTML elements and move content around, add attributes, or whatever else is needed.

## Built-in Validators

A number of validators are provided via for the purpose of validating [YAML option inputs](#options). If what you need
is not present, feel free to write your own.
is not present, feel free to write your own. All validators are imported from `pymdownx.blocks.block`.

### `type_any`

This a YAML input and simply passes it through. If you do not want to validate the input because it does not need to be
checked, or if you just want to do it manually in the [`on_parse` event](#on_parse-event), then this is what you'd
want to use.

```py
class Block:
OPTIONS = {'name': [{}, type_any]}
```

### `type_number`

Expand Down
82 changes: 14 additions & 68 deletions docs/src/markdown/extensions/blocks/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,89 +11,42 @@ directives, generic blocks are not meant to behave or mirror directives as a 1:1

The idea behind blocks is to solve a few issues that have existed within Python Markdown block extensions.

1. Markdown has numerous special syntaxes to define various elements, but as people try to add numerous plugins to
1. Markdown has numerous special syntaxes to define various elements, but as people try to add numerous extensions to
perform specialized translations, it can become difficult to continuously come up with new, sensible syntax that does
not conflict with some other plugin that is desired by users.
not conflict with some other extension that is desired by users.

2. Traditionally, Python Markdown has implemented any block processors that behave more as containers for multiple child
blocks using an indentation format (think Admonitions as an example). While this works, this can be tiring to some
authors who'd like to not have so many nested indentation levels in the documentation. Additionally, most code
editors will syntax highlight such nested constructs as indented code blocks which can make reading the source
difficult.

Blocks is a plugin that essentially allows for the creation of its own meta-plugins that function as fenced containers.
Each meta-plugin can do whatever it wants with the content inside the container and allow for a single generic format
to create as many different block style plugins as desired. As the blocks are fenced, indentation is not required to
determine the start and end of the blocks.
Blocks is an extension type that essentially allows for the creation of its own extension type that function as fenced
containers. Each extension can do whatever it wants with the content inside the container and allow for a single generic
format to create as many different block style extensions as desired. As the blocks are fenced, indentation is not
required to determine the start and end of the blocks.

Blocks also allows for per block options giving the extension authors an easy way to extend functionality and users an
easy, predictable way to specify such options.

To use Blocks, simply include the extension as shown below.
Blocks itself isn't used directly, but there are a variety of extensions created with it. All of which can be registered
in the traditional way.

```py3
import markdown
md = markdown.Markdown(extensions=['pymdownx.blocks'])
md = markdown.Markdown(extensions=['pymdownx.blocks.<extension>'])
```

## Included Meta-Plugins
Pymdown Extensions provides the following extensions.

Blocks provides a number of "meta-plugins" out of the box. Not are registered by default. You must select and register
the ones you wish to use.

Plugin | Description
Extension | Description
--------------------------------------- | -----------
[`admonition`](./plugins/admonition.md) | The admonition block allows for the creation of admonitions.
[`define`](./plugins/definition.md) | Allows for the creation of definition lists.
[`details`](./plugins/details.md) | The details block allows for the creation of collapsible details/summary constructs.
[`tab`](./plugins/tab.md) | Aims to replace the [Tabbed](../tabbed.md) extension and allows for the creation of tab containers.
[`html`](./plugins/html.md) | HTML is a block that allows for the arbitrary creation of HTML elements of various types.


## Configuring Meta-Plugins

By default, no meta-plugins are included, but you can specify the plugins you desire to use and configure them via the
`blocks` option.

```py
import markdown
from pymdownx.blocks.admonition import Admonition

md = markdown.Markdown(
extensions=['pymdownx.blocks'],
extension_configs={
'pymdownx.blocks': {
'blocks': {
Admonition: {'types': ['note', 'warning', 'some-custom-type']}
}
}
}
)
```

You can also use simple strings and the plugin will be located and registered accordingly. The syntax is:

```
path.to.module:PluginClass
```

A full example would be:

```py
import markdown

md = markdown.Markdown(
extensions=['pymdownx.blocks'],
extension_configs={
'pymdownx.blocks': {
'blocks': {
'pymdownx.blocks.admonition:Admonition': {'types': ['note', 'warning', 'some-custom-type']}
}
}
}
)
```

## Syntax

Syntax for Blocks requires the desired content to be included between fences. Fences are denoted by using three or
Expand All @@ -113,20 +66,20 @@ statement about all blocks. If in doubt, use an empty line before the first cont
///

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 plugin to decide.
arguments can be optional or sometimes enforced as a requirement. This is up to the given Blocks extension to decide.

```
/// note | Did you know?
You can create a note with Blocks!
///
```

Lastly, a given Block plugin may allow for additional options that don't make sense in the first line declaration.
Lastly, a given Block extension may allow for additional options that don't make sense in the first line declaration.
This may be because they are rarely used, more complicated, or just make the first line signature more confusing. These
options are per block specific use a YAML syntax. They must be part of the header, which means no new line between the
block declaration and the options or between individual options. The options also must be indented at least four spaces.

For instance, all plugins inherit an option `attrs` which allows you to set HTML attributes to the outer element of a
For instance, all extensions inherit an option `attrs` which allows you to set HTML attributes to the outer element of a
generic block. You can use [Emmet style syntax](https://docs.emmet.io/abbreviations/syntax/) to set an id, classes, or
other arbitrary attributes.

Expand Down Expand Up @@ -154,10 +107,3 @@ content
Content
////
```

## Options

Option | Type | Default | Description
--------------------- | ---------- | ---------------------------------- | -----------
`blocks` | \[Block\] | `#!py []` | A list of block meta-plugins to register.
`block_configs` | dictionary | `#!py {}` | A dictionary used to specify global options for registered meta-plugins.
2 changes: 1 addition & 1 deletion docs/src/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ nav:
- BetterEm: extensions/betterem.md
- Blocks:
- extensions/blocks/index.md
- Blocks Plugin API: extensions/blocks/api.md
- Blocks Extension API: extensions/blocks/api.md
- Admonition: extensions/blocks/plugins/admonition.md
- Definition: extensions/blocks/plugins/definition.md
- Details: extensions/blocks/plugins/details.md
Expand Down
Loading

0 comments on commit cfef8d7

Please sign in to comment.