@peaceroad/markdown-it-figure-with-p-caption 0.19.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,221 +1,114 @@
1
1
  # p7d-markdown-it-figure-with-p-caption
2
2
 
3
- This markdown-it plugin converts paragraphs representing captions before or after image/table/code/video/audio/iframe into `figcaption` element, and wraps them in `figure` element. Caption parsing (labels, filenames, spacing rules) is delegated to [`p7d-markdown-it-p-captions`](https://www.npmjs.com/package/p7d-markdown-it-p-captions), so this plugin focuses on detecting the surrounding structure. Optionally, you have the option to wrap it in a `figure` element, even if there is no caption paragraph.
3
+ A markdown-it plugin that converts adjacent caption paragraphs and supported
4
+ media blocks into semantic `<figure>` / `<figcaption>` structures.
4
5
 
5
- For images, even if they don't have a caption paragraph, they can be treated as captions if they have a caption string in the image's `alt`/`title` text (there is also an option to promote them to captions even if they don't have that string).
6
+ Caption-label parsing, numbering grammar, and language catalogs are delegated
7
+ to [`p7d-markdown-it-p-captions`](https://www.npmjs.com/package/p7d-markdown-it-p-captions).
8
+ This plugin owns figure-candidate detection, wrapping, figure classes, and
9
+ optional document/chapter-aware numbering.
6
10
 
7
- Optionally, you can auto-number image and table caption paragraphs starting from the beginning of the document if they only have label names.
11
+ Supported targets include images, tables, code fences, terminal/samp fences,
12
+ blockquotes, video/audio blocks, iframes, social embeds, and slide iframes.
8
13
 
9
- **Note.** If you want to adjust the image `width`/`height`, please also use [`@peaceroad/markdown-it-renderer-image`](https://www.npmjs.com/package/@peaceroad/markdown-it-renderer-image). Also, if you want to use the `samp` element when displaying terminal output, please also use [`@peaceroad/markdown-it-renderer-fence`](https://www.npmjs.com/package/@peaceroad/markdown-it-renderer-fence). This document shows output using the latter option.
14
+ ## Installation
10
15
 
11
- ## Behavior
12
-
13
- ### Image
14
-
15
- - Pure image paragraphs (`![...](...)`) become `<figure class="f-img">` blocks as soon as a caption paragraph (previous or next) or an auto-detected caption exists.
16
- - Auto detection runs per image paragraph when `autoCaptionDetection` is `true` (default). The priority is:
17
- 1. Caption paragraphs immediately before or after the image (standard syntax).
18
- 2. Image `alt` text that `p7d-markdown-it-p-captions` recognizes as an image caption start (`Figure. `, `Figure 1. `, `図 `, `図1 `, etc.).
19
- 3. Image `title` attribute that matches the same labels.
20
- 4. Optional fallbacks (`autoAltCaption`, `autoTitleCaption`) that inject the label when the alt/title lacks one.
21
- - `autoAltCaption`: `false` (default), `true`, or a string label. `true` uses locale-aware generated-label defaults from `p7d-markdown-it-p-captions`, so the label text and punctuation stay aligned with the upstream caption language data. A string is treated as a label stem that must be recognized by `p7d-markdown-it-p-captions`; setup throws if it cannot be parsed as an image caption label. This plugin appends the default joint/space unless the string already ends with punctuation such as `.` / `。` / `:`. Empty alt text does not generate a fallback caption.
22
- - `autoTitleCaption`: same behavior but sourced from the image `title`. It stays off by default so other plugins can keep using the `title` attribute for metadata.
23
- - Set `autoCaptionDetection: false` to disable the auto-caption workflow entirely.
24
- - Multi-image paragraphs are still wrapped as one figure when `multipleImages: true` (default). Layout-specific classes help with styling:
25
- - `f-img-horizontal` when images sit on the same line (space-delimited).
26
- - `f-img-vertical` when separated only by soft breaks.
27
- - `f-img-multiple` for mixed layouts.
28
- - Automatic detection inspects only the first image in the paragraph. If it yields a caption, the entire figure reuses that caption while later images keep their own `alt`/`title`.
29
- - Paragraphs that contain only images also convert when they appear inside loose lists (leave blank lines between items), blockquotes, or description lists.
30
- - Caption detection intentionally skips paragraphs that are the first block inside a list item (`list_item_open` immediately before the paragraph). In practice, `- Figure. ...` followed by an image in the same item is treated as plain text unless you insert another block first.
31
-
32
- ### Table
33
-
34
- - Markdown tables (including those produced by `markdown-it-multimd-table` or similar) convert into `<figure class="f-table">` blocks.
35
- - Caption paragraphs immediately before/after the table become `<figcaption>` element ahead of the `<table>`.
36
-
37
- ### Code block
38
-
39
- - Captions labeled `Code. `, `Terminal. `, etc. wrap the fence in `<figure class="f-pre-code">` / `<figure class="f-pre-samp">`.
40
- - If `roleDocExample: true`, these figures add `role="doc-example"`.
41
-
42
- ### Blockquote
43
-
44
- - Captioned blockquotes (e.g., `Source. A paragraph.` written immediately before or after `> ...`) become `<figure class="f-blockquote">` while keeping the original blockquote intact.
45
-
46
- ### Video & Audio
47
-
48
- - Inline HTML `<video>` and `<audio>` tags are detected as media figures (`<figure class="f-video">` and `<figure class="f-audio">`).
49
- - A caption paragraph labeled `Video. ` / `Audio. ` (or any registered label) is promoted to `<figcaption>` before/after the media so controls remain unobstructed.
50
-
51
- ### Embedded content by iframe
52
-
53
- - Inline HTML `<iframe>` elements become `<figure class="f-video">` when they point to known video hosts (YouTube `www.youtube.com`, `youtube.com`, `www.youtube-nocookie.com`, `youtube-nocookie.com`, Vimeo `player.vimeo.com`).
54
- - `<div>` wrappers are treated as iframe-type embeds only when the same HTML block contains an `<iframe ...>` tag (for example common video wrapper markup).
55
- - `videoWithoutCaption` classifies an iframe (including one nested in a `<div>` wrapper) as video only when its `src` host is one of the known YouTube/Vimeo hosts. A generic iframe wrapper still requires `iframeWithoutCaption`.
56
- - Blockquote-based social embeds (Twitter/X `twitter-tweet`, Mastodon `mastodon-embed`, Bluesky `bluesky-embed`, Instagram `instagram-media`, Tumblr `text-post-media`) are treated like iframe-type embeds when their class list contains one of those provider classes. Extra classes on the same blockquote do not block detection. By default they become `<figure class="f-img">` so the caption label behaves like an image label (Labels can also use quote labels). You can override that figure class with `figureClassThatWrapsIframeTypeBlockquote` or the global `allIframeTypeFigureClassName`.
57
- - `p7d-markdown-it-p-captions` ships with a `Slide.` label. When you use it (for example with Speaker Deck or SlideShare iframes), the `<figure>` wrapper automatically switches to `f-slide` (or whatever you set via `figureClassThatWrapsSlides`) so slides can get their own layout. If `allIframeTypeFigureClassName` is also configured, that class takes precedence even for slides, so you get a uniform embed wrapper without touching the slide option.
58
- - All other iframes fall back to `<figure class="f-iframe">` unless you override the class via `allIframeTypeFigureClassName`.
59
-
60
- ### label span class name
61
-
62
- - The label inside the figcaption (the `span` element used for the label) is generated by `p7d-markdown-it-p-captions`, not by this plugin. By default the class name is formed by combining `classPrefix` with the mark name, producing names such as `f-img-label`, `f-video-label`, `f-blockquote-label`, and `f-slide-label`.
63
- - With `markdown-it-attrs`, attributes attached to image-only paragraphs (for example `![...](...) {.foo #bar}`) are forwarded to the generated `<figure>`.
64
- - `styleProcess` controls parsing of a trailing `{...}` block from the last text token of an image-only paragraph in this plugin's own scanner. It supports simple `.class`, `#id`, bare attributes, and quoted `key="value with spaces"` / `key='value with spaces'` pairs. It is still a narrow fallback parser, not full `markdown-it-attrs` parity, and attributes already attached to paragraph tokens by `markdown-it-attrs` are still forwarded.
65
- - Attribute forwarding is not sanitization. If you render untrusted Markdown, keep using an HTML sanitizer or a trusted-host policy appropriate for your application; this plugin only decides which already-parsed or narrowly parsed attributes move onto `<figure>`.
66
- - Attributes attached to caption paragraphs stay on the converted `<figcaption>` token after paragraph-to-figcaption conversion.
67
-
68
- ## Behavior Customization
69
-
70
- ### Styles
71
-
72
- - Set `allIframeTypeFigureClassName: 'f-embed'` (recommended) to force a single CSS class across `<iframe>` and social-embed figures so they can share styles, ensuring every embed wrapper shares the same predictable class name.
73
- - `figureClassThatWrapsIframeTypeBlockquote`: override the class used when blockquote-based embeds (Twitter, Mastodon, Bluesky) are wrapped.
74
- - `figureClassThatWrapsSlides`: override the class assigned when a caption paragraph uses the `Slide.` label.
75
- - `classPrefix` (default `f`) controls the CSS namespace for every figure (`f-img`, `f-table`, etc.) so you can align with existing styles.
76
- - Wrapper/class-prefix options are trimmed during setup; whitespace-only values fall back to the default class for that option.
77
-
78
- ### Wrapping without captions
79
-
80
- - `imageOnlyParagraphWithoutCaption`: turn valid image-only paragraphs into `<figure>` elements even when no caption paragraph/auto caption is present. This includes single-image paragraphs and, when `multipleImages` is enabled, multi-image paragraphs that receive classes such as `f-img-horizontal`, `f-img-vertical`, or `f-img-multiple`. This is independent of automatic detection.
81
- - `oneImageWithoutCaption`: legacy alias for `imageOnlyParagraphWithoutCaption`. When both are provided, `imageOnlyParagraphWithoutCaption` wins.
82
- - `videoWithoutCaption`, `audioWithoutCaption`, `iframeWithoutCaption`, `iframeTypeBlockquoteWithoutCaption`: wrap the respective media blocks without caption.
83
-
84
- ### Caption text helpers (delegated to `p7d-markdown-it-p-captions`)
85
-
86
- Every option below is forwarded verbatim to `p7d-markdown-it-p-captions`, which owns the actual figcaption rendering:
87
-
88
- - `strongFilename` / `dquoteFilename`: pull out filenames from captions using `**filename**` or `"filename"` syntax and wrap them in `<strong class="f-*-filename">`.
89
- - `jointSpaceUseHalfWidth`: replace full-width space between Japanese labels and caption body with half-width space.
90
- - `bLabel` / `strongLabel`: emphasize the label span itself.
91
- - `removeUnnumberedLabel`: drop the leading label entirely when no label number is present. Use `removeUnnumberedLabelExceptMarks` to keep specific labels (e.g., `['blockquote']` keeps `Quote. `).
92
- - `removeMarkNameInCaptionClass`: replace `.f-img-label` / `.f-table-label` with the generic `.f-label`.
93
- - `wrapCaptionBody`: wrap the non-label caption text in a span element.
94
- - `hasNumClass`: add a class attribute to label span element if it has a label number.
95
- - `labelClassFollowsFigure`: mirror the resolved `<figure>` class onto the `figcaption` spans (`f-embed-label`, `f-embed-label-joint`, `f-embed-body`, etc.) when you want captions styled alongside the wrapper.
96
- - `figureToLabelClassMap`: extend `labelClassFollowsFigure` by mapping specific figure classes (e.g., `f-embed`) to custom caption label classes such as `caption-embed caption-social` for fine-grained control. When this map is provided and `labelClassFollowsFigure` is not set explicitly, figure-following mode is enabled automatically.
97
- - `labelPrefixMarker`: allow a leading marker before labels (string or array, e.g., `*Figure. ...`). Arrays are limited to two markers; extras are ignored.
98
- - `languages`: optional available caption-recognition catalogs delegated to `p7d-markdown-it-p-captions` (default: `['en', 'ja']`). Most users can leave this unset. Set it only when you want to restrict or extend which labels can be recognized (for example English `Figure.` and Japanese `図 `) and which catalogs are available for generated fallback labels. It is separate from the active locale used to choose among those available catalogs.
99
- - Automatic image-label fallback text and punctuation (`Figure. `, `図 `, etc.) are generated from `p7d-markdown-it-p-captions` locale metadata, not from a local hardcoded map in this plugin.
100
- - Generated fallback label tie-break is resolved once per render. Prefer passing the active locale through `env.locale` or `env.preferredLocales`. Compatibility fallbacks are `preferredLanguages`, `env.preferredLanguages`, `env.lang`, and `env.language`. If none of those selects an available catalog, this plugin finally uses a cheap document-script heuristic that skips a leading hyphen-fenced frontmatter block (`---` or longer, spaces allowed before newline), then falls back to the raw `languages` order. This tie-break only affects generated fallback labels; it does not change the caption-recognition dictionaries selected by `languages`. Compatibility note: for generated fallback labels, `env.locale` / `env.preferredLocales` intentionally take precedence over the legacy `preferredLanguages` option so a shared `md` instance can render different documents with different active locales.
16
+ ```console
17
+ npm install markdown-it @peaceroad/markdown-it-figure-with-p-caption
18
+ ```
101
19
 
102
- ### Automatic numbering
20
+ Optional companion plugins:
103
21
 
104
- - `autoLabelNumberSets`: enable numbering per media type. Pass an array such as `['img']`, `['table']`, or `['img', 'table']`.
105
- - `autoLabelNumber`: shorthand for turning numbering on for both images and tables without passing the array yourself. Provide `autoLabelNumberSets` explicitly (e.g., `['img']`) when you need finer control—the explicit array always wins.
106
- - Do not pass p-captions' lower-level `setFigureNumber` option to this plugin. Figure numbering is owned by `autoLabelNumber` / `autoLabelNumberSets`; `setFigureNumber` is rejected during setup to prevent two numbering systems from mutating the same caption.
107
- - Counters start at `1` near the top of the document and increment sequentially per media type. Figures and tables keep independent counters even when mixed together.
108
- - The counter only advances when a real caption exists (paragraph, auto-detected alt/title, or fallback text). Figures emitted solely because of `imageOnlyParagraphWithoutCaption` / `oneImageWithoutCaption` stay unnumbered.
109
- - Manual numbers inside the caption text (e.g., `Figure 5.`) always win. The plugin reads the exact `captionDecision.number` supplied by `p7d-markdown-it-p-captions` and updates its decimal counter so the next automatic number becomes `6`. Explicit compound/alphanumeric numbers (e.g., `Figure A.` or `Figure A.5.`) are preserved without appending an automatic number and do not seed the decimal counter. This applies to captions sourced from paragraphs, auto detection, and fallback captions.
22
+ - [`@peaceroad/markdown-it-renderer-image`](https://www.npmjs.com/package/@peaceroad/markdown-it-renderer-image)
23
+ for image width/height rendering.
24
+ - [`@peaceroad/markdown-it-renderer-fence`](https://www.npmjs.com/package/@peaceroad/markdown-it-renderer-fence)
25
+ when terminal fences should render as `<pre><samp>`.
26
+ - [`markdown-it-attrs`](https://www.npmjs.com/package/markdown-it-attrs)
27
+ for broader attribute syntax.
110
28
 
111
- ## Basic Usage
29
+ ## Quick start
112
30
 
113
31
  ```js
114
- import mdit from 'markdown-it'
115
- import mditFigureWithPCaption from '@peaceroad/markdown-it-figure-with-p-caption'
116
- import mditRendererFence from '@peaceroad/markdown-it-renderer-fence' // optional but keeps fences aligned with samples
117
-
118
- const md = mdit({ html: true, langPrefix: 'language-', })
119
- .use(mditFigureWithPCaption)
120
- .use(mditRendererFence)
121
-
122
- console.log(md.render('Figure. A Cat.\n\n![A cat](cat.jpg)'))
123
- // <figure class="f-img">
124
- // <figcaption><span class="f-img-label">Figure<span class="f-img-label-joint">.</span></span> A Cat.</figcaption>
125
- // <img src="cat.jpg" alt="A cat">
126
- // </figure>
127
- ```
128
-
129
- ### Basic Recommended Options
32
+ import markdownIt from 'markdown-it'
33
+ import figureWithCaption from '@peaceroad/markdown-it-figure-with-p-caption'
130
34
 
131
- Auto label numbering for images and tables.
35
+ const md = markdownIt({ html: true })
36
+ .use(figureWithCaption)
132
37
 
133
- ```js
134
- const figureOption = {
135
- // Opinionated defaults
136
- imageOnlyParagraphWithoutCaption: true,
137
- videoWithoutCaption: true,
138
- audioWithoutCaption: true,
139
- iframeWithoutCaption: true,
140
- iframeTypeBlockquoteWithoutCaption: true,
141
- removeUnnumberedLabelExceptMarks: ['blockquote'], // keep `Quote.` labels even when unnumbered
142
- allIframeTypeFigureClassName: 'f-embed', // apply a uniform class to every iframe-style embed
143
- autoLabelNumber: true,
144
-
145
- // If you want to enable auto alt/title captioning fallbacks without caption label.
146
- //autoAltCaption: true,
147
- //autoTitleCaption: true,
148
- }
38
+ console.log(md.render('Figure. A cat.\n\n![A cat](cat.jpg)'))
149
39
  ```
150
40
 
151
- If there is no label number, the label will also be deleted.
41
+ Output:
152
42
 
153
- ```js
154
- const figureOption = {
155
- imageOnlyParagraphWithoutCaption: true,
156
- videoWithoutCaption: true,
157
- audioWithoutCaption: true,
158
- iframeWithoutCaption: true,
159
- iframeTypeBlockquoteWithoutCaption: true,
160
- removeUnnumberedLabelExceptMarks: ['blockquote'],
161
- allIframeTypeFigureClassName: 'f-embed',
162
- removeUnnumberedLabel: true,
163
- }
43
+ ```html
44
+ <figure class="f-img">
45
+ <figcaption><span class="f-img-label">Figure<span class="f-img-label-joint">.</span></span> A cat.</figcaption>
46
+ <img src="cat.jpg" alt="A cat">
47
+ </figure>
164
48
  ```
165
49
 
166
- These options can be used as follows:
50
+ Caption paragraphs may appear immediately before or after a supported block.
51
+ Image `alt` / `title` text can also provide a caption when
52
+ `autoCaptionDetection` recognizes a configured label.
167
53
 
168
- ```
169
- const md = mdit({ html: true }).use(mditFigureWithPCaption, figureOption)
170
- ```
54
+ ## What gets converted
171
55
 
172
- ## Conversion Examples
56
+ | Markdown target | Typical caption | Default figure class |
57
+ | --- | --- | --- |
58
+ | Image-only paragraph | `Figure.` / `図` | `f-img` |
59
+ | Table | `Table.` / `表` | `f-table` |
60
+ | Code fence | `Code.` / `Listing.` | `f-pre-code` |
61
+ | Terminal/samp fence | `Terminal.` / `端末` | `f-pre-samp` |
62
+ | Blockquote | `Quote.` / `Source.` | `f-blockquote` |
63
+ | Video block or known video iframe | `Video.` / `動画` | `f-video` |
64
+ | Audio block | `Audio.` / `音声` | `f-audio` |
65
+ | Generic iframe | Any compatible caption label | `f-iframe` |
66
+ | Slide iframe | `Slide.` | `f-slide` |
173
67
 
174
- ### Default before/after caption paragraph detection
68
+ Exact labels come from the active `p7d-markdown-it-p-captions` language
69
+ catalogs. The default available catalogs are English and Japanese.
175
70
 
176
- ~~~
177
- [Markdown]
178
- ![A single cat](figure.jpg)
71
+ ## Representative conversions
179
72
 
180
- [HTML]
181
- <p><img src="figure.jpg" alt="A single cat"></p>
73
+ These examples show the default figure classes. Caption span markup is included
74
+ where it helps explain the output; long media contents are abbreviated.
182
75
 
183
- <!-- Above: If imageOnlyParagraphWithoutCaption (or its legacy alias oneImageWithoutCaption) is true, this img element has wrapped into figure element without caption. -->
76
+ ### Image
184
77
 
78
+ Markdown:
185
79
 
186
- [Markdown]
187
- Figure. A Caption.
188
-
189
- ![A single cat](figure.jpg)
190
- [HTML]
191
- <figure class="f-img">
192
- <figcaption><span class="f-img-label">Figure<span class="f-img-label-joint">.</span></span> A Caption.</figcaption>
193
- <img src="figure.jpg" alt="A single cat">
194
- </figure>
80
+ ```md {test id="readme-image"}
81
+ Figure. A cat.
195
82
 
83
+ ![A cat](cat.jpg)
84
+ ```
196
85
 
197
- [Markdown]
198
- ![A single cat](figure.jpg)
86
+ HTML:
199
87
 
200
- Figure. A Caption.
201
- [HTML]
88
+ ```html {test id="readme-image"}
202
89
  <figure class="f-img">
203
- <img src="figure.jpg" alt="A single cat">
204
- <figcaption><span class="f-img-label">Figure<span class="f-img-label-joint">.</span></span> A Caption.</figcaption>
90
+ <figcaption><span class="f-img-label">Figure<span class="f-img-label-joint">.</span></span> A cat.</figcaption>
91
+ <img src="cat.jpg" alt="A cat">
205
92
  </figure>
93
+ ```
94
+
95
+ ### Table
206
96
 
97
+ Markdown:
207
98
 
208
- [Markdown]
209
- Table. A Caption.
99
+ ```md {test id="readme-table"}
100
+ Table. Regional food.
210
101
 
211
102
  | Tokyo | Osaka |
212
- | ----- | ----- |
103
+ | --- | --- |
213
104
  | Sushi | Takoyaki |
105
+ ```
106
+
107
+ HTML:
214
108
 
215
- [HTML]
216
- <p>A paragraph.</p>
109
+ ```html {test id="readme-table"}
217
110
  <figure class="f-table">
218
- <figcaption><span class="f-table-label">Table<span class="f-table-label-joint">.</span></span> A Caption.</figcaption>
111
+ <figcaption><span class="f-table-label">Table<span class="f-table-label-joint">.</span></span> Regional food.</figcaption>
219
112
  <table>
220
113
  <thead>
221
114
  <tr>
@@ -231,380 +124,271 @@ Table. A Caption.
231
124
  </tbody>
232
125
  </table>
233
126
  </figure>
127
+ ```
128
+
129
+ ### Code block
234
130
 
131
+ Markdown:
235
132
 
236
- [Markdown]
237
- Code. A Caption.
133
+ ~~~md {test id="readme-code"}
134
+ Code. Logging example.
238
135
 
239
136
  ```js
240
- console.log('Hello World!');
137
+ console.log('Hello')
241
138
  ```
139
+ ~~~
140
+
141
+ HTML:
242
142
 
243
- [HTML]
143
+ ```html {test id="readme-code"}
244
144
  <figure class="f-pre-code">
245
- <figcaption><span class="f-pre-code-label">Code<span class="f-pre-code-label-joint">.</span></span> A Caption.</figcaption>
246
- <pre><code class="language-js">console.log('Hello World!');
145
+ <figcaption><span class="f-pre-code-label">Code<span class="f-pre-code-label-joint">.</span></span> Logging example.</figcaption>
146
+ <pre><code class="language-js">console.log('Hello')
247
147
  </code></pre>
248
148
  </figure>
149
+ ```
249
150
 
250
- <!-- Above: class attribute of code element is generated by markdown-it option. -->
251
-
252
-
253
- [Markdown]
254
- Source. A Caption.
255
-
256
- > A quoted paragraph.
257
-
258
- [HTML]
259
- <figure class="f-blockquote">
260
- <figcaption><span class="f-blockquote-label">Source<span class="f-blockquote-label-joint">.</span></span> A Caption.</figcaption>
261
- <blockquote>
262
- <p>A quoted paragraph.</p>
263
- </blockquote>
264
- </figure>
151
+ ### Terminal/samp block
265
152
 
153
+ Markdown:
266
154
 
267
- [Markdown]
268
- Terminal. A Caption.
155
+ ~~~md {test id="readme-samp" setup="renderer-fence"}
156
+ Terminal. Current directory.
269
157
 
270
158
  ```samp
271
159
  $ pwd
272
160
  /home/user
273
161
  ```
162
+ ~~~
274
163
 
275
- [HTML]
164
+ With `@peaceroad/markdown-it-renderer-fence`, the representative output is:
165
+
166
+ ```html {test id="readme-samp"}
276
167
  <figure class="f-pre-samp">
277
- <figcaption><span class="f-pre-samp-label">Terminal<span class="f-pre-samp-label-joint">.</span></span> A Caption.</figcaption>
168
+ <figcaption><span class="f-pre-samp-label">Terminal<span class="f-pre-samp-label-joint">.</span></span> Current directory.</figcaption>
278
169
  <pre><samp>$ pwd
279
170
  /home/user
280
171
  </samp></pre>
281
172
  </figure>
173
+ ```
282
174
 
283
- <!-- Above: When @peaceroad/markdown-it-renderer-fence is used, samp element are generated automatically for `samp` fences. -->
284
-
285
- [Markdown]
286
- Video. A mp4.
287
-
288
- <video controls width="400" height="300">
289
- <source src="example.mp4" type="video/mp4">
290
- </video>
291
-
292
- [HTML]
293
- <figure class="f-video">
294
- <figcaption><span class="f-video-label">Video<span class="f-video-label-joint">.</span></span> A mp4.</figcaption>
295
- <video controls width="400" height="300">
296
- <source src="example.mp4" type="video/mp4">
297
- </video>
298
- </figure>
299
-
300
-
301
- [Markdown]
302
- Audio. A narration.
175
+ The figure wrapper and caption classification do not depend on that optional
176
+ renderer; it is used here to show the semantic `<samp>` element.
303
177
 
304
- <audio controls>
305
- <source src="example.mp3" type="audio/mpeg">
306
- </audio>
178
+ ### Video
307
179
 
308
- [HTML]
309
- <figure class="f-audio">
310
- <figcaption><span class="f-audio-label">Audio<span class="f-audio-label-joint">.</span></span> A narration.</figcaption>
311
- <audio controls>
312
- <source src="example.mp3" type="audio/mpeg">
313
- </audio>
314
- </figure>
180
+ Markdown:
315
181
 
182
+ ```md {test id="readme-video"}
183
+ Video. Product demonstration.
316
184
 
317
- [Markdown]
318
- Video. A YouTube video.
185
+ <video controls>
186
+ <source src="demo.mp4" type="video/mp4">
187
+ </video>
188
+ ```
319
189
 
320
- <iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/XXXXXXXXXXX" ...></iframe>
190
+ HTML:
321
191
 
322
- [HTML]
192
+ ```html {test id="readme-video"}
323
193
  <figure class="f-video">
324
- <figcaption><span class="f-video-label">Video<span class="f-video-label-joint">.</span></span> A YouTube video.</figcaption>
325
- <iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/XXXXXXXXXXX" ...></iframe>
326
- </figure>
327
-
328
-
329
- [Markdown]
330
- Figure. Mastodon post.
331
-
332
- <blockquote class="mastodon-embed" ...> ...... </blockquote><script async src="https://example.com/embed.js"></script>
333
-
334
- [HTML]
335
- <figure class="f-img">
336
- <figcaption><span class="f-img-label">Figure<span class="f-img-label-joint">.</span></span> Mastodon post.</figcaption>
337
- <blockquote class="mastodon-embed" ...> ...... </blockquote><script async src="https://example.com/embed.js"></script>
194
+ <figcaption><span class="f-video-label">Video<span class="f-video-label-joint">.</span></span> Product demonstration.</figcaption>
195
+ <video controls>
196
+ <source src="demo.mp4" type="video/mp4">
197
+ </video>
338
198
  </figure>
199
+ ```
339
200
 
201
+ Known YouTube/Vimeo iframe hosts can also use the `f-video` wrapper.
202
+ An unknown iframe with an explicit `Video.` caption uses video numbering but
203
+ keeps the `f-iframe` wrapper.
340
204
 
341
- [Markdown]
342
- Quote. Mastodon post.
343
-
344
- <blockquote class="mastodon-embed" ...> ...... </blockquote><script async src="https://example.com/embed.js"></script>
205
+ ### Slide iframe
345
206
 
346
- [HTML]
347
- <figure class="f-img">
348
- <figcaption><span class="f-blockquote-label">Quote<span class="f-blockquote-label-joint">.</span></span> X post.</figcaption>
349
- <blockquote class="mastodon-embed" ...> ...... </blockquote><script async src="https://example.com/embed.js"></script>
350
- </figure>
207
+ Markdown:
351
208
 
352
-
353
- [Markdown]
209
+ ```md {test id="readme-slide"}
354
210
  Slide. A Speaker Deck.
355
211
 
356
- <iframe src="https://speakerdeck.com/player/XXXXXXXXXXX" width="640" height="360" frameborder="0" allowfullscreen></iframe>
357
-
358
- [HTML]
359
- <figure class="f-slide">
360
- <figcaption><span class="f-slide-label">Slide<span class="f-slide-label-joint">.</span></span> A Speaker Deck.</figcaption>
361
- <iframe src="https://speakerdeck.com/player/XXXXXXXXXXX" width="640" height="360" frameborder="0" allowfullscreen></iframe>
362
- </figure>
363
- ~~~
364
-
365
- ### Auto alt/title detection
366
-
367
- The consumed `alt` or `title` value is cleared only after the image paragraph is confirmed to be wrappable. Tight-list images and image paragraphs with trailing non-image text keep their original accessibility attributes and baseline rendering.
368
-
212
+ <iframe src="https://speakerdeck.com/player/XXXXXXXXXXX" width="640" height="360" allowfullscreen></iframe>
369
213
  ```
370
- [Markdown]
371
- ![Figure. A cat.](cat.jpg)
372
-
373
- [HTML]
374
- <figure class="f-img">
375
- <figcaption><span class="f-img-label">Figure<span class="f-img-label-joint">.</span></span> A cat.</figcaption>
376
- <img src="cat.jpg" alt="">
377
- </figure>
378
-
379
-
380
- [Markdown]
381
- ![A white cat eats fishs.](cat.jpg "Figure. A cat.")
382
-
383
- [HTML]
384
- <figure class="f-img">
385
- <figcaption><span class="f-img-label">Figure<span class="f-img-label-joint">.</span></span> A cat.</figcaption>
386
- <img src="cat.jpg" alt="A white cat eats fishs.">
387
- </figure>
388
- ```
389
-
390
- ### Multiple images
391
-
392
- ~~~
393
- [Markdown]
394
- A paragraph. multipleImages: true. horizontal images only.
395
-
396
- ![Sitting cat](cat1.jpg) ![Standing cat](cat2.jpg)
397
-
398
- Figure. Cats.
399
214
 
400
- A paragraph.
401
- [HTML]
402
- <p>A paragraph. multipleImages: true. horizontal images only</p>
403
- <figure class="f-img-horizontal">
404
- <img src="cat1.jpg" alt="Sitting cat"><img src="cat2.jpg" alt="Standing cat">
405
- <figcaption><span class="f-img-label">Figure<span class="f-img-label-joint">.</span></span> Cats.</figcaption>
406
- </figure>
407
- <p>A paragraph.</p>
408
-
409
- [Markdown]
410
- A paragraph. multipleImages: true. vertical images only.
411
-
412
- Figure. Cats.
413
-
414
- ![Sitting cat](cat1.jpg)
415
- ![Standing cat](cat2.jpg)
416
-
417
- A paragraph.
418
- [HTML]
419
- <p>A paragraph. multipleImages: true. vertical images only.</p>
420
- <figure class="f-img-vertical">
421
- <figcaption><span class="f-img-label">Figure<span class="f-img-label-joint">.</span></span> Cats.</figcaption>
422
- <img src="cat1.jpg" alt="Sitting cat">
423
- <img src="cat2.jpg" alt="Standing cat">
424
- </figure>
425
- <p>A paragraph.</p>
426
-
427
- [Markdown]
428
- A paragraph. multipleImages: true.
429
-
430
- Figure. Cats.
431
-
432
- ![Sitting cat](cat1.jpg) ![Standing cat](cat2.jpg)
433
- ![Sleeping cat](cat3.jpg)
434
-
435
- A paragraph.
436
- [HTML]
437
- <p>A paragraph. multipleImages: true.</p>
438
- <figure class="f-img-multiple">
439
- <figcaption><span class="f-img-label">Figure<span class="f-img-label-joint">.</span></span> Cats.</figcaption>
440
- <img src="cat1.jpg" alt="Sitting cat"><img src="cat2.jpg" alt="Standing cat">
441
- <img src="cat3.jpg" alt="Sleeping cat">
442
- </figure>
443
- <p>A paragraph.</p>
444
- ~~~
445
-
446
- ## Option Examples
215
+ HTML:
447
216
 
448
- ### Styles
449
-
450
- This example uses `classPrefix: 'custom'` and leaves `styleProcess: true` so a trailing `{.notice}` block moves onto the `<figure>` wrapper. This fallback only handles the final trailing attrs block on an image-only paragraph; it supports quoted values with spaces, but for broader attrs syntax support, keep using `markdown-it-attrs`.
451
-
452
- ```
453
- [Markdown]
454
- Figure. Highlighted cat.
455
-
456
- ![Highlighted cat](cat.jpg) {.notice}
457
- [HTML]
458
- <figure class="custom-img notice">
459
- <figcaption><span class="custom-img-label">Figure<span class="custom-img-label-joint">.</span></span> Highlighted cat.</figcaption>
460
- <img src="cat.jpg" alt="Highlighted cat">
217
+ ```html {test id="readme-slide"}
218
+ <figure class="f-slide">
219
+ <figcaption><span class="f-slide-label">Slide<span class="f-slide-label-joint">.</span></span> A Speaker Deck.</figcaption>
220
+ <iframe src="https://speakerdeck.com/player/XXXXXXXXXXX" width="640" height="360" allowfullscreen></iframe>
461
221
  </figure>
462
222
  ```
463
223
 
464
- ### Automatic detection fallbacks
465
-
466
- `autoCaptionDetection` combined with `autoAltCaption` / `autoTitleCaption` can still generate caption text even when the original alt/title lacks labels, as long as the alt/title body is non-empty. The corresponding attributes are cleared after conversion so the figcaption becomes the canonical source. When these fallbacks are `true`, the generated label text and punctuation come from `p7d-markdown-it-p-captions` locale metadata rather than a local hardcoded map. When these fallbacks are strings, the string must be a label stem recognized as an image caption label by `p7d-markdown-it-p-captions`; invalid strings fail during plugin setup instead of producing a stray paragraph.
224
+ `figureClassThatWrapsSlides` changes the slide wrapper class.
225
+ `allIframeTypeFigureClassName` takes precedence when all iframe-like embeds
226
+ should share one class.
467
227
 
468
- ```
469
- [Markdown]
470
- ![Alt fallback example](bird.jpg)
228
+ See the [complete conversion examples](docs/examples.md#conversion-examples)
229
+ for before/after captions, multiple images, blockquotes, audio, social embeds,
230
+ and option-specific output.
471
231
 
472
- [HTML]
473
- <figure class="f-img">
474
- <figcaption><span class="f-img-label">Figure<span class="f-img-label-joint">.</span></span> Alt fallback example</figcaption>
475
- <img src="bird.jpg" alt="">
476
- </figure>
232
+ ## Recommended options
477
233
 
234
+ The plugin defaults are conservative. The following is a practical,
235
+ opinionated configuration when valid captionless media should also receive
236
+ figure wrappers, iframe-like embeds should share one class, and images/tables
237
+ should be numbered:
478
238
 
479
- [Markdown]
480
- ![No caption](fish.jpg "Plain title text")
239
+ ```js
240
+ const recommendedFigureOptions = {
241
+ imageOnlyParagraphWithoutCaption: true,
242
+ videoWithoutCaption: true,
243
+ audioWithoutCaption: true,
244
+ iframeWithoutCaption: true,
245
+ iframeTypeBlockquoteWithoutCaption: true,
481
246
 
482
- [HTML]
483
- <figure class="f-img">
484
- <figcaption><span class="f-img-label">Figure<span class="f-img-label-joint">.</span></span> Plain title text</figcaption>
485
- <img src="fish.jpg" alt="No caption">
486
- </figure>
487
- ```
247
+ // Keep Quote./Source. labels when removeUnnumberedLabel is enabled later.
248
+ removeUnnumberedLabelExceptMarks: ['blockquote'],
488
249
 
489
- ### Role helpers
250
+ // Use one predictable wrapper class for iframe/social-embed figures.
251
+ allIframeTypeFigureClassName: 'f-embed',
490
252
 
491
- Set `roleDocExample: true` to add `role="doc-example"` to code/samp figures.
253
+ // Number image and table captions. Recognized Chapter/Appendix scopes
254
+ // in parsed frontmatter titles or H1 headings are applied automatically.
255
+ autoLabelNumber: true,
256
+ }
492
257
 
493
- ~~~
494
- [Markdown]
495
- ```samp
496
- $ pwd
497
- /home/user
258
+ const md = markdownIt({ html: true })
259
+ .use(figureWithCaption, recommendedFigureOptions)
498
260
  ```
499
261
 
500
- [HTML]
501
- <figure class="f-pre-samp" role="doc-example">
502
- ...
503
- </figure>
504
- ~~~
505
-
506
- ### Captionless conversion toggles
507
-
508
- If `imageOnlyParagraphWithoutCaption` (or the legacy alias `oneImageWithoutCaption`) is enabled, a single image paragraph will be wrapped with `<figure class="f-img">` even without a caption. Multi-image image-only paragraphs can also be wrapped, in which case the normal layout classes such as `f-img-horizontal`, `f-img-vertical`, or `f-img-multiple` are used.
262
+ Optional additions:
509
263
 
510
- ```
511
- [Markdown]
512
- ![A single cat](cat.jpg)
264
+ ```js
265
+ const recommendedFigureOptionsWithFallbacks = {
266
+ ...recommendedFigureOptions,
267
+ // Remove labels such as Figure./Table. when they remain unnumbered.
268
+ removeUnnumberedLabel: true,
513
269
 
514
- [HTML]
515
- <figure class="f-img">
516
- <img src="cat.jpg" alt="A single cat">
517
- </figure>
270
+ // Generate an image label when non-empty alt/title text has no label.
271
+ autoAltCaption: true,
272
+ // autoTitleCaption: true,
273
+ }
518
274
  ```
519
275
 
520
- If `videoWithoutCaption` is enabled, `<video>` elements and iframes pointing to known video hosts (such as `www.youtube.com`, `youtube.com`, `www.youtube-nocookie.com`, or Vimeo) will be wrapped with `<figure class="f-video">`.
521
-
522
- ```
523
- [Markdown]
524
- <iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/XXXXXXXXXXX" ...></iframe>
276
+ `autoAltCaption` and `autoTitleCaption` are disabled by default. When set to
277
+ `true`, generated labels follow p-captions locale metadata. They may also be a
278
+ recognized string label.
525
279
 
526
- [HTML]
527
- <figure class="f-video">
528
- <iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/XXXXXXXXXXX" ...></iframe>
529
- </figure>
280
+ See the [complete option reference](docs/reference.md#behavior-customization)
281
+ and [option examples](docs/examples.md#option-examples) for class mapping,
282
+ caption markers, role helpers, captionless conversion, and formatting options.
530
283
 
284
+ ## Automatic numbering
531
285
 
532
- [Markdown]
533
- <video controls width="400" height="300">
534
- <source src="example.mp4" type="video/mp4">
535
- </video>
536
- [HTML]
537
- <figure class="f-video">
538
- <video controls width="400" height="300">
539
- <source src="example.mp4" type="video/mp4">
540
- </video>
541
- </figure>
542
- ```
286
+ Automatic numbering is **disabled by default**.
543
287
 
544
- When `iframeWithoutCaption` is enabled, iframe elements will be wrapped with `<figure class="f-iframe">`. And if `iframeTypeBlockquoteWithoutCaption` is enabled, blockquote-based embeds (for example, X) will be wrapped with `<figure class="f-img">` (or another configured class).
288
+ Number image and table captions:
545
289
 
546
- ```
547
- [Markdown]
548
- <iframe>
549
- ...
550
- </iframe>
551
-
552
- [HTML]
553
- <figure class="f-iframe">
554
- <iframe>
555
- ...
556
- </iframe>
557
- </figure>
290
+ ```js
291
+ const md = markdownIt().use(figureWithCaption, {
292
+ autoLabelNumber: true,
293
+ })
558
294
  ```
559
295
 
560
- ### Iframe-type blockquote class override
296
+ Without a recognized chapter or appendix, this generates `Figure 1`,
297
+ `Figure 2`, and so on. Under `# Chapter 1: Introduction`, the same setup
298
+ automatically generates `Figure 1.1`, `Figure 1.2`, and so on. Parsed
299
+ `env.frontmatter.title` values use the same recognition.
561
300
 
562
- Set `figureClassThatWrapsIframeTypeBlockquote: 'f-social'` (or any class you prefer) to wrap blockquote-based embeds (for example, X, Mastodon, Bluesky) with that class.
301
+ The default scoped separator is `.`. Customize the automatic scope only when
302
+ the document structure differs from the defaults:
563
303
 
304
+ ```js
305
+ const md = markdownIt().use(figureWithCaption, {
306
+ autoLabelNumber: true,
307
+ autoLabelNumberPolicy: {
308
+ separator: '-',
309
+ scope: { headingLevels: [2] },
310
+ },
311
+ })
564
312
  ```
565
- [Markdown]
566
- Figure. Twitter embed.
567
-
568
- <blockquote class="twitter-tweet"><p>Embed content</p></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
569
-
570
- [HTML]
571
- <figure class="f-social">
572
- <figcaption><span class="f-img-label">Figure<span class="f-img-label-joint">.</span></span> Twitter embed.</figcaption>
573
- <blockquote class="twitter-tweet"><p>Embed content</p></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
574
- </figure>
575
- ```
576
-
577
- ### All iframe/embed class override
578
313
 
579
- Set `allIframeTypeFigureClassName: 'f-embed'` (or any class you prefer) to consolidate iframe-like embeds under one class.
314
+ This recognizes H2 chapter headings and generates `Figure 1-1`. Use
315
+ `scope: 'document'` when headings and frontmatter titles must not affect
316
+ numbering:
580
317
 
318
+ ```js
319
+ const md = markdownIt().use(figureWithCaption, {
320
+ autoLabelNumber: true,
321
+ autoLabelNumberPolicy: { scope: 'document' },
322
+ })
581
323
  ```
582
- [Markdown]
583
- Video. Custom embed.
584
324
 
585
- <iframe width="560" height="315" src="https://example.com/embed/123" title="Custom embed" frameborder="0" allowfullscreen></iframe>
325
+ Use `autoLabelNumberSets` for additional marks:
586
326
 
587
- [HTML]
588
- <figure class="f-embed">
589
- <figcaption><span class="f-video-label">Video<span class="f-video-label-joint">.</span></span> Custom embed.</figcaption>
590
- <iframe width="560" height="315" src="https://example.com/embed/123" title="Custom embed" frameborder="0" allowfullscreen></iframe>
591
- </figure>
327
+ ```js
328
+ const md = markdownIt().use(figureWithCaption, {
329
+ autoLabelNumberSets: ['img', 'table', 'code', 'samp', 'video'],
330
+ })
592
331
  ```
593
332
 
594
- Need matching caption classes too? Use `labelClassFollowsFigure` (and optionally `figureToLabelClassMap`) so the `figcaption` spans inherit the embed class you just applied (e.g., `f-embed-label`, `f-embed-label-joint`). If `figureToLabelClassMap` is provided, figure-following mode is enabled automatically unless `labelClassFollowsFigure` is set explicitly.
333
+ `autoLabelNumber` is only the image/table shorthand. An explicitly supplied
334
+ `autoLabelNumberSets` always wins, and `autoLabelNumberPolicy` changes
335
+ formatting/scope without enabling marks by itself. Automatic scope defaults to
336
+ parsed frontmatter titles plus top-level H1 headings; customize `sources`,
337
+ `headingLevels`, and `repeatScope` only when needed.
595
338
 
339
+ See the [complete automatic-numbering reference](docs/numbering.md#automatic-numbering)
340
+ for semantic counter series, shared samp labels, manual-number synchronization,
341
+ chapter/appendix recognition, repeat scopes, frontmatter configuration, and
342
+ render-level overrides.
596
343
 
597
- ### Caption markers
344
+ ## Integration API
598
345
 
599
- - `allowLabelPrefixMarkerWithoutLabel`: when `true`, marker-only paragraphs (e.g., `▼Caption`) are treated as captions without labels. If `labelPrefixMarker` is an array, the first entry is used for the previous caption and the second for the next caption. The marker is stripped from output.
346
+ Source editors and other markdown-it plugins can reuse the same figure-specific
347
+ scope, counter-series, and number-codec semantics without importing the
348
+ renderer walker:
600
349
 
601
350
  ```js
602
- const figureOption = {
603
- labelPrefixMarker: ['▼', '▲'],
604
- allowLabelPrefixMarkerWithoutLabel: true,
605
- }
606
-
607
- const md = mdit({ html: true }).use(mditFigureWithPCaption, figureOption)
351
+ import {
352
+ createFigureCaptionCounterKeyResolver,
353
+ createFigureCaptionNumberCodec,
354
+ createFigureCaptionScopeTimeline,
355
+ normalizeFigureCaptionNumberingPolicy,
356
+ } from '@peaceroad/markdown-it-figure-with-p-caption/caption-numbering.js'
608
357
  ```
609
358
 
610
- The first marker applies to captions before the figure, the second to captions after it.
359
+ See the [caption-numbering integration API reference](docs/numbering.md#caption-numbering-integration-api).
360
+ The public API intentionally does not expose figure-candidate detection,
361
+ wrapping, token mutation, or source editing.
362
+
363
+ ## Important behavior
364
+
365
+ - Repeated `.use(figureWithCaption, ...)` calls on one markdown-it instance use
366
+ first-install-wins behavior. Create separate instances for different
367
+ successful option sets.
368
+ - Tight-list image paragraphs are not wrapped. Loose lists, blockquotes, and
369
+ description lists are supported with documented container guards.
370
+ - `styleProcess` and forwarded markdown-it-attrs attributes are not
371
+ sanitization. Sanitize final rendered HTML when the Markdown input is
372
+ untrusted.
373
+ - Source HTML requires `markdownIt({ html: true })`.
374
+ - The plugin does not parse YAML or install a frontmatter rule. Supply parsed
375
+ metadata through `env.frontmatter`, or configure the documented raw-token
376
+ adapter.
377
+
378
+ ## Documentation
379
+
380
+ The detailed documentation is divided into three files:
381
+
382
+ - [Behavior and options reference](docs/reference.md) — detection, wrapping,
383
+ caption helpers, basic usage, and option contracts.
384
+ - [Automatic numbering and integration API](docs/numbering.md) — numbering
385
+ series, [chapter and appendix scopes](docs/numbering.md#chapter-and-appendix-scopes),
386
+ frontmatter/render overrides, and the
387
+ [public integration API](docs/numbering.md#caption-numbering-integration-api).
388
+ - [Complete conversion and option examples](docs/examples.md) — full
389
+ [conversion examples](docs/examples.md#conversion-examples) and
390
+ [option examples](docs/examples.md#option-examples).
391
+
392
+ ## License
393
+
394
+ MIT