@peaceroad/markdown-it-figure-with-p-caption 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -1,7 +1,7 @@
1
- import { setCaptionParagraph } from 'p7d-markdown-it-p-captions'
2
- import { imgAttrToPCaption, setAltToLabel, setTitleToLabel } from './imgAttrToPCaption.js'
1
+ import { setCaptionParagraph, markReg } from 'p7d-markdown-it-p-captions'
3
2
 
4
3
  const htmlRegCache = new Map()
4
+ const cleanCaptionRegCache = new Map()
5
5
  const classReg = /^f-(.+)$/
6
6
  const blueskyEmbedReg = /^<blockquote class="bluesky-embed"[^]*?>[\s\S]*?$/
7
7
  const videoIframeReg = /^<[^>]*? src="https:\/\/(?:www.youtube-nocookie.com|player.vimeo.com)\//i
@@ -10,9 +10,301 @@ const imageAttrsReg = /^ *\{(.*?)\} *$/
10
10
  const classAttrReg = /^\./
11
11
  const idAttrReg = /^#/
12
12
  const attrParseReg = /^(.*?)="?(.*)"?$/
13
- const whitespaceReg = /^ *$/
14
13
  const sampLangReg = /^ *(?:samp|shell|console)(?:(?= )|$)/
15
14
  const endBlockquoteScriptReg = /<\/blockquote> *<script[^>]*?><\/script>$/
15
+ const imgCaptionMarkReg = markReg && markReg.img ? markReg.img : null
16
+ const asciiLabelReg = /^[A-Za-z]/
17
+ const trailingDigitsReg = /(\d+)\s*$/
18
+ const CHECK_TYPE_TOKEN_MAP = {
19
+ table_open: 'table',
20
+ pre_open: 'pre',
21
+ blockquote_open: 'blockquote',
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
+ }
28
+
29
+ const normalizeSetLabelNumbers = (value) => {
30
+ const normalized = { img: false, table: false }
31
+ if (!value) return normalized
32
+ if (Array.isArray(value)) {
33
+ for (const entry of value) {
34
+ if (normalized.hasOwnProperty(entry)) normalized[entry] = true
35
+ }
36
+ return normalized
37
+ }
38
+ return normalized
39
+ }
40
+
41
+ const buildLabelClassLookup = (opt) => {
42
+ const classPrefix = opt.classPrefix ? opt.classPrefix + '-' : ''
43
+ const defaultClasses = [classPrefix + 'label']
44
+ const withType = (type) => {
45
+ if (opt.removeMarkNameInCaptionClass) return defaultClasses
46
+ return [classPrefix + type + '-label', ...defaultClasses]
47
+ }
48
+ return {
49
+ img: withType('img'),
50
+ table: withType('table'),
51
+ default: defaultClasses,
52
+ }
53
+ }
54
+
55
+ const shouldApplyLabelNumbering = (captionType, opt) => {
56
+ const setting = opt.setLabelNumbers
57
+ if (!setting) return false
58
+ return !!setting[captionType]
59
+ }
60
+
61
+ const isOnlySpacesText = (token) => {
62
+ if (!token || token.type !== 'text') return false
63
+ const content = token.content
64
+ if (typeof content !== 'string') return false
65
+ for (let i = 0; i < content.length; i++) {
66
+ if (content.charCodeAt(i) !== 0x20) return false
67
+ }
68
+ return true
69
+ }
70
+
71
+ const getTokenAttr = (token, attrName) => {
72
+ if (!token || !token.attrs) return ''
73
+ for (let i = 0; i < token.attrs.length; i++) {
74
+ if (token.attrs[i][0] === attrName) return token.attrs[i][1] || ''
75
+ }
76
+ return ''
77
+ }
78
+
79
+ const setTokenAttr = (token, attrName, value) => {
80
+ if (!token) return
81
+ if (!token.attrs) token.attrs = []
82
+ for (let i = 0; i < token.attrs.length; i++) {
83
+ if (token.attrs[i][0] === attrName) {
84
+ token.attrs[i][1] = value
85
+ return
86
+ }
87
+ }
88
+ token.attrs.push([attrName, value])
89
+ }
90
+
91
+ const removeTokenAttr = (token, attrName) => {
92
+ if (!token || !token.attrs) return
93
+ for (let i = token.attrs.length - 1; i >= 0; i--) {
94
+ if (token.attrs[i][0] === attrName) {
95
+ token.attrs.splice(i, 1)
96
+ }
97
+ }
98
+ }
99
+
100
+ const clearImageAltAttr = (token) => {
101
+ if (!token) return
102
+ setTokenAttr(token, 'alt', '')
103
+ token.content = ''
104
+ if (token.children) {
105
+ for (let i = 0; i < token.children.length; i++) {
106
+ token.children[i].content = ''
107
+ }
108
+ }
109
+ }
110
+
111
+ const clearImageTitleAttr = (token) => {
112
+ removeTokenAttr(token, 'title')
113
+ }
114
+
115
+ const getImageAltText = (token) => {
116
+ let alt = getTokenAttr(token, 'alt')
117
+ if (alt) return alt
118
+ if (typeof token.content === 'string' && token.content !== '') return token.content
119
+ if (token.children && token.children.length > 0) {
120
+ return token.children.map(child => child.content || '').join('')
121
+ }
122
+ return ''
123
+ }
124
+
125
+ const getImageTitleText = (token) => getTokenAttr(token, 'title')
126
+
127
+ const detectCaptionLanguage = (text) => {
128
+ const target = (text || '').trim()
129
+ if (!target) return 'en'
130
+ for (let i = 0; i < target.length; i++) {
131
+ const char = target[i]
132
+ const code = target.charCodeAt(i)
133
+ if (isJapaneseCharCode(code)) return 'ja'
134
+ if (isSentenceBoundaryChar(char) || char === '\n') break
135
+ }
136
+ return 'en'
137
+ }
138
+
139
+ const isJapaneseCharCode = (code) => {
140
+ return (
141
+ (code >= 0x3040 && code <= 0x30ff) || // Hiragana + Katakana
142
+ (code >= 0x31f0 && code <= 0x31ff) || // Katakana extensions
143
+ (code >= 0x4e00 && code <= 0x9fff) || // CJK Unified Ideographs
144
+ (code >= 0xff66 && code <= 0xff9f) // Half-width Katakana
145
+ )
146
+ }
147
+
148
+ const isSentenceBoundaryChar = (char) => {
149
+ return char === '.' || char === '!' || char === '?' || char === '。' || char === '!' || char === '?'
150
+ }
151
+
152
+ const getAutoFallbackLabel = (text, captionType) => {
153
+ const type = captionType === 'table' ? 'table' : 'img'
154
+ const lang = detectCaptionLanguage(text)
155
+ const defaults = fallbackLabelDefaults[type] || fallbackLabelDefaults.img
156
+ if (lang === 'ja') return defaults.ja || defaults.en || ''
157
+ return defaults.en || defaults.ja || ''
158
+ }
159
+
160
+ const getPersistedFallbackLabel = (text, captionType, fallbackState) => {
161
+ const type = captionType === 'table' ? 'table' : 'img'
162
+ if (!fallbackState) return getAutoFallbackLabel(text, type)
163
+ if (fallbackState[type]) return fallbackState[type]
164
+ const resolved = getAutoFallbackLabel(text, type)
165
+ fallbackState[type] = resolved
166
+ return resolved
167
+ }
168
+
169
+ const buildCaptionWithFallback = (text, fallbackOption, captionType = 'img', fallbackState) => {
170
+ const trimmedText = (text || '').trim()
171
+ if (!fallbackOption) return ''
172
+ let label = ''
173
+ if (typeof fallbackOption === 'string') {
174
+ label = fallbackOption.trim()
175
+ } else if (fallbackOption === true) {
176
+ label = getPersistedFallbackLabel(trimmedText, captionType, fallbackState)
177
+ }
178
+ if (!label) return trimmedText
179
+ const isAsciiLabel = asciiLabelReg.test(label)
180
+ if (!trimmedText) {
181
+ return isAsciiLabel ? label + '.' : label
182
+ }
183
+ return label + (isAsciiLabel ? '. ' : ' ') + trimmedText
184
+ }
185
+
186
+ const createAutoCaptionParagraph = (captionText, TokenConstructor) => {
187
+ const paragraphOpen = new TokenConstructor('paragraph_open', 'p', 1)
188
+ paragraphOpen.block = true
189
+ const inlineToken = new TokenConstructor('inline', '', 0)
190
+ inlineToken.block = true
191
+ inlineToken.content = captionText
192
+ const textToken = new TokenConstructor('text', '', 0)
193
+ textToken.content = captionText
194
+ inlineToken.children = [textToken]
195
+ const paragraphClose = new TokenConstructor('paragraph_close', 'p', -1)
196
+ paragraphClose.block = true
197
+ return [paragraphOpen, inlineToken, paragraphClose]
198
+ }
199
+
200
+ const getCaptionInlineToken = (tokens, range, caption) => {
201
+ if (caption.isPrev) {
202
+ const inlineIndex = range.start - 2
203
+ if (inlineIndex >= 0) return tokens[inlineIndex]
204
+ } else if (caption.isNext) {
205
+ return tokens[range.end + 2]
206
+ }
207
+ return null
208
+ }
209
+
210
+ const getInlineLabelTextToken = (inlineToken, type, opt) => {
211
+ if (!inlineToken || !inlineToken.children) return null
212
+ const children = inlineToken.children
213
+ const classNames = opt.labelClassLookup[type] || opt.labelClassLookup.default
214
+ for (let i = 0; i < children.length; i++) {
215
+ const child = children[i]
216
+ if (!child || !child.attrs) continue
217
+ const classAttr = getTokenAttr(child, 'class')
218
+ if (!classAttr) continue
219
+ const classes = classAttr.split(/\s+/)
220
+ const matched = classNames.some(className => classes.includes(className))
221
+ if (!matched) continue
222
+ const textToken = children[i + 1]
223
+ if (textToken && textToken.type === 'text') {
224
+ return textToken
225
+ }
226
+ }
227
+ return null
228
+ }
229
+
230
+ const updateInlineTokenContent = (inlineToken, originalText, newText) => {
231
+ if (!inlineToken || typeof inlineToken.content !== 'string') return
232
+ if (!originalText) return
233
+ const index = inlineToken.content.indexOf(originalText)
234
+ if (index === -1) return
235
+ inlineToken.content =
236
+ inlineToken.content.slice(0, index) +
237
+ newText +
238
+ inlineToken.content.slice(index + originalText.length)
239
+ }
240
+
241
+ const ensureAutoFigureNumbering = (tokens, range, caption, figureNumberState, opt) => {
242
+ const captionType = caption.name === 'img' ? 'img' : (caption.name === 'table' ? 'table' : '')
243
+ if (!captionType) return
244
+ if (!shouldApplyLabelNumbering(captionType, opt)) return
245
+ const inlineToken = getCaptionInlineToken(tokens, range, caption)
246
+ if (!inlineToken) return
247
+ const labelTextToken = getInlineLabelTextToken(inlineToken, captionType, opt)
248
+ 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
254
+ }
255
+ return
256
+ }
257
+ figureNumberState[captionType] = (figureNumberState[captionType] || 0) + 1
258
+ const baseLabel = labelTextToken.content.replace(/\s*\d+$/, '').trim() || labelTextToken.content.trim()
259
+ if (!baseLabel) return
260
+ const joint = asciiLabelReg.test(baseLabel) ? ' ' : ''
261
+ const newLabelText = baseLabel + joint + figureNumberState[captionType]
262
+ const originalText = labelTextToken.content
263
+ labelTextToken.content = newLabelText
264
+ updateInlineTokenContent(inlineToken, originalText, newLabelText)
265
+ }
266
+
267
+ const getAutoCaptionFromImage = (imageToken, opt, fallbackLabelState) => {
268
+ if (!opt.autoCaptionDetection) return ''
269
+ const tryMatch = (text) => {
270
+ if (!text) return ''
271
+ const trimmed = text.trim()
272
+ if (trimmed && imgCaptionMarkReg && trimmed.match(imgCaptionMarkReg)) {
273
+ return trimmed
274
+ }
275
+ return ''
276
+ }
277
+
278
+ const altText = getImageAltText(imageToken)
279
+ let caption = tryMatch(altText)
280
+ if (caption) {
281
+ clearImageAltAttr(imageToken)
282
+ return caption
283
+ }
284
+ if (!caption && opt.autoAltCaption) {
285
+ const altForFallback = altText || ''
286
+ caption = buildCaptionWithFallback(altForFallback, opt.autoAltCaption, 'img', fallbackLabelState)
287
+ if (imageToken) {
288
+ clearImageAltAttr(imageToken)
289
+ }
290
+ }
291
+ if (caption) return caption
292
+
293
+ const titleText = getImageTitleText(imageToken)
294
+ caption = tryMatch(titleText)
295
+ if (caption) {
296
+ clearImageTitleAttr(imageToken)
297
+ return caption
298
+ }
299
+ if (!caption && opt.autoTitleCaption) {
300
+ const titleForFallback = titleText || ''
301
+ caption = buildCaptionWithFallback(titleForFallback, opt.autoTitleCaption, 'img', fallbackLabelState)
302
+ if (imageToken) {
303
+ clearImageTitleAttr(imageToken)
304
+ }
305
+ }
306
+ return caption
307
+ }
16
308
 
17
309
  const getHtmlReg = (tag) => {
18
310
  if (htmlRegCache.has(tag)) return htmlRegCache.get(tag)
@@ -40,7 +332,7 @@ const checkPrevCaption = (tokens, n, caption, fNum, sp, opt, TokenConstructor) =
40
332
  const captionStartToken = tokens[n-3]
41
333
  const captionEndToken = tokens[n-1]
42
334
  if (captionStartToken === undefined || captionEndToken === undefined) return
43
- if (captionStartToken.type !== 'paragraph_open' && captionEndToken.type !== 'paragraph_close') return
335
+ if (captionStartToken.type !== 'paragraph_open' || captionEndToken.type !== 'paragraph_close') return
44
336
  setCaptionParagraph(n-3, { tokens, Token: TokenConstructor }, caption, fNum, sp, opt)
45
337
  const captionName = getCaptionName(captionStartToken)
46
338
  if(!captionName) return
@@ -54,7 +346,7 @@ const checkNextCaption = (tokens, en, caption, fNum, sp, opt, TokenConstructor)
54
346
  const captionStartToken = tokens[en+1]
55
347
  const captionEndToken = tokens[en+3]
56
348
  if (captionStartToken === undefined || captionEndToken === undefined) return
57
- if (captionStartToken.type !== 'paragraph_open' && captionEndToken.type !== 'paragraph_close') return
349
+ if (captionStartToken.type !== 'paragraph_open' || captionEndToken.type !== 'paragraph_close') return
58
350
  setCaptionParagraph(en+1, { tokens, Token: TokenConstructor }, caption, fNum, sp, opt)
59
351
  const captionName = getCaptionName(captionStartToken)
60
352
  if(!captionName) return
@@ -63,8 +355,6 @@ const checkNextCaption = (tokens, en, caption, fNum, sp, opt, TokenConstructor)
63
355
  return
64
356
  }
65
357
 
66
- const cleanCaptionRegCache = new Map()
67
-
68
358
  const cleanCaptionTokenAttrs = (token, captionName) => {
69
359
  if (!token.attrs) return
70
360
  let reg = cleanCaptionRegCache.get(captionName)
@@ -80,28 +370,44 @@ const cleanCaptionTokenAttrs = (token, captionName) => {
80
370
  }
81
371
  }
82
372
 
83
- const changePrevCaptionPosition = (tokens, n, caption, opt) => {
84
- const captionStartToken = tokens[n-3]
85
- const captionInlineToken = tokens[n-2]
86
- const captionEndToken = tokens[n-1]
87
-
88
- if (opt.imgAltCaption || opt.imgTitleCaption) {
89
- let isNoCaption = false
90
- if (captionInlineToken.attrs) {
91
- const attrs = captionInlineToken.attrs, len = attrs.length
92
- for (let i = 0; i < len; i++) {
93
- const attr = attrs[i]
94
- if (attr[0] === 'class' && attr[1] === 'nocaption') {
95
- isNoCaption = true
96
- break
97
- }
373
+ const resolveFigureClassName = (checkTokenTagName, caption, sp, opt) => {
374
+ let className = 'f-' + checkTokenTagName
375
+ if (opt.allIframeTypeFigureClassName === '') {
376
+ if (sp.isVideoIframe) {
377
+ className = 'f-video'
378
+ }
379
+ 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'
98
385
  }
386
+ className = figureClassThatWrapsIframeTypeBlockquote
99
387
  }
100
- if (isNoCaption) {
101
- tokens.splice(n-3, 3)
102
- return false
388
+ } else {
389
+ if (checkTokenTagName === 'iframe' || sp.isIframeTypeBlockquote) {
390
+ className = opt.allIframeTypeFigureClassName
103
391
  }
104
392
  }
393
+ return className
394
+ }
395
+
396
+ const applyCaptionDrivenFigureClass = (caption, sp, opt) => {
397
+ if (!sp) return
398
+ const figureClassForSlides = opt.figureClassThatWrapsSlides
399
+ if (!figureClassForSlides) return
400
+ const detectedMark = (sp.captionDecision && sp.captionDecision.mark) || (caption && caption.name) || ''
401
+ if (detectedMark !== 'slide') return
402
+ if (opt.allIframeTypeFigureClassName && sp.figureClassName === opt.allIframeTypeFigureClassName) return
403
+ sp.figureClassName = figureClassForSlides
404
+ }
405
+
406
+
407
+ const changePrevCaptionPosition = (tokens, n, caption, opt) => {
408
+ const captionStartToken = tokens[n-3]
409
+ const captionInlineToken = tokens[n-2]
410
+ const captionEndToken = tokens[n-1]
105
411
 
106
412
  cleanCaptionTokenAttrs(captionStartToken, caption.name)
107
413
  captionStartToken.type = 'figcaption_open'
@@ -113,7 +419,7 @@ const changePrevCaptionPosition = (tokens, n, caption, opt) => {
113
419
  return true
114
420
  }
115
421
 
116
- const changeNextCaptionPosition = (tokens, en, caption) => {
422
+ const changeNextCaptionPosition = (tokens, en, caption, opt) => {
117
423
  const captionStartToken = tokens[en+2] // +1: text node for figure.
118
424
  const captionInlineToken = tokens[en+3]
119
425
  const captionEndToken = tokens[en+4]
@@ -131,29 +437,8 @@ const wrapWithFigure = (tokens, range, checkTokenTagName, caption, replaceInstea
131
437
  let n = range.start
132
438
  let en = range.end
133
439
  const figureStartToken = new TokenConstructor('figure_open', 'figure', 1)
134
- figureStartToken.attrSet('class', 'f-' + checkTokenTagName)
135
-
136
- if (opt.allIframeTypeFigureClassName === '') {
137
- if (sp.isVideoIframe) {
138
- figureStartToken.attrSet('class', 'f-video')
139
- }
140
- if (sp.isIframeTypeBlockquote) {
141
- let figureClassThatWrapsIframeTypeBlockquote = 'i-frame'
142
- if (caption.isPrev || caption.isNext) {
143
- if (caption.name === 'blockquote' || caption.name === 'img') {
144
- figureClassThatWrapsIframeTypeBlockquote = 'f-img'
145
- }
146
- figureStartToken.attrSet('class', figureClassThatWrapsIframeTypeBlockquote)
147
- } else {
148
- figureClassThatWrapsIframeTypeBlockquote = opt.figureClassThatWrapsIframeTypeBlockquote
149
- figureStartToken.attrSet('class', figureClassThatWrapsIframeTypeBlockquote)
150
- }
151
- }
152
- } else {
153
- if (checkTokenTagName === 'iframe' || sp.isIframeTypeBlockquote) {
154
- figureStartToken.attrSet('class', opt.allIframeTypeFigureClassName)
155
- }
156
- }
440
+ const figureClassName = sp.figureClassName || resolveFigureClassName(checkTokenTagName, caption, sp, opt)
441
+ figureStartToken.attrSet('class', figureClassName)
157
442
 
158
443
  if(/pre-(?:code|samp)/.test(checkTokenTagName) && opt.roleDocExample) {
159
444
  figureStartToken.attrSet('role', 'doc-example')
@@ -193,51 +478,256 @@ const checkCaption = (tokens, n, en, caption, fNum, sp, opt, TokenConstructor) =
193
478
  return
194
479
  }
195
480
 
196
- const processTokensRecursively = (tokens, opt, fNum, TokenConstructor, parentType) => {
197
- const nestedContainers = ['blockquote', 'list_item', 'dd']
481
+ const nestedContainers = ['blockquote', 'list_item', 'dd']
482
+
483
+ 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
488
+ }
489
+ return null
490
+ }
491
+
492
+ const resetRangeState = (range, start) => {
493
+ range.start = start
494
+ range.end = start
495
+ }
496
+
497
+ const resetCaptionState = (caption) => {
498
+ caption.mark = ''
499
+ caption.name = ''
500
+ caption.nameSuffix = ''
501
+ caption.isPrev = false
502
+ caption.isNext = false
503
+ }
198
504
 
199
- figureWithCaptionCore(tokens, opt, fNum, TokenConstructor, parentType)
505
+ const resetSpecialState = (sp) => {
506
+ sp.attrs.length = 0
507
+ sp.isVideoIframe = false
508
+ sp.isIframeTypeBlockquote = false
509
+ sp.hasImgCaption = false
510
+ sp.figureClassName = ''
511
+ sp.captionDecision = null
512
+ }
200
513
 
201
- const nestedRanges = []
202
- let i = 0
514
+ const findClosingTokenIndex = (tokens, startIndex, tag) => {
515
+ let depth = 1
516
+ let i = startIndex + 1
203
517
  while (i < tokens.length) {
204
518
  const token = tokens[i]
205
- let containerType = null
206
- for (const container of nestedContainers) {
207
- if (token.type === `${container}_open`) {
208
- containerType = container
209
- break
210
- }
519
+ if (token.type === `${tag}_open`) depth++
520
+ if (token.type === `${tag}_close`) {
521
+ depth--
522
+ if (depth === 0) return i
211
523
  }
212
- if (containerType) {
213
- let depth = 1
214
- let endIndex = i + 1
215
- while (endIndex < tokens.length && depth > 0) {
216
- if (tokens[endIndex].type === `${containerType}_open`) depth++
217
- if (tokens[endIndex].type === `${containerType}_close`) depth--
218
- endIndex++
524
+ i++
525
+ }
526
+ return startIndex
527
+ }
528
+
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
533
+ if (n > 1 && tokens[n - 2] && tokens[n - 2].type === 'figure_open') return null
534
+ let tagName = token.tag
535
+ caption.name = baseType
536
+ 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
540
+ }
541
+ const en = findClosingTokenIndex(tokens, n, tagName)
542
+ return {
543
+ type: 'block',
544
+ tagName,
545
+ en,
546
+ replaceInsteadOfWrap: false,
547
+ wrapWithoutCaption: false,
548
+ canWrap: true,
549
+ }
550
+ }
551
+
552
+ const detectFenceToken = (token, n, caption) => {
553
+ if (!token || token.type !== 'fence' || token.tag !== 'code' || !token.block) return null
554
+ let tagName = 'pre-code'
555
+ if (sampLangReg.test(token.info)) {
556
+ token.tag = 'samp'
557
+ tagName = 'pre-samp'
558
+ }
559
+ caption.name = tagName
560
+ return {
561
+ type: 'fence',
562
+ tagName,
563
+ en: n,
564
+ replaceInsteadOfWrap: false,
565
+ wrapWithoutCaption: false,
566
+ canWrap: true,
567
+ }
568
+ }
569
+
570
+ const detectHtmlBlockToken = (tokens, token, n, caption, sp, opt) => {
571
+ if (!token || token.type !== 'html_block') return null
572
+ const blueskyContMatch = token.content.match(blueskyEmbedReg)
573
+ let matchedTag = ''
574
+ for (let i = 0; i < HTML_TAG_CANDIDATES.length; i++) {
575
+ const candidate = HTML_TAG_CANDIDATES[i]
576
+ const treatDivAsIframe = candidate === 'div'
577
+ const lookupTag = treatDivAsIframe ? 'div' : candidate
578
+ const hasTag = token.content.match(getHtmlReg(lookupTag))
579
+ const isBlueskyBlockquote = !hasTag && blueskyContMatch && candidate === 'blockquote'
580
+ if (!(hasTag || isBlueskyBlockquote)) continue
581
+ if (hasTag) {
582
+ if ((hasTag[2] && hasTag[3] !== '\n') || (hasTag[1] !== '\n' && hasTag[2] === undefined)) {
583
+ token.content += '\n'
219
584
  }
220
- if (depth === 0 && endIndex - i > 2) {
221
- nestedRanges.push({
222
- start: i + 1,
223
- end: endIndex - 1,
224
- type: containerType
225
- })
585
+ matchedTag = treatDivAsIframe ? 'iframe' : candidate
586
+ if (treatDivAsIframe) {
587
+ sp.isVideoIframe = true
226
588
  }
227
- i = endIndex
228
589
  } else {
229
- i++
590
+ let addedCont = ''
591
+ const tokensLength = tokens.length
592
+ let j = n + 1
593
+ while (j < tokensLength) {
594
+ const nextToken = tokens[j]
595
+ if (nextToken.type === 'inline' && endBlockquoteScriptReg.test(nextToken.content)) {
596
+ addedCont += nextToken.content + '\n'
597
+ if (tokens[j + 1] && tokens[j + 1].type === 'paragraph_close') {
598
+ tokens.splice(j + 1, 1)
599
+ }
600
+ nextToken.content = ''
601
+ nextToken.children.forEach((child) => {
602
+ child.content = ''
603
+ })
604
+ break
605
+ }
606
+ if (nextToken.type === 'paragraph_open') {
607
+ addedCont += '\n'
608
+ tokens.splice(j, 1)
609
+ continue
610
+ }
611
+ j++
612
+ }
613
+ token.content += addedCont
614
+ matchedTag = 'blockquote'
615
+ }
616
+ break
617
+ }
618
+ if (!matchedTag) return null
619
+ if (matchedTag === 'blockquote') {
620
+ const isIframeTypeBlockquote = token.content.match(classNameReg)
621
+ if (isIframeTypeBlockquote) {
622
+ sp.isIframeTypeBlockquote = true
623
+ } else {
624
+ return null
230
625
  }
231
626
  }
627
+ if (matchedTag === 'iframe' && videoIframeReg.test(token.content)) {
628
+ sp.isVideoIframe = true
629
+ }
630
+ caption.name = matchedTag
631
+ let wrapWithoutCaption = false
632
+ if (matchedTag === 'iframe' && opt.iframeWithoutCaption) {
633
+ wrapWithoutCaption = true
634
+ } else if (matchedTag === 'video' && opt.videoWithoutCaption) {
635
+ wrapWithoutCaption = true
636
+ } else if (matchedTag === 'audio' && opt.audioWithoutCaption) {
637
+ wrapWithoutCaption = true
638
+ } else if (matchedTag === 'blockquote' && sp.isIframeTypeBlockquote && opt.iframeTypeBlockquoteWithoutCaption) {
639
+ wrapWithoutCaption = true
640
+ }
641
+ return {
642
+ type: 'html',
643
+ tagName: matchedTag,
644
+ en: n,
645
+ replaceInsteadOfWrap: false,
646
+ wrapWithoutCaption,
647
+ canWrap: true,
648
+ }
649
+ }
232
650
 
233
- for (let j = nestedRanges.length - 1; j >= 0; j--) {
234
- const range = nestedRanges[j]
235
- const innerTokens = tokens.slice(range.start, range.end)
236
- if (innerTokens.length > 0) {
237
- processTokensRecursively(innerTokens, opt, fNum, TokenConstructor, range.type)
238
- tokens.splice(range.start, range.end - range.start, ...innerTokens)
651
+ const detectImageParagraph = (tokens, token, nextToken, n, caption, sp, opt) => {
652
+ if (!token || token.type !== 'paragraph_open') return null
653
+ if (!nextToken || nextToken.type !== 'inline' || !nextToken.children || nextToken.children.length === 0) return null
654
+ if (nextToken.children[0].type !== 'image') return null
655
+
656
+ let imageNum = 1
657
+ let isMultipleImagesHorizontal = true
658
+ let isMultipleImagesVertical = true
659
+ let isValid = true
660
+ caption.name = 'img'
661
+ const children = nextToken.children
662
+ const childrenLength = children.length
663
+ for (let childIndex = 1; childIndex < childrenLength; childIndex++) {
664
+ const child = children[childIndex]
665
+ if (childIndex === childrenLength - 1 && child.type === 'text') {
666
+ let imageAttrs = child.content && child.content.match(imageAttrsReg)
667
+ 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=')
675
+ }
676
+ const imageAttr = imageAttrs[i].match(attrParseReg)
677
+ if (!imageAttr || !imageAttr[1]) continue
678
+ sp.attrs.push([imageAttr[1], imageAttr[2]])
679
+ }
680
+ break
681
+ }
682
+ }
683
+
684
+ if (!opt.multipleImages) {
685
+ isValid = false
686
+ break
687
+ }
688
+ if (child.type === 'image') {
689
+ imageNum++
690
+ continue
691
+ }
692
+ if (isOnlySpacesText(child)) {
693
+ isMultipleImagesVertical = false
694
+ continue
695
+ }
696
+ if (child.type === 'softbreak') {
697
+ isMultipleImagesHorizontal = false
698
+ continue
699
+ }
700
+ isValid = false
701
+ break
702
+ }
703
+ if (isValid && imageNum > 1 && opt.multipleImages) {
704
+ if (isMultipleImagesHorizontal) {
705
+ caption.nameSuffix = '-horizontal'
706
+ } else if (isMultipleImagesVertical) {
707
+ caption.nameSuffix = '-vertical'
708
+ } else {
709
+ caption.nameSuffix = '-multiple'
710
+ }
711
+ for (let i = 0; i < childrenLength; i++) {
712
+ const child = children[i]
713
+ if (isOnlySpacesText(child)) {
714
+ child.content = ''
715
+ }
239
716
  }
240
717
  }
718
+ nextToken.children[0].type = 'image'
719
+ const en = n + 2
720
+ let tagName = 'img'
721
+ if (caption.nameSuffix) tagName += caption.nameSuffix
722
+ return {
723
+ type: 'image',
724
+ tagName,
725
+ en,
726
+ replaceInsteadOfWrap: true,
727
+ wrapWithoutCaption: isValid && !!opt.oneImageWithoutCaption,
728
+ canWrap: isValid,
729
+ imageToken: children[0]
730
+ }
241
731
  }
242
732
 
243
733
  const figureWithCaption = (state, opt) => {
@@ -246,302 +736,147 @@ const figureWithCaption = (state, opt) => {
246
736
  table: 0,
247
737
  }
248
738
 
249
- processTokensRecursively(state.tokens, opt, fNum, state.Token, null)
250
- }
739
+ const figureNumberState = {
740
+ img: 0,
741
+ table: 0,
742
+ }
743
+
744
+ const fallbackLabelState = {
745
+ img: null,
746
+ table: null,
747
+ }
251
748
 
252
- const figureWithCaptionCore = (tokens, opt, fNum, TokenConstructor, parentType) => {
253
- const checkTypes = ['table', 'pre', 'blockquote']
254
- const htmlTags = ['video', 'audio', 'iframe', 'blockquote', 'div']
749
+ figureWithCaptionCore(state.tokens, opt, fNum, figureNumberState, fallbackLabelState, state.Token, null, 0)
750
+ }
255
751
 
256
- const rRange = { start: 0, end: 0 }
752
+ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLabelState, TokenConstructor, parentType = null, startIndex = 0) => {
753
+ const rRange = { start: startIndex, end: startIndex }
257
754
  const rCaption = {
258
755
  mark: '', name: '', nameSuffix: '', isPrev: false, isNext: false
259
756
  }
260
757
  const rSp = {
261
- attrs: [], isVideoIframe: false, isIframeTypeBlockquote: false, hasImgCaption: false
758
+ attrs: [], isVideoIframe: false, isIframeTypeBlockquote: false, hasImgCaption: false, captionDecision: null
262
759
  }
263
760
 
264
- let n = 0
761
+ let n = startIndex
265
762
  while (n < tokens.length) {
266
763
  const token = tokens[n]
267
- const nextToken = tokens[n+1]
268
- let en = n
269
-
270
- rRange.start = n
271
- rRange.end = en
272
- let checkToken = false
273
- let checkTokenTagName = ''
274
- rCaption.mark = ''
275
- rCaption.name = ''
276
- rCaption.nameSuffix = ''
277
- rCaption.isPrev = false
278
- rCaption.isNext = false
279
-
280
- rSp.attrs.length = 0
281
- rSp.isVideoIframe = false
282
- rSp.isIframeTypeBlockquote = false
283
- rSp.hasImgCaption = false
284
-
285
- let cti = 0
286
- while (cti < checkTypes.length) {
287
- if (token.type === checkTypes[cti] + '_open') {
288
- // for n-1 token is line-break
289
- if (n > 1 && tokens[n-2].type === 'figure_open') {
290
- cti++; continue
291
- }
292
- checkToken = true
293
- checkTokenTagName = token.tag
294
- rCaption.name = checkTypes[cti]
295
- if (checkTypes[cti] === 'pre') {
296
- if (tokens[n+1].tag === 'code') rCaption.mark = 'pre-code'
297
- if (tokens[n+1].tag === 'samp') rCaption.mark = 'pre-samp'
298
- rCaption.name = rCaption.mark
299
- }
300
- while (en < tokens.length) {
301
- if(tokens[en].type === checkTokenTagName + '_close') {
302
- break
303
- }
304
- en++
305
- }
306
- rRange.end = en
307
- checkCaption(tokens, n, en, rCaption, fNum, rSp, opt, TokenConstructor)
308
- if (rCaption.isPrev || rCaption.isNext) {
309
- wrapWithFigure(tokens, rRange, checkTokenTagName, rCaption, false, rSp, opt, TokenConstructor)
310
- }
311
- break
312
- }
764
+ const containerType = getNestedContainerType(token)
313
765
 
314
- if (token.type === 'fence') {
315
- if (token.tag === 'code' && token.block) {
316
- checkToken = true
317
- let isSamp = false
318
- if (sampLangReg.test(token.info)) {
319
- token.tag = 'samp'
320
- isSamp = true
321
- }
322
- if (isSamp) {
323
- checkTokenTagName = 'pre-samp'
324
- rCaption.name = 'pre-samp'
325
- } else {
326
- checkTokenTagName = 'pre-code'
327
- rCaption.name = 'pre-code'
328
- }
329
- checkCaption(tokens, n, en, rCaption, fNum, rSp, opt, TokenConstructor)
330
- if (rCaption.isPrev || rCaption.isNext) {
331
- wrapWithFigure(tokens, rRange, checkTokenTagName, rCaption, false, rSp, opt, TokenConstructor)
332
- break
333
- }
334
- }
335
- break
336
- }
337
- cti++
338
- }
339
-
340
- if (token.type === 'html_block') {
341
- let ctj = 0
342
- let hasTag
343
- while (ctj < htmlTags.length) {
344
- if (htmlTags[ctj] === 'div') {
345
- // for vimeo
346
- hasTag = token.content.match(getHtmlReg('div'))
347
- htmlTags[ctj] = 'iframe'
348
- rSp.isVideoIframe = true
349
- } else {
350
- hasTag = token.content.match(getHtmlReg(htmlTags[ctj]))
351
- }
352
- const blueskyContMatch = token.content.match(blueskyEmbedReg)
353
- if (!(hasTag || (blueskyContMatch && htmlTags[ctj] === 'blockquote'))) {
354
- ctj++
355
- continue
356
- }
357
- if (hasTag) {
358
- if ((hasTag[2] && hasTag[3] !== '\n') || (hasTag[1] !== '\n' && hasTag[2] === undefined)) {
359
- token.content += '\n'
360
- }
361
- } else if (blueskyContMatch) {
362
- let addedCont = ''
363
- const tokensChildren = tokens
364
- const tokensLength = tokensChildren.length
365
- let j = n + 1
366
- let hasEndBlockquote = true
367
- while (j < tokensLength) {
368
- const nextToken = tokens[j]
369
- if (nextToken.type === 'inline' && endBlockquoteScriptReg.test(nextToken.content)) {
370
- addedCont += nextToken.content + '\n'
371
- if (tokens[j + 1] && tokens[j + 1].type === 'paragraph_close') {
372
- tokens.splice(j + 1, 1)
373
- }
374
- nextToken.content = ''
375
- nextToken.children.forEach((child) => {
376
- child.content = ''
377
- })
378
- break
379
- }
380
- if (nextToken.type === 'paragraph_open') {
381
- addedCont += '\n'
382
- tokens.splice(j, 1)
383
- continue
384
- }
385
- j++
386
- }
387
- token.content += addedCont
388
- if (!hasEndBlockquote) {
389
- ctj++
390
- continue
391
- }
392
- }
766
+ if (containerType && containerType !== 'blockquote') {
767
+ const closeIndex = figureWithCaptionCore(tokens, opt, fNum, figureNumberState, fallbackLabelState, TokenConstructor, containerType, n + 1)
768
+ n = (typeof closeIndex === 'number' ? closeIndex : n) + 1
769
+ continue
770
+ }
393
771
 
394
- checkTokenTagName = htmlTags[ctj]
395
- rCaption.name = htmlTags[ctj]
396
- checkToken = true
397
- if (checkTokenTagName === 'blockquote') {
398
- const isIframeTypeBlockquote = token.content.match(classNameReg)
399
- if(isIframeTypeBlockquote) {
400
- rSp.isIframeTypeBlockquote = true
401
- } else {
402
- ctj++
403
- continue
404
- }
405
- }
406
- break
407
- }
408
- if (!checkToken) {n++; continue;}
409
- if (checkTokenTagName === 'iframe') {
410
- if(videoIframeReg.test(token.content)) {
411
- rSp.isVideoIframe = true
412
- }
413
- }
414
772
 
415
- checkCaption(tokens, n, en, rCaption, fNum, rSp, opt, TokenConstructor)
416
- if (rCaption.isPrev || rCaption.isNext) {
417
- wrapWithFigure(tokens, rRange, checkTokenTagName, rCaption, false, rSp, opt, TokenConstructor)
418
- n = en + 2
419
- } else if ((opt.iframeWithoutCaption && (checkTokenTagName === 'iframe')) ||
420
- (opt.videoWithoutCaption && (checkTokenTagName === 'video')) ||
421
- (opt.iframeTypeBlockquoteWithoutCaption && (checkTokenTagName === 'blockquote'))) {
422
- wrapWithFigure(tokens, rRange, checkTokenTagName, rCaption, false, rSp, opt, TokenConstructor)
423
- n = en + 2
773
+ if (parentType && token.type === `${parentType}_close`) {
774
+ return n
775
+ }
776
+
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)
787
+
788
+ if (!detection) {
789
+ if (containerType === 'blockquote') {
790
+ const closeIndex = figureWithCaptionCore(tokens, opt, fNum, figureNumberState, fallbackLabelState, TokenConstructor, containerType, n + 1)
791
+ n = (typeof closeIndex === 'number' ? closeIndex : n) + 1
792
+ } else {
793
+ n++
424
794
  }
795
+ continue
425
796
  }
426
797
 
427
- if (token.type === 'paragraph_open' && nextToken.type === 'inline' && nextToken.children[0].type === 'image') {
428
- let ntChildTokenIndex = 1
429
- let imageNum = 1
430
- let isMultipleImagesHorizontal = true
431
- let isMultipleImagesVertical = true
432
- checkToken = true
433
- rCaption.name = 'img'
434
- const children = nextToken.children
435
- const childrenLength = children.length
436
- while (ntChildTokenIndex < childrenLength) {
437
- const ntChildToken = children[ntChildTokenIndex]
438
- if (ntChildTokenIndex === childrenLength - 1) {
439
- let imageAttrs = ntChildToken.content.match(imageAttrsReg)
440
- if(ntChildToken.type === 'text' && imageAttrs) {
441
- imageAttrs = imageAttrs[1].split(/ +/)
442
- let iai = 0
443
- const attrsLength = imageAttrs.length
444
- while (iai < attrsLength) {
445
- if (classAttrReg.test(imageAttrs[iai])) {
446
- imageAttrs[iai] = imageAttrs[iai].replace(classAttrReg, "class=")
447
- }
448
- if (idAttrReg.test(imageAttrs[iai])) {
449
- imageAttrs[iai] = imageAttrs[iai].replace(idAttrReg, "id=")
450
- }
451
- let imageAttr = imageAttrs[iai].match(attrParseReg)
452
- if (!imageAttr || !imageAttr[1]) {
453
- iai++
454
- continue
455
- }
456
- rSp.attrs.push([imageAttr[1], imageAttr[2]])
457
- iai++
458
- }
459
- break
460
- }
461
- }
798
+ rRange.end = detection.en
462
799
 
463
- if (!opt.multipleImages) {
464
- checkToken = false
465
- break
466
- }
467
- if (ntChildToken.type === 'image') {
468
- imageNum += 1
469
- } else if (ntChildToken.type === 'text' && whitespaceReg.test(ntChildToken.content)) {
470
- isMultipleImagesVertical = false
471
- if (isMultipleImagesVertical) {
472
- isMultipleImagesHorizontal = false
473
- }
474
- } else if (ntChildToken.type === 'softbreak') {
475
- isMultipleImagesHorizontal = false
476
- if (isMultipleImagesHorizontal) {
477
- isMultipleImagesVertical = false
478
- }
479
- } else {
480
- checkToken = false
481
- break
482
- }
483
- ntChildTokenIndex++
484
- }
485
- if (checkToken && imageNum > 1 && opt.multipleImages) {
486
- if (isMultipleImagesHorizontal) {
487
- rCaption.nameSuffix = '-horizontal'
488
- } else if (isMultipleImagesVertical) {
489
- rCaption.nameSuffix = '-vertical'
490
- } else {
491
- rCaption.nameSuffix = '-multiple'
492
- }
493
- ntChildTokenIndex = 0
494
- while (ntChildTokenIndex < childrenLength) {
495
- const ccToken = children[ntChildTokenIndex]
496
- if (ccToken.type === 'text' && whitespaceReg.test(ccToken.content)) {
497
- ccToken.content = ''
498
- }
499
- ntChildTokenIndex++
500
- }
800
+ rSp.figureClassName = resolveFigureClassName(detection.tagName, rCaption, rSp, opt)
801
+ checkCaption(tokens, rRange.start, rRange.end, rCaption, fNum, rSp, opt, TokenConstructor)
802
+ applyCaptionDrivenFigureClass(rCaption, rSp, opt)
803
+
804
+ let hasCaption = rCaption.isPrev || rCaption.isNext
805
+ let pendingAutoCaption = ''
806
+ if (!hasCaption && detection.type === 'image' && opt.autoCaptionDetection) {
807
+ pendingAutoCaption = getAutoCaptionFromImage(detection.imageToken, opt, fallbackLabelState)
808
+ if (pendingAutoCaption) {
809
+ hasCaption = true
501
810
  }
502
- en = n + 2
503
- rRange.end = en
504
- checkTokenTagName = 'img'
505
- nextToken.children[0].type = 'image'
811
+ }
506
812
 
507
- if (opt.imgAltCaption) setAltToLabel({ tokens, Token: TokenConstructor }, n)
508
- if (opt.imgTitleCaption) setTitleToLabel({ tokens, Token: TokenConstructor }, n)
509
- checkCaption(tokens, n, en, rCaption, fNum, rSp, opt, TokenConstructor)
813
+ if (detection.canWrap === false) {
814
+ let nextIndex = rRange.end + 1
815
+ if (containerType === 'blockquote') {
816
+ const closeIndex = figureWithCaptionCore(tokens, opt, fNum, figureNumberState, fallbackLabelState, TokenConstructor, containerType, rRange.start + 1)
817
+ nextIndex = Math.max(nextIndex, (typeof closeIndex === 'number' ? closeIndex : rRange.end) + 1)
818
+ }
819
+ n = nextIndex
820
+ continue
821
+ }
510
822
 
823
+ let shouldWrap = false
824
+ if (detection.type === 'html') {
825
+ shouldWrap = detection.canWrap !== false && (hasCaption || detection.wrapWithoutCaption)
826
+ } else if (detection.type === 'image') {
827
+ shouldWrap = detection.canWrap !== false && (hasCaption || detection.wrapWithoutCaption)
511
828
  if (parentType === 'list_item' || isInListItem(tokens, n)) {
512
829
  const isInTightList = token.hidden === true
513
830
  if (isInTightList) {
514
- checkToken = false
515
- } else {
516
- if (!opt.oneImageWithoutCaption && !rCaption.isPrev && !rCaption.isNext) {
517
- checkToken = false
518
- }
831
+ shouldWrap = false
832
+ } else if (!hasCaption && !opt.oneImageWithoutCaption) {
833
+ shouldWrap = false
519
834
  }
520
835
  }
836
+ } else {
837
+ shouldWrap = detection.canWrap !== false && hasCaption
838
+ }
521
839
 
522
- if (checkToken && (opt.oneImageWithoutCaption || rCaption.isPrev || rCaption.isNext)) {
523
- if (rCaption.nameSuffix) checkTokenTagName += rCaption.nameSuffix
524
- wrapWithFigure(tokens, rRange, checkTokenTagName, rCaption, true, rSp, opt, TokenConstructor)
840
+ if (shouldWrap) {
841
+ if (pendingAutoCaption) {
842
+ const captionTokens = createAutoCaptionParagraph(pendingAutoCaption, TokenConstructor)
843
+ tokens.splice(rRange.start, 0, ...captionTokens)
844
+ const insertedLength = captionTokens.length
845
+ rRange.start += insertedLength
846
+ rRange.end += insertedLength
847
+ n += insertedLength
848
+ checkCaption(tokens, rRange.start, rRange.end, rCaption, fNum, rSp, opt, TokenConstructor)
849
+ applyCaptionDrivenFigureClass(rCaption, rSp, opt)
525
850
  }
851
+ ensureAutoFigureNumbering(tokens, rRange, rCaption, figureNumberState, opt)
852
+ wrapWithFigure(tokens, rRange, detection.tagName, rCaption, detection.replaceInsteadOfWrap, rSp, opt, TokenConstructor)
526
853
  }
527
854
 
528
- if (!checkToken || !rCaption.name) {n++; continue;}
529
-
530
- n = rRange.start
531
- en = rRange.end
532
- if (rCaption.isPrev) {
533
- changePrevCaptionPosition(tokens, n, rCaption, opt)
534
- n = en + 1
535
- continue
855
+ let nextIndex
856
+ if (!rCaption.name) {
857
+ nextIndex = n + 1
858
+ } else {
859
+ const en = rRange.end
860
+ if (rCaption.isPrev) {
861
+ changePrevCaptionPosition(tokens, rRange.start, rCaption, opt)
862
+ nextIndex = en + 1
863
+ } else if (rCaption.isNext) {
864
+ changeNextCaptionPosition(tokens, en, rCaption)
865
+ nextIndex = en + 4
866
+ } else {
867
+ nextIndex = en + 1
868
+ }
536
869
  }
537
- if (rCaption.isNext) {
538
- changeNextCaptionPosition(tokens, en, rCaption)
539
- n = en + 4
540
- continue
870
+
871
+ if (containerType === 'blockquote') {
872
+ const closeIndex = figureWithCaptionCore(tokens, opt, fNum, figureNumberState, fallbackLabelState, TokenConstructor, containerType, rRange.start + 1)
873
+ const fallbackIndex = rCaption.name ? rRange.end : n
874
+ nextIndex = Math.max(nextIndex, (typeof closeIndex === 'number' ? closeIndex : fallbackIndex) + 1)
541
875
  }
542
- n = en + 1
876
+
877
+ n = nextIndex
543
878
  }
544
- return
879
+ return tokens.length
545
880
  }
546
881
 
547
882
  const isInListItem = (() => {
@@ -576,44 +911,53 @@ const isInListItem = (() => {
576
911
 
577
912
  const mditFigureWithPCaption = (md, option) => {
578
913
  let opt = {
914
+ // --- figure-wrapper behavior ---
579
915
  classPrefix: 'f',
580
916
  figureClassThatWrapsIframeTypeBlockquote: 'f-img',
917
+ figureClassThatWrapsSlides: 'f-slide',
581
918
  styleProcess : true,
919
+ oneImageWithoutCaption: false,
920
+ iframeWithoutCaption: false,
921
+ videoWithoutCaption: false,
922
+ audioWithoutCaption: false,
923
+ iframeTypeBlockquoteWithoutCaption: false,
924
+ multipleImages: true,
925
+ roleDocExample: false,
926
+ allIframeTypeFigureClassName: '', // e.g. 'f-embed' to force a single class for iframe-like embeds (recommended)
927
+
928
+ // --- automatic caption detection heuristics ---
929
+ // Applies only to the first image within an image-only paragraph (even when multipleImages is true).
930
+ // Priority: caption paragraphs (before/after) > alt text > title attribute; auto detection only runs when no paragraph caption exists.
931
+ autoCaptionDetection: true,
932
+ autoAltCaption: false, // allow alt text (when matching markReg.img or fallback) to build captions automatically
933
+ autoTitleCaption: false, // same as above but reads from the title attribute when alt isn't usable
934
+
935
+ // --- numbering controls ---
936
+ autoLabelNumber: false, // shorthand for enabling numbering for both img/table unless setLabelNumbers is provided explicitly
937
+ setLabelNumbers: [], // preferred; supports ['img'], ['table'], or both
938
+
939
+ // --- caption text formatting (delegated to p7d-markdown-it-p-captions) ---
582
940
  hasNumClass: false,
583
- scaleSuffix: false,
584
941
  dquoteFilename: false,
585
942
  strongFilename: false,
586
943
  bLabel: false,
587
944
  strongLabel: false,
588
945
  jointSpaceUseHalfWidth: false,
589
- oneImageWithoutCaption: false,
590
- iframeWithoutCaption: false,
591
- videoWithoutCaption: false,
592
- iframeTypeBlockquoteWithoutCaption: false,
593
946
  removeUnnumberedLabel: false,
594
947
  removeUnnumberedLabelExceptMarks: [],
595
948
  removeMarkNameInCaptionClass: false,
596
- multipleImages: true,
597
- imgAltCaption: false,
598
- setFigureNumber: false,
599
- imgTitleCaption: false,
600
- roleDocExample: false,
601
- allIframeTypeFigureClassName: '',
949
+ wrapCaptionBody: false,
602
950
  }
951
+ const hasExplicitSetLabelNumbers = option && Object.prototype.hasOwnProperty.call(option, 'setLabelNumbers')
603
952
  if (option) Object.assign(opt, option)
604
-
605
- if (opt.imgAltCaption || opt.imgTitleCaption) {
606
- opt.oneImageWithoutCaption = true
607
- opt.multipleImages = false
608
- if (opt.setFigureNumber) {
609
- opt.removeUnnumberedLabelExceptMarks = opt.removeUnnumberedLabelExceptMarks.filter(
610
- mark => mark !== 'img' && mark !== 'table'
611
- )
612
- }
613
- md.block.ruler.before('paragraph', 'img_attr_caption', (state) => {
614
- imgAttrToPCaption(state, state.line, opt)
615
- })
953
+ // Normalize option shorthands now so downstream logic works with a consistent { img, table } shape.
954
+ opt.setLabelNumbers = normalizeSetLabelNumbers(opt.setLabelNumbers)
955
+ if (opt.autoLabelNumber && !hasExplicitSetLabelNumbers) {
956
+ opt.setLabelNumbers.img = true
957
+ opt.setLabelNumbers.table = true
616
958
  }
959
+ // Precompute `.f-*-label` permutations so numbering lookup doesn't rebuild arrays per caption.
960
+ opt.labelClassLookup = buildLabelClassLookup(opt)
617
961
 
618
962
  //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.
619
963
  md.core.ruler.before('replacements', 'figure_with_caption', (state) => {