@peaceroad/markdown-it-figure-with-p-caption 0.15.2 → 0.16.1

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.
Files changed (3) hide show
  1. package/README.md +11 -6
  2. package/index.js +327 -158
  3. package/package.json +10 -7
package/README.md CHANGED
@@ -27,6 +27,7 @@ Optionally, you can auto-number image and table caption paragraphs starting from
27
27
  - `f-img-multiple` for mixed layouts.
28
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
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.
30
31
 
31
32
  ### Table
32
33
 
@@ -50,6 +51,7 @@ Optionally, you can auto-number image and table caption paragraphs starting from
50
51
  ### Embedded content by iframe
51
52
 
52
53
  - Inline HTML `<iframe>` elements become `<figure class="f-video">` when they point to known video hosts (YouTube `youtube.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).
53
55
  - 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` matches those providers. 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`.
54
56
  - `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.
55
57
  - All other iframes fall back to `<figure class="f-iframe">` unless you override the class via `allIframeTypeFigureClassName`.
@@ -57,7 +59,9 @@ Optionally, you can auto-number image and table caption paragraphs starting from
57
59
  ### label span class name
58
60
 
59
61
  - 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`.
60
- - With `markdown-it-attrs`, any attribute block (`{ .foo #bar }`) attached to the caption paragraph is moved to the generated `<figure>` by default (`styleProcess: true`). This keeps per-figure classes/IDs on the wrapper instead of the original paragraph; disable the option only if you explicitly want the attributes to stay on the paragraph.
62
+ - With `markdown-it-attrs`, attributes attached to image-only paragraphs (for example `![...](...) {.foo #bar}`) are forwarded to the generated `<figure>`.
63
+ - `styleProcess` controls parsing of a trailing `{...}` block from the last text token of an image-only paragraph in this plugin's own scanner. It is a narrow fallback parser, not full `markdown-it-attrs` parity, and attributes already attached to paragraph tokens by `markdown-it-attrs` are still forwarded.
64
+ - Attributes attached to caption paragraphs stay on the converted `<figcaption>` token after paragraph-to-figcaption conversion.
61
65
 
62
66
  ## Behavior Customization
63
67
 
@@ -67,6 +71,7 @@ Optionally, you can auto-number image and table caption paragraphs starting from
67
71
  - `figureClassThatWrapsIframeTypeBlockquote`: override the class used when blockquote-based embeds (Twitter, Mastodon, Bluesky) are wrapped.
68
72
  - `figureClassThatWrapsSlides`: override the class assigned when a caption paragraph uses the `Slide.` label.
69
73
  - `classPrefix` (default `f`) controls the CSS namespace for every figure (`f-img`, `f-table`, etc.) so you can align with existing styles.
74
+ - Wrapper/class-prefix options are trimmed during setup; whitespace-only values fall back to the default class for that option.
70
75
 
71
76
  ### Wrapping without captions
72
77
 
@@ -85,7 +90,7 @@ Every option below is forwarded verbatim to `p7d-markdown-it-p-captions`, which
85
90
  - `wrapCaptionBody`: wrap the non-label caption text in a span element.
86
91
  - `hasNumClass`: add a class attribute to label span element if it has a label number.
87
92
  - `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.
88
- - `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.
93
+ - `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.
89
94
  - `labelPrefixMarker`: allow a leading marker before labels (string or array, e.g., `*Figure. ...`). Arrays are limited to two markers; extras are ignored.
90
95
 
91
96
  ### Automatic numbering
@@ -433,13 +438,13 @@ A paragraph.
433
438
 
434
439
  ### Styles
435
440
 
436
- This example uses `classPrefix: 'custom'` and leaves `styleProcess: true` so a trailing `{.notice}` block moves onto the `<figure>` wrapper.
441
+ 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; for broader attrs syntax support, keep using `markdown-it-attrs`.
437
442
 
438
443
  ```
439
444
  [Markdown]
440
- Figure. Highlighted cat. {.notice}
445
+ Figure. Highlighted cat.
441
446
 
442
- ![Highlighted cat](cat.jpg)
447
+ ![Highlighted cat](cat.jpg) {.notice}
443
448
  [HTML]
444
449
  <figure class="custom-img notice">
445
450
  <figcaption><span class="custom-img-label">Figure<span class="custom-img-label-joint">.</span></span> Highlighted cat.</figcaption>
@@ -577,7 +582,7 @@ Video. Custom embed.
577
582
  </figure>
578
583
  ```
579
584
 
580
- Need matching caption classes too? Combine this option with `labelClassFollowsFigure` (and optionally `figureToLabelClassMap`) so the `figcaption` spans inherit the embed class you just applied (e.g., `f-embed-label`, `f-embed-label-joint`).
585
+ 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.
581
586
 
582
587
 
583
588
  ### Caption markers
package/index.js CHANGED
@@ -1,7 +1,9 @@
1
- import { setCaptionParagraph, markReg } from 'p7d-markdown-it-p-captions'
1
+ import {
2
+ setCaptionParagraph,
3
+ getMarkRegStateForLanguages,
4
+ } from 'p7d-markdown-it-p-captions'
2
5
 
3
6
  const htmlRegCache = new Map()
4
- const cleanCaptionRegCache = new Map()
5
7
  const blueskyEmbedReg = /^<blockquote class="bluesky-embed"[^]*?>[\s\S]*?$/
6
8
  const videoIframeReg = /^<[^>]*? src="https:\/\/(?:www.youtube-nocookie.com|player.vimeo.com)\//i
7
9
  const classNameReg = /^<[^>]*? class="(twitter-tweet|instagram-media|text-post-media|bluesky-embed|mastodon-embed)"/
@@ -11,22 +13,58 @@ const idAttrReg = /^#/
11
13
  const attrParseReg = /^(.*?)="?(.*)"?$/
12
14
  const sampLangReg = /^ *(?:samp|shell|console)(?:(?= )|$)/
13
15
  const endBlockquoteScriptReg = /<\/blockquote> *<script[^>]*?><\/script>$/
14
- const imgCaptionMarkReg = markReg && markReg.img ? markReg.img : null
16
+ const iframeTagReg = /<iframe(?=[\s>])/i
15
17
  const asciiLabelReg = /^[A-Za-z]/
16
- const trailingDigitsReg = /(\d+)\s*$/
17
18
  const CHECK_TYPE_TOKEN_MAP = {
18
19
  table_open: 'table',
19
20
  pre_open: 'pre',
20
21
  blockquote_open: 'blockquote',
21
22
  }
22
- const HTML_TAG_CANDIDATES = ['video', 'audio', 'iframe', 'blockquote', 'div']
23
- const fallbackLabelDefaults = {
24
- img: { en: 'Figure', ja: '' },
25
- table: { en: 'Table', ja: '' },
26
- }
23
+ const HTML_TAG_DETECTORS = [
24
+ { candidate: 'video', lookupTag: 'video', hintKey: 'hasVideoHint' },
25
+ { candidate: 'audio', lookupTag: 'audio', hintKey: 'hasAudioHint' },
26
+ { candidate: 'iframe', lookupTag: 'iframe', hintKey: 'hasIframeHint' },
27
+ { candidate: 'blockquote', lookupTag: 'blockquote', hintKey: 'hasBlockquoteHint' },
28
+ {
29
+ candidate: 'div',
30
+ lookupTag: 'div',
31
+ hintKey: 'hasDivHint',
32
+ requiresIframeTag: true,
33
+ matchedTag: 'iframe',
34
+ setVideoIframe: true,
35
+ },
36
+ ]
37
+ const fallbackLabelDefaults = { en: 'Figure', ja: '図' }
27
38
 
28
39
  const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
29
- const buildClassPrefix = (value) => (value ? value + '-' : '')
40
+ const normalizeOptionalClassName = (value) => {
41
+ if (value === null || value === undefined) return ''
42
+ const normalized = String(value).trim()
43
+ return normalized || ''
44
+ }
45
+ const buildClassPrefix = (value) => {
46
+ const normalized = normalizeOptionalClassName(value)
47
+ return normalized ? normalized + '-' : ''
48
+ }
49
+ const normalizeClassOptionWithFallback = (value, fallbackValue) => {
50
+ const normalized = normalizeOptionalClassName(value)
51
+ return normalized || fallbackValue
52
+ }
53
+ const normalizeLanguages = (value) => {
54
+ if (!Array.isArray(value)) return ['en', 'ja']
55
+ const normalized = []
56
+ const seen = new Set()
57
+ for (let i = 0; i < value.length; i++) {
58
+ const lang = value[i]
59
+ if (typeof lang !== 'string') continue
60
+ const trimmed = lang.trim()
61
+ if (!trimmed || seen.has(trimmed)) continue
62
+ seen.add(trimmed)
63
+ normalized.push(trimmed)
64
+ }
65
+ if (normalized.length === 0) return ['en', 'ja']
66
+ return normalized
67
+ }
30
68
  const normalizeLabelPrefixMarkers = (value) => {
31
69
  if (typeof value === 'string') {
32
70
  return value ? [value] : []
@@ -228,31 +266,28 @@ const isSentenceBoundaryChar = (char) => {
228
266
  return char === '.' || char === '!' || char === '?' || char === '。' || char === '!' || char === '?'
229
267
  }
230
268
 
231
- const getAutoFallbackLabel = (text, captionType) => {
232
- const type = captionType === 'table' ? 'table' : 'img'
269
+ const getAutoFallbackLabel = (text) => {
233
270
  const lang = detectCaptionLanguage(text)
234
- const defaults = fallbackLabelDefaults[type] || fallbackLabelDefaults.img
235
- if (lang === 'ja') return defaults.ja || defaults.en || ''
236
- return defaults.en || defaults.ja || ''
271
+ if (lang === 'ja') return fallbackLabelDefaults.ja || fallbackLabelDefaults.en || ''
272
+ return fallbackLabelDefaults.en || fallbackLabelDefaults.ja || ''
237
273
  }
238
274
 
239
- const getPersistedFallbackLabel = (text, captionType, fallbackState) => {
240
- const type = captionType === 'table' ? 'table' : 'img'
241
- if (!fallbackState) return getAutoFallbackLabel(text, type)
242
- if (fallbackState[type]) return fallbackState[type]
243
- const resolved = getAutoFallbackLabel(text, type)
244
- fallbackState[type] = resolved
275
+ const getPersistedFallbackLabel = (text, fallbackState) => {
276
+ if (!fallbackState) return getAutoFallbackLabel(text)
277
+ if (fallbackState.img) return fallbackState.img
278
+ const resolved = getAutoFallbackLabel(text)
279
+ fallbackState.img = resolved
245
280
  return resolved
246
281
  }
247
282
 
248
- const buildCaptionWithFallback = (text, fallbackOption, captionType = 'img', fallbackState) => {
283
+ const buildCaptionWithFallback = (text, fallbackOption, fallbackState) => {
249
284
  const trimmedText = (text || '').trim()
250
285
  if (!fallbackOption) return ''
251
286
  let label = ''
252
287
  if (typeof fallbackOption === 'string') {
253
288
  label = fallbackOption.trim()
254
289
  } else if (fallbackOption === true) {
255
- label = getPersistedFallbackLabel(trimmedText, captionType, fallbackState)
290
+ label = getPersistedFallbackLabel(trimmedText, fallbackState)
256
291
  }
257
292
  if (!label) return trimmedText
258
293
  const isAsciiLabel = asciiLabelReg.test(label)
@@ -331,6 +366,28 @@ const updateInlineTokenContent = (inlineToken, originalText, newText) => {
331
366
  inlineToken.content.slice(index + originalText.length)
332
367
  }
333
368
 
369
+ const parseTrailingPositiveInteger = (text) => {
370
+ if (typeof text !== 'string' || text.length === 0) return null
371
+ let end = text.length - 1
372
+ while (end >= 0 && text.charCodeAt(end) === 0x20) end--
373
+ if (end < 0) return null
374
+ const lastCode = text.charCodeAt(end)
375
+ if (lastCode < 0x30 || lastCode > 0x39) return null
376
+ let start = end
377
+ while (start >= 0) {
378
+ const code = text.charCodeAt(start)
379
+ if (code < 0x30 || code > 0x39) break
380
+ start--
381
+ }
382
+ let value = 0
383
+ let digitBase = 1
384
+ for (let i = end; i > start; i--) {
385
+ value += (text.charCodeAt(i) - 0x30) * digitBase
386
+ digitBase *= 10
387
+ }
388
+ return value
389
+ }
390
+
334
391
  const ensureAutoFigureNumbering = (tokens, range, caption, figureNumberState, opt) => {
335
392
  const captionType = caption.name === 'img' ? 'img' : (caption.name === 'table' ? 'table' : '')
336
393
  if (!captionType) return
@@ -345,10 +402,9 @@ const ensureAutoFigureNumbering = (tokens, range, caption, figureNumberState, op
345
402
  if (end >= 0) {
346
403
  const code = originalText.charCodeAt(end)
347
404
  if (code >= 0x30 && code <= 0x39) {
348
- const existingMatch = originalText.match(trailingDigitsReg)
349
- if (existingMatch && existingMatch[1]) {
350
- const explicitValue = parseInt(existingMatch[1], 10)
351
- if (!Number.isNaN(explicitValue) && explicitValue > (figureNumberState[captionType] || 0)) {
405
+ const explicitValue = parseTrailingPositiveInteger(originalText)
406
+ if (explicitValue !== null) {
407
+ if (explicitValue > (figureNumberState[captionType] || 0)) {
352
408
  figureNumberState[captionType] = explicitValue
353
409
  }
354
410
  return
@@ -364,27 +420,27 @@ const ensureAutoFigureNumbering = (tokens, range, caption, figureNumberState, op
364
420
  updateInlineTokenContent(inlineToken, originalText, newLabelText)
365
421
  }
366
422
 
423
+ const matchAutoCaptionText = (text, reg) => {
424
+ if (!text || !reg) return ''
425
+ const trimmed = text.trim()
426
+ if (trimmed && reg.test(trimmed)) return trimmed
427
+ return ''
428
+ }
429
+
367
430
  const getAutoCaptionFromImage = (imageToken, opt, fallbackLabelState) => {
431
+ const imgCaptionMarkReg = opt && opt.imgCaptionMarkReg ? opt.imgCaptionMarkReg : null
368
432
  if (!opt.autoCaptionDetection) return ''
369
433
  if (!imgCaptionMarkReg && !opt.autoAltCaption && !opt.autoTitleCaption) return ''
370
- const tryMatch = (text) => {
371
- if (!text) return ''
372
- const trimmed = text.trim()
373
- if (trimmed && imgCaptionMarkReg && imgCaptionMarkReg.test(trimmed)) {
374
- return trimmed
375
- }
376
- return ''
377
- }
378
434
 
379
435
  const altText = getImageAltText(imageToken)
380
- let caption = tryMatch(altText)
436
+ let caption = matchAutoCaptionText(altText, imgCaptionMarkReg)
381
437
  if (caption) {
382
438
  clearImageAltAttr(imageToken)
383
439
  return caption
384
440
  }
385
441
  if (!caption && opt.autoAltCaption) {
386
442
  const altForFallback = altText || ''
387
- caption = buildCaptionWithFallback(altForFallback, opt.autoAltCaption, 'img', fallbackLabelState)
443
+ caption = buildCaptionWithFallback(altForFallback, opt.autoAltCaption, fallbackLabelState)
388
444
  if (imageToken) {
389
445
  clearImageAltAttr(imageToken)
390
446
  }
@@ -392,14 +448,14 @@ const getAutoCaptionFromImage = (imageToken, opt, fallbackLabelState) => {
392
448
  if (caption) return caption
393
449
 
394
450
  const titleText = getImageTitleText(imageToken)
395
- caption = tryMatch(titleText)
451
+ caption = matchAutoCaptionText(titleText, imgCaptionMarkReg)
396
452
  if (caption) {
397
453
  clearImageTitleAttr(imageToken)
398
454
  return caption
399
455
  }
400
456
  if (!caption && opt.autoTitleCaption) {
401
457
  const titleForFallback = titleText || ''
402
- caption = buildCaptionWithFallback(titleForFallback, opt.autoTitleCaption, 'img', fallbackLabelState)
458
+ caption = buildCaptionWithFallback(titleForFallback, opt.autoTitleCaption, fallbackLabelState)
403
459
  if (imageToken) {
404
460
  clearImageTitleAttr(imageToken)
405
461
  }
@@ -408,21 +464,118 @@ const getAutoCaptionFromImage = (imageToken, opt, fallbackLabelState) => {
408
464
  }
409
465
 
410
466
  const getHtmlReg = (tag) => {
411
- if (htmlRegCache.has(tag)) return htmlRegCache.get(tag)
467
+ const cached = htmlRegCache.get(tag)
468
+ if (cached) return cached
412
469
  const regexStr = `^<${tag} ?[^>]*?>[\\s\\S]*?<\\/${tag}>(\\n| *?)(<script [^>]*?>(?:<\\/script>)?)? *(\\n|$)`
413
470
  const reg = new RegExp(regexStr)
414
471
  htmlRegCache.set(tag, reg)
415
472
  return reg
416
473
  }
417
474
 
418
- const checkPrevCaption = (tokens, n, caption, fNum, sp, opt, captionState) => {
475
+ const getHtmlDetectionHints = (content) => {
476
+ const hasBlueskyHint = content.indexOf('bluesky-embed') !== -1
477
+ const hasVideoHint = content.indexOf('<video') !== -1
478
+ const hasAudioHint = content.indexOf('<audio') !== -1
479
+ const hasIframeHint = content.indexOf('<iframe') !== -1
480
+ const hasBlockquoteHint = content.indexOf('<blockquote') !== -1
481
+ const hasDivHint = content.indexOf('<div') !== -1
482
+ return {
483
+ hasBlueskyHint,
484
+ hasVideoHint,
485
+ hasAudioHint,
486
+ hasIframeHint,
487
+ hasBlockquoteHint,
488
+ hasDivHint,
489
+ hasIframeTag: hasIframeHint || (hasDivHint && iframeTagReg.test(content)),
490
+ hasBlueskyEmbed: hasBlueskyHint && blueskyEmbedReg.test(content),
491
+ }
492
+ }
493
+
494
+ const hasAnyHtmlDetectionHint = (hints) => {
495
+ return !!(
496
+ hints.hasBlueskyHint ||
497
+ hints.hasVideoHint ||
498
+ hints.hasAudioHint ||
499
+ hints.hasIframeHint ||
500
+ hints.hasBlockquoteHint ||
501
+ hints.hasDivHint
502
+ )
503
+ }
504
+
505
+ const appendHtmlBlockNewlineIfNeeded = (token, hasTag) => {
506
+ if ((hasTag[2] && hasTag[3] !== '\n') || (hasTag[1] !== '\n' && hasTag[2] === undefined)) {
507
+ token.content += '\n'
508
+ }
509
+ }
510
+
511
+ const consumeBlockquoteEmbedScript = (tokens, token, startIndex) => {
512
+ let addedCont = ''
513
+ let j = startIndex + 1
514
+ while (j < tokens.length) {
515
+ const nextToken = tokens[j]
516
+ if (nextToken.type === 'inline' && endBlockquoteScriptReg.test(nextToken.content)) {
517
+ addedCont += nextToken.content + '\n'
518
+ if (tokens[j + 1] && tokens[j + 1].type === 'paragraph_close') {
519
+ tokens.splice(j + 1, 1)
520
+ }
521
+ nextToken.content = ''
522
+ if (nextToken.children) {
523
+ for (let k = 0; k < nextToken.children.length; k++) {
524
+ nextToken.children[k].content = ''
525
+ }
526
+ }
527
+ break
528
+ }
529
+ if (nextToken.type === 'paragraph_open') {
530
+ addedCont += '\n'
531
+ tokens.splice(j, 1)
532
+ continue
533
+ }
534
+ j++
535
+ }
536
+ token.content += addedCont
537
+ }
538
+
539
+ const detectHtmlTagCandidate = (tokens, token, startIndex, detector, hints, sp) => {
540
+ if (detector.requiresIframeTag && !hints.hasIframeTag) return ''
541
+ const hasTagHint = !!(detector.hintKey && hints[detector.hintKey])
542
+ const allowBlueskyFallback = detector.candidate === 'blockquote' && hints.hasBlueskyEmbed
543
+ if (!hasTagHint && !allowBlueskyFallback) return ''
544
+ const hasTag = hasTagHint ? token.content.match(getHtmlReg(detector.lookupTag)) : null
545
+ const isBlueskyBlockquote = detector.candidate === 'blockquote' && !hasTag && hints.hasBlueskyEmbed
546
+ if (!hasTag && !isBlueskyBlockquote) return ''
547
+ if (hasTag) {
548
+ appendHtmlBlockNewlineIfNeeded(token, hasTag)
549
+ if (detector.setVideoIframe) {
550
+ sp.isVideoIframe = true
551
+ }
552
+ return detector.matchedTag || detector.candidate
553
+ }
554
+ consumeBlockquoteEmbedScript(tokens, token, startIndex)
555
+ return 'blockquote'
556
+ }
557
+
558
+ const isIframeTypeEmbedBlockquote = (content) => {
559
+ return content.indexOf('class="') !== -1 && classNameReg.test(content)
560
+ }
561
+
562
+ const resolveHtmlWrapWithoutCaption = (matchedTag, sp, opt) => {
563
+ const htmlWrapWithoutCaption = opt.htmlWrapWithoutCaption
564
+ if (!htmlWrapWithoutCaption) return false
565
+ if (matchedTag === 'blockquote') {
566
+ return !!(sp.isIframeTypeBlockquote && htmlWrapWithoutCaption.iframeTypeBlockquote)
567
+ }
568
+ return !!htmlWrapWithoutCaption[matchedTag]
569
+ }
570
+
571
+ const checkPrevCaption = (tokens, n, caption, sp, opt, captionState) => {
419
572
  if(n < 3) return caption
420
573
  const captionStartToken = tokens[n-3]
421
574
  const captionInlineToken = tokens[n-2]
422
575
  const captionEndToken = tokens[n-1]
423
576
  if (captionStartToken === undefined || captionEndToken === undefined) return
424
577
  if (captionStartToken.type !== 'paragraph_open' || captionEndToken.type !== 'paragraph_close') return
425
- setCaptionParagraph(n-3, captionState, caption, fNum, sp, opt)
578
+ setCaptionParagraph(n-3, captionState, caption, null, sp, opt)
426
579
  const captionName = sp && sp.captionDecision ? sp.captionDecision.mark : ''
427
580
  if(!captionName) {
428
581
  if (opt.labelPrefixMarkerWithoutLabelPrevReg) {
@@ -439,14 +592,14 @@ const checkPrevCaption = (tokens, n, caption, fNum, sp, opt, captionState) => {
439
592
  return
440
593
  }
441
594
 
442
- const checkNextCaption = (tokens, en, caption, fNum, sp, opt, captionState) => {
595
+ const checkNextCaption = (tokens, en, caption, sp, opt, captionState) => {
443
596
  if (en + 2 > tokens.length) return
444
597
  const captionStartToken = tokens[en+1]
445
598
  const captionInlineToken = tokens[en+2]
446
599
  const captionEndToken = tokens[en+3]
447
600
  if (captionStartToken === undefined || captionEndToken === undefined) return
448
601
  if (captionStartToken.type !== 'paragraph_open' || captionEndToken.type !== 'paragraph_close') return
449
- setCaptionParagraph(en+1, captionState, caption, fNum, sp, opt)
602
+ setCaptionParagraph(en+1, captionState, caption, null, sp, opt)
450
603
  const captionName = sp && sp.captionDecision ? sp.captionDecision.mark : ''
451
604
  if(!captionName) {
452
605
  if (opt.labelPrefixMarkerWithoutLabelNextReg) {
@@ -468,10 +621,13 @@ const cleanCaptionTokenAttrs = (token, captionName, opt) => {
468
621
  const prefix = opt.captionClassPrefix || ''
469
622
  const targetClass = prefix + captionName
470
623
  if (!targetClass) return
471
- let reg = cleanCaptionRegCache.get(targetClass)
624
+ const cleanCaptionRegCache = opt.cleanCaptionRegCache
625
+ let reg = cleanCaptionRegCache && cleanCaptionRegCache.get(targetClass)
472
626
  if (!reg) {
473
627
  reg = new RegExp('(?:^|\\s)' + escapeRegExp(targetClass) + '(?=\\s|$)', 'g')
474
- cleanCaptionRegCache.set(targetClass, reg)
628
+ if (cleanCaptionRegCache) {
629
+ cleanCaptionRegCache.set(targetClass, reg)
630
+ }
475
631
  }
476
632
  for (let i = token.attrs.length - 1; i >= 0; i--) {
477
633
  if (token.attrs[i][0] === 'class') {
@@ -568,28 +724,31 @@ const wrapWithFigure = (tokens, range, checkTokenTagName, caption, replaceInstea
568
724
  if (rangeEndMap) {
569
725
  figureEndToken.map = [rangeEndMap[0], rangeEndMap[1]]
570
726
  }
571
- const breakToken = new TokenConstructor('text', '', 0)
572
- breakToken.content = '\n'
573
- if (opt.styleProcess && caption.isNext && sp.attrs.length > 0) {
574
- for (let i = 0; i < sp.attrs.length; i++) {
575
- const attr = sp.attrs[i]
576
- figureStartToken.attrJoin(attr[0], attr[1])
577
- }
727
+ const createBreakToken = () => {
728
+ const breakToken = new TokenConstructor('text', '', 0)
729
+ breakToken.content = '\n'
730
+ return breakToken
578
731
  }
579
- // For vsce
580
- if (caption.name === 'img' && tokens[n].attrs) {
581
- for (let i = 0; i < tokens[n].attrs.length; i++) {
582
- const attr = tokens[n].attrs[i]
583
- figureStartToken.attrJoin(attr[0], attr[1])
732
+ if (caption.name === 'img') {
733
+ const joinAttrs = (attrs) => {
734
+ if (!attrs || attrs.length === 0) return
735
+ for (let i = 0; i < attrs.length; i++) {
736
+ const attr = attrs[i]
737
+ figureStartToken.attrJoin(attr[0], attr[1])
738
+ }
584
739
  }
740
+ // `styleProcess` should keep working even when markdown-it-attrs is absent.
741
+ if (opt.styleProcess) joinAttrs(sp.attrs)
742
+ // Forward attrs already materialized by markdown-it-attrs on the image paragraph.
743
+ joinAttrs(tokens[n].attrs)
585
744
  }
586
745
  if (replaceInsteadOfWrap) {
587
- tokens.splice(en, 1, breakToken, figureEndToken, breakToken)
588
- tokens.splice(n, 1, figureStartToken, breakToken)
746
+ tokens.splice(en, 1, createBreakToken(), figureEndToken, createBreakToken())
747
+ tokens.splice(n, 1, figureStartToken, createBreakToken())
589
748
  en = en + 2
590
749
  } else {
591
- tokens.splice(en+1, 0, figureEndToken, breakToken)
592
- tokens.splice(n, 0, figureStartToken, breakToken)
750
+ tokens.splice(en+1, 0, figureEndToken, createBreakToken())
751
+ tokens.splice(n, 0, figureStartToken, createBreakToken())
593
752
  en = en + 3
594
753
  }
595
754
  range.start = n
@@ -597,10 +756,10 @@ const wrapWithFigure = (tokens, range, checkTokenTagName, caption, replaceInstea
597
756
  return
598
757
  }
599
758
 
600
- const checkCaption = (tokens, n, en, caption, fNum, sp, opt, captionState) => {
601
- checkPrevCaption(tokens, n, caption, fNum, sp, opt, captionState)
759
+ const checkCaption = (tokens, n, en, caption, sp, opt, captionState) => {
760
+ checkPrevCaption(tokens, n, caption, sp, opt, captionState)
602
761
  if (caption.isPrev) return
603
- checkNextCaption(tokens, en, caption, fNum, sp, opt, captionState)
762
+ checkNextCaption(tokens, en, caption, sp, opt, captionState)
604
763
  return
605
764
  }
606
765
 
@@ -695,60 +854,16 @@ const detectFenceToken = (token, n, caption) => {
695
854
 
696
855
  const detectHtmlBlockToken = (tokens, token, n, caption, sp, opt) => {
697
856
  if (!token || token.type !== 'html_block') return null
698
- const content = token.content
699
- const hasBlueskyHint = content.indexOf('bluesky-embed') !== -1
700
- const hasBlueskyEmbed = hasBlueskyHint && blueskyEmbedReg.test(content)
857
+ const hints = getHtmlDetectionHints(token.content)
858
+ if (!hasAnyHtmlDetectionHint(hints)) return null
701
859
  let matchedTag = ''
702
- for (let i = 0; i < HTML_TAG_CANDIDATES.length; i++) {
703
- const candidate = HTML_TAG_CANDIDATES[i]
704
- const treatDivAsIframe = candidate === 'div'
705
- const lookupTag = treatDivAsIframe ? 'div' : candidate
706
- const hasTagHint = content.indexOf('<' + lookupTag) !== -1
707
- if (!hasTagHint && !(candidate === 'blockquote' && hasBlueskyEmbed)) continue
708
- const hasTag = hasTagHint ? content.match(getHtmlReg(lookupTag)) : null
709
- const isBlueskyBlockquote = !hasTag && hasBlueskyEmbed && candidate === 'blockquote'
710
- if (!(hasTag || isBlueskyBlockquote)) continue
711
- if (hasTag) {
712
- if ((hasTag[2] && hasTag[3] !== '\n') || (hasTag[1] !== '\n' && hasTag[2] === undefined)) {
713
- token.content += '\n'
714
- }
715
- matchedTag = treatDivAsIframe ? 'iframe' : candidate
716
- if (treatDivAsIframe) {
717
- sp.isVideoIframe = true
718
- }
719
- } else {
720
- let addedCont = ''
721
- let j = n + 1
722
- while (j < tokens.length) {
723
- const nextToken = tokens[j]
724
- if (nextToken.type === 'inline' && endBlockquoteScriptReg.test(nextToken.content)) {
725
- addedCont += nextToken.content + '\n'
726
- if (tokens[j + 1] && tokens[j + 1].type === 'paragraph_close') {
727
- tokens.splice(j + 1, 1)
728
- }
729
- nextToken.content = ''
730
- if (nextToken.children) {
731
- for (let k = 0; k < nextToken.children.length; k++) {
732
- nextToken.children[k].content = ''
733
- }
734
- }
735
- break
736
- }
737
- if (nextToken.type === 'paragraph_open') {
738
- addedCont += '\n'
739
- tokens.splice(j, 1)
740
- continue
741
- }
742
- j++
743
- }
744
- token.content += addedCont
745
- matchedTag = 'blockquote'
746
- }
747
- break
860
+ for (let i = 0; i < HTML_TAG_DETECTORS.length; i++) {
861
+ matchedTag = detectHtmlTagCandidate(tokens, token, n, HTML_TAG_DETECTORS[i], hints, sp)
862
+ if (matchedTag) break
748
863
  }
749
864
  if (!matchedTag) return null
750
865
  if (matchedTag === 'blockquote') {
751
- if (classNameReg.test(token.content)) {
866
+ if (isIframeTypeEmbedBlockquote(token.content)) {
752
867
  sp.isIframeTypeBlockquote = true
753
868
  } else {
754
869
  return null
@@ -758,16 +873,7 @@ const detectHtmlBlockToken = (tokens, token, n, caption, sp, opt) => {
758
873
  sp.isVideoIframe = true
759
874
  }
760
875
  caption.name = matchedTag
761
- let wrapWithoutCaption = false
762
- if (matchedTag === 'iframe' && opt.iframeWithoutCaption) {
763
- wrapWithoutCaption = true
764
- } else if (matchedTag === 'video' && opt.videoWithoutCaption) {
765
- wrapWithoutCaption = true
766
- } else if (matchedTag === 'audio' && opt.audioWithoutCaption) {
767
- wrapWithoutCaption = true
768
- } else if (matchedTag === 'blockquote' && sp.isIframeTypeBlockquote && opt.iframeTypeBlockquoteWithoutCaption) {
769
- wrapWithoutCaption = true
770
- }
876
+ const wrapWithoutCaption = resolveHtmlWrapWithoutCaption(matchedTag, sp, opt)
771
877
  return {
772
878
  type: 'html',
773
879
  tagName: matchedTag,
@@ -778,23 +884,54 @@ const detectHtmlBlockToken = (tokens, token, n, caption, sp, opt) => {
778
884
  }
779
885
  }
780
886
 
781
- const detectImageParagraph = (tokens, token, nextToken, n, caption, sp, opt) => {
782
- if (!token || token.type !== 'paragraph_open') return null
783
- if (!nextToken || nextToken.type !== 'inline' || !nextToken.children || nextToken.children.length === 0) return null
784
- if (nextToken.children[0].type !== 'image') return null
887
+ const hasLeadingImageChild = (token) => {
888
+ return !!(token &&
889
+ token.type === 'inline' &&
890
+ token.children &&
891
+ token.children.length > 0 &&
892
+ token.children[0] &&
893
+ token.children[0].type === 'image')
894
+ }
785
895
 
896
+ const detectImageParagraph = (nextToken, n, caption, sp, opt) => {
897
+ const multipleImagesEnabled = !!opt.multipleImages
898
+ const styleProcessEnabled = !!opt.styleProcess
899
+ const allowSingleImageWithoutCaption = !!opt.oneImageWithoutCaption
900
+ const children = nextToken.children
901
+ const imageToken = children[0]
902
+ const childrenLength = children.length
786
903
  let imageNum = 1
787
904
  let isMultipleImagesHorizontal = true
788
905
  let isMultipleImagesVertical = true
789
906
  let isValid = true
790
907
  caption.name = 'img'
791
- const children = nextToken.children
792
- const childrenLength = children.length
908
+ if (childrenLength === 1) {
909
+ return {
910
+ type: 'image',
911
+ tagName: 'img',
912
+ en: n + 2,
913
+ replaceInsteadOfWrap: true,
914
+ wrapWithoutCaption: allowSingleImageWithoutCaption,
915
+ canWrap: true,
916
+ imageToken,
917
+ }
918
+ }
919
+ if (!multipleImagesEnabled && childrenLength > 2) {
920
+ return {
921
+ type: 'image',
922
+ tagName: 'img',
923
+ en: n + 2,
924
+ replaceInsteadOfWrap: true,
925
+ wrapWithoutCaption: false,
926
+ canWrap: false,
927
+ imageToken,
928
+ }
929
+ }
793
930
  for (let childIndex = 1; childIndex < childrenLength; childIndex++) {
794
931
  const child = children[childIndex]
795
932
  if (childIndex === childrenLength - 1 && child.type === 'text') {
796
933
  const rawContent = child.content
797
- if (opt.styleProcess && rawContent && rawContent.indexOf('{') !== -1) {
934
+ if (styleProcessEnabled && rawContent && rawContent.indexOf('{') !== -1 && rawContent.indexOf('}') !== -1) {
798
935
  const imageAttrs = rawContent.match(imageAttrsReg)
799
936
  if (imageAttrs) {
800
937
  const parsedAttrs = parseImageAttrs(imageAttrs[1])
@@ -802,6 +939,7 @@ const detectImageParagraph = (tokens, token, nextToken, n, caption, sp, opt) =>
802
939
  for (let i = 0; i < parsedAttrs.length; i++) {
803
940
  sp.attrs.push(parsedAttrs[i])
804
941
  }
942
+ child.content = ''
805
943
  }
806
944
  break
807
945
  }
@@ -812,7 +950,7 @@ const detectImageParagraph = (tokens, token, nextToken, n, caption, sp, opt) =>
812
950
  break
813
951
  }
814
952
 
815
- if (!opt.multipleImages) {
953
+ if (!multipleImagesEnabled) {
816
954
  isValid = false
817
955
  break
818
956
  }
@@ -831,7 +969,7 @@ const detectImageParagraph = (tokens, token, nextToken, n, caption, sp, opt) =>
831
969
  isValid = false
832
970
  break
833
971
  }
834
- if (isValid && imageNum > 1 && opt.multipleImages) {
972
+ if (isValid && imageNum > 1 && multipleImagesEnabled) {
835
973
  if (isMultipleImagesHorizontal) {
836
974
  caption.nameSuffix = '-horizontal'
837
975
  } else if (isMultipleImagesVertical) {
@@ -854,18 +992,13 @@ const detectImageParagraph = (tokens, token, nextToken, n, caption, sp, opt) =>
854
992
  tagName,
855
993
  en,
856
994
  replaceInsteadOfWrap: true,
857
- wrapWithoutCaption: isValid && !!opt.oneImageWithoutCaption,
995
+ wrapWithoutCaption: isValid && allowSingleImageWithoutCaption,
858
996
  canWrap: isValid,
859
- imageToken: children[0]
997
+ imageToken,
860
998
  }
861
999
  }
862
1000
 
863
1001
  const figureWithCaption = (state, opt) => {
864
- let fNum = {
865
- img: 0,
866
- table: 0,
867
- }
868
-
869
1002
  const figureNumberState = {
870
1003
  img: 0,
871
1004
  table: 0,
@@ -873,14 +1006,13 @@ const figureWithCaption = (state, opt) => {
873
1006
 
874
1007
  const fallbackLabelState = {
875
1008
  img: null,
876
- table: null,
877
1009
  }
878
1010
 
879
1011
  const captionState = { tokens: state.tokens, Token: state.Token }
880
- figureWithCaptionCore(state.tokens, opt, fNum, figureNumberState, fallbackLabelState, state.Token, captionState, null, 0)
1012
+ figureWithCaptionCore(state.tokens, opt, figureNumberState, fallbackLabelState, state.Token, captionState, null, 0)
881
1013
  }
882
1014
 
883
- const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLabelState, TokenConstructor, captionState, parentType = null, startIndex = 0) => {
1015
+ const figureWithCaptionCore = (tokens, opt, figureNumberState, fallbackLabelState, TokenConstructor, captionState, parentType = null, startIndex = 0) => {
884
1016
  const rRange = { start: startIndex, end: startIndex }
885
1017
  const rCaption = {
886
1018
  name: '', nameSuffix: '', isPrev: false, isNext: false
@@ -898,7 +1030,7 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
898
1030
  const containerType = getNestedContainerType(token)
899
1031
 
900
1032
  if (containerType && containerType !== 'blockquote') {
901
- const closeIndex = figureWithCaptionCore(tokens, opt, fNum, figureNumberState, fallbackLabelState, TokenConstructor, captionState, containerType, n + 1)
1033
+ const closeIndex = figureWithCaptionCore(tokens, opt, figureNumberState, fallbackLabelState, TokenConstructor, captionState, containerType, n + 1)
902
1034
  n = (typeof closeIndex === 'number' ? closeIndex : n) + 1
903
1035
  continue
904
1036
  }
@@ -912,11 +1044,13 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
912
1044
  const tokenType = token.type
913
1045
  const blockType = CHECK_TYPE_TOKEN_MAP[tokenType]
914
1046
  if (tokenType === 'paragraph_open') {
915
- resetRangeState(rRange, n)
916
- resetCaptionState(rCaption)
917
- resetSpecialState(rSp)
918
1047
  const nextToken = tokens[n + 1]
919
- detection = detectImageParagraph(tokens, token, nextToken, n, rCaption, rSp, opt)
1048
+ if (hasLeadingImageChild(nextToken)) {
1049
+ resetRangeState(rRange, n)
1050
+ resetCaptionState(rCaption)
1051
+ resetSpecialState(rSp)
1052
+ detection = detectImageParagraph(nextToken, n, rCaption, rSp, opt)
1053
+ }
920
1054
  } else if (tokenType === 'html_block') {
921
1055
  resetRangeState(rRange, n)
922
1056
  resetCaptionState(rCaption)
@@ -936,7 +1070,7 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
936
1070
 
937
1071
  if (!detection) {
938
1072
  if (containerType === 'blockquote') {
939
- const closeIndex = figureWithCaptionCore(tokens, opt, fNum, figureNumberState, fallbackLabelState, TokenConstructor, captionState, containerType, n + 1)
1073
+ const closeIndex = figureWithCaptionCore(tokens, opt, figureNumberState, fallbackLabelState, TokenConstructor, captionState, containerType, n + 1)
940
1074
  n = (typeof closeIndex === 'number' ? closeIndex : n) + 1
941
1075
  } else {
942
1076
  n++
@@ -947,7 +1081,7 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
947
1081
  rRange.end = detection.en
948
1082
 
949
1083
  rSp.figureClassName = resolveFigureClassName(detection.tagName, rSp, opt)
950
- checkCaption(tokens, rRange.start, rRange.end, rCaption, fNum, rSp, opt, captionState)
1084
+ checkCaption(tokens, rRange.start, rRange.end, rCaption, rSp, opt, captionState)
951
1085
  applyCaptionDrivenFigureClass(rCaption, rSp, opt)
952
1086
 
953
1087
  let hasCaption = rCaption.isPrev || rCaption.isNext
@@ -962,7 +1096,7 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
962
1096
  if (detection.canWrap === false) {
963
1097
  let nextIndex = rRange.end + 1
964
1098
  if (containerType === 'blockquote') {
965
- const closeIndex = figureWithCaptionCore(tokens, opt, fNum, figureNumberState, fallbackLabelState, TokenConstructor, captionState, containerType, rRange.start + 1)
1099
+ const closeIndex = figureWithCaptionCore(tokens, opt, figureNumberState, fallbackLabelState, TokenConstructor, captionState, containerType, rRange.start + 1)
966
1100
  nextIndex = Math.max(nextIndex, (typeof closeIndex === 'number' ? closeIndex : rRange.end) + 1)
967
1101
  }
968
1102
  n = nextIndex
@@ -989,7 +1123,7 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
989
1123
  rRange.start += insertedLength
990
1124
  rRange.end += insertedLength
991
1125
  n += insertedLength
992
- checkCaption(tokens, rRange.start, rRange.end, rCaption, fNum, rSp, opt, captionState)
1126
+ checkCaption(tokens, rRange.start, rRange.end, rCaption, rSp, opt, captionState)
993
1127
  applyCaptionDrivenFigureClass(rCaption, rSp, opt)
994
1128
  }
995
1129
  ensureAutoFigureNumbering(tokens, rRange, rCaption, figureNumberState, opt)
@@ -1017,7 +1151,7 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
1017
1151
  }
1018
1152
 
1019
1153
  if (containerType === 'blockquote') {
1020
- const closeIndex = figureWithCaptionCore(tokens, opt, fNum, figureNumberState, fallbackLabelState, TokenConstructor, captionState, containerType, rRange.start + 1)
1154
+ const closeIndex = figureWithCaptionCore(tokens, opt, figureNumberState, fallbackLabelState, TokenConstructor, captionState, containerType, rRange.start + 1)
1021
1155
  const fallbackIndex = rCaption.name ? rRange.end : n
1022
1156
  nextIndex = Math.max(nextIndex, (typeof closeIndex === 'number' ? closeIndex : fallbackIndex) + 1)
1023
1157
  }
@@ -1029,11 +1163,14 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
1029
1163
 
1030
1164
  const mditFigureWithPCaption = (md, option) => {
1031
1165
  let opt = {
1166
+ // Caption languages delegated to p-captions.
1167
+ languages: ['en', 'ja'],
1168
+
1032
1169
  // --- figure-wrapper behavior ---
1033
1170
  classPrefix: 'f',
1034
1171
  figureClassThatWrapsIframeTypeBlockquote: null,
1035
1172
  figureClassThatWrapsSlides: null,
1036
- styleProcess : true,
1173
+ styleProcess: true,
1037
1174
  oneImageWithoutCaption: false,
1038
1175
  iframeWithoutCaption: false,
1039
1176
  videoWithoutCaption: false,
@@ -1047,7 +1184,7 @@ const mditFigureWithPCaption = (md, option) => {
1047
1184
  // Applies only to the first image within an image-only paragraph (even when multipleImages is true).
1048
1185
  // Priority: caption paragraphs (before/after) > alt text > title attribute; auto detection only runs when no paragraph caption exists.
1049
1186
  autoCaptionDetection: true,
1050
- autoAltCaption: false, // allow alt text (when matching markReg.img or fallback) to build captions automatically
1187
+ autoAltCaption: false, // allow alt text (when matching markReg.img) to build captions automatically
1051
1188
  autoTitleCaption: false, // same as above but reads from the title attribute when alt isn't usable
1052
1189
 
1053
1190
  // --- label prefix marker helpers ---
@@ -1069,11 +1206,30 @@ const mditFigureWithPCaption = (md, option) => {
1069
1206
  removeUnnumberedLabelExceptMarks: [],
1070
1207
  removeMarkNameInCaptionClass: false,
1071
1208
  wrapCaptionBody: false,
1209
+ labelClassFollowsFigure: false,
1210
+ figureToLabelClassMap: null,
1072
1211
  }
1073
1212
  const hasExplicitAutoLabelNumberSets = option && Object.prototype.hasOwnProperty.call(option, 'autoLabelNumberSets')
1074
1213
  const hasExplicitFigureClassThatWrapsIframeTypeBlockquote = option && Object.prototype.hasOwnProperty.call(option, 'figureClassThatWrapsIframeTypeBlockquote')
1075
1214
  const hasExplicitFigureClassThatWrapsSlides = option && Object.prototype.hasOwnProperty.call(option, 'figureClassThatWrapsSlides')
1215
+ const hasExplicitLabelClassFollowsFigure = option && Object.prototype.hasOwnProperty.call(option, 'labelClassFollowsFigure')
1076
1216
  if (option) Object.assign(opt, option)
1217
+ if (!hasExplicitLabelClassFollowsFigure && opt.figureToLabelClassMap) {
1218
+ opt.labelClassFollowsFigure = true
1219
+ }
1220
+ opt.classPrefix = normalizeOptionalClassName(opt.classPrefix)
1221
+ opt.allIframeTypeFigureClassName = normalizeOptionalClassName(opt.allIframeTypeFigureClassName)
1222
+ opt.languages = normalizeLanguages(opt.languages)
1223
+ opt.markRegState = getMarkRegStateForLanguages(opt.languages)
1224
+ opt.imgCaptionMarkReg = opt.markRegState && opt.markRegState.markReg
1225
+ ? opt.markRegState.markReg.img
1226
+ : null
1227
+ opt.htmlWrapWithoutCaption = {
1228
+ iframe: !!opt.iframeWithoutCaption,
1229
+ video: !!opt.videoWithoutCaption,
1230
+ audio: !!opt.audioWithoutCaption,
1231
+ iframeTypeBlockquote: !!opt.iframeTypeBlockquoteWithoutCaption,
1232
+ }
1077
1233
  // Normalize option shorthands now so downstream logic works with a consistent { img, table } shape.
1078
1234
  opt.autoLabelNumberSets = normalizeAutoLabelNumberSets(opt.autoLabelNumberSets)
1079
1235
  if (opt.autoLabelNumber && !hasExplicitAutoLabelNumberSets) {
@@ -1083,16 +1239,29 @@ const mditFigureWithPCaption = (md, option) => {
1083
1239
  const classPrefix = buildClassPrefix(opt.classPrefix)
1084
1240
  opt.figureClassPrefix = classPrefix
1085
1241
  opt.captionClassPrefix = classPrefix
1242
+ const defaultIframeTypeBlockquoteClass = classPrefix + 'img'
1243
+ const defaultSlideFigureClass = classPrefix + 'slide'
1086
1244
  if (!hasExplicitFigureClassThatWrapsIframeTypeBlockquote) {
1087
- opt.figureClassThatWrapsIframeTypeBlockquote = classPrefix + 'img'
1245
+ opt.figureClassThatWrapsIframeTypeBlockquote = defaultIframeTypeBlockquoteClass
1246
+ } else {
1247
+ opt.figureClassThatWrapsIframeTypeBlockquote = normalizeClassOptionWithFallback(
1248
+ opt.figureClassThatWrapsIframeTypeBlockquote,
1249
+ defaultIframeTypeBlockquoteClass,
1250
+ )
1088
1251
  }
1089
1252
  if (!hasExplicitFigureClassThatWrapsSlides) {
1090
- opt.figureClassThatWrapsSlides = classPrefix + 'slide'
1253
+ opt.figureClassThatWrapsSlides = defaultSlideFigureClass
1254
+ } else {
1255
+ opt.figureClassThatWrapsSlides = normalizeClassOptionWithFallback(
1256
+ opt.figureClassThatWrapsSlides,
1257
+ defaultSlideFigureClass,
1258
+ )
1091
1259
  }
1092
1260
  // Precompute label-class permutations so numbering lookup doesn't rebuild arrays per caption.
1093
1261
  opt.labelClassLookup = buildLabelClassLookup(opt)
1094
1262
  const markerList = normalizeLabelPrefixMarkers(opt.labelPrefixMarker)
1095
1263
  opt.labelPrefixMarkerReg = buildLabelPrefixMarkerRegFromList(markerList)
1264
+ opt.cleanCaptionRegCache = new Map()
1096
1265
  if (opt.allowLabelPrefixMarkerWithoutLabel === true) {
1097
1266
  const markerPair = resolveLabelPrefixMarkerPair(markerList)
1098
1267
  opt.labelPrefixMarkerWithoutLabelPrevReg = buildLabelPrefixMarkerRegFromList(markerPair.prev)
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@peaceroad/markdown-it-figure-with-p-caption",
3
- "version": "0.15.2",
3
+ "version": "0.16.1",
4
4
  "description": "A markdown-it plugin. For a paragraph with only one image, a table or code block or blockquote, and by writing a caption paragraph immediately before or after, they are converted into the figure element with the figcaption element.",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "scripts": {
8
- "test": "node test/test.js"
8
+ "test": "node test/test.js",
9
+ "test:p-captions": "node test/test-p-captions.js",
10
+ "test:all": "npm run test && npm run test:p-captions",
11
+ "perf": "node test/performance/benchmark.js"
9
12
  },
10
13
  "repository": {
11
14
  "type": "git",
@@ -17,16 +20,16 @@
17
20
  "url": "https://github.com/peaceroad/p7d-markdown-it-figure-with-p-caption/issues"
18
21
  },
19
22
  "devDependencies": {
20
- "@peaceroad/markdown-it-cjk-breaks-mod": "^0.1.4",
21
- "@peaceroad/markdown-it-renderer-fence": "^0.3.1",
22
- "@peaceroad/markdown-it-renderer-image": "^0.7.0",
23
- "@peaceroad/markdown-it-strong-ja": "^0.6.2",
23
+ "@peaceroad/markdown-it-cjk-breaks-mod": "^0.1.10",
24
+ "@peaceroad/markdown-it-renderer-fence": "^0.6.1",
25
+ "@peaceroad/markdown-it-renderer-image": "^0.13.0",
26
+ "@peaceroad/markdown-it-strong-ja": "^0.9.0",
24
27
  "highlight.js": "^11.11.1",
25
28
  "markdown-it": "^14.1.0",
26
29
  "markdown-it-attrs": "^4.3.1"
27
30
  },
28
31
  "dependencies": {
29
- "p7d-markdown-it-p-captions": "^0.20.1"
32
+ "p7d-markdown-it-p-captions": "^0.21.0"
30
33
  },
31
34
  "files": [
32
35
  "index.js",