From 2c7f8c02c6195d991d4d181665f7cd57cb95a649 Mon Sep 17 00:00:00 2001 From: Isaac Muse Date: Fri, 18 Oct 2024 16:44:23 -0600 Subject: [PATCH] Add catpion extension (#2492) Co-authored-by: Joan Puigcerver --- docs/src/dictionary/en-custom.txt | 1 + docs/src/markdown/.snippets/blocksbeta.md | 9 - docs/src/markdown/about/changelog.md | 4 + docs/src/markdown/extensions/blocks/index.md | 2 - .../extensions/blocks/plugins/admonition.md | 4 +- .../extensions/blocks/plugins/caption.md | 514 ++++++ .../extensions/blocks/plugins/definition.md | 4 +- .../extensions/blocks/plugins/details.md | 6 +- .../extensions/blocks/plugins/html.md | 4 +- .../markdown/extensions/blocks/plugins/tab.md | 4 +- docs/src/markdown/index.md | 24 +- docs/src/mkdocs.yml | 4 +- mkdocs.yml | 4 +- pymdownx/blocks/admonition.py | 1 - pymdownx/blocks/caption.py | 398 +++++ pymdownx/blocks/details.py | 1 - .../test_blocks/test_captions.py | 1469 +++++++++++++++++ 17 files changed, 2411 insertions(+), 42 deletions(-) delete mode 100644 docs/src/markdown/.snippets/blocksbeta.md create mode 100644 docs/src/markdown/extensions/blocks/plugins/caption.md create mode 100644 pymdownx/blocks/caption.py create mode 100644 tests/test_extensions/test_blocks/test_captions.py diff --git a/docs/src/dictionary/en-custom.txt b/docs/src/dictionary/en-custom.txt index d97ab6a9d..fe9f98b8c 100644 --- a/docs/src/dictionary/en-custom.txt +++ b/docs/src/dictionary/en-custom.txt @@ -180,6 +180,7 @@ github highlighter's hostnames html +incrementing indepth indexable injectable diff --git a/docs/src/markdown/.snippets/blocksbeta.md b/docs/src/markdown/.snippets/blocksbeta.md deleted file mode 100644 index 52250d08b..000000000 --- a/docs/src/markdown/.snippets/blocksbeta.md +++ /dev/null @@ -1,9 +0,0 @@ -/// new | 9.10 New Experimental Feature -Blocks is currently a new, experimental extension type available in Pymdown Extensions that allows for writing a new -kind of block extension in Python Markdown. With this new addition, we've added a number of new extensions utilizing -this new extension type. While its intention is to hopefully replace extensions like Details and Tabbed, there are -currently no immediate plans to deprecate those plugins. - -Any and all feedback regarding these new, experimental blocks is appreciated. Please provide feedback here: -https://github.com/facelessuser/pymdown-extensions/discussions/1973. -/// diff --git a/docs/src/markdown/about/changelog.md b/docs/src/markdown/about/changelog.md index 89e0181c8..f169c6c7d 100644 --- a/docs/src/markdown/about/changelog.md +++ b/docs/src/markdown/about/changelog.md @@ -2,6 +2,10 @@ ## 10.12 +- **NEW**: Blocks: Blocks extensions no longer considered in beta. +- **NEW**: Details: Details is marked as "legacy" in documentation in favor of the new `pymdownx.blocks.details` approach. +- **NEW**: Tabbed: Tabbed is marked as "legacy" in documentation in favor of the new `pymdownx.blocks.tab` approach. +- **NEW**: Caption: Add new "blocks" style extension called Caption which helps with specifying figures with captions. - **NEW**: Emoji: Add a new `strict` option that will raise an exception if an emoji is used whose name has changed, removed, or never existed. - **FIX**: Emoji: Emoji links should be generated such that they point to the new CDN version. diff --git a/docs/src/markdown/extensions/blocks/index.md b/docs/src/markdown/extensions/blocks/index.md index 7ba3e1e4b..74451422f 100644 --- a/docs/src/markdown/extensions/blocks/index.md +++ b/docs/src/markdown/extensions/blocks/index.md @@ -2,8 +2,6 @@ # Blocks ---8<-- "blocksbeta.md" - ## Overview Blocks is an extension aimed at providing generic blocks inspired by reStructuredText directives. While inspired by diff --git a/docs/src/markdown/extensions/blocks/plugins/admonition.md b/docs/src/markdown/extensions/blocks/plugins/admonition.md index a2bcdf4c9..708516607 100644 --- a/docs/src/markdown/extensions/blocks/plugins/admonition.md +++ b/docs/src/markdown/extensions/blocks/plugins/admonition.md @@ -10,8 +10,6 @@ can cause issues as they both generate the same output and confuse each other. If you are switching from `admonition` to `pymdownx.blocks.admonition`, ensure you disable `admonition` to avoid issues. /// ---8<-- "blocksbeta.md" - ## Overview Admonition blocks are an alternative to using Python Markdown's [built-in extension][admonition]. The output is @@ -165,4 +163,4 @@ Options | Type | Descriptions Options | Type | Descriptions ------------ | ---------- | ------------ `type` | string | A class name to apply as the admonition type. -`attrs` | string | A string that defines attributes for the outer, wrapper element. +`attrs` | dictionary | A dictionary that defines attributes for the outer, wrapper element. diff --git a/docs/src/markdown/extensions/blocks/plugins/caption.md b/docs/src/markdown/extensions/blocks/plugins/caption.md new file mode 100644 index 000000000..a9d3c13aa --- /dev/null +++ b/docs/src/markdown/extensions/blocks/plugins/caption.md @@ -0,0 +1,514 @@ +[:octicons-file-code-24:][_admonition_block]{: .source-link } + +# Caption + +/// New | New in 10.12 +/// + +## Overview + +The Caption extension allows for wrapping blocks in `#!html
` tags and inserting a `#!html
` tag +with specified content. + +```py3 +import markdown +md = markdown.Markdown(extensions=['pymdownx.blocks.caption']) +``` + +## Usage +### Adding Captions + +Caption is easy to use and simply requires the user to place a caption directly after a block that they'd like to wrap +in a figure with a caption. Figures can have any content: images, tables, quotes, anything. + +```text title="Adding Caption" +Fruit | Amount +---------- | ------ +Apple | 20 +Peach | 10 +Banana | 3 +Watermelon | 1 + +/// caption +Fruit Count +/// +``` + +//// html | div.result +Fruit | Amount +---------- | ------ +Apple | 20 +Peach | 10 +Banana | 3 +Watermelon | 1 + +/// caption +Fruit Count +/// +//// + +### Nesting Captions + +Captions can be nested, and if multiple captions are used on a single block, the block will be wrapped in multiple +nested figures. + +```text title="Nested Captions" +Paragraph + +/// caption +Inner caption +/// + +/// caption +Outer caption +/// +``` + +//// html | div.result +Paragraph + +/// caption +Inner caption +/// + +/// caption +Outer caption +/// +//// + +If a caption is provided after a figure element that does not already contain a caption, the caption will be inserted +into that figure. This allows for more complex figures with sub-figures. + +```html title="Complex Captions" +//// html | figure +Paragraph 1 + +/// caption +Caption 1 +/// + +Paragraph 2 + +/// caption +Caption 2 +/// +//// + +/// caption + attrs: {class: "general"} +General caption +/// + + +``` + +///// html | div.result +//// html | figure +Paragraph 1 + +/// caption +Caption 1 +/// + +Paragraph 2 + +/// caption +Caption 2 +/// +//// + +/// caption + attrs: {class: "general"} +General caption +/// + + +///// + +This can be used with Python Markdown's [`md_in_html` extension][md-in-html] as well, but keep in mind that figures +specified in this way must include the `markdown` attribute or they will be invisible to the Caption extension. Further, +if `#!html
` elements do not have the `markdown` attribute, Caption will not know a figure already has a +caption. + +```html title="Complex Captions: md_in_html" +
+Paragraph 1 + +/// caption +Caption 1 +/// + +Paragraph 2 + +/// caption +Caption 2 +/// +
+ +/// caption + attrs: {class: "general"} +General caption +/// + + +``` + +///// html | div.result +
+Paragraph 1 + +/// caption +Caption 1 +/// + +Paragraph 2 + +/// caption +Caption 2 +/// +
+ +/// caption + attrs: {class: "general"} +General caption +/// + + +///// + +### Prepending Captions + +Captions are appended by default, but can be configured to be prepended by default is desired. Simply set the global +[`prepend` option](#options) to `#!py True`. + +```text title="Prepend Caption" +Fruit | Amount +---------- | ------ +Apple | 20 +Peach | 10 +Banana | 3 +Watermelon | 1 + +/// caption +Fruit Count +/// +``` + +//// html | div.result +```md-render +--- +extensions: +- pymdownx.blocks.caption +- tables +extension_configs: + pymdownx.blocks.caption: + prepend: true +--- +Fruit | Amount +---------- | ------ +Apple | 20 +Peach | 10 +Banana | 3 +Watermelon | 1 + +/// caption +Fruit Count +/// +``` +//// + +Regardless of whether `prepend` is enabled or disabled globally, you can force prepend or append per block by specifying +`<` or `>` in the header for prepending or appending respectively. + +```text title="Manual Prepend/Append" +Paragraph 1 + +/// caption | < +Caption 1 +/// + +Paragraph 2 | > + +/// caption +Caption 2 +/// +``` + +//// html | div.result +Paragraph 1 + +/// caption | < +Caption 1 +/// + +Paragraph 2 | > + +/// caption +Caption 2 +/// +//// + +### Auto Prefix and IDs + +Caption defines multiple caption types by default: a generic `caption`, `figure-caption`, and `table-caption`. By +default `figure-caption` and `table-caption` are configured with prefix templates, and when used, the figure will have +special prefixes added to the caption with auto incrementing numbers. Additionally, a corresponding ID will be assigned +to the figure that wraps the content for linking. + +````text title="Auto Prefix and IDs" +```diagram +graph TD + A[Hard] -->|Text| B(Round) + B --> C{Decision} + C -->|One| D[Result 1] + C -->|Two| E[Result 2] +``` +/// figure-caption +Decision Diagram +/// + +Fruit | Amount +---------- | ------ +Apple | 20 +Peach | 10 +Banana | 3 +Watermelon | 1 + +/// table-caption +Fruit Count +/// +```` + +//// html | div.result +```diagram +graph TD + A[Hard] -->|Text| B(Round) + B --> C{Decision} + C -->|One| D[Result 1] + C -->|Two| E[Result 2] +``` +/// figure-caption +Decision Diagram +/// + +Fruit | Amount +---------- | ------ +Apple | 20 +Peach | 10 +Banana | 3 +Watermelon | 1 + +/// table-caption +Fruit Count +/// +//// + +Numbers are tracked separately for each caption type that specifies a prefix template. When a caption is nested under +other figures, captions will have a dotted number corresponding with the nested level, e.g. 1, 1.1, 1.1.1, etc. + +If desired, auto prefixing of nested sub-levels can be restricted via `auto_levels`. When specifying a non-zero, +positive number, auto-generated IDs and prefixes will be restricted to the specified nesting level. A nesting level of 1 +only applies IDs and prefixes to the outermost figure of a given type where a nesting level of 2 will apply to the +outermost figure plus one level of nested children of the same type. + +### Increase Nesting Depth + +/// warning +Requires [`auto`](#options) to be set to `#!py True` (the default). +/// + +To accommodate various needs, some more control over numbering is exposed. If a user would like to show sub-figure +numbers but not have the figures directly nested under a parent figure, automatic numbers can be influenced by +specifying a specific nesting depth in the header of the caption block. By specifying a number prefixed with `^`, we can +increase the numbering depth for that specific figure by the specified value relative to its current depth. Figures are +still subject to the `auto_levels` limit. + +```text title="Nesting Depth" +Paragraph +/// figure-caption +Caption +/// + +Paragraph +/// figure-caption | ^1 +Caption +/// + +Paragraph +/// figure-caption | ^1 +Caption +/// + +Paragraph +/// figure-caption +Caption +/// +``` + +//// html | div.result +Paragraph +/// figure-caption +Caption +/// + +Paragraph +/// figure-caption | ^1 +Caption +/// + +Paragraph +/// figure-caption | ^1 +Caption +/// + +Paragraph +/// figure-caption +Caption +/// +//// + +/// tip +Nesting depths can be used with per block prepend notation as long as `<` or `>` comes before the depth notation. +/// + +### Manual Figure Numbers + +/// warning +It is recommended to use [`auto`](#options) set to `#!py False`, i.e. "manual mode", but can be used with when it is set +to `#!py True` with some limitations. +/// + +Captions allows for setting figures numbers for prefixes and IDs on the fly. This is mainly designed for use in "manual +mode" ([`auto`](#options) set to `#!py False`). The idea was to provide a mode where the user has complete control of +figure numbers if so desired. When in manual mode, the user will only get prefixes when specifying numbers in the header +of figure types that specify a prefix template. Manual mode does not not check or validate these numbers beyond ensuring +they are in fact numbers that are formatted properly. + +```text title="Manual Numbers" +Paragraph +/// figure-caption | 12 +Caption +/// +``` + +//// html | div.result +Paragraph +/// figure-caption | 12 +Caption +/// +//// + +/// tip +Manual numbers can be used with per block prepend notation as long as `<` or `>` comes before the number. +/// + +In "automatic mode" (the default) the number will be used if, and only if, the specified number is greater than the last +figure number (automatic or otherwise) that was used for the specific figure type at the specific number depth. If, for +any reason, this is not true, an automatic figure number at the specified number depth will be used instead. +Essentially, the internal, automatic counter can be increased at any time, but can never be decreased. + +As an example, if `1.1` is specified, but the last number for current figure type was `2`, `2.1` will be used instead. +If the number is much bigger, Caption will not care and accept it readily, but it may create a gap in numbering. + +Additionally, manual numbers are subject to the `auto_levels` limit when in "automatic mode". + +### Static IDs + +While automatic mode will generate IDs for prefixed captions, some may prefer an unchanging, static ID for linking. If +you'd like to link to a figure with a static ID, you can specify one on any figure type (prefixed or non-prefixed) and +the ID will be applied instead of any auto-generated ID. Manually specified IDs will not affect auto-incrementing +numerical prefixes. + +``` +Paragraph + +/// figure-caption + attrs: {id: static-id} +caption +/// +``` + +IDs specified in this way will also override IDs generated in [manual mode](#manual-figure-numbers). + +### Configuring Figure Types + +While Caption provides a few default figure types, users are free to define their own with different prefixes. + +If a figure type is defined with no prefix, that type will never have IDs or prefixes generated for it. The default +generic `caption` type is an example of a non-prefix caption type. + +Users can override the default types with their own via the [`types` option](#options). `types` is defined as a list +of types which can either be a single string specifying the name of the caption type (with no prefix), or a dictionary +object that specifies, the `name` and `prefix` template for the caption prefix. An empty string for the `prefix` is the +same as no prefix being specified. `{}` must be specified for non-empty templates and is used to insert the +auto-generated numerical number of the figure. + +```py +extenconfigs = { + "pymdownx.blocks.caption": { + "types": [ + 'caption', + { + 'name': 'figure-caption', + 'prefix': 'Figure {}.' + }, + { + 'name': 'table-caption', + 'prefix': 'Table {}.' + } + ] + } +} +``` + +If desired, you can also specify optional classes to be added to a specific figure type via the `classes` option. + +```py +extenconfigs = { + "pymdownx.blocks.caption": { + "types": [ + 'caption', + { + 'name': 'figure-caption', + 'prefix': 'Figure {}.' + 'classes': 'one-class two-class' + }, + { + 'name': 'table-caption', + 'prefix': 'Table {}.' + } + ] + } +} +``` + +## Global Options + +Options | Type | Descriptions +------------- | ---------- | ------------ +`types` | list | A list of figure types. Figure types must specify a name and can specify an optional prefix template where `{}` is where the figure number will be inserted. +`prepend` | boolean | Prepend captions opposed appending captions to figures. Disabled by default. +`auto` | boolean | Auto add IDs to all figure types that include prefix templates. Enabled by default. +`auto_level` | integer | Limit auto prefixing to a certain depth. Default is 0 which disables limiting. + +## Per Block Options + +Options | Type | Descriptions +------------ | ---------- | ------------ +`attrs` | dictionary | A string that defines attributes for the outer, wrapper element. diff --git a/docs/src/markdown/extensions/blocks/plugins/definition.md b/docs/src/markdown/extensions/blocks/plugins/definition.md index 79ba690d1..fe2c5ca49 100644 --- a/docs/src/markdown/extensions/blocks/plugins/definition.md +++ b/docs/src/markdown/extensions/blocks/plugins/definition.md @@ -10,8 +10,6 @@ can cause issues as they both generate the same output and confuse each other. If you are switching from `def_list` to `pymdownx.blocks.definition`, ensure you disable `def_list` to avoid issues. /// ---8<-- "blocksbeta.md" - ## Overview The definition blocks are an alternative to using Python Markdown's [built-in extension][def-list]. The output is very @@ -131,4 +129,4 @@ Definitions provide no global options. Options | Type | Descriptions ------------ | ---------- | ------------ -`attrs` | string | A string that defines attributes for the outer, wrapper element. +`attrs` | dictionary | A dictionary that defines attributes for the outer, wrapper element. diff --git a/docs/src/markdown/extensions/blocks/plugins/details.md b/docs/src/markdown/extensions/blocks/plugins/details.md index b5e992dcf..429068ede 100644 --- a/docs/src/markdown/extensions/blocks/plugins/details.md +++ b/docs/src/markdown/extensions/blocks/plugins/details.md @@ -10,8 +10,6 @@ If you are switching from `pymdownx.details` to `pymdownx.blocks.details`, ensur avoid issues. /// ---8<-- "blocksbeta.md" - ## Overview Details blocks are an alternative to using [`pymdownx.details`](../../details.md) and, in fact, aim to potentially replace @@ -116,7 +114,7 @@ md = markdown.Markdown( extensions=['pymdownx.blocks.details'], extension_configs={ 'pymdownx.blocks.details": { - 'types': [{'name': some-custom-type', 'class': 'custom', 'title': 'My Default title'}] + 'types': [{'name': 'some-custom-type', 'class': 'custom', 'title': 'My Default title'}] } } ) @@ -166,4 +164,4 @@ Options | Type | Descriptions ------------ | ---------- | ------------ `open` | bool | A boolean that determines if the details block is open or closed. `type` | string | A class name to apply as the admonition type. -`attrs` | string | A string that defines attributes for the outer, wrapper element. +`attrs` | dictionary | A dictionary that defines attributes for the outer, wrapper element. diff --git a/docs/src/markdown/extensions/blocks/plugins/html.md b/docs/src/markdown/extensions/blocks/plugins/html.md index 440dece0e..40a3ef138 100644 --- a/docs/src/markdown/extensions/blocks/plugins/html.md +++ b/docs/src/markdown/extensions/blocks/plugins/html.md @@ -2,8 +2,6 @@ # HTML ---8<-- "blocksbeta.md" - ## Overview The HTML block allows a user to wrap Markdown in arbitrary HTML elements. @@ -116,4 +114,4 @@ some *markdown* content Options | Type | Descriptions ------------ | ---------- | ------------ `markdown` | string | String value to control how Markdown content is processed. Valid options are: `auto`, `block`, `inline`, `html`, and `raw`. -`attrs` | string | A string that defines attributes for the outer, wrapper element. +`attrs` | dictionary | A dictionary that defines attributes for the outer, wrapper element. diff --git a/docs/src/markdown/extensions/blocks/plugins/tab.md b/docs/src/markdown/extensions/blocks/plugins/tab.md index 89ce98b90..61bd9fe44 100644 --- a/docs/src/markdown/extensions/blocks/plugins/tab.md +++ b/docs/src/markdown/extensions/blocks/plugins/tab.md @@ -11,8 +11,6 @@ If you are switching from `pymdownx.tabbed` to `pymdownx.blocks.tab`, ensure you issues. /// ---8<-- "blocksbeta.md" - ## Overview Tab blocks are aimed at replacing the [Tabbed extension](../../tabbed.md). They function identical to Tabbed in every @@ -168,4 +166,4 @@ Options | Type | Descriptions ------------ | ---------- | ------------ `new` | bool | Force the current tab to start a new tab container. `select` | bool | Force the given tab to be selected in the parent tab container. -`attrs` | string | A string that defines attributes for the outer, wrapper element. +`attrs` | dictionary | A dictionary that defines attributes for the outer, wrapper element. diff --git a/docs/src/markdown/index.md b/docs/src/markdown/index.md index 28e2c4408..166d7fb73 100644 --- a/docs/src/markdown/index.md +++ b/docs/src/markdown/index.md @@ -27,14 +27,16 @@ aware of when using these extensions. ## Extensions -  | Extensions |   --------------------------------------------- | -------------------------------------------- | ------ -[Arithmatex](extensions/arithmatex.md) | [B64](extensions/b64.md) | [BetterEm](extensions/betterem.md) -[Blocks](extensions/blocks/index.md) | [Caret](extensions/caret.md) | [Critic](extensions/critic.md) -[Details](extensions/details.md) | [Emoji](extensions/emoji.md) | [EscapeAll](extensions/escapeall.md) -[Extra](extensions/extra.md) | [FancyLists](extensions/fancylists.md) | [Highlight](extensions/highlight.md) -[InlineHilite](extensions/inlinehilite.md) | [Keys](extensions/keys.md) | [MagicLink](extensions/magiclink.md) -[Mark](extensions/mark.md) | [PathConverter](extensions/pathconverter.md) | [ProgressBar](extensions/progressbar.md) -[SaneHeaders](extensions/saneheaders.md) | [SmartSymbols](extensions/smartsymbols.md) | [Snippets](extensions/snippets.md) -[StripHTML](extensions/striphtml.md) | [SuperFences](extensions/superfences.md) | [Tabbed](extensions/tabbed.md) -[Tasklist](extensions/tasklist.md) | [Tilde](extensions/tilde.md) +  | Extensions |   +------------------------------------------------------------- | ------------------------------------------------------- | ------ +[Arithmatex](extensions/arithmatex.md) | [B64](extensions/b64.md) | [BetterEm](extensions/betterem.md) +[Blocks: Admonition](extensions/blocks/plugins/admonition.md) | [Blocks: Caption](extensions/blocks/plugins/caption.md) | [Blocks: Definition](extensions/blocks/plugins/definition.md) +[Blocks: Details](extensions/blocks/plugins/details.md) | [Blocks: HTML](extensions/blocks/plugins/html.md) | [Blocks: Tab](extensions/blocks/plugins/tab.md) +[Caret](extensions/caret.md) | [Critic](extensions/critic.md) | [Emoji](extensions/emoji.md) +[EscapeAll](extensions/escapeall.md) | [Extra](extensions/extra.md) | [FancyLists](extensions/fancylists.md) +[Highlight](extensions/highlight.md) | [InlineHilite](extensions/inlinehilite.md) | [Keys](extensions/keys.md) +[Legacy: Details](extensions/details.md) | [Legacy: Tabbed](extensions/tabbed.md) | [MagicLink](extensions/magiclink.md) +[Mark](extensions/mark.md) | [PathConverter](extensions/pathconverter.md) | [ProgressBar](extensions/progressbar.md) +[SaneHeaders](extensions/saneheaders.md) | [SmartSymbols](extensions/smartsymbols.md) | [Snippets](extensions/snippets.md) +[StripHTML](extensions/striphtml.md) | [SuperFences](extensions/superfences.md) | [Tasklist](extensions/tasklist.md) +[Tilde](extensions/tilde.md) diff --git a/docs/src/mkdocs.yml b/docs/src/mkdocs.yml index 6d6d3e684..926452f7a 100644 --- a/docs/src/mkdocs.yml +++ b/docs/src/mkdocs.yml @@ -48,6 +48,7 @@ nav: - extensions/blocks/index.md - Blocks Extension API: extensions/blocks/api.md - Admonition: extensions/blocks/plugins/admonition.md + - Caption: extensions/blocks/plugins/caption.md - Definition: extensions/blocks/plugins/definition.md - Details: extensions/blocks/plugins/details.md - HTML: extensions/blocks/plugins/html.md @@ -227,6 +228,7 @@ markdown_extensions: alternate_style: True combine_header_slug: True slugify: !!python/object/apply:pymdownx.slugs.slugify {kwds: {case: lower}} + - pymdownx.blocks.caption: - tools.collapse_code: expand_text: '' collapse_text: '' @@ -239,7 +241,7 @@ extra_css: # - https://cdn.jsdelivr.net/npm/katex@0.13.18/dist/katex.min.css - assets/pymdownx-extras/extra.css extra_javascript: - - https://unpkg.com/mermaid@10.6.1/dist/mermaid.min.js + - https://cdn.jsdelivr.net/npm/mermaid@11.3.0/dist/mermaid.min.js # - https://cdn.jsdelivr.net/npm/katex@0.13.18/dist/katex.min.js - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js - assets/pymdownx-extras/extra-loader.js diff --git a/mkdocs.yml b/mkdocs.yml index 0a683ec9e..727e61bf2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -48,6 +48,7 @@ nav: - extensions/blocks/index.md - Blocks Extension API: extensions/blocks/api.md - Admonition: extensions/blocks/plugins/admonition.md + - Caption: extensions/blocks/plugins/caption.md - Definition: extensions/blocks/plugins/definition.md - Details: extensions/blocks/plugins/details.md - HTML: extensions/blocks/plugins/html.md @@ -227,6 +228,7 @@ markdown_extensions: alternate_style: True combine_header_slug: True slugify: !!python/object/apply:pymdownx.slugs.slugify {kwds: {case: lower}} + - pymdownx.blocks.caption: - tools.collapse_code: expand_text: '' collapse_text: '' @@ -239,7 +241,7 @@ extra_css: # - https://cdn.jsdelivr.net/npm/katex@0.13.18/dist/katex.min.css - assets/pymdownx-extras/extra-add764b01e.css extra_javascript: - - https://unpkg.com/mermaid@10.6.1/dist/mermaid.min.js + - https://cdn.jsdelivr.net/npm/mermaid@11.3.0/dist/mermaid.min.js # - https://cdn.jsdelivr.net/npm/katex@0.13.18/dist/katex.min.js - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js - assets/pymdownx-extras/extra-loader-B5CKpNQx.js diff --git a/pymdownx/blocks/admonition.py b/pymdownx/blocks/admonition.py index 7d7b452dd..d2c94b884 100644 --- a/pymdownx/blocks/admonition.py +++ b/pymdownx/blocks/admonition.py @@ -5,7 +5,6 @@ import re RE_SEP = re.compile(r'[_-]+') -RE_VALID_NAME = re.compile(r'[\w-]+') class Admonition(Block): diff --git a/pymdownx/blocks/caption.py b/pymdownx/blocks/caption.py new file mode 100644 index 000000000..c0023a642 --- /dev/null +++ b/pymdownx/blocks/caption.py @@ -0,0 +1,398 @@ +""" +Captions. + +Captions should be placed after a block, that block will be wrapped in a `figure` +and captions will be inserted either at the end of the figure or at the beginning. +If the preceding block happens to be a `figure`, if no `figcaption` is detected +within, the caption will be injected into that figure instead of wrapping. +Keep in mind that when `md_in_html` is used and raw HTML is used, if `markdown=1` +is not present on the caption, the caption will be invisible to this extension. + +Class, IDs, or other attributes will be attached to the figure, not the caption. + +`types`: + A dictionary with figure type names and prefix templates. A template will be + used depending on whether the current type is assumed or directly specified. +`prepend`: + Will prepend `figcaption` at the start of a `figure` instead of the end. +`auto`: + Will generate IDs and prefixes via the provided template for all figures of + a given type as long as they also define a prefix template. +`auto_level`: + Auto number will not be shown below the given level depth. A value of 0, the + default, disables the feature, 1 would show only auto-generate IDs and + prefixes for the outermost figures with prefixes, etc. This level is only + considered for each figure type individually. + +""" +import xml.etree.ElementTree as etree +from .block import Block, type_html_identifier +from .. blocks import BlocksExtension +from markdown.treeprocessors import Treeprocessor +import re + +RE_FIG_NUM = re.compile(r'^(\^)?([1-9][0-9]*(?:.[1-9][0-9]*)*)(?= |$)') +RE_SEP = re.compile(r'[_-]+') + + +def update_tag(el, fig_type, fig_num, template, prepend): + """Update tag ID and caption prefix.""" + + # Auto add an ID + if 'id' not in el.attrib: + el.attrib['id'] = f'__{fig_type}_' + '_'.join(str(x) for x in fig_num.split('.')) + + # Prefix the caption with a given numbered prefix + if template: + for child in list(el) if prepend else reversed(el): + if child.tag == 'figcaption': + children = list(child) + value = template.format(fig_num) + if not len(children) or children[0].tag != 'p': + p = etree.Element('p') + span = etree.SubElement(p, 'span', {'class': 'caption-prefix'}) + span.text = value + p.tail = child.text + child.text = None + child.insert(0, p) + else: + p = children[0] + span = etree.Element('span', {'class': 'caption-prefix'}) + span.text = value + span.tail = (' ' + p.text) if p.text is not None else p.text + p.text = None + p.insert(0, span) + + +class CaptionTreeprocessor(Treeprocessor): + """Caption tree processor.""" + + def __init__(self, md, types, config): + """Initialize.""" + + super().__init__(md) + + self.auto = config['auto'] + self.prepend = config['prepend'] + self.type = '' + self.auto_level = max(0, config['auto_level']) + self.fig_types = types + + def run(self, doc): + """Update caption IDs and prefixes.""" + + parent_map = {c: p for p in doc.iter() for c in p} + last = {k: 0 for k in self.fig_types} + counters = {k: [0] for k in self.fig_types} + fig_type = last_type = self.type + figs = [] + fig_num = '' + + # Calculate the depth and iteration at that depth of the given figure. + for el in doc.iter(): + fig_num = '' + stack = -1 + if el.tag == 'figure': + fig_type = last_type + prepend = False + skip = False + + # Find caption appended or prepended + if '__figure_prepend' in el.attrib: + prepend = True + del el.attrib['__figure_prepend'] + + # Determine figure type + if '__figure_type' in el.attrib: + fig_type = el.attrib['__figure_type'] + figs.append(el) + # See if we have an unknown type or the type has no prefix template. + if fig_type not in self.fig_types or not self.fig_types[fig_type]: + continue + else: + # Found a figure that was not generated by this plugin. + continue + + # Handle a specified relative nesting depth + if '__figure_level' in el.attrib: + stack += int(el.attrib['__figure_level']) + 1 + if self.auto_level and stack >= self.auto_level: + continue + else: + stack += 1 + + current = el + while True: + parent = parent_map.get(current, None) + + # No more parents + if parent is None: + break + + # Check if parent element is a figure of the current type + if parent.tag == 'figure' and parent.attrib['__figure_type'] == fig_type: + # See if position in stack is manually specified + level = '__figure_level' in parent.attrib + if level: + stack += int(parent.attrib['__figure_level']) + 1 + else: + stack += 1 + if level: + el.attrib['__figure_level'] = str(stack + 1) + # Ensure position in stack is not deeper than the specified level + if self.auto_level and stack >= self.auto_level: + skip = True + break + + current = parent + + if skip: + # Parent has been skipped so all children are also skipped + continue + + # Found an appropriate figure at an acceptable depth + if stack > -1: + # Handle a manual number + if '__figure_num' in el.attrib: + fig_num = [int(x) for x in el.attrib['__figure_num'].split('.')] + del el.attrib['__figure_num'] + new_stack = len(fig_num) - 1 + el.attrib['__figure_level'] = new_stack - stack + stack = new_stack + + # Increment counter + l = last[fig_type] + counter = counters[fig_type] + if stack > l: + counter.extend([1] * (stack - l)) + elif stack == l: + counter[stack] += 1 + else: + del counter[stack + 1:] + counter[-1] += 1 + last[fig_type] = stack + last_type = fig_type + + # Determine if manual number is not smaller than existing figure numbers at that depth + if fig_num and all(a <= b for a, b in zip(counter, fig_num)): + counter[:] = fig_num[:] + + # Apply prefix and ID + update_tag( + el, + fig_type, + '.'.join(str(x) for x in counter[:stack + 1]), + self.fig_types.get(fig_type, ''), + prepend + ) + + # Clean up attributes + for fig in figs: + del fig.attrib['__figure_type'] + if '__figure_level' in fig.attrib: + del fig.attrib['__figure_level'] + + +class Caption(Block): + """Figure captions.""" + + NAME = '' + PREFIX = '' + CLASSES = '' + ARGUMENT = None + OPTIONS = { + 'type': ['', type_html_identifier] + } + + def on_init(self): + """Initialize.""" + + self.auto = self.config['auto'] + self.prepend = self.config['prepend'] + self.caption = None + self.fig_num = '' + self.level = '' + self.classes = self.CLASSES.split() + + def on_validate(self, parent): + """Handle on validate event.""" + + argument = self.argument + if argument: + if argument.startswith('>'): + self.prepend = False + argument = argument[1:].lstrip() + elif argument.startswith('<'): + self.prepend = True + argument = argument[1:].lstrip() + + m = RE_FIG_NUM.match(argument) + if m: + if m.group(1): + self.level = m.group(2) + else: + self.fig_num = m.group(2) + argument = argument[m.end():].lstrip() + + if argument: + return False + return True + + def on_create(self, parent): + """Create the element.""" + + # Find sibling to add caption to. + fig = None + child = None + children = list(parent) + if children: + child = children[-1] + # Do we have a figure with no caption? + if child.tag == 'figure': + fig = child + for c in list(child): + if c.tag == 'figcaption': + fig = None + break + + # Create a new figure if sibling is not a figure or already has a caption. + # Add sibling to the new figure. + if fig is None: + attrib = {} if not self.classes else {'class': ' '.join(self.classes)} + fig = etree.SubElement(parent, 'figure', attrib) + if child is not None: + fig.append(child) + parent.remove(child) + + # Add classes to existing figure + elif self.CLASSES: + classes = fig.attrib.get('class', '').strip() + if classes: + class_list = classes.split() + for c in self.classes: + if c not in class_list: + classes += " " + c + else: + classes = ' '.join(self.classes) + fig.attrib['class'] = classes + + if self.auto: + fig.attrib['__figure_type'] = self.NAME + if self.level: + fig.attrib['__figure_level'] = self.level + if self.fig_num: + fig.attrib['__figure_num'] = self.fig_num + + # Add caption to the target figure. + if self.prepend: + if self.auto: + fig.attrib['__figure_prepend'] = "1" + self.caption = etree.Element('figcaption') + fig.insert(0, self.caption) + else: + self.caption = etree.SubElement(fig, 'figcaption') + + return fig + + def on_add(self, block): + """Return caption as the target container for content.""" + + return self.caption + + def on_end(self, block): + """Handle explicit, manual prefixes on block end.""" + + prefix = self.PREFIX + if prefix and not self.auto: + # Levels should not be used in manual mode, but if they are, give a generic result. + if self.level: + self.fig_num = '.'.join(['1'] * (int(self.level) + 1)) + if self.fig_num: + update_tag( + block, + self.NAME, + self.fig_num, + prefix, + self.prepend + ) + + +class CaptionExtension(BlocksExtension): + """Caption Extension.""" + + def __init__(self, *args, **kwargs): + """Initialize.""" + + self.config = { + "types": [ + [ + 'caption', + { + 'name': 'figure-caption', + 'prefix': 'Figure {}.' + }, + { + 'name': 'table-caption', + 'prefix': 'Table {}.' + } + ], + "Configure types a list of types, each type is a dictionary that defines a 'name' and 'prefix' " + "A template must contain '{}' for numerical insertions unless the template is an empty string " + "which will assume no prefix should be used." + ], + "auto_level": [ + 0, + "Depth of children to add prefixes to - Default: 0" + ], + "auto": [ + True, + "Auto add IDs with prefixes (prefixes are only added if prefix template is defined) - Default: False" + ], + "prepend": [ + False, + "Prepend captions opposed to appending - Default: False" + ] + } + + super().__init__(*args, **kwargs) + + def extendMarkdownBlocks(self, md, block_mgr): + """Extend Markdown blocks.""" + + config = self.getConfigs() + + # Generate an details subclass based on the given names. + types = {} + for obj in config['types']: + if isinstance(obj, dict): + name = obj['name'] + prefix = obj.get('prefix', '') + classes = obj.get('classes', '') + else: + name = obj + prefix = '' + classes = '' + types[name] = prefix + subclass = RE_SEP.sub('', name).title() + block_mgr.register( + type( + subclass, + (Caption,), + { + 'OPTIONS': {}, + 'NAME': name, + 'PREFIX': prefix, + 'CLASSES': classes + } + ), + {'auto_level': config['auto_level'], 'auto': config['auto'], 'prepend': config['prepend']} + ) + + if config['auto']: + md.treeprocessors.register(CaptionTreeprocessor(md, types, config), 'caption-auto', 4) + + +def makeExtension(*args, **kwargs): + """Return extension.""" + + return CaptionExtension(*args, **kwargs) diff --git a/pymdownx/blocks/details.py b/pymdownx/blocks/details.py index eb3e7ac0e..6a2862596 100644 --- a/pymdownx/blocks/details.py +++ b/pymdownx/blocks/details.py @@ -5,7 +5,6 @@ import re RE_SEP = re.compile(r'[_-]+') -RE_VALID_NAME = re.compile(r'[\w-]+') class Details(Block): diff --git a/tests/test_extensions/test_blocks/test_captions.py b/tests/test_extensions/test_blocks/test_captions.py new file mode 100644 index 000000000..5af7fd575 --- /dev/null +++ b/tests/test_extensions/test_blocks/test_captions.py @@ -0,0 +1,1469 @@ +"""Test cases for Blocks (caption).""" +from ... import util + + +class TestBlocksCaption(util.MdCase): + """Test Blocks caption cases with default configuration.""" + + extension = ['pymdownx.blocks.caption', 'md_in_html', 'pymdownx.blocks.html'] + extension_configs = { + 'pymdownx.blocks.caption': { + 'auto': False + } + } + + def test_caption(self): + """Test basic caption.""" + + self.check_markdown( + R''' + A paragraph with a caption. + /// caption + This is the caption. + /// + ''', + R''' +
+

A paragraph with a caption.

+
+

This is the caption.

+
+
+ ''', + True + ) + + def test_caption_with_markdown(self): + """Test caption with markdown.""" + + self.check_markdown( + R''' + A paragraph with a caption. + /// caption + This is the **caption**. + /// + ''', + R''' +
+

A paragraph with a caption.

+
+

This is the caption.

+
+
+ ''', + True + ) + + def test_image_caption(self): + """Test image caption.""" + + self.check_markdown( + R''' + ![Alt text](/path/to/img.jpg) + /// caption + This is the caption. + /// + ''', + R''' +
+

Alt text

+
+

This is the caption.

+
+
+ ''', + True + ) + + def test_nested_caption(self): + """Test nested caption.""" + + self.check_markdown( + R''' + A paragraph with a caption. + /// caption + This is the nested caption. + /// + /// caption + This is the caption. + /// + ''', + R''' +
+
+

A paragraph with a caption.

+
+

This is the nested caption.

+
+
+
+

This is the caption.

+
+
+ ''', + True + ) + + def test_caption_in_figure_block(self): + """Test that captions are injected into existing figures.""" + + self.check_markdown( + R""" +
+ Some content +
+ /// caption + Caption. + /// + + /// html | figure + Some content + /// + /// caption + Caption. + /// + """, + """ +
+

Some content

+
+

Caption.

+
+
+
+

Some content

+
+

Caption.

+
+
+ """, + True + ) + + def test_caption_not_in_figure_block(self): + """Test that captions are not injected into existing figures that already have captions.""" + + self.check_markdown( + R""" +
+ Some content + +
+ Existing caption +
+ +
+ /// caption + Caption. + /// + + /// html | figure + Some content + //// html | figcaption + Existing caption + //// + /// + /// caption + Caption. + /// + """, + R""" +
+
+

Some content

+
+

Existing caption

+
+
+
+

Caption.

+
+
+
+
+

Some content

+
+

Existing caption

+
+
+
+

Caption.

+
+
+ """, + True + ) + + def test_manual_prepend(self): + """Test manual prepend.""" + + self.check_markdown( + R""" + Text + /// caption | < + Prepended + /// + + Text + /// caption | > + Appended + /// + """, + R""" +
+
+

Prepended

+
+

Text

+
+
+

Text

+
+

Appended

+
+
+ """, + True + ) + + def test_bad_header(self): + """Test a bad header.""" + + self.check_markdown( + R""" + Test + /// caption | bad + /// + """, + """ +

Test + /// caption | bad + ///

+ """, + True + ) + + +class TestBlocksCaptionClass(util.MdCase): + """Test caption classes.""" + + extension = ['pymdownx.blocks.caption', 'md_in_html', 'pymdownx.blocks.html'] + extension_configs = { + 'pymdownx.blocks.caption': { + 'types': [ + {"name": "figure-class", "classes": "class1 class2"} + ] + } + } + + def test_class_insertion(self): + """Test class insertion.""" + + self.check_markdown( + R''' + A paragraph with a caption. + /// figure-class + This is the caption. + /// + ''', + R''' +
+

A paragraph with a caption.

+
+

This is the caption.

+
+
+ ''', + True + ) + + def test_class_insertion_existing_figure(self): + """Test class insertion on existing figure.""" + + self.check_markdown( + R''' + /// html | figure + A paragraph with a caption. + /// + /// figure-class + This is the caption. + /// + ''', + R''' +
+

A paragraph with a caption.

+
+

This is the caption.

+
+
+ ''', + True + ) + + def test_class_insertion_existing_class(self): + """Test class insertion on existing figure with existing class.""" + + self.check_markdown( + R''' + /// html | figure.class1 + A paragraph with a caption. + /// + /// figure-class + This is the caption. + /// + ''', + R''' +
+

A paragraph with a caption.

+
+

This is the caption.

+
+
+ ''', + True + ) + + +class TestBlocksCaptionPrefix(util.MdCase): + """Test Blocks caption cases with enabled `auto`.""" + + extension = ['pymdownx.blocks.caption', 'md_in_html'] + extension_configs = { + 'pymdownx.blocks.caption': { + 'auto': False + } + } + + def test_caption(self): + """Test basic caption with `auto`.""" + + self.check_markdown( + R''' + A paragraph with a caption. + /// figure-caption | 1 + This is the caption. + /// + ''', + R''' +
+

A paragraph with a caption.

+
+

Figure 1. This is the caption.

+
+
+ ''', + True + ) + + def test_consecutive_captions(self): + """Test consecutive captions with `auto`.""" + + self.check_markdown( + R''' + A paragraph with a caption. + /// figure-caption | 1 + This is the caption. + /// + + A paragraph with a caption. + /// figure-caption | 2 + This is the caption. + /// + ''', + R''' +
+

A paragraph with a caption.

+
+

Figure 1. This is the caption.

+
+
+
+

A paragraph with a caption.

+
+

Figure 2. This is the caption.

+
+
+ ''', + True + ) + + def test_nested_captions(self): + """Test nested captions with `auto`.""" + + self.check_markdown( + R''' + A paragraph with a caption. + /// figure-caption | 1.1.1 + Level 3 caption. + /// + /// figure-caption | 1.1 + Level 2 caption. + /// + /// figure-caption | 1 + Level 1 caption. + /// + ''', + R''' +
+
+
+

A paragraph with a caption.

+
+

Figure 1.1.1. Level 3 caption.

+
+
+
+

Figure 1.1. Level 2 caption.

+
+
+
+

Figure 1. Level 1 caption.

+
+
+ ''', + True + ) + + def test_nested_consecutive_captions(self): + """Test nested captions with `auto`.""" + + self.check_markdown( + R''' + A paragraph with a caption. + /// figure-caption | 1.1.1 + Level 3 caption. + /// + /// figure-caption | 1.1 + Level 2 caption. + /// + /// figure-caption | 1 + Level 1 caption. + /// + + A paragraph with a caption. + /// figure-caption | 2.1 + Level 2 caption. + /// + /// figure-caption | 2 + Level 1 caption. + /// + ''', + R''' +
+
+
+

A paragraph with a caption.

+
+

Figure 1.1.1. Level 3 caption.

+
+
+
+

Figure 1.1. Level 2 caption.

+
+
+
+

Figure 1. Level 1 caption.

+
+
+
+
+

A paragraph with a caption.

+
+

Figure 2.1. Level 2 caption.

+
+
+
+

Figure 2. Level 1 caption.

+
+
+ ''', + True + ) + + def test_inject_new_p_in_caption(self): + """Test `auto` cases that require the prefix to be injected in a new paragraph.""" + + self.check_markdown( + R""" + Test + /// figure-caption | 1 + /// + + Test + /// figure-caption | 2 + > blockquote + /// + """, + R""" +
+

Test

+
+

Figure 1.

+
+
+
+

Test

+
+

Figure 2.

+
+

blockquote

+
+
+
+ """, + True + ) + + def test_empty_paragraph(self): + """Test `auto` cases that require prefix to inject a new paragraph.""" + + self.check_markdown( + R""" + Test + /// figure-caption | 1 +

+ /// + """, + R""" +
+

Test

+
+

Figure 1.

+
+
+ """, + True + ) + + def test_manual_prepend(self): + """Test manual prepend.""" + + self.check_markdown( + R""" + Text + /// figure-caption | < 2 + Prepended + /// + + Text + /// figure-caption | > 5 + Appended + /// + """, + R""" +
+
+

Figure 2. Prepended

+
+

Text

+
+
+

Text

+
+

Figure 5. Appended

+
+
+ """, + True + ) + + def test_depth(self): + """Depth is not really supported in manual, so a generic response is expected.""" + + self.check_markdown( + R""" + Paragraph + /// figure-caption + Caption 1 + /// + + Paragraph + /// figure-caption | ^1 + Caption 1.1 + /// + + Paragraph + /// figure-caption | ^2 + Caption 1.1.1 + /// + + Paragraph + /// figure-caption | ^3 + Caption 1.1.1.1 + /// + """, + """ +
+

Paragraph

+
+

Caption 1

+
+
+
+

Paragraph

+
+

Figure 1.1. Caption 1.1

+
+
+
+

Paragraph

+
+

Figure 1.1.1. Caption 1.1.1

+
+
+
+

Paragraph

+
+

Figure 1.1.1.1. Caption 1.1.1.1

+
+
+ """, + True + ) + + +class TestBlocksCaptionAutoPrefix(util.MdCase): + """Test Blocks caption cases with enabled `auto`.""" + + extension = ['pymdownx.blocks.caption', 'md_in_html'] + extension_configs = { + 'pymdownx.blocks.caption': { + } + } + + def test_caption(self): + """Test basic caption with `auto`.""" + + self.check_markdown( + R''' + A paragraph with a caption. + /// figure-caption + This is the caption. + /// + ''', + R''' +
+

A paragraph with a caption.

+
+

Figure 1. This is the caption.

+
+
+ ''', + True + ) + + def test_consecutive_captions(self): + """Test consecutive captions with `auto`.""" + + self.check_markdown( + R''' + A paragraph with a caption. + /// figure-caption + This is the caption. + /// + + A paragraph with a caption. + /// figure-caption + This is the caption. + /// + ''', + R''' +
+

A paragraph with a caption.

+
+

Figure 1. This is the caption.

+
+
+
+

A paragraph with a caption.

+
+

Figure 2. This is the caption.

+
+
+ ''', + True + ) + + def test_nested_captions(self): + """Test nested captions with `auto`.""" + + self.check_markdown( + R''' + A paragraph with a caption. + /// figure-caption + Level 3 caption. + /// + /// figure-caption + Level 2 caption. + /// + /// figure-caption + Level 1 caption. + /// + ''', + R''' +
+
+
+

A paragraph with a caption.

+
+

Figure 1.1.1. Level 3 caption.

+
+
+
+

Figure 1.1. Level 2 caption.

+
+
+
+

Figure 1. Level 1 caption.

+
+
+ ''', + True + ) + + def test_nested_consecutive_captions(self): + """Test nested captions with `auto`.""" + + self.check_markdown( + R''' + A paragraph with a caption. + /// figure-caption + Level 3 caption. + /// + /// figure-caption + Level 2 caption. + /// + /// figure-caption + Level 1 caption. + /// + + A paragraph with a caption. + /// figure-caption + Level 2 caption. + /// + /// figure-caption + Level 1 caption. + /// + ''', + R''' +
+
+
+

A paragraph with a caption.

+
+

Figure 1.1.1. Level 3 caption.

+
+
+
+

Figure 1.1. Level 2 caption.

+
+
+
+

Figure 1. Level 1 caption.

+
+
+
+
+

A paragraph with a caption.

+
+

Figure 2.1. Level 2 caption.

+
+
+
+

Figure 2. Level 1 caption.

+
+
+ ''', + True + ) + + def test_manual_prepend(self): + """Test manual prepend.""" + + self.check_markdown( + R""" + Text + /// figure-caption | < 2 + Prepended and number ignored + /// + + Text + /// figure-caption | > + Appended + /// + """, + R""" +
+
+

Figure 2. Prepended and number ignored

+
+

Text

+
+
+

Text

+
+

Figure 3. Appended

+
+
+ """, + True + ) + + def test_mixed_captions(self): + """Test mixed captions with `auto`.""" + + self.check_markdown( + R''' + Paragraph + /// caption + Not numbered + /// + + Paragraph + /// figure-caption + Numbered level 2 caption. + /// + /// caption + Not numbered level 1. + /// + /// figure-caption + Numbered level 1 caption. + /// + ''', + R''' +
+

Paragraph

+
+

Not numbered

+
+
+
+
+
+

Paragraph

+
+

Figure 1.1. Numbered level 2 caption.

+
+
+
+

Not numbered level 1.

+
+
+
+

Figure 1. Numbered level 1 caption.

+
+
+ ''', + True + ) + + def test_existing_fig_caption(self): + """Test when a figure has a figure caption and we don't know what type it is.""" + + self.check_markdown( + R""" +
+ Some text. +
+ Caption. +
+
+ """, + R""" +
+

Some text.

+
+

Caption.

+
+
+ """, + True + ) + + def test_inject_new_p_in_caption(self): + """Test `auto` cases that require the prefix to be injected in a new paragraph.""" + + self.check_markdown( + R""" + Test + /// figure-caption + /// + + Test + /// figure-caption + > blockquote + /// + """, + R""" +
+

Test

+

Figure 1.

+
+
+

Test

+

Figure 2.

+
+

blockquote

+
+
+
+ """, + True + ) + + def test_empty_paragraph(self): + """Test `auto` cases that require prefix to inject a new paragraph.""" + + self.check_markdown( + R""" + Test + /// figure-caption +

+ /// + """, + R""" +
+

Test

+
+

Figure 1.

+
+
+ """, + True + ) + + def test_nested_captions_manual_id(self): + """Test nested captions with `auto` and `auto` with manual IDs.""" + + self.check_markdown( + R''' + A paragraph with a caption. + /// figure-caption + Level 4 caption. + /// + /// figure-caption + attrs: {id: test} + Level 3 caption. + /// + /// figure-caption + Level 2 caption. + /// + /// figure-caption + Level 1 caption. + /// + ''', + R''' +
+
+
+
+

A paragraph with a caption.

+
+

Figure 1.1.1.1. Level 4 caption.

+
+
+
+

Figure 1.1.1. Level 3 caption.

+
+
+
+

Figure 1.1. Level 2 caption.

+
+
+
+

Figure 1. Level 1 caption.

+
+
+ ''', + True + ) + + def test_depth(self): + """Test level depth.""" + + self.check_markdown( + R""" + Paragraph + /// figure-caption + Caption 1 + /// + + Paragraph + /// figure-caption + Caption 1.1.1 + /// + + /// figure-caption | ^1 + Caption 1.1 + /// + + Paragraph + /// figure-caption | ^1 + Caption 2.1 + /// + + /// figure-caption + Caption 2 + /// + """, + """ +
+

Paragraph

+
+

Figure 1. Caption 1

+
+
+
+
+

Paragraph

+
+

Figure 1.1.1. Caption 1.1.1

+
+
+
+

Figure 1.1. Caption 1.1

+
+
+
+
+

Paragraph

+
+

Figure 2.1.1. Caption 2.1

+
+
+
+

Figure 2. Caption 2

+
+
+ """, + True + ) + + def test_manual_number(self): + """Test manual number.""" + + self.check_markdown( + R""" + Paragraph + /// figure-caption + Caption 4.2.1 + /// + + /// figure-caption | 4.2 + Caption 4.2 + /// + + + Paragraph + /// figure-caption + Caption 5.2.1 + /// + + /// figure-caption | 5.2 + Caption 5.2 + /// + + /// figure-caption + Caption 5 + /// + """, + """ +
+
+

Paragraph

+
+

Figure 4.2.1. Caption 4.2.1

+
+
+
+

Figure 4.2. Caption 4.2

+
+
+
+
+
+

Paragraph

+
+

Figure 5.2.1. Caption 5.2.1

+
+
+
+

Figure 5.2. Caption 5.2

+
+
+
+

Figure 5. Caption 5

+
+
+ """, + True + ) + + +class TestBlocksCaptionAutoLevel(util.MdCase): + """Test Blocks caption cases with `auto` level.""" + + extension = ['pymdownx.blocks.caption'] + extension_configs = { + 'pymdownx.blocks.caption': { + 'auto_level': 2 + } + } + + def test_caption(self): + """Test basic caption with `auto` level.""" + + self.check_markdown( + R''' + A paragraph with a caption. + /// figure-caption + This is the caption. + /// + ''', + R''' +
+

A paragraph with a caption.

+
+

Figure 1. This is the caption.

+
+
+ ''', + True + ) + + def test_nested_captions(self): + """Test nested captions with `auto` level.""" + + self.check_markdown( + R''' + A paragraph with a caption. + /// figure-caption + Level 3 caption. + /// + /// figure-caption + Level 2 caption. + /// + /// figure-caption + Level 1 caption. + /// + ''', + R''' +
+
+
+

A paragraph with a caption.

+
+

Level 3 caption.

+
+
+
+

Figure 1.1. Level 2 caption.

+
+
+
+

Figure 1. Level 1 caption.

+
+
+ ''', + True + ) + + def test_nested_consecutive_captions(self): + """Test nested consecutive captions with `auto` level.""" + + self.check_markdown( + R''' + A paragraph with a caption. + /// figure-caption + Level 3 caption. + /// + /// figure-caption + Level 2 caption. + /// + /// figure-caption + Level 1 caption. + /// + + A paragraph with a caption. + /// figure-caption + Level 2 caption. + /// + /// figure-caption + Level 1 caption. + /// + ''', + R''' +
+
+
+

A paragraph with a caption.

+
+

Level 3 caption.

+
+
+
+

Figure 1.1. Level 2 caption.

+
+
+
+

Figure 1. Level 1 caption.

+
+
+
+
+

A paragraph with a caption.

+
+

Figure 2.1. Level 2 caption.

+
+
+
+

Figure 2. Level 1 caption.

+
+
+ ''', + True + ) + + def test_depth(self): + """Test depth with auto level limit.""" + + self.check_markdown( + R""" + Paragraph + /// figure-caption + Caption 1 + /// + + Paragraph + /// figure-caption | ^1 + Caption 1.1 + /// + + Paragraph + /// figure-caption | ^2 + Caption None + /// + """, + """ +
+

Paragraph

+
+

Figure 1. Caption 1

+
+
+
+

Paragraph

+
+

Figure 1.1. Caption 1.1

+
+
+
+

Paragraph

+
+

Caption None

+
+
+ """, + True + ) + + def test_manual_numbers(self): + """Test manual numbers with auto level limit.""" + + self.check_markdown( + R""" + Paragraph + /// figure-caption | 1 + Caption 1 + /// + + Paragraph + /// figure-caption | 2.3 + Caption 2.3 + /// + + Paragraph + /// figure-caption | 4.3.1 + Caption None + /// + """, + """ +
+

Paragraph

+
+

Figure 1. Caption 1

+
+
+
+

Paragraph

+
+

Figure 2.3. Caption 2.3

+
+
+
+

Paragraph

+
+

Figure 4.3.1. Caption None

+
+
+ """, + True + ) + + +class TestBlocksCaptionAutoLevelPrepend(util.MdCase): + """Test Blocks caption cases with `auto` level.""" + + extension = ['pymdownx.blocks.caption'] + extension_configs = { + 'pymdownx.blocks.caption': { + 'auto_level': 2, + 'prepend': True + } + } + + def test_caption(self): + """Test basic caption with `auto` level and `prepend`.""" + + self.check_markdown( + R''' + A paragraph with a caption. + /// figure-caption + This is the caption. + /// + ''', + R''' +
+
+

Figure 1. This is the caption.

+
+

A paragraph with a caption.

+
+ ''', + True + ) + + def test_nested_captions(self): + """Test nested captions with `auto` level and `prepend`.""" + + self.check_markdown( + R''' + A paragraph with a caption. + /// figure-caption + Level 3 caption. + /// + /// figure-caption + Level 2 caption. + /// + /// figure-caption + Level 1 caption. + /// + ''', + R''' +
+
+

Figure 1. Level 1 caption.

+
+
+
+

Figure 1.1. Level 2 caption.

+
+
+
+

Level 3 caption.

+
+

A paragraph with a caption.

+
+
+
+ ''', + True + ) + + def test_nested_consecutive_captions(self): + """Test nested consecutive captions with `auto` level and `prepend`.""" + + self.check_markdown( + R''' + A paragraph with a caption. + /// figure-caption + Level 3 caption. + /// + /// figure-caption + Level 2 caption. + /// + /// figure-caption + Level 1 caption. + /// + + A paragraph with a caption. + /// figure-caption + Level 2 caption. + /// + /// figure-caption + Level 1 caption. + /// + ''', + R''' +
+
+

Figure 1. Level 1 caption.

+
+
+
+

Figure 1.1. Level 2 caption.

+
+
+
+

Level 3 caption.

+
+

A paragraph with a caption.

+
+
+
+
+
+

Figure 2. Level 1 caption.

+
+
+
+

Figure 2.1. Level 2 caption.

+
+

A paragraph with a caption.

+
+
+ ''', + True + ) + + def test_manual_prepend(self): + """Test manual prepend.""" + + self.check_markdown( + R""" + Text + /// figure-caption | < + Prepended + /// + + Text + /// figure-caption | > + Appended + /// + """, + R""" +
+
+

Figure 1. Prepended

+
+

Text

+
+
+

Text

+
+

Figure 2. Appended

+
+
+ """, + True + )