@peaceroad/markdown-it-figure-with-p-caption 0.14.2 → 0.15.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.
Files changed (3) hide show
  1. package/README.md +596 -579
  2. package/index.js +290 -167
  3. package/package.json +3 -3
package/index.js CHANGED
@@ -2,7 +2,6 @@ import { setCaptionParagraph, markReg } from 'p7d-markdown-it-p-captions'
2
2
 
3
3
  const htmlRegCache = new Map()
4
4
  const cleanCaptionRegCache = new Map()
5
- const classReg = /^f-(.+)$/
6
5
  const blueskyEmbedReg = /^<blockquote class="bluesky-embed"[^]*?>[\s\S]*?$/
7
6
  const videoIframeReg = /^<[^>]*? src="https:\/\/(?:www.youtube-nocookie.com|player.vimeo.com)\//i
8
7
  const classNameReg = /^<[^>]*? class="(twitter-tweet|instagram-media|text-post-media|bluesky-embed|mastodon-embed)"/
@@ -26,6 +25,80 @@ const fallbackLabelDefaults = {
26
25
  table: { en: 'Table', ja: '表' },
27
26
  }
28
27
 
28
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
29
+ const buildClassPrefix = (value) => (value ? value + '-' : '')
30
+ const normalizeLabelPrefixMarkers = (value) => {
31
+ if (typeof value === 'string') {
32
+ return value ? [value] : []
33
+ }
34
+ if (Array.isArray(value)) {
35
+ const normalized = value.map(entry => String(entry)).filter(Boolean)
36
+ return normalized.length > 2 ? normalized.slice(0, 2) : normalized
37
+ }
38
+ return []
39
+ }
40
+ const buildLabelPrefixMarkerRegFromList = (markers) => {
41
+ if (!markers || markers.length === 0) return null
42
+ const pattern = markers.map(escapeRegExp).join('|')
43
+ return new RegExp('^(?:' + pattern + ')(?:[ \\t ]+)?')
44
+ }
45
+ const resolveLabelPrefixMarkerPair = (markers) => {
46
+ if (!markers || markers.length === 0) return { prev: [], next: [] }
47
+ if (markers.length === 1) {
48
+ return { prev: [markers[0]], next: [markers[0]] }
49
+ }
50
+ return { prev: [markers[0]], next: [markers[1]] }
51
+ }
52
+ const stripLeadingPrefix = (text, prefix) => {
53
+ if (typeof text !== 'string' || !text || !prefix) return text
54
+ if (text.startsWith(prefix)) return text.slice(prefix.length)
55
+ return text
56
+ }
57
+ const stripLabelPrefixMarkerFromInline = (inlineToken, markerText) => {
58
+ if (!inlineToken || !markerText) return
59
+ if (typeof inlineToken.content === 'string') {
60
+ inlineToken.content = stripLeadingPrefix(inlineToken.content, markerText)
61
+ }
62
+ if (inlineToken.children && inlineToken.children.length) {
63
+ for (let i = 0; i < inlineToken.children.length; i++) {
64
+ const child = inlineToken.children[i]
65
+ if (child && child.type === 'text' && typeof child.content === 'string') {
66
+ child.content = stripLeadingPrefix(child.content, markerText)
67
+ break
68
+ }
69
+ }
70
+ }
71
+ }
72
+ const getLabelPrefixMarkerMatch = (inlineToken, markerReg) => {
73
+ if (!markerReg || !inlineToken || inlineToken.type !== 'inline') return null
74
+ const content = typeof inlineToken.content === 'string' ? inlineToken.content : ''
75
+ if (!content) return null
76
+ const match = content.match(markerReg)
77
+ if (!match) return null
78
+ const remaining = content.slice(match[0].length)
79
+ if (!remaining || !remaining.trim()) return null
80
+ return match[0]
81
+ }
82
+
83
+ const parseImageAttrs = (raw) => {
84
+ if (raw === null || raw === undefined) return null
85
+ const attrs = []
86
+ const parts = raw.split(/ +/)
87
+ for (let i = 0; i < parts.length; i++) {
88
+ let entry = parts[i]
89
+ if (!entry) continue
90
+ if (classAttrReg.test(entry)) {
91
+ entry = entry.replace(classAttrReg, 'class=')
92
+ } else if (idAttrReg.test(entry)) {
93
+ entry = entry.replace(idAttrReg, 'id=')
94
+ }
95
+ const imageAttr = entry.match(attrParseReg)
96
+ if (!imageAttr || !imageAttr[1]) continue
97
+ attrs.push([imageAttr[1], imageAttr[2]])
98
+ }
99
+ return attrs
100
+ }
101
+
29
102
  const normalizeAutoLabelNumberSets = (value) => {
30
103
  const normalized = { img: false, table: false }
31
104
  if (!value) return normalized
@@ -117,7 +190,12 @@ const getImageAltText = (token) => {
117
190
  if (alt) return alt
118
191
  if (typeof token.content === 'string' && token.content !== '') return token.content
119
192
  if (token.children && token.children.length > 0) {
120
- return token.children.map(child => child.content || '').join('')
193
+ let combined = ''
194
+ for (let i = 0; i < token.children.length; i++) {
195
+ const child = token.children[i]
196
+ if (child && child.content) combined += child.content
197
+ }
198
+ return combined
121
199
  }
122
200
  return ''
123
201
  }
@@ -207,6 +285,22 @@ const getCaptionInlineToken = (tokens, range, caption) => {
207
285
  return null
208
286
  }
209
287
 
288
+ const hasClassName = (classAttr, className) => {
289
+ const index = classAttr.indexOf(className)
290
+ if (index === -1) return false
291
+ const end = index + className.length
292
+ if (index > 0 && classAttr.charCodeAt(index - 1) > 0x20) return false
293
+ if (end < classAttr.length && classAttr.charCodeAt(end) > 0x20) return false
294
+ return true
295
+ }
296
+
297
+ const hasAnyClassName = (classAttr, classNames) => {
298
+ for (let i = 0; i < classNames.length; i++) {
299
+ if (hasClassName(classAttr, classNames[i])) return true
300
+ }
301
+ return false
302
+ }
303
+
210
304
  const getInlineLabelTextToken = (inlineToken, type, opt) => {
211
305
  if (!inlineToken || !inlineToken.children) return null
212
306
  const children = inlineToken.children
@@ -216,9 +310,7 @@ const getInlineLabelTextToken = (inlineToken, type, opt) => {
216
310
  if (!child || !child.attrs) continue
217
311
  const classAttr = getTokenAttr(child, 'class')
218
312
  if (!classAttr) continue
219
- const classes = classAttr.split(/\s+/)
220
- const matched = classNames.some(className => classes.includes(className))
221
- if (!matched) continue
313
+ if (!hasAnyClassName(classAttr, classNames)) continue
222
314
  const textToken = children[i + 1]
223
315
  if (textToken && textToken.type === 'text') {
224
316
  return textToken
@@ -246,30 +338,38 @@ const ensureAutoFigureNumbering = (tokens, range, caption, figureNumberState, op
246
338
  if (!inlineToken) return
247
339
  const labelTextToken = getInlineLabelTextToken(inlineToken, captionType, opt)
248
340
  if (!labelTextToken || typeof labelTextToken.content !== 'string') return
249
- const existingMatch = labelTextToken.content.match(trailingDigitsReg)
250
- if (existingMatch && existingMatch[1]) {
251
- const explicitValue = parseInt(existingMatch[1], 10)
252
- if (!Number.isNaN(explicitValue) && explicitValue > (figureNumberState[captionType] || 0)) {
253
- figureNumberState[captionType] = explicitValue
341
+ const originalText = labelTextToken.content
342
+ let end = originalText.length - 1
343
+ while (end >= 0 && originalText.charCodeAt(end) === 0x20) end--
344
+ if (end >= 0) {
345
+ const code = originalText.charCodeAt(end)
346
+ if (code >= 0x30 && code <= 0x39) {
347
+ const existingMatch = originalText.match(trailingDigitsReg)
348
+ if (existingMatch && existingMatch[1]) {
349
+ const explicitValue = parseInt(existingMatch[1], 10)
350
+ if (!Number.isNaN(explicitValue) && explicitValue > (figureNumberState[captionType] || 0)) {
351
+ figureNumberState[captionType] = explicitValue
352
+ }
353
+ return
354
+ }
254
355
  }
255
- return
256
356
  }
257
357
  figureNumberState[captionType] = (figureNumberState[captionType] || 0) + 1
258
- const baseLabel = labelTextToken.content.replace(/\s*\d+$/, '').trim() || labelTextToken.content.trim()
358
+ const baseLabel = originalText.trim()
259
359
  if (!baseLabel) return
260
360
  const joint = asciiLabelReg.test(baseLabel) ? ' ' : ''
261
361
  const newLabelText = baseLabel + joint + figureNumberState[captionType]
262
- const originalText = labelTextToken.content
263
362
  labelTextToken.content = newLabelText
264
363
  updateInlineTokenContent(inlineToken, originalText, newLabelText)
265
364
  }
266
365
 
267
366
  const getAutoCaptionFromImage = (imageToken, opt, fallbackLabelState) => {
268
367
  if (!opt.autoCaptionDetection) return ''
368
+ if (!imgCaptionMarkReg && !opt.autoAltCaption && !opt.autoTitleCaption) return ''
269
369
  const tryMatch = (text) => {
270
370
  if (!text) return ''
271
371
  const trimmed = text.trim()
272
- if (trimmed && imgCaptionMarkReg && trimmed.match(imgCaptionMarkReg)) {
372
+ if (trimmed && imgCaptionMarkReg && imgCaptionMarkReg.test(trimmed)) {
273
373
  return trimmed
274
374
  }
275
375
  return ''
@@ -314,76 +414,87 @@ const getHtmlReg = (tag) => {
314
414
  return reg
315
415
  }
316
416
 
317
- const getCaptionName = (token) => {
318
- if (!token.attrs) return ''
319
- const attrs = token.attrs
320
- for (let i = 0, len = attrs.length; i < len; i++) {
321
- const attr = attrs[i]
322
- if (attr[0] === 'class') {
323
- const match = attr[1].match(classReg)
324
- if (match) return match[1]
325
- }
326
- }
327
- return ''
328
- }
329
-
330
- const checkPrevCaption = (tokens, n, caption, fNum, sp, opt, TokenConstructor) => {
417
+ const checkPrevCaption = (tokens, n, caption, fNum, sp, opt, captionState) => {
331
418
  if(n < 3) return caption
332
419
  const captionStartToken = tokens[n-3]
420
+ const captionInlineToken = tokens[n-2]
333
421
  const captionEndToken = tokens[n-1]
334
422
  if (captionStartToken === undefined || captionEndToken === undefined) return
335
423
  if (captionStartToken.type !== 'paragraph_open' || captionEndToken.type !== 'paragraph_close') return
336
- setCaptionParagraph(n-3, { tokens, Token: TokenConstructor }, caption, fNum, sp, opt)
337
- const captionName = getCaptionName(captionStartToken)
338
- if(!captionName) return
424
+ setCaptionParagraph(n-3, captionState, caption, fNum, sp, opt)
425
+ const captionName = sp && sp.captionDecision ? sp.captionDecision.mark : ''
426
+ if(!captionName) {
427
+ if (opt.labelPrefixMarkerWithoutLabelPrevReg) {
428
+ const markerMatch = getLabelPrefixMarkerMatch(captionInlineToken, opt.labelPrefixMarkerWithoutLabelPrevReg)
429
+ if (markerMatch) {
430
+ stripLabelPrefixMarkerFromInline(captionInlineToken, markerMatch)
431
+ caption.isPrev = true
432
+ }
433
+ }
434
+ return
435
+ }
339
436
  caption.name = captionName
340
437
  caption.isPrev = true
341
438
  return
342
439
  }
343
440
 
344
- const checkNextCaption = (tokens, en, caption, fNum, sp, opt, TokenConstructor) => {
441
+ const checkNextCaption = (tokens, en, caption, fNum, sp, opt, captionState) => {
345
442
  if (en + 2 > tokens.length) return
346
443
  const captionStartToken = tokens[en+1]
444
+ const captionInlineToken = tokens[en+2]
347
445
  const captionEndToken = tokens[en+3]
348
446
  if (captionStartToken === undefined || captionEndToken === undefined) return
349
447
  if (captionStartToken.type !== 'paragraph_open' || captionEndToken.type !== 'paragraph_close') return
350
- setCaptionParagraph(en+1, { tokens, Token: TokenConstructor }, caption, fNum, sp, opt)
351
- const captionName = getCaptionName(captionStartToken)
352
- if(!captionName) return
448
+ setCaptionParagraph(en+1, captionState, caption, fNum, sp, opt)
449
+ const captionName = sp && sp.captionDecision ? sp.captionDecision.mark : ''
450
+ if(!captionName) {
451
+ if (opt.labelPrefixMarkerWithoutLabelNextReg) {
452
+ const markerMatch = getLabelPrefixMarkerMatch(captionInlineToken, opt.labelPrefixMarkerWithoutLabelNextReg)
453
+ if (markerMatch) {
454
+ stripLabelPrefixMarkerFromInline(captionInlineToken, markerMatch)
455
+ caption.isNext = true
456
+ }
457
+ }
458
+ return
459
+ }
353
460
  caption.name = captionName
354
461
  caption.isNext = true
355
462
  return
356
463
  }
357
464
 
358
- const cleanCaptionTokenAttrs = (token, captionName) => {
359
- if (!token.attrs) return
360
- let reg = cleanCaptionRegCache.get(captionName)
465
+ const cleanCaptionTokenAttrs = (token, captionName, opt) => {
466
+ if (!captionName || !token.attrs || !opt) return
467
+ const prefix = opt.captionClassPrefix || ''
468
+ const targetClass = prefix + captionName
469
+ if (!targetClass) return
470
+ let reg = cleanCaptionRegCache.get(targetClass)
361
471
  if (!reg) {
362
- reg = new RegExp(' *?f-' + captionName)
363
- cleanCaptionRegCache.set(captionName, reg)
472
+ reg = new RegExp('(?:^|\\s)' + escapeRegExp(targetClass) + '(?=\\s|$)', 'g')
473
+ cleanCaptionRegCache.set(targetClass, reg)
364
474
  }
365
475
  for (let i = token.attrs.length - 1; i >= 0; i--) {
366
476
  if (token.attrs[i][0] === 'class') {
367
- token.attrs[i][1] = token.attrs[i][1].replace(reg, '').trim()
368
- if (token.attrs[i][1] === '') token.attrs.splice(i, 1)
477
+ const classValue = token.attrs[i][1] || ''
478
+ if (!classValue || classValue.indexOf(targetClass) === -1) continue
479
+ const cleaned = classValue.replace(reg, '').replace(/\s+/g, ' ').trim()
480
+ if (cleaned) {
481
+ token.attrs[i][1] = cleaned
482
+ } else {
483
+ token.attrs.splice(i, 1)
484
+ }
369
485
  }
370
486
  }
371
487
  }
372
488
 
373
- const resolveFigureClassName = (checkTokenTagName, caption, sp, opt) => {
374
- let className = 'f-' + checkTokenTagName
489
+ const resolveFigureClassName = (checkTokenTagName, sp, opt) => {
490
+ const prefix = opt.figureClassPrefix || ''
491
+ let className = prefix + checkTokenTagName
375
492
  if (opt.allIframeTypeFigureClassName === '') {
376
493
  if (sp.isVideoIframe) {
377
- className = 'f-video'
494
+ className = prefix + 'video'
378
495
  }
379
496
  if (sp.isIframeTypeBlockquote) {
380
- let figureClassThatWrapsIframeTypeBlockquote = opt.figureClassThatWrapsIframeTypeBlockquote
381
- if ((caption.isPrev || caption.isNext) &&
382
- figureClassThatWrapsIframeTypeBlockquote === 'f-img' &&
383
- (caption.name === 'blockquote' || caption.name === 'img')) {
384
- figureClassThatWrapsIframeTypeBlockquote = 'f-img'
385
- }
386
- className = figureClassThatWrapsIframeTypeBlockquote
497
+ className = opt.figureClassThatWrapsIframeTypeBlockquote
387
498
  }
388
499
  } else {
389
500
  if (checkTokenTagName === 'iframe' || sp.isIframeTypeBlockquote) {
@@ -409,7 +520,7 @@ const changePrevCaptionPosition = (tokens, n, caption, opt) => {
409
520
  const captionInlineToken = tokens[n-2]
410
521
  const captionEndToken = tokens[n-1]
411
522
 
412
- cleanCaptionTokenAttrs(captionStartToken, caption.name)
523
+ cleanCaptionTokenAttrs(captionStartToken, caption.name, opt)
413
524
  captionStartToken.type = 'figcaption_open'
414
525
  captionStartToken.tag = 'figcaption'
415
526
  captionEndToken.type = 'figcaption_close'
@@ -423,7 +534,7 @@ const changeNextCaptionPosition = (tokens, en, caption, opt) => {
423
534
  const captionStartToken = tokens[en+2] // +1: text node for figure.
424
535
  const captionInlineToken = tokens[en+3]
425
536
  const captionEndToken = tokens[en+4]
426
- cleanCaptionTokenAttrs(captionStartToken, caption.name)
537
+ cleanCaptionTokenAttrs(captionStartToken, caption.name, opt)
427
538
  captionStartToken.type = 'figcaption_open'
428
539
  captionStartToken.tag = 'figcaption'
429
540
  captionEndToken.type = 'figcaption_close'
@@ -437,23 +548,25 @@ const wrapWithFigure = (tokens, range, checkTokenTagName, caption, replaceInstea
437
548
  let n = range.start
438
549
  let en = range.end
439
550
  const figureStartToken = new TokenConstructor('figure_open', 'figure', 1)
440
- const figureClassName = sp.figureClassName || resolveFigureClassName(checkTokenTagName, caption, sp, opt)
551
+ const figureClassName = sp.figureClassName || resolveFigureClassName(checkTokenTagName, sp, opt)
441
552
  figureStartToken.attrSet('class', figureClassName)
442
553
 
443
- if(/pre-(?:code|samp)/.test(checkTokenTagName) && opt.roleDocExample) {
554
+ if (opt.roleDocExample && (checkTokenTagName === 'pre-code' || checkTokenTagName === 'pre-samp')) {
444
555
  figureStartToken.attrSet('role', 'doc-example')
445
556
  }
446
557
  const figureEndToken = new TokenConstructor('figure_close', 'figure', -1)
447
558
  const breakToken = new TokenConstructor('text', '', 0)
448
559
  breakToken.content = '\n'
449
560
  if (opt.styleProcess && caption.isNext && sp.attrs.length > 0) {
450
- for (let attr of sp.attrs) {
561
+ for (let i = 0; i < sp.attrs.length; i++) {
562
+ const attr = sp.attrs[i]
451
563
  figureStartToken.attrJoin(attr[0], attr[1])
452
564
  }
453
565
  }
454
566
  // For vsce
455
- if(tokens[n].attrs && caption.name === 'img') {
456
- for (let attr of tokens[n].attrs) {
567
+ if (caption.name === 'img' && tokens[n].attrs) {
568
+ for (let i = 0; i < tokens[n].attrs.length; i++) {
569
+ const attr = tokens[n].attrs[i]
457
570
  figureStartToken.attrJoin(attr[0], attr[1])
458
571
  }
459
572
  }
@@ -471,22 +584,25 @@ const wrapWithFigure = (tokens, range, checkTokenTagName, caption, replaceInstea
471
584
  return
472
585
  }
473
586
 
474
- const checkCaption = (tokens, n, en, caption, fNum, sp, opt, TokenConstructor) => {
475
- checkPrevCaption(tokens, n, caption, fNum, sp, opt, TokenConstructor)
587
+ const checkCaption = (tokens, n, en, caption, fNum, sp, opt, captionState) => {
588
+ checkPrevCaption(tokens, n, caption, fNum, sp, opt, captionState)
476
589
  if (caption.isPrev) return
477
- checkNextCaption(tokens, en, caption, fNum, sp, opt, TokenConstructor)
590
+ checkNextCaption(tokens, en, caption, fNum, sp, opt, captionState)
478
591
  return
479
592
  }
480
593
 
481
- const nestedContainers = ['blockquote', 'list_item', 'dd']
482
-
483
594
  const getNestedContainerType = (token) => {
484
- if (!token || token.type.indexOf('_open') === -1) return null
485
- const typeName = token.type.replace('_open', '')
486
- if (nestedContainers.includes(typeName)) {
487
- return typeName
595
+ if (!token) return null
596
+ switch (token.type) {
597
+ case 'blockquote_open':
598
+ return 'blockquote'
599
+ case 'list_item_open':
600
+ return 'list_item'
601
+ case 'dd_open':
602
+ return 'dd'
603
+ default:
604
+ return null
488
605
  }
489
- return null
490
606
  }
491
607
 
492
608
  const resetRangeState = (range, start) => {
@@ -495,7 +611,6 @@ const resetRangeState = (range, start) => {
495
611
  }
496
612
 
497
613
  const resetCaptionState = (caption) => {
498
- caption.mark = ''
499
614
  caption.name = ''
500
615
  caption.nameSuffix = ''
501
616
  caption.isPrev = false
@@ -506,18 +621,19 @@ const resetSpecialState = (sp) => {
506
621
  sp.attrs.length = 0
507
622
  sp.isVideoIframe = false
508
623
  sp.isIframeTypeBlockquote = false
509
- sp.hasImgCaption = false
510
624
  sp.figureClassName = ''
511
625
  sp.captionDecision = null
512
626
  }
513
627
 
514
628
  const findClosingTokenIndex = (tokens, startIndex, tag) => {
629
+ const openType = tag + '_open'
630
+ const closeType = tag + '_close'
515
631
  let depth = 1
516
632
  let i = startIndex + 1
517
633
  while (i < tokens.length) {
518
- const token = tokens[i]
519
- if (token.type === `${tag}_open`) depth++
520
- if (token.type === `${tag}_close`) {
634
+ const tokenType = tokens[i].type
635
+ if (tokenType === openType) depth++
636
+ if (tokenType === closeType) {
521
637
  depth--
522
638
  if (depth === 0) return i
523
639
  }
@@ -526,17 +642,14 @@ const findClosingTokenIndex = (tokens, startIndex, tag) => {
526
642
  return startIndex
527
643
  }
528
644
 
529
- const detectCheckTypeOpen = (tokens, token, n, caption) => {
530
- if (!token) return null
531
- const baseType = CHECK_TYPE_TOKEN_MAP[token.type]
532
- if (!baseType) return null
645
+ const detectCheckTypeOpen = (tokens, token, n, caption, baseType) => {
646
+ if (!token || !baseType) return null
533
647
  if (n > 1 && tokens[n - 2] && tokens[n - 2].type === 'figure_open') return null
534
648
  let tagName = token.tag
535
649
  caption.name = baseType
536
650
  if (baseType === 'pre') {
537
- if (tokens[n + 1] && tokens[n + 1].tag === 'code') caption.mark = 'pre-code'
538
- if (tokens[n + 1] && tokens[n + 1].tag === 'samp') caption.mark = 'pre-samp'
539
- caption.name = caption.mark
651
+ if (tokens[n + 1] && tokens[n + 1].tag === 'code') caption.name = 'pre-code'
652
+ if (tokens[n + 1] && tokens[n + 1].tag === 'samp') caption.name = 'pre-samp'
540
653
  }
541
654
  const en = findClosingTokenIndex(tokens, n, tagName)
542
655
  return {
@@ -569,14 +682,18 @@ const detectFenceToken = (token, n, caption) => {
569
682
 
570
683
  const detectHtmlBlockToken = (tokens, token, n, caption, sp, opt) => {
571
684
  if (!token || token.type !== 'html_block') return null
572
- const blueskyContMatch = token.content.match(blueskyEmbedReg)
685
+ const content = token.content
686
+ const hasBlueskyHint = content.indexOf('bluesky-embed') !== -1
687
+ const hasBlueskyEmbed = hasBlueskyHint && blueskyEmbedReg.test(content)
573
688
  let matchedTag = ''
574
689
  for (let i = 0; i < HTML_TAG_CANDIDATES.length; i++) {
575
690
  const candidate = HTML_TAG_CANDIDATES[i]
576
691
  const treatDivAsIframe = candidate === 'div'
577
692
  const lookupTag = treatDivAsIframe ? 'div' : candidate
578
- const hasTag = token.content.match(getHtmlReg(lookupTag))
579
- const isBlueskyBlockquote = !hasTag && blueskyContMatch && candidate === 'blockquote'
693
+ const hasTagHint = content.indexOf('<' + lookupTag) !== -1
694
+ if (!hasTagHint && !(candidate === 'blockquote' && hasBlueskyEmbed)) continue
695
+ const hasTag = hasTagHint ? content.match(getHtmlReg(lookupTag)) : null
696
+ const isBlueskyBlockquote = !hasTag && hasBlueskyEmbed && candidate === 'blockquote'
580
697
  if (!(hasTag || isBlueskyBlockquote)) continue
581
698
  if (hasTag) {
582
699
  if ((hasTag[2] && hasTag[3] !== '\n') || (hasTag[1] !== '\n' && hasTag[2] === undefined)) {
@@ -588,9 +705,8 @@ const detectHtmlBlockToken = (tokens, token, n, caption, sp, opt) => {
588
705
  }
589
706
  } else {
590
707
  let addedCont = ''
591
- const tokensLength = tokens.length
592
708
  let j = n + 1
593
- while (j < tokensLength) {
709
+ while (j < tokens.length) {
594
710
  const nextToken = tokens[j]
595
711
  if (nextToken.type === 'inline' && endBlockquoteScriptReg.test(nextToken.content)) {
596
712
  addedCont += nextToken.content + '\n'
@@ -598,9 +714,11 @@ const detectHtmlBlockToken = (tokens, token, n, caption, sp, opt) => {
598
714
  tokens.splice(j + 1, 1)
599
715
  }
600
716
  nextToken.content = ''
601
- nextToken.children.forEach((child) => {
602
- child.content = ''
603
- })
717
+ if (nextToken.children) {
718
+ for (let k = 0; k < nextToken.children.length; k++) {
719
+ nextToken.children[k].content = ''
720
+ }
721
+ }
604
722
  break
605
723
  }
606
724
  if (nextToken.type === 'paragraph_open') {
@@ -617,8 +735,7 @@ const detectHtmlBlockToken = (tokens, token, n, caption, sp, opt) => {
617
735
  }
618
736
  if (!matchedTag) return null
619
737
  if (matchedTag === 'blockquote') {
620
- const isIframeTypeBlockquote = token.content.match(classNameReg)
621
- if (isIframeTypeBlockquote) {
738
+ if (classNameReg.test(token.content)) {
622
739
  sp.isIframeTypeBlockquote = true
623
740
  } else {
624
741
  return null
@@ -665,17 +782,11 @@ const detectImageParagraph = (tokens, token, nextToken, n, caption, sp, opt) =>
665
782
  if (childIndex === childrenLength - 1 && child.type === 'text') {
666
783
  let imageAttrs = child.content && child.content.match(imageAttrsReg)
667
784
  if (imageAttrs) {
668
- imageAttrs = imageAttrs[1].split(/ +/)
669
- for (let i = 0; i < imageAttrs.length; i++) {
670
- if (classAttrReg.test(imageAttrs[i])) {
671
- imageAttrs[i] = imageAttrs[i].replace(classAttrReg, 'class=')
672
- }
673
- if (idAttrReg.test(imageAttrs[i])) {
674
- imageAttrs[i] = imageAttrs[i].replace(idAttrReg, 'id=')
785
+ const parsedAttrs = parseImageAttrs(imageAttrs[1])
786
+ if (parsedAttrs && parsedAttrs.length) {
787
+ for (let i = 0; i < parsedAttrs.length; i++) {
788
+ sp.attrs.push(parsedAttrs[i])
675
789
  }
676
- const imageAttr = imageAttrs[i].match(attrParseReg)
677
- if (!imageAttr || !imageAttr[1]) continue
678
- sp.attrs.push([imageAttr[1], imageAttr[2]])
679
790
  }
680
791
  break
681
792
  }
@@ -715,7 +826,6 @@ const detectImageParagraph = (tokens, token, nextToken, n, caption, sp, opt) =>
715
826
  }
716
827
  }
717
828
  }
718
- nextToken.children[0].type = 'image'
719
829
  const en = n + 2
720
830
  let tagName = 'img'
721
831
  if (caption.nameSuffix) tagName += caption.nameSuffix
@@ -746,25 +856,29 @@ const figureWithCaption = (state, opt) => {
746
856
  table: null,
747
857
  }
748
858
 
749
- figureWithCaptionCore(state.tokens, opt, fNum, figureNumberState, fallbackLabelState, state.Token, null, 0)
859
+ const captionState = { tokens: state.tokens, Token: state.Token }
860
+ figureWithCaptionCore(state.tokens, opt, fNum, figureNumberState, fallbackLabelState, state.Token, captionState, null, 0)
750
861
  }
751
862
 
752
- const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLabelState, TokenConstructor, parentType = null, startIndex = 0) => {
863
+ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLabelState, TokenConstructor, captionState, parentType = null, startIndex = 0) => {
753
864
  const rRange = { start: startIndex, end: startIndex }
754
865
  const rCaption = {
755
- mark: '', name: '', nameSuffix: '', isPrev: false, isNext: false
866
+ name: '', nameSuffix: '', isPrev: false, isNext: false
756
867
  }
757
868
  const rSp = {
758
- attrs: [], isVideoIframe: false, isIframeTypeBlockquote: false, hasImgCaption: false, captionDecision: null
869
+ attrs: [],
870
+ isVideoIframe: false,
871
+ isIframeTypeBlockquote: false,
872
+ figureClassName: '',
873
+ captionDecision: null
759
874
  }
760
-
761
875
  let n = startIndex
762
876
  while (n < tokens.length) {
763
877
  const token = tokens[n]
764
878
  const containerType = getNestedContainerType(token)
765
879
 
766
880
  if (containerType && containerType !== 'blockquote') {
767
- const closeIndex = figureWithCaptionCore(tokens, opt, fNum, figureNumberState, fallbackLabelState, TokenConstructor, containerType, n + 1)
881
+ const closeIndex = figureWithCaptionCore(tokens, opt, fNum, figureNumberState, fallbackLabelState, TokenConstructor, captionState, containerType, n + 1)
768
882
  n = (typeof closeIndex === 'number' ? closeIndex : n) + 1
769
883
  continue
770
884
  }
@@ -774,20 +888,35 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
774
888
  return n
775
889
  }
776
890
 
777
- const nextToken = tokens[n + 1]
778
- resetRangeState(rRange, n)
779
- resetCaptionState(rCaption)
780
- resetSpecialState(rSp)
781
-
782
- const detection =
783
- detectCheckTypeOpen(tokens, token, n, rCaption) ||
784
- detectFenceToken(token, n, rCaption) ||
785
- detectHtmlBlockToken(tokens, token, n, rCaption, rSp, opt) ||
786
- detectImageParagraph(tokens, token, nextToken, n, rCaption, rSp, opt)
891
+ let detection = null
892
+ const tokenType = token.type
893
+ const blockType = CHECK_TYPE_TOKEN_MAP[tokenType]
894
+ if (tokenType === 'paragraph_open') {
895
+ resetRangeState(rRange, n)
896
+ resetCaptionState(rCaption)
897
+ resetSpecialState(rSp)
898
+ const nextToken = tokens[n + 1]
899
+ detection = detectImageParagraph(tokens, token, nextToken, n, rCaption, rSp, opt)
900
+ } else if (tokenType === 'html_block') {
901
+ resetRangeState(rRange, n)
902
+ resetCaptionState(rCaption)
903
+ resetSpecialState(rSp)
904
+ detection = detectHtmlBlockToken(tokens, token, n, rCaption, rSp, opt)
905
+ } else if (tokenType === 'fence') {
906
+ resetRangeState(rRange, n)
907
+ resetCaptionState(rCaption)
908
+ resetSpecialState(rSp)
909
+ detection = detectFenceToken(token, n, rCaption)
910
+ } else if (blockType) {
911
+ resetRangeState(rRange, n)
912
+ resetCaptionState(rCaption)
913
+ resetSpecialState(rSp)
914
+ detection = detectCheckTypeOpen(tokens, token, n, rCaption, blockType)
915
+ }
787
916
 
788
917
  if (!detection) {
789
918
  if (containerType === 'blockquote') {
790
- const closeIndex = figureWithCaptionCore(tokens, opt, fNum, figureNumberState, fallbackLabelState, TokenConstructor, containerType, n + 1)
919
+ const closeIndex = figureWithCaptionCore(tokens, opt, fNum, figureNumberState, fallbackLabelState, TokenConstructor, captionState, containerType, n + 1)
791
920
  n = (typeof closeIndex === 'number' ? closeIndex : n) + 1
792
921
  } else {
793
922
  n++
@@ -797,8 +926,8 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
797
926
 
798
927
  rRange.end = detection.en
799
928
 
800
- rSp.figureClassName = resolveFigureClassName(detection.tagName, rCaption, rSp, opt)
801
- checkCaption(tokens, rRange.start, rRange.end, rCaption, fNum, rSp, opt, TokenConstructor)
929
+ rSp.figureClassName = resolveFigureClassName(detection.tagName, rSp, opt)
930
+ checkCaption(tokens, rRange.start, rRange.end, rCaption, fNum, rSp, opt, captionState)
802
931
  applyCaptionDrivenFigureClass(rCaption, rSp, opt)
803
932
 
804
933
  let hasCaption = rCaption.isPrev || rCaption.isNext
@@ -813,7 +942,7 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
813
942
  if (detection.canWrap === false) {
814
943
  let nextIndex = rRange.end + 1
815
944
  if (containerType === 'blockquote') {
816
- const closeIndex = figureWithCaptionCore(tokens, opt, fNum, figureNumberState, fallbackLabelState, TokenConstructor, containerType, rRange.start + 1)
945
+ const closeIndex = figureWithCaptionCore(tokens, opt, fNum, figureNumberState, fallbackLabelState, TokenConstructor, captionState, containerType, rRange.start + 1)
817
946
  nextIndex = Math.max(nextIndex, (typeof closeIndex === 'number' ? closeIndex : rRange.end) + 1)
818
947
  }
819
948
  n = nextIndex
@@ -825,13 +954,8 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
825
954
  shouldWrap = detection.canWrap !== false && (hasCaption || detection.wrapWithoutCaption)
826
955
  } else if (detection.type === 'image') {
827
956
  shouldWrap = detection.canWrap !== false && (hasCaption || detection.wrapWithoutCaption)
828
- if (parentType === 'list_item' || isInListItem(tokens, n)) {
829
- const isInTightList = token.hidden === true
830
- if (isInTightList) {
831
- shouldWrap = false
832
- } else if (!hasCaption && !opt.oneImageWithoutCaption) {
833
- shouldWrap = false
834
- }
957
+ if (token.hidden === true) {
958
+ shouldWrap = false
835
959
  }
836
960
  } else {
837
961
  shouldWrap = detection.canWrap !== false && hasCaption
@@ -845,7 +969,7 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
845
969
  rRange.start += insertedLength
846
970
  rRange.end += insertedLength
847
971
  n += insertedLength
848
- checkCaption(tokens, rRange.start, rRange.end, rCaption, fNum, rSp, opt, TokenConstructor)
972
+ checkCaption(tokens, rRange.start, rRange.end, rCaption, fNum, rSp, opt, captionState)
849
973
  applyCaptionDrivenFigureClass(rCaption, rSp, opt)
850
974
  }
851
975
  ensureAutoFigureNumbering(tokens, rRange, rCaption, figureNumberState, opt)
@@ -853,15 +977,19 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
853
977
  }
854
978
 
855
979
  let nextIndex
856
- if (!rCaption.name) {
857
- nextIndex = n + 1
980
+ if (!rCaption.isPrev && !rCaption.isNext) {
981
+ if (shouldWrap && detection.type === 'html') {
982
+ nextIndex = rRange.end + 1
983
+ } else {
984
+ nextIndex = n + 1
985
+ }
858
986
  } else {
859
987
  const en = rRange.end
860
988
  if (rCaption.isPrev) {
861
989
  changePrevCaptionPosition(tokens, rRange.start, rCaption, opt)
862
990
  nextIndex = en + 1
863
991
  } else if (rCaption.isNext) {
864
- changeNextCaptionPosition(tokens, en, rCaption)
992
+ changeNextCaptionPosition(tokens, en, rCaption, opt)
865
993
  nextIndex = en + 4
866
994
  } else {
867
995
  nextIndex = en + 1
@@ -869,7 +997,7 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
869
997
  }
870
998
 
871
999
  if (containerType === 'blockquote') {
872
- const closeIndex = figureWithCaptionCore(tokens, opt, fNum, figureNumberState, fallbackLabelState, TokenConstructor, containerType, rRange.start + 1)
1000
+ const closeIndex = figureWithCaptionCore(tokens, opt, fNum, figureNumberState, fallbackLabelState, TokenConstructor, captionState, containerType, rRange.start + 1)
873
1001
  const fallbackIndex = rCaption.name ? rRange.end : n
874
1002
  nextIndex = Math.max(nextIndex, (typeof closeIndex === 'number' ? closeIndex : fallbackIndex) + 1)
875
1003
  }
@@ -879,42 +1007,12 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
879
1007
  return tokens.length
880
1008
  }
881
1009
 
882
- const isInListItem = (() => {
883
- const cache = new WeakMap()
884
- return (tokens, idx) => {
885
- if (cache.has(tokens)) {
886
- const cachedResult = cache.get(tokens)
887
- if (cachedResult[idx] !== undefined) {
888
- return cachedResult[idx]
889
- }
890
- } else {
891
- cache.set(tokens, {})
892
- }
893
-
894
- const result = cache.get(tokens)
895
-
896
- for (let i = idx - 1; i >= 0; i--) {
897
- if (tokens[i].type === 'list_item_open') {
898
- result[idx] = true
899
- return true
900
- }
901
- if (tokens[i].type === 'list_item_close' || tokens[i].type === 'list_open') {
902
- result[idx] = false
903
- return false
904
- }
905
- }
906
-
907
- result[idx] = false
908
- return false
909
- }
910
- })()
911
-
912
1010
  const mditFigureWithPCaption = (md, option) => {
913
1011
  let opt = {
914
1012
  // --- figure-wrapper behavior ---
915
1013
  classPrefix: 'f',
916
- figureClassThatWrapsIframeTypeBlockquote: 'f-img',
917
- figureClassThatWrapsSlides: 'f-slide',
1014
+ figureClassThatWrapsIframeTypeBlockquote: null,
1015
+ figureClassThatWrapsSlides: null,
918
1016
  styleProcess : true,
919
1017
  oneImageWithoutCaption: false,
920
1018
  iframeWithoutCaption: false,
@@ -932,6 +1030,10 @@ const mditFigureWithPCaption = (md, option) => {
932
1030
  autoAltCaption: false, // allow alt text (when matching markReg.img or fallback) to build captions automatically
933
1031
  autoTitleCaption: false, // same as above but reads from the title attribute when alt isn't usable
934
1032
 
1033
+ // --- label prefix marker helpers ---
1034
+ labelPrefixMarker: null, // optional leading marker(s) before label, e.g. '*' or ['*', '>']
1035
+ allowLabelPrefixMarkerWithoutLabel: false, // when true, reuse labelPrefixMarker for marker-only captions (array uses [prev, next])
1036
+
935
1037
  // --- numbering controls ---
936
1038
  autoLabelNumber: false, // shorthand for enabling numbering for both img/table unless autoLabelNumberSets is provided explicitly
937
1039
  autoLabelNumberSets: [], // preferred; supports ['img'], ['table'], or both
@@ -949,6 +1051,8 @@ const mditFigureWithPCaption = (md, option) => {
949
1051
  wrapCaptionBody: false,
950
1052
  }
951
1053
  const hasExplicitAutoLabelNumberSets = option && Object.prototype.hasOwnProperty.call(option, 'autoLabelNumberSets')
1054
+ const hasExplicitFigureClassThatWrapsIframeTypeBlockquote = option && Object.prototype.hasOwnProperty.call(option, 'figureClassThatWrapsIframeTypeBlockquote')
1055
+ const hasExplicitFigureClassThatWrapsSlides = option && Object.prototype.hasOwnProperty.call(option, 'figureClassThatWrapsSlides')
952
1056
  if (option) Object.assign(opt, option)
953
1057
  // Normalize option shorthands now so downstream logic works with a consistent { img, table } shape.
954
1058
  opt.autoLabelNumberSets = normalizeAutoLabelNumberSets(opt.autoLabelNumberSets)
@@ -956,8 +1060,27 @@ const mditFigureWithPCaption = (md, option) => {
956
1060
  opt.autoLabelNumberSets.img = true
957
1061
  opt.autoLabelNumberSets.table = true
958
1062
  }
959
- // Precompute `.f-*-label` permutations so numbering lookup doesn't rebuild arrays per caption.
1063
+ const classPrefix = buildClassPrefix(opt.classPrefix)
1064
+ opt.figureClassPrefix = classPrefix
1065
+ opt.captionClassPrefix = classPrefix
1066
+ if (!hasExplicitFigureClassThatWrapsIframeTypeBlockquote) {
1067
+ opt.figureClassThatWrapsIframeTypeBlockquote = classPrefix + 'img'
1068
+ }
1069
+ if (!hasExplicitFigureClassThatWrapsSlides) {
1070
+ opt.figureClassThatWrapsSlides = classPrefix + 'slide'
1071
+ }
1072
+ // Precompute label-class permutations so numbering lookup doesn't rebuild arrays per caption.
960
1073
  opt.labelClassLookup = buildLabelClassLookup(opt)
1074
+ const markerList = normalizeLabelPrefixMarkers(opt.labelPrefixMarker)
1075
+ opt.labelPrefixMarkerReg = buildLabelPrefixMarkerRegFromList(markerList)
1076
+ if (opt.allowLabelPrefixMarkerWithoutLabel === true) {
1077
+ const markerPair = resolveLabelPrefixMarkerPair(markerList)
1078
+ opt.labelPrefixMarkerWithoutLabelPrevReg = buildLabelPrefixMarkerRegFromList(markerPair.prev)
1079
+ opt.labelPrefixMarkerWithoutLabelNextReg = buildLabelPrefixMarkerRegFromList(markerPair.next)
1080
+ } else {
1081
+ opt.labelPrefixMarkerWithoutLabelPrevReg = null
1082
+ opt.labelPrefixMarkerWithoutLabelNextReg = null
1083
+ }
961
1084
 
962
1085
  //If nextCaption has `{}` style and `f-img-multipleImages`, when upgraded to markdown-it-attrs@4.2.0, the existing script will have `{}` style on nextCaption. Therefore, since markdown-it-attrs is md.core.ruler.before('linkify'), figure_with_caption will be processed after it.
963
1086
  md.core.ruler.before('replacements', 'figure_with_caption', (state) => {