Repository: facelessuser/pymdown-extensions

Issue:
Support for wrapping caption number (index) in a nested tag for styling and extraction
### Description

Currently, the caption prefix (both surrounding text and the auto-incremented number) is output together in a single `<span.caption-prefix>`. This makes it difficult to individually **style, extract, or manipulate** the number separate from the surrounding text.

The incremented number should be wrapped in its own nested tag (for example: `<span.caption-index>`) inside the `<span.caption-prefix>`. This would allow for more precise control and customization.


### Benefits

- **Styling**: Easily target just the index with CSS
- **Extraction**: Simpler parsing for generating a Table of Figures (ToF)
- **Post-processing**: Enables easier DOM manipulation via JavaScript or Python


### Solution Idea

Given this configuration:

```python
extenconfigs = {
    "pymdownx.blocks.caption": {
        "types": [
            'caption',
            {
                'name': 'test-caption',
                'prefix': 'Test {} -',
                'classes': 'figure-test'
            }
        ]
    }
}
```

And Markdown content:

```markdown
This is a test

/// test-caption
This is a caption
///
```

The expected HTML output would be:

```html
<figure class="figure-test">
    <p>This is a test</p>
    <figcaption>
        <p><span class="caption-prefix">Test <span class="caption-index">1</span> -</span> This is a caption</p>
    </figcaption>
</figure>
```

Discussion:
@facelessuser: I guess I'm not really understanding. What kind of common customization examples do you imagine people doing? I personally have no use for targeting and styling the number differently from the rest of the caption, and I'm not sure how common the use of such a request would actually be. The desire sounds pretty niche, but I'm not sure.@Thiti517: Thanks for the follow-up ! Let me try to show *why* this matters in practice, and the kinds of things this enables that aren’t possible (or are complex) without the separation.

## My use-case

I’m using `mkdocs` + `mkdocs-material` + [`mkdocs-to-pdf`](https://github.com/domWalters/mkdocs-to-pdf) to produce **PDFs for distribution purposes**, where:

- Each **figure number is reindexed** to follow chapter-based numbering.
- A **Table of Figures (ToF)** is auto-generated and appended at the PDF.
- I modify HTML via a hook script before passing it to the PDF generation process.

Right now, `Fig. 23` is rendered as:

```html
<span class="caption-prefix">Fig. 23</span>
```

To extract and rewrite just the `23`, I have to:

- Use regex or DOM parsing to detect and replace numbers in-place.
- Handle edge cases if the prefix changes.
- Risk breaking formatting or punctuation.

(In my specific case, I limit myself to the prefix “Fig.” and no suffix, so it’s manageable—but still fragile.)

### Example

Here’s a simplified visualization of the flow:

Input:
```
Chapter 1
├── Figure 1: "Project overview"
├── Figure 2: "System architecture"
└── Figure 3: "Component separation"
Chapter 2
├── Figure 4: "Test results"
└── Figure 5: "Alternative visualization"
Chapter 3
├── Sub-chapter 3.1
│   ├── Figure 6: "User interface layout"
│   └── Figure 7: "Navigation flow"
└── Sub-chapter 3.2
    ├── Figure 8: "Performance metrics"
    └── Figure 9: "Scalability diagram"
```

After reindexing:
```
1 -> 1.1
2 -> 1.2
3 -> 1.3
4 -> 2.1
5 -> 2.2
6 -> 3.1
7 -> 3.2
8 -> 3.3
9 -> 3.4
```

Generated ToF
```
Table of Figures

1.1 Project overview .............................  1  
1.2 System architecture .......................... 23  
1.3 Component separation ......................... 34  

2.1 Test results ................................. 37  
2.2 Alternative visualization .................... 42  

3.1 User interface layout ........................ 51  
3.2 Navigation flow .............................. 52  
3.3 Performance metrics .......................... 55  
3.4 Scalability diagram .......................... 57  
```

## Why separation matters

If the HTML looked like this instead:

```html
<span class="caption-prefix">Fig. <span class="caption-index">23</span></span>
```

Then I could:

- Easily select `.caption-index` to **replace it with a computed chapter-relative index** without touching the "Fig." label.
- Style `.caption-index` independently (bold or different color).
- Cleanly extract entries to build a ToF without having to parse whole prefix.

## General value (beyond my case)

Even though this use case is advanced, the structure would help:

- Anyone doing post-processing on figure numbers ToF generation, automatic link text generation (see #2670).
- Anyone wanting **custom styling** on the number (bold, colors, accessibility).
- And it introduces **no breaking change** (that i know of), it only adds a nested span for those who want to target indexes.
@facelessuser: So, I'm still not sure if what you are asking for is explicitly needed. Now, in my evaluation, I think I also found a bug, but let explain how this was intended to work.

So, when captions were put together, it was designed with some of this in mind. Ideally, you should be able to do something like what is below. So for a new chapter, you initialize the first figure and then all subsequent figures in that chapter can be noted to be nested one one level deeper, so 1.x opposed to x.

```
test
/// figure-caption | 1.1
test
///

test
/// figure-caption | ^1
test
///
```

The problem is that the current code has a bug where the initialization of Chapter 2 is not handled correctly, but this _should_ work. But I need to fix the bug preventing it from working as intended.

```
test
/// figure-caption | 1.1
test
///

test
/// figure-caption | ^1
test
///

test
/// figure-caption | 2.1
test
///

test
/// figure-caption | ^1
test
///
```

The only thing you could do to further simplify this is maybe have a minimum limit that forces x.x instead of x as the starting count, but you'd still have to manually force chapters to be initialized with `x.1`, but then you wouldn't need to specify the `^1`. I don't view this as a necessity though, and I'm not even sure if it is something I care to address, but it is something that maybe could be addressed.@facelessuser: Intended behavior should be fixed on main.@facelessuser: Since the prefix template does not allow markdown parsing, we could allow users to specify raw HTML in them if they prefer.

```
'prefix': 'Test <span class="caption-prefix">{}</span> -',
```

Currently, it HTML escapes the content, but there is probably no need to do so and we can just note that that content is inserted literally as specified.
@facelessuser: I've issued a PR that prototypes my suggestion. In that PR, you can now use HTML tags in your prefix templates and wrap numbers if you prefer. So, if you don't like my earlier suggestion of initializing figure numbers for a given chapter, you can just leave it as is and specify your own wrapper in the prefix template.  See PR #2679.@facelessuser: I've decided to move ahead with the template change. We've fixed manual manipulation within "auto" mode and allow raw HTML to be inserted into prefix templates, the latter allowing you to do what was originally desired.@Thiti517: Sound good to me ! (sorry for not responding earlier it was night time for me)@facelessuser:  No worries, I would have waited had I thought the direction was controversial. 

Solution patch:
diff --git a/docs/src/markdown/about/changelog.md b/docs/src/markdown/about/changelog.md
index 07d7461b..d9a1c14e 100644
--- a/docs/src/markdown/about/changelog.md
+++ b/docs/src/markdown/about/changelog.md
@@ -3,8 +3,10 @@
 ## 10.16
 
 -   **NEW**: Drop support for Python 3.8.
--   **NEW**: Snippets: Added `max_retries` and `backoff_retries` options to configure new retry logic for HTTP 429 errors
-    (Too Many Requests client error).
+-   **NEW**: Snippets: Added `max_retries` and `backoff_retries` options to configure new retry logic for HTTP 429
+    errors (Too Many Requests client error).
+-   **NEW**: Caption: Prefix templates are now preserved exactly as specified allowing the insertion of HTML tags if
+    desired.
 -   **FIX**: Caption: Fix issue where manual numbers in auto were not respected appropriately.
 
 ## 10.15
diff --git a/docs/src/markdown/extensions/blocks/plugins/caption.md b/docs/src/markdown/extensions/blocks/plugins/caption.md
index a4ad04c0..9c50eaab 100644
--- a/docs/src/markdown/extensions/blocks/plugins/caption.md
+++ b/docs/src/markdown/extensions/blocks/plugins/caption.md
@@ -498,6 +498,32 @@ extenconfigs = {
 }
 ```
 
+Prefix templates are inserted exactly as specified, that means you can also use raw HTML. That also means you must
+HTML escape anything that needs it as well.
+
+```py
+extenconfigs = {
+    "pymdownx.blocks.caption": {
+        "types": [
+            'caption',
+            {
+                'name': 'figure-caption',
+                'prefix': 'Figure <span class="prefix-fig-num">{}</span>.'
+                'classes': 'one-class two-class'
+            },
+            {
+                'name': 'table-caption',
+                'prefix': 'Table <span class="prefix-fig-num">{}</span>.'
+            }
+        ]
+    }
+}
+```
+
+/// new | New 10.16
+Using raw HTML in prefix templates was added in 10.16.
+///
+
 ## Global Options
 
 Options        | Type       | Descriptions
diff --git a/pymdownx/blocks/caption.py b/pymdownx/blocks/caption.py
index a98f5e7d..65a469ff 100644
--- a/pymdownx/blocks/caption.py
+++ b/pymdownx/blocks/caption.py
@@ -35,7 +35,7 @@ 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):
+def update_tag(el, fig_type, fig_num, template, prepend, md):
     """Update tag ID and caption prefix."""
 
     # Auto add an ID
@@ -47,7 +47,7 @@ def update_tag(el, fig_type, fig_num, template, prepend):
         for child in list(el) if prepend else reversed(el):
             if child.tag == 'figcaption':
                 children = list(child)
-                value = template.format(fig_num)
+                value = md.htmlStash.store(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'})
@@ -184,7 +184,8 @@ class CaptionTreeprocessor(Treeprocessor):
                     fig_type,
                     '.'.join(str(x) for x in counter[:stack + 1]),
                     self.fig_types.get(fig_type, ''),
-                    prepend
+                    prepend,
+                    self.md
                 )
 
         # Clean up attributes
@@ -314,7 +315,8 @@ class Caption(Block):
                     self.NAME,
                     self.fig_num,
                     prefix,
-                    self.prepend
+                    self.prepend,
+                    self.md
                 )
 
 


Test patch:
diff --git a/tests/test_extensions/test_blocks/test_captions.py b/tests/test_extensions/test_blocks/test_captions.py
index b78c2a5f..c2ecd6d8 100644
--- a/tests/test_extensions/test_blocks/test_captions.py
+++ b/tests/test_extensions/test_blocks/test_captions.py
@@ -1522,3 +1522,45 @@ class TestBlocksCaptionAutoLevelPrepend(util.MdCase):
             """,
             True
         )
+
+
+class TestBlocksCaptionCustomPrefix(util.MdCase):
+    """Test Blocks caption cases with `auto` level."""
+
+    extension = ['pymdownx.blocks.caption']
+    extension_configs = {
+        'pymdownx.blocks.caption': {
+            'types': [
+                'caption',
+                {
+                    'name': 'figure-caption',
+                    'prefix': 'Figure <span class="caption-num">{}</span>.'
+                },
+                {
+                    'name': 'table-caption',
+                    'prefix': 'Table <span class="caption-num">{}</span>.'
+                }
+            ]
+        }
+    }
+
+    def test_custom_prefix(self):
+        """Test custom prefix."""
+
+        self.check_markdown(
+            R'''
+            A paragraph with a caption.
+            /// figure-caption
+            This is the caption.
+            ///
+            ''',
+            R'''
+            <figure id="__figure-caption_1">
+            <p>A paragraph with a caption.</p>
+            <figcaption>
+            <p><span class="caption-prefix">Figure <span class="caption-num">1</span>.</span> This is the caption.</p>
+            </figcaption>
+            </figure>
+            ''',
+            True
+        )