diff --git a/docs/src/markdown/extensions/blocks/api.md b/docs/src/markdown/extensions/blocks/api.md index 99002e3c0..6f56b9b5d 100644 --- a/docs/src/markdown/extensions/blocks/api.md +++ b/docs/src/markdown/extensions/blocks/api.md @@ -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) @@ -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()])) ``` -
- +/// html | div.result ```html

content

``` +/// -
+## 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 @@ -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 @@ -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. @@ -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` diff --git a/docs/src/markdown/extensions/blocks/index.md b/docs/src/markdown/extensions/blocks/index.md index 07578ffae..63367a623 100644 --- a/docs/src/markdown/extensions/blocks/index.md +++ b/docs/src/markdown/extensions/blocks/index.md @@ -11,9 +11,9 @@ 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 @@ -21,27 +21,25 @@ The idea behind blocks is to solve a few issues that have existed within Python 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.']) ``` -## 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. @@ -49,51 +47,6 @@ Plugin | Description [`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 @@ -113,7 +66,7 @@ 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? @@ -121,12 +74,12 @@ 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. @@ -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. diff --git a/docs/src/mkdocs.yml b/docs/src/mkdocs.yml index 9a7a21b50..127eb735d 100644 --- a/docs/src/mkdocs.yml +++ b/docs/src/mkdocs.yml @@ -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 diff --git a/mkdocs.yml b/mkdocs.yml index 16396575f..54442c72e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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 diff --git a/pymdownx/blocks/__init__.py b/pymdownx/blocks/__init__.py index f816ffa78..77b9ca4ac 100644 --- a/pymdownx/blocks/__init__.py +++ b/pymdownx/blocks/__init__.py @@ -451,5 +451,5 @@ def extendMarkdown(self, md): mgr = self.register_block_mgr(md) self.extendMarkdownBlocks(md, mgr) - def extendMarkdownBlocks(self, md, blocks): + def extendMarkdownBlocks(self, md, block_mgr): """Extend Markdown blocks.""" diff --git a/pymdownx/blocks/admonition.py b/pymdownx/blocks/admonition.py index 55110ad4e..356546afe 100644 --- a/pymdownx/blocks/admonition.py +++ b/pymdownx/blocks/admonition.py @@ -79,15 +79,15 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - def extendMarkdownBlocks(self, md, blocks): + def extendMarkdownBlocks(self, md, block_mgr): """Extend Markdown blocks.""" - blocks.register(Admonition, self.getConfigs()) + block_mgr.register(Admonition, self.getConfigs()) # Generate an admonition subclass based on the given names. for b in self.getConfig('types', []): subclass = RE_SEP.sub('', b.title()) - blocks.register(type(subclass, (Admonition,), {'OPTIONS': {}, 'NAME': b, 'CONFIG': {}}), {}) + block_mgr.register(type(subclass, (Admonition,), {'OPTIONS': {}, 'NAME': b, 'CONFIG': {}}), {}) def makeExtension(*args, **kwargs): diff --git a/pymdownx/blocks/block.py b/pymdownx/blocks/block.py index d107ad548..aeb82f3a4 100644 --- a/pymdownx/blocks/block.py +++ b/pymdownx/blocks/block.py @@ -13,6 +13,12 @@ ) +def type_any(value): + """Accepts any type.""" + + return value + + def _ranged_number(value, minimum, maximum, number_type): """Check the range of the given number type.""" diff --git a/pymdownx/blocks/definition.py b/pymdownx/blocks/definition.py index 8c10dad24..56b61058c 100644 --- a/pymdownx/blocks/definition.py +++ b/pymdownx/blocks/definition.py @@ -53,10 +53,10 @@ def on_end(self, block): class DefinitionExtension(BlocksExtension): """Definition Blocks Extension.""" - def extendMarkdownBlocks(self, md, blocks): + def extendMarkdownBlocks(self, md, block_mgr): """Extend Markdown blocks.""" - blocks.register(Definition, self.getConfigs()) + block_mgr.register(Definition, self.getConfigs()) def makeExtension(*args, **kwargs): diff --git a/pymdownx/blocks/details.py b/pymdownx/blocks/details.py index ed6fde40a..ecfa18f3a 100644 --- a/pymdownx/blocks/details.py +++ b/pymdownx/blocks/details.py @@ -91,15 +91,15 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - def extendMarkdownBlocks(self, md, blocks): + def extendMarkdownBlocks(self, md, block_mgr): """Extend Markdown blocks.""" - blocks.register(Details, self.getConfigs()) + block_mgr.register(Details, self.getConfigs()) # Generate an details subclass based on the given names. for b in self.getConfig('types', []): subclass = RE_SEP.sub('', b.title()) - blocks.register( + block_mgr.register( type(subclass, (Details,), {'OPTIONS': {'open': [False, type_boolean]}, 'NAME': b, 'CONFIG': {}}), {} ) diff --git a/pymdownx/blocks/html.py b/pymdownx/blocks/html.py index debf80589..c5ac9458d 100644 --- a/pymdownx/blocks/html.py +++ b/pymdownx/blocks/html.py @@ -159,10 +159,10 @@ def on_create(self, parent): class HTMLExtension(BlocksExtension): """HTML Blocks Extension.""" - def extendMarkdownBlocks(self, md, blocks): + def extendMarkdownBlocks(self, md, block_mgr): """Extend Markdown blocks.""" - blocks.register(HTML, self.getConfigs()) + block_mgr.register(HTML, self.getConfigs()) def makeExtension(*args, **kwargs): diff --git a/pymdownx/blocks/tab.py b/pymdownx/blocks/tab.py index 16a190e7e..252eee73b 100644 --- a/pymdownx/blocks/tab.py +++ b/pymdownx/blocks/tab.py @@ -258,10 +258,10 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - def extendMarkdownBlocks(self, md, blocks): + def extendMarkdownBlocks(self, md, block_mgr): """Extend Markdown blocks.""" - blocks.register(Tab, self.getConfigs()) + block_mgr.register(Tab, self.getConfigs()) if callable(self.getConfig('slugify')): slugs = TabbedTreeprocessor(md, self.getConfigs()) md.treeprocessors.register(slugs, 'tab_slugs', 4) diff --git a/tests/test_extensions/test_blocks/test_general_blocks.py b/tests/test_extensions/test_blocks/test_general_blocks.py index 099128d0c..fd2e93861 100644 --- a/tests/test_extensions/test_blocks/test_general_blocks.py +++ b/tests/test_extensions/test_blocks/test_general_blocks.py @@ -9,6 +9,13 @@ class TestTypeFunctions(unittest.TestCase): """Validate various type functions.""" + def test_type_any(self): + """Test `type_any`.""" + + self.assertEqual(3, block.type_any(3)) + self.assertEqual({}, block.type_any({})) + self.assertEqual('string', block.type_any('string')) + def test_type_number(self): """Test `type_number`."""