@peaceroad/markdown-it-figure-with-p-caption 0.16.0 → 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 +5 -4
  2. package/index.js +220 -152
  3. package/package.json +5 -5
package/README.md CHANGED
@@ -60,7 +60,7 @@ Optionally, you can auto-number image and table caption paragraphs starting from
60
60
 
61
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`.
62
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 trailing `{...}` from inline text in this plugin's own image scanner, but attributes already attached to paragraph tokens by `markdown-it-attrs` are still forwarded.
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
64
  - Attributes attached to caption paragraphs stay on the converted `<figcaption>` token after paragraph-to-figcaption conversion.
65
65
 
66
66
  ## Behavior Customization
@@ -71,6 +71,7 @@ Optionally, you can auto-number image and table caption paragraphs starting from
71
71
  - `figureClassThatWrapsIframeTypeBlockquote`: override the class used when blockquote-based embeds (Twitter, Mastodon, Bluesky) are wrapped.
72
72
  - `figureClassThatWrapsSlides`: override the class assigned when a caption paragraph uses the `Slide.` label.
73
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.
74
75
 
75
76
  ### Wrapping without captions
76
77
 
@@ -437,13 +438,13 @@ A paragraph.
437
438
 
438
439
  ### Styles
439
440
 
440
- 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`.
441
442
 
442
443
  ```
443
444
  [Markdown]
444
- Figure. Highlighted cat. {.notice}
445
+ Figure. Highlighted cat.
445
446
 
446
- ![Highlighted cat](cat.jpg)
447
+ ![Highlighted cat](cat.jpg) {.notice}
447
448
  [HTML]
448
449
  <figure class="custom-img notice">
449
450
  <figcaption><span class="custom-img-label">Figure<span class="custom-img-label-joint">.</span></span> Highlighted cat.</figcaption>
package/index.js CHANGED
@@ -20,14 +20,36 @@ const CHECK_TYPE_TOKEN_MAP = {
20
20
  pre_open: 'pre',
21
21
  blockquote_open: 'blockquote',
22
22
  }
23
- const HTML_TAG_CANDIDATES = ['video', 'audio', 'iframe', 'blockquote', 'div']
24
- const fallbackLabelDefaults = {
25
- img: { en: 'Figure', ja: '' },
26
- table: { en: 'Table', ja: '' },
27
- }
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: '図' }
28
38
 
29
39
  const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
30
- 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
+ }
31
53
  const normalizeLanguages = (value) => {
32
54
  if (!Array.isArray(value)) return ['en', 'ja']
33
55
  const normalized = []
@@ -244,31 +266,28 @@ const isSentenceBoundaryChar = (char) => {
244
266
  return char === '.' || char === '!' || char === '?' || char === '。' || char === '!' || char === '?'
245
267
  }
246
268
 
247
- const getAutoFallbackLabel = (text, captionType) => {
248
- const type = captionType === 'table' ? 'table' : 'img'
269
+ const getAutoFallbackLabel = (text) => {
249
270
  const lang = detectCaptionLanguage(text)
250
- const defaults = fallbackLabelDefaults[type] || fallbackLabelDefaults.img
251
- if (lang === 'ja') return defaults.ja || defaults.en || ''
252
- return defaults.en || defaults.ja || ''
271
+ if (lang === 'ja') return fallbackLabelDefaults.ja || fallbackLabelDefaults.en || ''
272
+ return fallbackLabelDefaults.en || fallbackLabelDefaults.ja || ''
253
273
  }
254
274
 
255
- const getPersistedFallbackLabel = (text, captionType, fallbackState) => {
256
- const type = captionType === 'table' ? 'table' : 'img'
257
- if (!fallbackState) return getAutoFallbackLabel(text, type)
258
- if (fallbackState[type]) return fallbackState[type]
259
- const resolved = getAutoFallbackLabel(text, type)
260
- 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
261
280
  return resolved
262
281
  }
263
282
 
264
- const buildCaptionWithFallback = (text, fallbackOption, captionType = 'img', fallbackState) => {
283
+ const buildCaptionWithFallback = (text, fallbackOption, fallbackState) => {
265
284
  const trimmedText = (text || '').trim()
266
285
  if (!fallbackOption) return ''
267
286
  let label = ''
268
287
  if (typeof fallbackOption === 'string') {
269
288
  label = fallbackOption.trim()
270
289
  } else if (fallbackOption === true) {
271
- label = getPersistedFallbackLabel(trimmedText, captionType, fallbackState)
290
+ label = getPersistedFallbackLabel(trimmedText, fallbackState)
272
291
  }
273
292
  if (!label) return trimmedText
274
293
  const isAsciiLabel = asciiLabelReg.test(label)
@@ -421,7 +440,7 @@ const getAutoCaptionFromImage = (imageToken, opt, fallbackLabelState) => {
421
440
  }
422
441
  if (!caption && opt.autoAltCaption) {
423
442
  const altForFallback = altText || ''
424
- caption = buildCaptionWithFallback(altForFallback, opt.autoAltCaption, 'img', fallbackLabelState)
443
+ caption = buildCaptionWithFallback(altForFallback, opt.autoAltCaption, fallbackLabelState)
425
444
  if (imageToken) {
426
445
  clearImageAltAttr(imageToken)
427
446
  }
@@ -436,7 +455,7 @@ const getAutoCaptionFromImage = (imageToken, opt, fallbackLabelState) => {
436
455
  }
437
456
  if (!caption && opt.autoTitleCaption) {
438
457
  const titleForFallback = titleText || ''
439
- caption = buildCaptionWithFallback(titleForFallback, opt.autoTitleCaption, 'img', fallbackLabelState)
458
+ caption = buildCaptionWithFallback(titleForFallback, opt.autoTitleCaption, fallbackLabelState)
440
459
  if (imageToken) {
441
460
  clearImageTitleAttr(imageToken)
442
461
  }
@@ -445,21 +464,118 @@ const getAutoCaptionFromImage = (imageToken, opt, fallbackLabelState) => {
445
464
  }
446
465
 
447
466
  const getHtmlReg = (tag) => {
448
- if (htmlRegCache.has(tag)) return htmlRegCache.get(tag)
467
+ const cached = htmlRegCache.get(tag)
468
+ if (cached) return cached
449
469
  const regexStr = `^<${tag} ?[^>]*?>[\\s\\S]*?<\\/${tag}>(\\n| *?)(<script [^>]*?>(?:<\\/script>)?)? *(\\n|$)`
450
470
  const reg = new RegExp(regexStr)
451
471
  htmlRegCache.set(tag, reg)
452
472
  return reg
453
473
  }
454
474
 
455
- 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) => {
456
572
  if(n < 3) return caption
457
573
  const captionStartToken = tokens[n-3]
458
574
  const captionInlineToken = tokens[n-2]
459
575
  const captionEndToken = tokens[n-1]
460
576
  if (captionStartToken === undefined || captionEndToken === undefined) return
461
577
  if (captionStartToken.type !== 'paragraph_open' || captionEndToken.type !== 'paragraph_close') return
462
- setCaptionParagraph(n-3, captionState, caption, fNum, sp, opt)
578
+ setCaptionParagraph(n-3, captionState, caption, null, sp, opt)
463
579
  const captionName = sp && sp.captionDecision ? sp.captionDecision.mark : ''
464
580
  if(!captionName) {
465
581
  if (opt.labelPrefixMarkerWithoutLabelPrevReg) {
@@ -476,14 +592,14 @@ const checkPrevCaption = (tokens, n, caption, fNum, sp, opt, captionState) => {
476
592
  return
477
593
  }
478
594
 
479
- const checkNextCaption = (tokens, en, caption, fNum, sp, opt, captionState) => {
595
+ const checkNextCaption = (tokens, en, caption, sp, opt, captionState) => {
480
596
  if (en + 2 > tokens.length) return
481
597
  const captionStartToken = tokens[en+1]
482
598
  const captionInlineToken = tokens[en+2]
483
599
  const captionEndToken = tokens[en+3]
484
600
  if (captionStartToken === undefined || captionEndToken === undefined) return
485
601
  if (captionStartToken.type !== 'paragraph_open' || captionEndToken.type !== 'paragraph_close') return
486
- setCaptionParagraph(en+1, captionState, caption, fNum, sp, opt)
602
+ setCaptionParagraph(en+1, captionState, caption, null, sp, opt)
487
603
  const captionName = sp && sp.captionDecision ? sp.captionDecision.mark : ''
488
604
  if(!captionName) {
489
605
  if (opt.labelPrefixMarkerWithoutLabelNextReg) {
@@ -613,18 +729,18 @@ const wrapWithFigure = (tokens, range, checkTokenTagName, caption, replaceInstea
613
729
  breakToken.content = '\n'
614
730
  return breakToken
615
731
  }
616
- if (opt.styleProcess && caption.isNext && sp.attrs.length > 0) {
617
- for (let i = 0; i < sp.attrs.length; i++) {
618
- const attr = sp.attrs[i]
619
- figureStartToken.attrJoin(attr[0], attr[1])
620
- }
621
- }
622
- // For vsce
623
- if (caption.name === 'img' && tokens[n].attrs) {
624
- for (let i = 0; i < tokens[n].attrs.length; i++) {
625
- const attr = tokens[n].attrs[i]
626
- 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
+ }
627
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)
628
744
  }
629
745
  if (replaceInsteadOfWrap) {
630
746
  tokens.splice(en, 1, createBreakToken(), figureEndToken, createBreakToken())
@@ -640,10 +756,10 @@ const wrapWithFigure = (tokens, range, checkTokenTagName, caption, replaceInstea
640
756
  return
641
757
  }
642
758
 
643
- const checkCaption = (tokens, n, en, caption, fNum, sp, opt, captionState) => {
644
- 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)
645
761
  if (caption.isPrev) return
646
- checkNextCaption(tokens, en, caption, fNum, sp, opt, captionState)
762
+ checkNextCaption(tokens, en, caption, sp, opt, captionState)
647
763
  return
648
764
  }
649
765
 
@@ -738,86 +854,16 @@ const detectFenceToken = (token, n, caption) => {
738
854
 
739
855
  const detectHtmlBlockToken = (tokens, token, n, caption, sp, opt) => {
740
856
  if (!token || token.type !== 'html_block') return null
741
- const content = token.content
742
- const hasBlueskyHint = content.indexOf('bluesky-embed') !== -1
743
- const hasVideoHint = content.indexOf('<video') !== -1
744
- const hasAudioHint = content.indexOf('<audio') !== -1
745
- const hasIframeHint = content.indexOf('<iframe') !== -1
746
- const hasBlockquoteHint = content.indexOf('<blockquote') !== -1
747
- const hasDivHint = content.indexOf('<div') !== -1
748
- const hasIframeTag = hasIframeHint || (hasDivHint && iframeTagReg.test(content))
749
- const hasBlueskyEmbed = hasBlueskyHint && blueskyEmbedReg.test(content)
750
- if (!hasBlueskyHint
751
- && !hasVideoHint
752
- && !hasAudioHint
753
- && !hasIframeHint
754
- && !hasBlockquoteHint
755
- && !hasDivHint) {
756
- return null
757
- }
857
+ const hints = getHtmlDetectionHints(token.content)
858
+ if (!hasAnyHtmlDetectionHint(hints)) return null
758
859
  let matchedTag = ''
759
- for (let i = 0; i < HTML_TAG_CANDIDATES.length; i++) {
760
- const candidate = HTML_TAG_CANDIDATES[i]
761
- const treatDivAsIframe = candidate === 'div'
762
- const lookupTag = treatDivAsIframe ? 'div' : candidate
763
- let hasTagHint = false
764
- if (candidate === 'video') {
765
- hasTagHint = hasVideoHint
766
- } else if (candidate === 'audio') {
767
- hasTagHint = hasAudioHint
768
- } else if (candidate === 'iframe') {
769
- hasTagHint = hasIframeHint
770
- } else if (candidate === 'blockquote') {
771
- hasTagHint = hasBlockquoteHint
772
- } else {
773
- hasTagHint = hasDivHint
774
- }
775
- if (candidate === 'div' && !hasIframeTag) continue
776
- if (!hasTagHint && !(candidate === 'blockquote' && hasBlueskyEmbed)) continue
777
- const hasTag = hasTagHint ? content.match(getHtmlReg(lookupTag)) : null
778
- const isBlueskyBlockquote = !hasTag && hasBlueskyEmbed && candidate === 'blockquote'
779
- if (!(hasTag || isBlueskyBlockquote)) continue
780
- if (hasTag) {
781
- if ((hasTag[2] && hasTag[3] !== '\n') || (hasTag[1] !== '\n' && hasTag[2] === undefined)) {
782
- token.content += '\n'
783
- }
784
- matchedTag = treatDivAsIframe ? 'iframe' : candidate
785
- if (treatDivAsIframe) {
786
- sp.isVideoIframe = true
787
- }
788
- } else {
789
- let addedCont = ''
790
- let j = n + 1
791
- while (j < tokens.length) {
792
- const nextToken = tokens[j]
793
- if (nextToken.type === 'inline' && endBlockquoteScriptReg.test(nextToken.content)) {
794
- addedCont += nextToken.content + '\n'
795
- if (tokens[j + 1] && tokens[j + 1].type === 'paragraph_close') {
796
- tokens.splice(j + 1, 1)
797
- }
798
- nextToken.content = ''
799
- if (nextToken.children) {
800
- for (let k = 0; k < nextToken.children.length; k++) {
801
- nextToken.children[k].content = ''
802
- }
803
- }
804
- break
805
- }
806
- if (nextToken.type === 'paragraph_open') {
807
- addedCont += '\n'
808
- tokens.splice(j, 1)
809
- continue
810
- }
811
- j++
812
- }
813
- token.content += addedCont
814
- matchedTag = 'blockquote'
815
- }
816
- 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
817
863
  }
818
864
  if (!matchedTag) return null
819
865
  if (matchedTag === 'blockquote') {
820
- if (token.content.indexOf('class="') !== -1 && classNameReg.test(token.content)) {
866
+ if (isIframeTypeEmbedBlockquote(token.content)) {
821
867
  sp.isIframeTypeBlockquote = true
822
868
  } else {
823
869
  return null
@@ -827,13 +873,7 @@ const detectHtmlBlockToken = (tokens, token, n, caption, sp, opt) => {
827
873
  sp.isVideoIframe = true
828
874
  }
829
875
  caption.name = matchedTag
830
- let wrapWithoutCaption = false
831
- const htmlWrapWithoutCaption = opt.htmlWrapWithoutCaption
832
- if (matchedTag === 'blockquote') {
833
- wrapWithoutCaption = !!(sp.isIframeTypeBlockquote && htmlWrapWithoutCaption && htmlWrapWithoutCaption.iframeTypeBlockquote)
834
- } else if (htmlWrapWithoutCaption) {
835
- wrapWithoutCaption = !!htmlWrapWithoutCaption[matchedTag]
836
- }
876
+ const wrapWithoutCaption = resolveHtmlWrapWithoutCaption(matchedTag, sp, opt)
837
877
  return {
838
878
  type: 'html',
839
879
  tagName: matchedTag,
@@ -844,21 +884,38 @@ const detectHtmlBlockToken = (tokens, token, n, caption, sp, opt) => {
844
884
  }
845
885
  }
846
886
 
847
- const detectImageParagraph = (tokens, token, nextToken, n, caption, sp, opt) => {
848
- if (!token || token.type !== 'paragraph_open') return null
849
- if (!nextToken || nextToken.type !== 'inline' || !nextToken.children || nextToken.children.length === 0) return null
850
- 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
+ }
851
895
 
896
+ const detectImageParagraph = (nextToken, n, caption, sp, opt) => {
852
897
  const multipleImagesEnabled = !!opt.multipleImages
853
898
  const styleProcessEnabled = !!opt.styleProcess
854
899
  const allowSingleImageWithoutCaption = !!opt.oneImageWithoutCaption
900
+ const children = nextToken.children
901
+ const imageToken = children[0]
902
+ const childrenLength = children.length
855
903
  let imageNum = 1
856
904
  let isMultipleImagesHorizontal = true
857
905
  let isMultipleImagesVertical = true
858
906
  let isValid = true
859
907
  caption.name = 'img'
860
- const children = nextToken.children
861
- 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
+ }
862
919
  if (!multipleImagesEnabled && childrenLength > 2) {
863
920
  return {
864
921
  type: 'image',
@@ -867,7 +924,7 @@ const detectImageParagraph = (tokens, token, nextToken, n, caption, sp, opt) =>
867
924
  replaceInsteadOfWrap: true,
868
925
  wrapWithoutCaption: false,
869
926
  canWrap: false,
870
- imageToken: children[0]
927
+ imageToken,
871
928
  }
872
929
  }
873
930
  for (let childIndex = 1; childIndex < childrenLength; childIndex++) {
@@ -882,6 +939,7 @@ const detectImageParagraph = (tokens, token, nextToken, n, caption, sp, opt) =>
882
939
  for (let i = 0; i < parsedAttrs.length; i++) {
883
940
  sp.attrs.push(parsedAttrs[i])
884
941
  }
942
+ child.content = ''
885
943
  }
886
944
  break
887
945
  }
@@ -936,16 +994,11 @@ const detectImageParagraph = (tokens, token, nextToken, n, caption, sp, opt) =>
936
994
  replaceInsteadOfWrap: true,
937
995
  wrapWithoutCaption: isValid && allowSingleImageWithoutCaption,
938
996
  canWrap: isValid,
939
- imageToken: children[0]
997
+ imageToken,
940
998
  }
941
999
  }
942
1000
 
943
1001
  const figureWithCaption = (state, opt) => {
944
- let fNum = {
945
- img: 0,
946
- table: 0,
947
- }
948
-
949
1002
  const figureNumberState = {
950
1003
  img: 0,
951
1004
  table: 0,
@@ -953,14 +1006,13 @@ const figureWithCaption = (state, opt) => {
953
1006
 
954
1007
  const fallbackLabelState = {
955
1008
  img: null,
956
- table: null,
957
1009
  }
958
1010
 
959
1011
  const captionState = { tokens: state.tokens, Token: state.Token }
960
- figureWithCaptionCore(state.tokens, opt, fNum, figureNumberState, fallbackLabelState, state.Token, captionState, null, 0)
1012
+ figureWithCaptionCore(state.tokens, opt, figureNumberState, fallbackLabelState, state.Token, captionState, null, 0)
961
1013
  }
962
1014
 
963
- 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) => {
964
1016
  const rRange = { start: startIndex, end: startIndex }
965
1017
  const rCaption = {
966
1018
  name: '', nameSuffix: '', isPrev: false, isNext: false
@@ -978,7 +1030,7 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
978
1030
  const containerType = getNestedContainerType(token)
979
1031
 
980
1032
  if (containerType && containerType !== 'blockquote') {
981
- 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)
982
1034
  n = (typeof closeIndex === 'number' ? closeIndex : n) + 1
983
1035
  continue
984
1036
  }
@@ -992,11 +1044,13 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
992
1044
  const tokenType = token.type
993
1045
  const blockType = CHECK_TYPE_TOKEN_MAP[tokenType]
994
1046
  if (tokenType === 'paragraph_open') {
995
- resetRangeState(rRange, n)
996
- resetCaptionState(rCaption)
997
- resetSpecialState(rSp)
998
1047
  const nextToken = tokens[n + 1]
999
- 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
+ }
1000
1054
  } else if (tokenType === 'html_block') {
1001
1055
  resetRangeState(rRange, n)
1002
1056
  resetCaptionState(rCaption)
@@ -1016,7 +1070,7 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
1016
1070
 
1017
1071
  if (!detection) {
1018
1072
  if (containerType === 'blockquote') {
1019
- 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)
1020
1074
  n = (typeof closeIndex === 'number' ? closeIndex : n) + 1
1021
1075
  } else {
1022
1076
  n++
@@ -1027,7 +1081,7 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
1027
1081
  rRange.end = detection.en
1028
1082
 
1029
1083
  rSp.figureClassName = resolveFigureClassName(detection.tagName, rSp, opt)
1030
- checkCaption(tokens, rRange.start, rRange.end, rCaption, fNum, rSp, opt, captionState)
1084
+ checkCaption(tokens, rRange.start, rRange.end, rCaption, rSp, opt, captionState)
1031
1085
  applyCaptionDrivenFigureClass(rCaption, rSp, opt)
1032
1086
 
1033
1087
  let hasCaption = rCaption.isPrev || rCaption.isNext
@@ -1042,7 +1096,7 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
1042
1096
  if (detection.canWrap === false) {
1043
1097
  let nextIndex = rRange.end + 1
1044
1098
  if (containerType === 'blockquote') {
1045
- 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)
1046
1100
  nextIndex = Math.max(nextIndex, (typeof closeIndex === 'number' ? closeIndex : rRange.end) + 1)
1047
1101
  }
1048
1102
  n = nextIndex
@@ -1069,7 +1123,7 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
1069
1123
  rRange.start += insertedLength
1070
1124
  rRange.end += insertedLength
1071
1125
  n += insertedLength
1072
- checkCaption(tokens, rRange.start, rRange.end, rCaption, fNum, rSp, opt, captionState)
1126
+ checkCaption(tokens, rRange.start, rRange.end, rCaption, rSp, opt, captionState)
1073
1127
  applyCaptionDrivenFigureClass(rCaption, rSp, opt)
1074
1128
  }
1075
1129
  ensureAutoFigureNumbering(tokens, rRange, rCaption, figureNumberState, opt)
@@ -1097,7 +1151,7 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
1097
1151
  }
1098
1152
 
1099
1153
  if (containerType === 'blockquote') {
1100
- 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)
1101
1155
  const fallbackIndex = rCaption.name ? rRange.end : n
1102
1156
  nextIndex = Math.max(nextIndex, (typeof closeIndex === 'number' ? closeIndex : fallbackIndex) + 1)
1103
1157
  }
@@ -1116,7 +1170,7 @@ const mditFigureWithPCaption = (md, option) => {
1116
1170
  classPrefix: 'f',
1117
1171
  figureClassThatWrapsIframeTypeBlockquote: null,
1118
1172
  figureClassThatWrapsSlides: null,
1119
- styleProcess : true,
1173
+ styleProcess: true,
1120
1174
  oneImageWithoutCaption: false,
1121
1175
  iframeWithoutCaption: false,
1122
1176
  videoWithoutCaption: false,
@@ -1163,6 +1217,8 @@ const mditFigureWithPCaption = (md, option) => {
1163
1217
  if (!hasExplicitLabelClassFollowsFigure && opt.figureToLabelClassMap) {
1164
1218
  opt.labelClassFollowsFigure = true
1165
1219
  }
1220
+ opt.classPrefix = normalizeOptionalClassName(opt.classPrefix)
1221
+ opt.allIframeTypeFigureClassName = normalizeOptionalClassName(opt.allIframeTypeFigureClassName)
1166
1222
  opt.languages = normalizeLanguages(opt.languages)
1167
1223
  opt.markRegState = getMarkRegStateForLanguages(opt.languages)
1168
1224
  opt.imgCaptionMarkReg = opt.markRegState && opt.markRegState.markReg
@@ -1183,11 +1239,23 @@ const mditFigureWithPCaption = (md, option) => {
1183
1239
  const classPrefix = buildClassPrefix(opt.classPrefix)
1184
1240
  opt.figureClassPrefix = classPrefix
1185
1241
  opt.captionClassPrefix = classPrefix
1242
+ const defaultIframeTypeBlockquoteClass = classPrefix + 'img'
1243
+ const defaultSlideFigureClass = classPrefix + 'slide'
1186
1244
  if (!hasExplicitFigureClassThatWrapsIframeTypeBlockquote) {
1187
- opt.figureClassThatWrapsIframeTypeBlockquote = classPrefix + 'img'
1245
+ opt.figureClassThatWrapsIframeTypeBlockquote = defaultIframeTypeBlockquoteClass
1246
+ } else {
1247
+ opt.figureClassThatWrapsIframeTypeBlockquote = normalizeClassOptionWithFallback(
1248
+ opt.figureClassThatWrapsIframeTypeBlockquote,
1249
+ defaultIframeTypeBlockquoteClass,
1250
+ )
1188
1251
  }
1189
1252
  if (!hasExplicitFigureClassThatWrapsSlides) {
1190
- opt.figureClassThatWrapsSlides = classPrefix + 'slide'
1253
+ opt.figureClassThatWrapsSlides = defaultSlideFigureClass
1254
+ } else {
1255
+ opt.figureClassThatWrapsSlides = normalizeClassOptionWithFallback(
1256
+ opt.figureClassThatWrapsSlides,
1257
+ defaultSlideFigureClass,
1258
+ )
1191
1259
  }
1192
1260
  // Precompute label-class permutations so numbering lookup doesn't rebuild arrays per caption.
1193
1261
  opt.labelClassLookup = buildLabelClassLookup(opt)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peaceroad/markdown-it-figure-with-p-caption",
3
- "version": "0.16.0",
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",
@@ -20,10 +20,10 @@
20
20
  "url": "https://github.com/peaceroad/p7d-markdown-it-figure-with-p-caption/issues"
21
21
  },
22
22
  "devDependencies": {
23
- "@peaceroad/markdown-it-cjk-breaks-mod": "^0.1.6",
24
- "@peaceroad/markdown-it-renderer-fence": "^0.4.1",
25
- "@peaceroad/markdown-it-renderer-image": "^0.9.1",
26
- "@peaceroad/markdown-it-strong-ja": "^0.7.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",
27
27
  "highlight.js": "^11.11.1",
28
28
  "markdown-it": "^14.1.0",
29
29
  "markdown-it-attrs": "^4.3.1"