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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -1,27 +1,52 @@
1
1
  import {
2
+ analyzeCaptionParagraph,
2
3
  analyzeCaptionStart,
3
- buildLabelClassLookup,
4
+ applyCaptionParagraph,
4
5
  buildLabelPrefixMarkerRegFromMarkers,
6
+ canonicalizeCaptionNumberingMark,
7
+ createCaptionNumberingPolicy,
8
+ createCaptionNumberingRuntime,
5
9
  getGeneratedLabelDefaults,
10
+ normalizeAutoLabelNumberSets,
6
11
  normalizeLabelPrefixMarkers,
7
12
  setCaptionParagraph,
8
13
  getMarkRegStateForLanguages,
9
14
  stripLabelPrefixMarker,
10
15
  } from 'p7d-markdown-it-p-captions'
11
- import { detectHtmlFigureCandidate } from './embeds/detect.js'
16
+ import { applyHtmlFigureTransform, detectHtmlFigureCandidate } from './embeds/detect.js'
17
+ import {
18
+ createFigureCaptionCounterKeyResolverFromMarkRegState,
19
+ } from './caption-numbering/counter-series.js'
20
+ import {
21
+ createUnscopedNumberingContext,
22
+ formatFigureCaptionGeneratedNumberUnchecked,
23
+ getCaptionNumberingContext,
24
+ parseFigureCaptionExplicitNumberUnchecked,
25
+ } from './caption-numbering/number-codec.js'
26
+ import {
27
+ getFigureCaptionNumberingPolicyState,
28
+ normalizeFigureCaptionNumberingPolicy,
29
+ } from './caption-numbering/options.js'
30
+ import {
31
+ createFigureCaptionScopeRuntime,
32
+ getFigureCaptionScopeNestedContainerType,
33
+ updateFigureCaptionScopeFromHeading,
34
+ } from './caption-numbering/scope.js'
12
35
 
13
36
  const imageAttrsReg = /^ *\{(.*?)\} *$/
14
- const classAttrReg = /^\./
15
- const idAttrReg = /^#/
16
37
  const sampLangReg = /^ *(?:samp|shell|console)(?:(?= )|$)/
17
38
  const asciiLabelReg = /^[A-Za-z]/
18
39
  const attrNameReg = /^[^\s=]+$/
19
- const labelTrailingJointReg = /[.\u3002\uff0e::]\s*$/
40
+ const labelTrailingJointReg = /[.\u3002\uff0e::\u3000]\s*$/
41
+ const installedKey = Symbol.for('p7d-markdown-it-figure-with-p-caption.installed')
20
42
  const CHECK_TYPE_TOKEN_MAP = {
21
43
  table_open: 'table',
22
44
  pre_open: 'pre',
23
45
  blockquote_open: 'blockquote',
24
46
  }
47
+ const hasOwnOption = (option, name) => !!(
48
+ option && Object.prototype.hasOwnProperty.call(option, name)
49
+ )
25
50
  const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
26
51
  const normalizeLanguageCode = (value) => {
27
52
  if (value === null || value === undefined) return ''
@@ -31,11 +56,10 @@ const normalizeLanguageCode = (value) => {
31
56
  return separatorIndex === -1 ? normalized : normalized.slice(0, separatorIndex)
32
57
  }
33
58
  const appendAvailableLanguage = (target, lang, availableLanguages) => {
34
- if (!lang) return false
35
- if (availableLanguages.indexOf(lang) === -1) return false
36
- if (target.indexOf(lang) !== -1) return false
59
+ if (!lang) return
60
+ if (availableLanguages.indexOf(lang) === -1) return
61
+ if (target.indexOf(lang) !== -1) return
37
62
  target.push(lang)
38
- return true
39
63
  }
40
64
  const normalizePreferredLanguages = (value, availableLanguages) => {
41
65
  if (!Array.isArray(availableLanguages) || availableLanguages.length === 0) return []
@@ -100,31 +124,30 @@ const isHyphenFenceLine = (src, lineStart) => {
100
124
  if (index >= src.length || src.charCodeAt(index) !== 0x0a) return 0
101
125
  return hyphenCount
102
126
  }
103
- const skipLeadingFrontmatter = (src) => {
104
- if (typeof src !== 'string' || isHyphenFenceLine(src, 0) === 0) return src
127
+ const getBodyStartAfterLeadingFrontmatter = (src) => {
128
+ if (typeof src !== 'string' || isHyphenFenceLine(src, 0) === 0) return 0
105
129
  let lineStart = src.indexOf('\n')
106
- if (lineStart === -1) return src
130
+ if (lineStart === -1) return 0
107
131
  lineStart++
108
132
  while (lineStart < src.length) {
109
133
  if (isHyphenFenceLine(src, lineStart) > 0) {
110
134
  const nextLineStart = src.indexOf('\n', lineStart)
111
- if (nextLineStart === -1) return ''
112
- return src.slice(nextLineStart + 1)
135
+ return nextLineStart === -1 ? src.length : nextLineStart + 1
113
136
  }
114
137
  const nextLineStart = src.indexOf('\n', lineStart)
115
138
  if (nextLineStart === -1) break
116
139
  lineStart = nextLineStart + 1
117
140
  }
118
- return src
141
+ return 0
119
142
  }
120
143
  const detectDocumentPrimaryLanguage = (src, availableLanguages) => {
121
144
  if (!src || availableLanguages.indexOf('ja') === -1) return ''
122
- const body = skipLeadingFrontmatter(src)
123
- const limit = Math.min(body.length, 8192)
145
+ const bodyStart = getBodyStartAfterLeadingFrontmatter(src)
146
+ const limit = Math.min(src.length, bodyStart + 8192)
124
147
  let japaneseCount = 0
125
148
  let asciiAlphaCount = 0
126
- for (let i = 0; i < limit; i++) {
127
- const code = body.charCodeAt(i)
149
+ for (let i = bodyStart; i < limit; i++) {
150
+ const code = src.charCodeAt(i)
128
151
  if (isJapaneseCharCode(code)) {
129
152
  japaneseCount++
130
153
  continue
@@ -137,10 +160,6 @@ const detectDocumentPrimaryLanguage = (src, availableLanguages) => {
137
160
  if (asciiAlphaCount === 0) return 'ja'
138
161
  return japaneseCount * 2 >= asciiAlphaCount ? 'ja' : ''
139
162
  }
140
- const sourceMayNeedPreferredLanguages = (state) => {
141
- const src = state && typeof state.src === 'string' ? state.src : ''
142
- return src.indexOf('![') !== -1
143
- }
144
163
  const resolvePreferredLanguagesForState = (state, opt) => {
145
164
  const availableLanguages = (
146
165
  opt &&
@@ -149,10 +168,7 @@ const resolvePreferredLanguagesForState = (state, opt) => {
149
168
  ) ? opt.markRegState.languages : []
150
169
  if (availableLanguages.length === 0) return []
151
170
 
152
- const optionLanguages = opt && Array.isArray(opt.normalizedOptionLanguages)
153
- ? opt.normalizedOptionLanguages
154
- : []
155
- const baseLanguages = optionLanguages.length > 0 ? optionLanguages : availableLanguages
171
+ const baseLanguages = availableLanguages
156
172
  const env = state && state.env ? state.env : null
157
173
 
158
174
  const envLocale = normalizeLanguageCode(env && env.locale)
@@ -190,18 +206,15 @@ const resolvePreferredLanguagesForState = (state, opt) => {
190
206
  }
191
207
  const needsPreferredLanguagesResolution = (opt) => {
192
208
  if (!opt || !opt.markRegState || !Array.isArray(opt.markRegState.languages)) return false
209
+ if (!opt.autoCaptionDetection) return false
193
210
  if (opt.markRegState.languages.length <= 1) return false
194
211
  return opt.autoAltCaption === true || opt.autoTitleCaption === true
195
212
  }
196
213
  const normalizeOptionalClassName = (value) => {
197
214
  if (value === null || value === undefined) return ''
198
- const normalized = String(value).trim()
199
- return normalized || ''
200
- }
201
- const buildClassPrefix = (value) => {
202
- const normalized = normalizeOptionalClassName(value)
203
- return normalized ? normalized + '-' : ''
215
+ return String(value).trim()
204
216
  }
217
+ const buildClassPrefix = (normalized) => normalized ? normalized + '-' : ''
205
218
  const normalizeClassOptionWithFallback = (value, fallbackValue) => {
206
219
  const normalized = normalizeOptionalClassName(value)
207
220
  return normalized || fallbackValue
@@ -283,10 +296,11 @@ const parseImageAttrs = (raw) => {
283
296
  for (let i = 0; i < parts.length; i++) {
284
297
  let entry = parts[i]
285
298
  if (!entry) continue
286
- if (classAttrReg.test(entry)) {
287
- entry = entry.replace(classAttrReg, 'class=')
288
- } else if (idAttrReg.test(entry)) {
289
- entry = entry.replace(idAttrReg, 'id=')
299
+ const firstCode = entry.charCodeAt(0)
300
+ if (firstCode === 0x2e) {
301
+ entry = 'class=' + entry.slice(1)
302
+ } else if (firstCode === 0x23) {
303
+ entry = 'id=' + entry.slice(1)
290
304
  }
291
305
  const equalIndex = entry.indexOf('=')
292
306
  if (equalIndex === -1) {
@@ -301,22 +315,50 @@ const parseImageAttrs = (raw) => {
301
315
  return attrs
302
316
  }
303
317
 
304
- const normalizeAutoLabelNumberSets = (value) => {
305
- const normalized = { img: false, table: false }
306
- if (!value) return normalized
307
- if (Array.isArray(value)) {
308
- for (const entry of value) {
309
- if (normalized.hasOwnProperty(entry)) normalized[entry] = true
310
- }
311
- return normalized
312
- }
313
- return normalized
314
- }
315
-
316
- const shouldApplyLabelNumbering = (captionType, opt) => {
317
- const setting = opt.autoLabelNumberSets
318
- if (!setting) return false
319
- return !!setting[captionType]
318
+ const getEnabledCaptionNumberingMarks = (sets, opt) => {
319
+ if (!Array.isArray(sets) || sets.length === 0) return []
320
+ const exceptMarks = opt && Array.isArray(opt.removeUnnumberedLabelExceptMarks)
321
+ ? opt.removeUnnumberedLabelExceptMarks
322
+ : []
323
+ if (!opt || !opt.removeUnnumberedLabel) return sets
324
+ return sets.filter(mark => exceptMarks.indexOf(mark) !== -1)
325
+ }
326
+
327
+ const createFigureCaptionNumberingPolicy = (enabledMarks, advancedPolicy, markRegState) => {
328
+ if (enabledMarks.length === 0) return null
329
+ const resolveCounterKey = createFigureCaptionCounterKeyResolverFromMarkRegState(markRegState)
330
+ const getCounterKey = ({ captionDecision }) => resolveCounterKey(captionDecision)
331
+ if (!advancedPolicy) {
332
+ return createCaptionNumberingPolicy({
333
+ enabledMarks,
334
+ explicitCounter: 'max',
335
+ generatedNumberHasNumClass: false,
336
+ getCounterKey,
337
+ })
338
+ }
339
+ return createCaptionNumberingPolicy({
340
+ enabledMarks,
341
+ explicitCounter: 'max',
342
+ generatedNumberHasNumClass: false,
343
+ getCounterKey,
344
+ getSequenceKey({ captionContext }) {
345
+ return getCaptionNumberingContext(captionContext).sequenceKey
346
+ },
347
+ // p-captions resolves getSequenceKey first, so the following callbacks can
348
+ // reuse that already-branded context without a second WeakSet lookup.
349
+ parseExplicitNumber({ number, captionContext }) {
350
+ return parseFigureCaptionExplicitNumberUnchecked(
351
+ number,
352
+ captionContext && captionContext.numbering,
353
+ )
354
+ },
355
+ formatGeneratedNumber({ sequence, captionContext }) {
356
+ return formatFigureCaptionGeneratedNumberUnchecked(
357
+ sequence,
358
+ captionContext && captionContext.numbering,
359
+ )
360
+ },
361
+ })
320
362
  }
321
363
 
322
364
  const isOnlySpacesText = (token) => {
@@ -418,7 +460,24 @@ const buildCaptionWithFallback = (text, fallbackOption, mark, markRegState, pref
418
460
  return label + getFallbackStringLabelJoint(label) + trimmedText
419
461
  }
420
462
 
463
+ const resolveCaptionPreferredLanguages = (captionState, opt) => {
464
+ const renderState = captionState.preferredLanguageState
465
+ if (!renderState) return captionState.preferredLanguages
466
+ captionState.preferredLanguages = resolvePreferredLanguagesForState(renderState, opt)
467
+ captionState.preferredLanguageState = null
468
+ return captionState.preferredLanguages
469
+ }
470
+
421
471
  const validateFallbackCaptionLabelOption = (optionName, fallbackOption, markRegState) => {
472
+ if (
473
+ fallbackOption !== undefined &&
474
+ fallbackOption !== null &&
475
+ fallbackOption !== false &&
476
+ fallbackOption !== true &&
477
+ typeof fallbackOption !== 'string'
478
+ ) {
479
+ throw new TypeError(`${optionName} must be false, true, a string label, or null.`)
480
+ }
422
481
  if (typeof fallbackOption !== 'string') return
423
482
  const sampleCaption = buildCaptionWithFallback('caption', fallbackOption, 'img', markRegState, null)
424
483
  const analysis = analyzeCaptionStart(sampleCaption, {
@@ -444,121 +503,6 @@ const createAutoCaptionParagraph = (captionText, TokenConstructor) => {
444
503
  return [paragraphOpen, inlineToken, paragraphClose]
445
504
  }
446
505
 
447
- const getCaptionInlineToken = (tokens, range, caption) => {
448
- if (caption.isPrev) {
449
- const inlineIndex = range.start - 2
450
- if (inlineIndex >= 0) return tokens[inlineIndex]
451
- } else if (caption.isNext) {
452
- return tokens[range.end + 2]
453
- }
454
- return null
455
- }
456
-
457
- const hasClassName = (classAttr, className) => {
458
- if (!classAttr || !className) return false
459
- let index = 0
460
- while (index < classAttr.length) {
461
- index = classAttr.indexOf(className, index)
462
- if (index === -1) return false
463
- const end = index + className.length
464
- const beforeBoundary = index === 0 || classAttr.charCodeAt(index - 1) <= 0x20
465
- const afterBoundary = end >= classAttr.length || classAttr.charCodeAt(end) <= 0x20
466
- if (beforeBoundary && afterBoundary) return true
467
- index = end
468
- }
469
- return false
470
- }
471
-
472
- const hasAnyClassName = (classAttr, classNames) => {
473
- for (let i = 0; i < classNames.length; i++) {
474
- if (hasClassName(classAttr, classNames[i])) return true
475
- }
476
- return false
477
- }
478
-
479
- const getInlineLabelTextToken = (inlineToken, type, opt) => {
480
- if (!inlineToken || !inlineToken.children) return null
481
- const children = inlineToken.children
482
- const classNames = opt.labelClassLookup[type] || opt.labelClassLookup.default
483
- for (let i = 0; i < children.length; i++) {
484
- const child = children[i]
485
- if (!child || !child.attrs) continue
486
- const classAttr = getTokenAttr(child, 'class')
487
- if (!classAttr) continue
488
- if (!hasAnyClassName(classAttr, classNames)) continue
489
- const textToken = children[i + 1]
490
- if (textToken && textToken.type === 'text') {
491
- return textToken
492
- }
493
- }
494
- return null
495
- }
496
-
497
- const updateInlineTokenContent = (inlineToken, originalText, newText) => {
498
- if (!inlineToken || typeof inlineToken.content !== 'string') return
499
- if (!originalText) return
500
- const index = inlineToken.content.indexOf(originalText)
501
- if (index === -1) return
502
- inlineToken.content =
503
- inlineToken.content.slice(0, index) +
504
- newText +
505
- inlineToken.content.slice(index + originalText.length)
506
- }
507
-
508
- const parseTrailingPositiveInteger = (text) => {
509
- if (typeof text !== 'string' || text.length === 0) return null
510
- let end = text.length - 1
511
- while (end >= 0 && text.charCodeAt(end) === 0x20) end--
512
- if (end < 0) return null
513
- const lastCode = text.charCodeAt(end)
514
- if (lastCode < 0x30 || lastCode > 0x39) return null
515
- let start = end
516
- while (start >= 0) {
517
- const code = text.charCodeAt(start)
518
- if (code < 0x30 || code > 0x39) break
519
- start--
520
- }
521
- let value = 0
522
- let digitBase = 1
523
- for (let i = end; i > start; i--) {
524
- value += (text.charCodeAt(i) - 0x30) * digitBase
525
- digitBase *= 10
526
- }
527
- return value
528
- }
529
-
530
- const ensureAutoFigureNumbering = (tokens, range, caption, figureNumberState, opt) => {
531
- const captionType = caption.name === 'img' ? 'img' : (caption.name === 'table' ? 'table' : '')
532
- if (!captionType) return
533
- if (!shouldApplyLabelNumbering(captionType, opt)) return
534
- const inlineToken = getCaptionInlineToken(tokens, range, caption)
535
- if (!inlineToken) return
536
- const labelTextToken = getInlineLabelTextToken(inlineToken, captionType, opt)
537
- if (!labelTextToken || typeof labelTextToken.content !== 'string') return
538
- const originalText = labelTextToken.content
539
- let end = originalText.length - 1
540
- while (end >= 0 && originalText.charCodeAt(end) === 0x20) end--
541
- if (end >= 0) {
542
- const code = originalText.charCodeAt(end)
543
- if (code >= 0x30 && code <= 0x39) {
544
- const explicitValue = parseTrailingPositiveInteger(originalText)
545
- if (explicitValue !== null) {
546
- if (explicitValue > (figureNumberState[captionType] || 0)) {
547
- figureNumberState[captionType] = explicitValue
548
- }
549
- return
550
- }
551
- }
552
- }
553
- figureNumberState[captionType] = (figureNumberState[captionType] || 0) + 1
554
- const baseLabel = originalText.trim()
555
- if (!baseLabel) return
556
- const joint = asciiLabelReg.test(baseLabel) ? ' ' : ''
557
- const newLabelText = baseLabel + joint + figureNumberState[captionType]
558
- labelTextToken.content = newLabelText
559
- updateInlineTokenContent(inlineToken, originalText, newLabelText)
560
- }
561
-
562
506
  const matchAutoCaptionText = (text, opt, preferredMark = 'img') => {
563
507
  if (!text || !opt || !opt.markRegState) return ''
564
508
  const trimmed = text.trim()
@@ -571,89 +515,116 @@ const matchAutoCaptionText = (text, opt, preferredMark = 'img') => {
571
515
  return ''
572
516
  }
573
517
 
574
- const getAutoCaptionFromImage = (imageToken, opt) => {
575
- if (!opt.autoCaptionDetection) return ''
576
- if (!opt.autoAltCaption && !opt.autoTitleCaption && !(opt.markRegState && opt.markRegState.markReg && opt.markRegState.markReg.img)) return ''
518
+ const getAutoCaptionFromImage = (imageToken, opt, captionState) => {
519
+ if (!opt.autoAltCaption && !opt.autoTitleCaption && !(opt.markRegState && opt.markRegState.markReg && opt.markRegState.markReg.img)) return null
577
520
 
578
521
  const altText = getImageAltText(imageToken)
579
522
  let caption = matchAutoCaptionText(altText, opt)
580
523
  if (caption) {
581
- clearImageAltAttr(imageToken)
582
- return caption
524
+ return { text: caption, source: 'alt' }
583
525
  }
584
- if (!caption && opt.autoAltCaption) {
585
- const altForFallback = altText || ''
586
- const fallbackCaption = buildCaptionWithFallback(altForFallback, opt.autoAltCaption, 'img', opt.markRegState, opt.preferredLanguages)
587
- if (fallbackCaption && imageToken) {
588
- clearImageAltAttr(imageToken)
589
- }
590
- caption = fallbackCaption
526
+ if (opt.autoAltCaption) {
527
+ const preferredLanguages = opt.autoAltCaption === true
528
+ ? resolveCaptionPreferredLanguages(captionState, opt)
529
+ : captionState.preferredLanguages
530
+ caption = buildCaptionWithFallback(altText, opt.autoAltCaption, 'img', opt.markRegState, preferredLanguages)
591
531
  }
592
- if (caption) return caption
532
+ if (caption) return { text: caption, source: 'alt' }
593
533
 
594
534
  const titleText = getImageTitleText(imageToken)
595
535
  caption = matchAutoCaptionText(titleText, opt)
596
536
  if (caption) {
597
- clearImageTitleAttr(imageToken)
598
- return caption
537
+ return { text: caption, source: 'title' }
599
538
  }
600
- if (!caption && opt.autoTitleCaption) {
601
- const titleForFallback = titleText || ''
602
- const fallbackCaption = buildCaptionWithFallback(titleForFallback, opt.autoTitleCaption, 'img', opt.markRegState, opt.preferredLanguages)
603
- if (fallbackCaption && imageToken) {
604
- clearImageTitleAttr(imageToken)
605
- }
606
- caption = fallbackCaption
539
+ if (opt.autoTitleCaption) {
540
+ const preferredLanguages = opt.autoTitleCaption === true
541
+ ? resolveCaptionPreferredLanguages(captionState, opt)
542
+ : captionState.preferredLanguages
543
+ caption = buildCaptionWithFallback(titleText, opt.autoTitleCaption, 'img', opt.markRegState, preferredLanguages)
544
+ }
545
+ return caption ? { text: caption, source: 'title' } : null
546
+ }
547
+
548
+ const consumeAutoCaptionSource = (imageToken, autoCaption) => {
549
+ if (!imageToken || !autoCaption) return
550
+ if (autoCaption.source === 'alt') {
551
+ clearImageAltAttr(imageToken)
552
+ } else if (autoCaption.source === 'title') {
553
+ clearImageTitleAttr(imageToken)
607
554
  }
608
- return caption
555
+ }
556
+
557
+ const setFigureCaptionParagraph = (index, captionState, caption, sp, opt) => {
558
+ const needsCaptionDrivenClassBeforeApply = !!(
559
+ opt.labelClassFollowsFigure &&
560
+ caption.name === 'iframe' &&
561
+ !opt.allIframeTypeFigureClassName
562
+ )
563
+ if (!needsCaptionDrivenClassBeforeApply) {
564
+ return setCaptionParagraph(index, captionState, caption, captionState.numberingRuntime, sp, opt)
565
+ }
566
+ const decision = analyzeCaptionParagraph(index, captionState, {
567
+ captionName: caption.name,
568
+ isIframeTypeBlockquote: sp.isIframeTypeBlockquote,
569
+ isVideoIframe: sp.isVideoIframe,
570
+ }, opt)
571
+ if (!decision) return false
572
+ // Figure-following label classes need the final caption-driven wrapper class
573
+ // before p-captions constructs the label/body spans.
574
+ applyCaptionDrivenFigureClass(caption, sp, opt, decision)
575
+ return applyCaptionParagraph(
576
+ decision,
577
+ captionState,
578
+ sp,
579
+ captionState.numberingRuntime,
580
+ opt,
581
+ )
609
582
  }
610
583
 
611
584
  const checkPrevCaption = (tokens, n, caption, sp, opt, captionState) => {
612
- if(n < 3) return caption
613
- const captionStartToken = tokens[n-3]
614
- const captionInlineToken = tokens[n-2]
615
- const captionEndToken = tokens[n-1]
585
+ if (n < 3) return
586
+ const captionStartToken = tokens[n - 3]
587
+ const captionInlineToken = tokens[n - 2]
588
+ const captionEndToken = tokens[n - 1]
616
589
  if (captionStartToken === undefined || captionEndToken === undefined) return
617
590
  if (captionStartToken.type !== 'paragraph_open' || captionEndToken.type !== 'paragraph_close') return
618
- setCaptionParagraph(n-3, captionState, caption, null, sp, opt)
591
+ setFigureCaptionParagraph(n - 3, captionState, caption, sp, opt)
619
592
  const captionName = sp && sp.captionDecision ? sp.captionDecision.mark : ''
620
- if(!captionName) {
593
+ if (!captionName) {
621
594
  if (opt.labelPrefixMarkerWithoutLabelPrevReg) {
622
- const markerMatch = getLabelPrefixMarkerMatch(captionInlineToken, opt.labelPrefixMarkerWithoutLabelPrevReg)
623
- if (markerMatch) {
624
- stripLabelPrefixMarker(captionInlineToken, markerMatch)
625
- caption.isPrev = true
626
- }
595
+ const markerMatch = getLabelPrefixMarkerMatch(captionInlineToken, opt.labelPrefixMarkerWithoutLabelPrevReg)
596
+ if (markerMatch) {
597
+ stripLabelPrefixMarker(captionInlineToken, markerMatch)
598
+ caption.isPrev = true
599
+ }
627
600
  }
628
601
  return
629
602
  }
630
603
  caption.name = captionName
631
604
  caption.isPrev = true
632
- return
633
605
  }
634
606
 
635
607
  const checkNextCaption = (tokens, en, caption, sp, opt, captionState) => {
636
- if (en + 2 > tokens.length) return
637
- const captionStartToken = tokens[en+1]
638
- const captionInlineToken = tokens[en+2]
639
- const captionEndToken = tokens[en+3]
608
+ if (en + 3 >= tokens.length) return
609
+ const captionStartToken = tokens[en + 1]
610
+ const captionInlineToken = tokens[en + 2]
611
+ const captionEndToken = tokens[en + 3]
640
612
  if (captionStartToken === undefined || captionEndToken === undefined) return
641
613
  if (captionStartToken.type !== 'paragraph_open' || captionEndToken.type !== 'paragraph_close') return
642
- setCaptionParagraph(en+1, captionState, caption, null, sp, opt)
614
+ setFigureCaptionParagraph(en + 1, captionState, caption, sp, opt)
643
615
  const captionName = sp && sp.captionDecision ? sp.captionDecision.mark : ''
644
- if(!captionName) {
616
+ if (!captionName) {
645
617
  if (opt.labelPrefixMarkerWithoutLabelNextReg) {
646
- const markerMatch = getLabelPrefixMarkerMatch(captionInlineToken, opt.labelPrefixMarkerWithoutLabelNextReg)
647
- if (markerMatch) {
648
- stripLabelPrefixMarker(captionInlineToken, markerMatch)
649
- caption.isNext = true
650
- }
618
+ const markerMatch = getLabelPrefixMarkerMatch(captionInlineToken, opt.labelPrefixMarkerWithoutLabelNextReg)
619
+ if (markerMatch) {
620
+ stripLabelPrefixMarker(captionInlineToken, markerMatch)
621
+ caption.isNext = true
622
+ }
651
623
  }
652
624
  return
653
625
  }
654
626
  caption.name = captionName
655
627
  caption.isNext = true
656
- return
657
628
  }
658
629
 
659
630
  const cleanCaptionTokenAttrs = (token, captionName, opt) => {
@@ -701,24 +672,26 @@ const resolveFigureClassName = (checkTokenTagName, sp, opt) => {
701
672
  return className
702
673
  }
703
674
 
704
- const applyCaptionDrivenFigureClass = (caption, sp, opt) => {
675
+ const applyCaptionDrivenFigureClass = (caption, sp, opt, decision = sp && sp.captionDecision) => {
705
676
  if (!sp) return
706
677
  const figureClassForSlides = opt.figureClassThatWrapsSlides
707
678
  if (!figureClassForSlides) return
708
- const detectedMark = (sp.captionDecision && sp.captionDecision.mark) || (caption && caption.name) || ''
679
+ const detectedMark = (decision && decision.mark) || (caption && caption.name) || ''
709
680
  if (detectedMark !== 'slide') return
710
681
  if (opt.allIframeTypeFigureClassName && sp.figureClassName === opt.allIframeTypeFigureClassName) return
682
+ if (sp.figureClassName === figureClassForSlides) return
711
683
  sp.figureClassName = figureClassForSlides
712
684
  }
713
685
 
714
-
715
- const changePrevCaptionPosition = (tokens, n, caption, opt) => {
716
- const captionStartToken = tokens[n-3]
717
- const captionInlineToken = tokens[n-2]
718
- const captionEndToken = tokens[n-1]
719
- const figureBaseLevel = getTokenLevel(tokens[n])
720
-
721
- cleanCaptionTokenAttrs(captionStartToken, caption.name, opt)
686
+ const convertCaptionTokens = (
687
+ captionStartToken,
688
+ captionInlineToken,
689
+ captionEndToken,
690
+ figureBaseLevel,
691
+ captionName,
692
+ opt,
693
+ ) => {
694
+ cleanCaptionTokenAttrs(captionStartToken, captionName, opt)
722
695
  captionStartToken.type = 'figcaption_open'
723
696
  captionStartToken.tag = 'figcaption'
724
697
  captionStartToken.block = true
@@ -728,33 +701,61 @@ const changePrevCaptionPosition = (tokens, n, caption, opt) => {
728
701
  captionEndToken.tag = 'figcaption'
729
702
  captionEndToken.block = true
730
703
  captionEndToken.level = figureBaseLevel + 1
731
- tokens.splice(n + 2, 0, captionStartToken, captionInlineToken, captionEndToken)
732
- tokens.splice(n-3, 3)
733
- return true
704
+ }
705
+
706
+ const changePrevCaptionPosition = (tokens, n, caption, opt) => {
707
+ const captionStartToken = tokens[n-3]
708
+ const captionInlineToken = tokens[n-2]
709
+ const captionEndToken = tokens[n-1]
710
+ const figureStartToken = tokens[n]
711
+ const figureStartPaddingToken = tokens[n+1]
712
+ convertCaptionTokens(
713
+ captionStartToken,
714
+ captionInlineToken,
715
+ captionEndToken,
716
+ getTokenLevel(tokens[n]),
717
+ caption.name,
718
+ opt,
719
+ )
720
+ tokens[n-3] = figureStartToken
721
+ tokens[n-2] = figureStartPaddingToken
722
+ tokens[n-1] = captionStartToken
723
+ tokens[n] = captionInlineToken
724
+ tokens[n+1] = captionEndToken
734
725
  }
735
726
 
736
727
  const changeNextCaptionPosition = (tokens, en, caption, opt) => {
728
+ const figureEndToken = tokens[en]
737
729
  const captionStartToken = tokens[en+1]
738
730
  const captionInlineToken = tokens[en+2]
739
731
  const captionEndToken = tokens[en+3]
740
- const figureBaseLevel = getTokenLevel(tokens[en])
741
- cleanCaptionTokenAttrs(captionStartToken, caption.name, opt)
742
- captionStartToken.type = 'figcaption_open'
743
- captionStartToken.tag = 'figcaption'
744
- captionStartToken.block = true
745
- captionStartToken.level = figureBaseLevel + 1
746
- captionInlineToken.level = figureBaseLevel + 2
747
- captionEndToken.type = 'figcaption_close'
748
- captionEndToken.tag = 'figcaption'
749
- captionEndToken.block = true
750
- captionEndToken.level = figureBaseLevel + 1
751
- tokens.splice(en, 0, captionStartToken, captionInlineToken, captionEndToken)
752
- tokens.splice(en+4, 3)
753
- return true
732
+ convertCaptionTokens(
733
+ captionStartToken,
734
+ captionInlineToken,
735
+ captionEndToken,
736
+ getTokenLevel(tokens[en]),
737
+ caption.name,
738
+ opt,
739
+ )
740
+ tokens[en] = captionStartToken
741
+ tokens[en+1] = captionInlineToken
742
+ tokens[en+2] = captionEndToken
743
+ tokens[en+3] = figureEndToken
754
744
  }
755
745
 
756
746
  const getTokenMap = (token) => {
757
- return token && Array.isArray(token.map) && token.map.length === 2 ? token.map : null
747
+ if (!token || !Array.isArray(token.map) || token.map.length !== 2) return null
748
+ const startLine = token.map[0]
749
+ const endLine = token.map[1]
750
+ if (
751
+ !Number.isSafeInteger(startLine) ||
752
+ !Number.isSafeInteger(endLine) ||
753
+ startLine < 0 ||
754
+ endLine < startLine
755
+ ) {
756
+ return null
757
+ }
758
+ return token.map
758
759
  }
759
760
 
760
761
  const findNearestMapInRange = (tokens, start, end, step) => {
@@ -773,9 +774,6 @@ const getRangeMap = (tokens, start, end) => {
773
774
  const endMap = getTokenMap(tokens[end]) || findNearestMapInRange(tokens, end, start, -1) || startMap
774
775
  const startLine = startMap[0]
775
776
  const endLine = Math.max(startMap[1], endMap[1])
776
- if (typeof startLine !== 'number' || typeof endLine !== 'number' || endLine < startLine) {
777
- return [startMap[0], startMap[1]]
778
- }
779
777
  return [startLine, endLine]
780
778
  }
781
779
 
@@ -793,14 +791,27 @@ const adjustTokenLevels = (tokens, start, end, delta) => {
793
791
  }
794
792
  }
795
793
 
794
+ const createTextToken = (TokenConstructor, content, level) => {
795
+ const token = new TokenConstructor('text', '', 0)
796
+ token.content = content
797
+ token.level = level
798
+ return token
799
+ }
800
+
801
+ const joinTokenAttrs = (token, attrs) => {
802
+ if (!attrs || attrs.length === 0) return
803
+ for (let i = 0; i < attrs.length; i++) {
804
+ token.attrJoin(attrs[i][0], attrs[i][1])
805
+ }
806
+ }
807
+
796
808
  const wrapWithFigure = (tokens, range, checkTokenTagName, caption, replaceInsteadOfWrap, sp, opt, TokenConstructor) => {
797
809
  let n = range.start
798
810
  let en = range.end
799
811
  const baseLevel = getTokenLevel(tokens[n])
800
812
  const childLevel = baseLevel + 1
801
813
  const figureStartToken = new TokenConstructor('figure_open', 'figure', 1)
802
- const figureClassName = sp.figureClassName || resolveFigureClassName(checkTokenTagName, sp, opt)
803
- figureStartToken.attrSet('class', figureClassName)
814
+ figureStartToken.attrSet('class', sp.figureClassName)
804
815
  figureStartToken.block = true
805
816
  figureStartToken.level = baseLevel
806
817
 
@@ -815,65 +826,50 @@ const wrapWithFigure = (tokens, range, checkTokenTagName, caption, replaceInstea
815
826
  figureStartToken.map = [rangeMap[0], rangeMap[1]]
816
827
  figureEndToken.map = [rangeMap[0], rangeMap[1]]
817
828
  }
818
- const createBreakToken = () => {
819
- const breakToken = new TokenConstructor('text', '', 0)
820
- breakToken.content = '\n'
821
- breakToken.level = childLevel
822
- return breakToken
823
- }
824
- const createEmptyTextToken = () => {
825
- const emptyToken = new TokenConstructor('text', '', 0)
826
- emptyToken.content = ''
827
- emptyToken.level = childLevel
828
- return emptyToken
829
- }
830
829
  if (caption.name === 'img') {
831
- const joinAttrs = (attrs) => {
832
- if (!attrs || attrs.length === 0) return
833
- for (let i = 0; i < attrs.length; i++) {
834
- const attr = attrs[i]
835
- figureStartToken.attrJoin(attr[0], attr[1])
836
- }
837
- }
838
830
  // `styleProcess` should keep working even when markdown-it-attrs is absent.
839
- if (opt.styleProcess) joinAttrs(sp.attrs)
831
+ if (opt.styleProcess) joinTokenAttrs(figureStartToken, sp.attrs)
840
832
  // Forward attrs already materialized by markdown-it-attrs on the image paragraph.
841
- joinAttrs(tokens[n].attrs)
833
+ joinTokenAttrs(figureStartToken, tokens[n].attrs)
842
834
  }
843
835
  if (replaceInsteadOfWrap) {
844
- tokens.splice(en, 1, createBreakToken(), figureEndToken)
845
- tokens.splice(n, 1, figureStartToken, createEmptyTextToken())
836
+ const contentToken = tokens[n + 1]
837
+ tokens.splice(
838
+ n,
839
+ en - n + 1,
840
+ figureStartToken,
841
+ createTextToken(TokenConstructor, '', childLevel),
842
+ contentToken,
843
+ createTextToken(TokenConstructor, '\n', childLevel),
844
+ figureEndToken,
845
+ )
846
846
  en = en + 2
847
+ } else if (n === en) {
848
+ const contentToken = tokens[n]
849
+ adjustTokenLevels(tokens, n, en, 1)
850
+ tokens.splice(
851
+ n,
852
+ 1,
853
+ figureStartToken,
854
+ createTextToken(TokenConstructor, '', childLevel),
855
+ contentToken,
856
+ figureEndToken,
857
+ )
858
+ en = en + 3
847
859
  } else {
848
860
  adjustTokenLevels(tokens, n, en, 1)
849
861
  tokens.splice(en+1, 0, figureEndToken)
850
- tokens.splice(n, 0, figureStartToken, createEmptyTextToken())
862
+ tokens.splice(n, 0, figureStartToken, createTextToken(TokenConstructor, '', childLevel))
851
863
  en = en + 3
852
864
  }
853
865
  range.start = n
854
866
  range.end = en
855
- return
856
867
  }
857
868
 
858
869
  const checkCaption = (tokens, n, en, caption, sp, opt, captionState) => {
859
870
  checkPrevCaption(tokens, n, caption, sp, opt, captionState)
860
871
  if (caption.isPrev) return
861
872
  checkNextCaption(tokens, en, caption, sp, opt, captionState)
862
- return
863
- }
864
-
865
- const getNestedContainerType = (token) => {
866
- if (!token) return null
867
- switch (token.type) {
868
- case 'blockquote_open':
869
- return 'blockquote'
870
- case 'list_item_open':
871
- return 'list_item'
872
- case 'dd_open':
873
- return 'dd'
874
- default:
875
- return null
876
- }
877
873
  }
878
874
 
879
875
  const resetRangeState = (range, start) => {
@@ -894,6 +890,7 @@ const resetSpecialState = (sp) => {
894
890
  sp.isIframeTypeBlockquote = false
895
891
  sp.figureClassName = ''
896
892
  sp.captionDecision = null
893
+ sp.numbering = null
897
894
  }
898
895
 
899
896
  const findClosingTokenIndex = (tokens, startIndex, tag) => {
@@ -910,19 +907,20 @@ const findClosingTokenIndex = (tokens, startIndex, tag) => {
910
907
  }
911
908
  i++
912
909
  }
913
- return startIndex
910
+ return -1
914
911
  }
915
912
 
916
913
  const detectCheckTypeOpen = (tokens, token, n, caption, baseType) => {
917
914
  if (!token || !baseType) return null
918
915
  if (n > 1 && tokens[n - 2] && tokens[n - 2].type === 'figure_open') return null
919
- let tagName = token.tag
916
+ const tagName = token.tag
920
917
  caption.name = baseType
921
918
  if (baseType === 'pre') {
922
919
  if (tokens[n + 1] && tokens[n + 1].tag === 'code') caption.name = 'pre-code'
923
920
  if (tokens[n + 1] && tokens[n + 1].tag === 'samp') caption.name = 'pre-samp'
924
921
  }
925
922
  const en = findClosingTokenIndex(tokens, n, tagName)
923
+ if (en < 0) return null
926
924
  return {
927
925
  type: 'block',
928
926
  tagName,
@@ -935,11 +933,8 @@ const detectCheckTypeOpen = (tokens, token, n, caption, baseType) => {
935
933
 
936
934
  const detectFenceToken = (token, n, caption) => {
937
935
  if (!token || token.type !== 'fence' || token.tag !== 'code' || !token.block) return null
938
- let tagName = 'pre-code'
939
- if (sampLangReg.test(token.info)) {
940
- token.tag = 'samp'
941
- tagName = 'pre-samp'
942
- }
936
+ const useSampTag = sampLangReg.test(token.info)
937
+ const tagName = useSampTag ? 'pre-samp' : 'pre-code'
943
938
  caption.name = tagName
944
939
  return {
945
940
  type: 'fence',
@@ -948,6 +943,8 @@ const detectFenceToken = (token, n, caption) => {
948
943
  replaceInsteadOfWrap: false,
949
944
  wrapWithoutCaption: false,
950
945
  canWrap: true,
946
+ token,
947
+ useSampTag,
951
948
  }
952
949
  }
953
950
 
@@ -971,6 +968,7 @@ const detectImageParagraph = (nextToken, n, caption, sp, opt) => {
971
968
  let isMultipleImagesHorizontal = true
972
969
  let isMultipleImagesVertical = true
973
970
  let isValid = true
971
+ let trailingAttrToken = null
974
972
  caption.name = 'img'
975
973
  if (childrenLength === 1) {
976
974
  return {
@@ -1006,7 +1004,7 @@ const detectImageParagraph = (nextToken, n, caption, sp, opt) => {
1006
1004
  for (let i = 0; i < parsedAttrs.length; i++) {
1007
1005
  sp.attrs.push(parsedAttrs[i])
1008
1006
  }
1009
- child.content = ''
1007
+ trailingAttrToken = child
1010
1008
  break
1011
1009
  }
1012
1010
  }
@@ -1044,12 +1042,6 @@ const detectImageParagraph = (nextToken, n, caption, sp, opt) => {
1044
1042
  } else {
1045
1043
  caption.nameSuffix = '-multiple'
1046
1044
  }
1047
- for (let i = 0; i < childrenLength; i++) {
1048
- const child = children[i]
1049
- if (isOnlySpacesText(child)) {
1050
- child.content = ''
1051
- }
1052
- }
1053
1045
  }
1054
1046
  const en = n + 2
1055
1047
  let tagName = 'img'
@@ -1062,28 +1054,56 @@ const detectImageParagraph = (nextToken, n, caption, sp, opt) => {
1062
1054
  wrapWithoutCaption: isValid && allowImageParagraphWithoutCaption,
1063
1055
  canWrap: isValid,
1064
1056
  imageToken,
1057
+ inlineToken: nextToken,
1058
+ trailingAttrToken,
1065
1059
  }
1066
1060
  }
1067
1061
 
1068
- const figureWithCaption = (state, opt) => {
1069
- const figureNumberState = {
1070
- img: 0,
1071
- table: 0,
1062
+ const applyImageParagraphTransform = (detection) => {
1063
+ if (!detection || detection.type !== 'image') return
1064
+ if (detection.trailingAttrToken) {
1065
+ detection.trailingAttrToken.content = ''
1072
1066
  }
1067
+ if (!detection.inlineToken || detection.tagName === 'img') return
1068
+ const children = detection.inlineToken.children
1069
+ for (let i = 0; i < children.length; i++) {
1070
+ if (isOnlySpacesText(children[i])) {
1071
+ children[i].content = ''
1072
+ }
1073
+ }
1074
+ }
1073
1075
 
1074
- const captionState = { tokens: state.tokens, Token: state.Token }
1075
- const shouldResolvePreferredLanguages = !!(
1076
- opt.shouldResolvePreferredLanguages &&
1077
- sourceMayNeedPreferredLanguages(state)
1078
- )
1079
- const renderOpt = shouldResolvePreferredLanguages ? Object.create(opt) : opt
1080
- if (shouldResolvePreferredLanguages) {
1081
- renderOpt.preferredLanguages = resolvePreferredLanguagesForState(state, opt)
1076
+ const applyWrappedCandidateTransform = (detection) => {
1077
+ if (detection.type === 'image') {
1078
+ applyImageParagraphTransform(detection)
1079
+ } else if (detection.type === 'fence' && detection.useSampTag) {
1080
+ detection.token.tag = 'samp'
1081
+ }
1082
+ }
1083
+
1084
+ const figureWithCaption = (state, opt) => {
1085
+ const numberingRuntime = opt.captionNumberingPolicy
1086
+ ? createCaptionNumberingRuntime(opt.captionNumberingPolicy, { env: state.env })
1087
+ : null
1088
+ const numberingScopeState = numberingRuntime
1089
+ ? createFigureCaptionScopeRuntime(
1090
+ state,
1091
+ opt.normalizedAutoLabelNumberPolicy,
1092
+ opt.unscopedNumberingContext,
1093
+ )
1094
+ : null
1095
+ const captionState = {
1096
+ tokens: state.tokens,
1097
+ Token: state.Token,
1098
+ preferredLanguages: opt.preferredLanguages,
1099
+ numberingRuntime,
1100
+ numberingScopeState,
1082
1101
  }
1083
- figureWithCaptionCore(state.tokens, renderOpt, figureNumberState, state.Token, captionState, null, 0)
1102
+ if (opt.shouldResolvePreferredLanguages) captionState.preferredLanguageState = state
1103
+ figureWithCaptionCore(state.tokens, opt, state.Token, captionState, null, 0)
1084
1104
  }
1085
1105
 
1086
- const figureWithCaptionCore = (tokens, opt, figureNumberState, TokenConstructor, captionState, parentType = null, startIndex = 0) => {
1106
+ const figureWithCaptionCore = (tokens, opt, TokenConstructor, captionState, parentType = null, startIndex = 0) => {
1087
1107
  const rRange = { start: startIndex, end: startIndex }
1088
1108
  const rCaption = {
1089
1109
  name: '', nameSuffix: '', isPrev: false, isNext: false
@@ -1093,21 +1113,31 @@ const figureWithCaptionCore = (tokens, opt, figureNumberState, TokenConstructor,
1093
1113
  isVideoIframe: false,
1094
1114
  isIframeTypeBlockquote: false,
1095
1115
  figureClassName: '',
1096
- captionDecision: null
1097
- }
1116
+ captionDecision: null,
1117
+ numbering: null,
1118
+ }
1119
+ const numberingScopeState = parentType ? null : captionState.numberingScopeState
1120
+ const headingScopeState = numberingScopeState &&
1121
+ !numberingScopeState.fixed &&
1122
+ numberingScopeState.scopeConfig.usesHeading
1123
+ ? numberingScopeState
1124
+ : null
1125
+ const parentCloseType = parentType ? parentType + '_close' : ''
1098
1126
  let n = startIndex
1099
1127
  while (n < tokens.length) {
1100
1128
  const token = tokens[n]
1101
- const containerType = getNestedContainerType(token)
1129
+ if (headingScopeState && token.type === 'heading_open') {
1130
+ updateFigureCaptionScopeFromHeading(tokens, n, headingScopeState)
1131
+ }
1132
+ const containerType = getFigureCaptionScopeNestedContainerType(token)
1102
1133
 
1103
1134
  if (containerType && containerType !== 'blockquote') {
1104
- const closeIndex = figureWithCaptionCore(tokens, opt, figureNumberState, TokenConstructor, captionState, containerType, n + 1)
1105
- n = (typeof closeIndex === 'number' ? closeIndex : n) + 1
1135
+ const closeIndex = figureWithCaptionCore(tokens, opt, TokenConstructor, captionState, containerType, n + 1)
1136
+ n = closeIndex + 1
1106
1137
  continue
1107
1138
  }
1108
1139
 
1109
-
1110
- if (parentType && token.type === `${parentType}_close`) {
1140
+ if (parentCloseType && token.type === parentCloseType) {
1111
1141
  return n
1112
1142
  }
1113
1143
 
@@ -1146,8 +1176,8 @@ const figureWithCaptionCore = (tokens, opt, figureNumberState, TokenConstructor,
1146
1176
 
1147
1177
  if (!detection) {
1148
1178
  if (containerType === 'blockquote') {
1149
- const closeIndex = figureWithCaptionCore(tokens, opt, figureNumberState, TokenConstructor, captionState, containerType, n + 1)
1150
- n = (typeof closeIndex === 'number' ? closeIndex : n) + 1
1179
+ const closeIndex = figureWithCaptionCore(tokens, opt, TokenConstructor, captionState, containerType, n + 1)
1180
+ n = closeIndex + 1
1151
1181
  } else {
1152
1182
  n++
1153
1183
  }
@@ -1156,63 +1186,77 @@ const figureWithCaptionCore = (tokens, opt, figureNumberState, TokenConstructor,
1156
1186
 
1157
1187
  rRange.end = detection.en
1158
1188
 
1189
+ if (detection.canWrap === false || (detection.type === 'image' && token.hidden === true)) {
1190
+ let nextIndex = rRange.end + 1
1191
+ if (containerType === 'blockquote') {
1192
+ const closeIndex = figureWithCaptionCore(tokens, opt, TokenConstructor, captionState, containerType, rRange.start + 1)
1193
+ nextIndex = Math.max(nextIndex, closeIndex + 1)
1194
+ }
1195
+ n = nextIndex
1196
+ continue
1197
+ }
1198
+
1199
+ if (detection.type === 'html') {
1200
+ rRange.end = applyHtmlFigureTransform(tokens, detection)
1201
+ }
1202
+
1203
+ if (captionState.numberingRuntime) {
1204
+ rSp.numbering = captionState.numberingScopeState
1205
+ ? captionState.numberingScopeState.currentContext
1206
+ : opt.unscopedNumberingContext
1207
+ }
1159
1208
  rSp.figureClassName = resolveFigureClassName(detection.tagName, rSp, opt)
1160
1209
  checkCaption(tokens, rRange.start, rRange.end, rCaption, rSp, opt, captionState)
1161
- applyCaptionDrivenFigureClass(rCaption, rSp, opt)
1162
1210
 
1163
1211
  let hasCaption = rCaption.isPrev || rCaption.isNext
1164
- let pendingAutoCaption = ''
1212
+ if (hasCaption) applyCaptionDrivenFigureClass(rCaption, rSp, opt)
1213
+ let pendingAutoCaption = null
1165
1214
  if (!hasCaption && detection.type === 'image' && opt.autoCaptionDetection) {
1166
- pendingAutoCaption = getAutoCaptionFromImage(detection.imageToken, opt)
1215
+ pendingAutoCaption = getAutoCaptionFromImage(detection.imageToken, opt, captionState)
1167
1216
  if (pendingAutoCaption) {
1168
1217
  hasCaption = true
1169
1218
  }
1170
1219
  }
1171
1220
 
1172
- if (detection.canWrap === false) {
1173
- let nextIndex = rRange.end + 1
1174
- if (containerType === 'blockquote') {
1175
- const closeIndex = figureWithCaptionCore(tokens, opt, figureNumberState, TokenConstructor, captionState, containerType, rRange.start + 1)
1176
- nextIndex = Math.max(nextIndex, (typeof closeIndex === 'number' ? closeIndex : rRange.end) + 1)
1177
- }
1178
- n = nextIndex
1179
- continue
1221
+ let shouldWrap = hasCaption
1222
+ if (detection.type === 'html' || detection.type === 'image') {
1223
+ shouldWrap = shouldWrap || detection.wrapWithoutCaption
1180
1224
  }
1181
-
1182
- let shouldWrap = false
1183
- if (detection.type === 'html') {
1184
- shouldWrap = detection.canWrap !== false && (hasCaption || detection.wrapWithoutCaption)
1185
- } else if (detection.type === 'image') {
1186
- shouldWrap = detection.canWrap !== false && (hasCaption || detection.wrapWithoutCaption)
1187
- if (token.hidden === true) {
1188
- shouldWrap = false
1225
+ if (pendingAutoCaption) {
1226
+ const captionTokens = createAutoCaptionParagraph(pendingAutoCaption.text, TokenConstructor)
1227
+ const insertIndex = rRange.start
1228
+ const insertedLength = captionTokens.length
1229
+ tokens.splice(insertIndex, 0, ...captionTokens)
1230
+ rRange.start += insertedLength
1231
+ rRange.end += insertedLength
1232
+ n += insertedLength
1233
+ try {
1234
+ checkCaption(tokens, rRange.start, rRange.end, rCaption, rSp, opt, captionState)
1235
+ } catch (error) {
1236
+ tokens.splice(insertIndex, insertedLength)
1237
+ throw error
1238
+ }
1239
+ if (rCaption.isPrev || rCaption.isNext) {
1240
+ consumeAutoCaptionSource(detection.imageToken, pendingAutoCaption)
1241
+ applyCaptionDrivenFigureClass(rCaption, rSp, opt)
1242
+ } else {
1243
+ tokens.splice(insertIndex, insertedLength)
1244
+ rRange.start -= insertedLength
1245
+ rRange.end -= insertedLength
1246
+ n -= insertedLength
1247
+ shouldWrap = detection.wrapWithoutCaption
1189
1248
  }
1190
- } else {
1191
- shouldWrap = detection.canWrap !== false && hasCaption
1192
1249
  }
1193
-
1194
1250
  if (shouldWrap) {
1195
- if (pendingAutoCaption) {
1196
- const captionTokens = createAutoCaptionParagraph(pendingAutoCaption, TokenConstructor)
1197
- tokens.splice(rRange.start, 0, ...captionTokens)
1198
- const insertedLength = captionTokens.length
1199
- rRange.start += insertedLength
1200
- rRange.end += insertedLength
1201
- n += insertedLength
1202
- checkCaption(tokens, rRange.start, rRange.end, rCaption, rSp, opt, captionState)
1203
- applyCaptionDrivenFigureClass(rCaption, rSp, opt)
1251
+ if (detection.type !== 'html') {
1252
+ applyWrappedCandidateTransform(detection)
1204
1253
  }
1205
- ensureAutoFigureNumbering(tokens, rRange, rCaption, figureNumberState, opt)
1206
1254
  wrapWithFigure(tokens, rRange, detection.tagName, rCaption, detection.replaceInsteadOfWrap, rSp, opt, TokenConstructor)
1207
1255
  }
1208
1256
 
1209
1257
  let nextIndex
1210
1258
  if (!rCaption.isPrev && !rCaption.isNext) {
1211
- if (shouldWrap && detection.type === 'html') {
1212
- nextIndex = rRange.end + 1
1213
- } else {
1214
- nextIndex = n + 1
1215
- }
1259
+ nextIndex = shouldWrap ? rRange.end + 1 : n + 1
1216
1260
  } else {
1217
1261
  const en = rRange.end
1218
1262
  if (rCaption.isPrev) {
@@ -1221,15 +1265,20 @@ const figureWithCaptionCore = (tokens, opt, figureNumberState, TokenConstructor,
1221
1265
  } else if (rCaption.isNext) {
1222
1266
  changeNextCaptionPosition(tokens, en, rCaption, opt)
1223
1267
  nextIndex = en + 4
1224
- } else {
1225
- nextIndex = en + 1
1226
1268
  }
1227
1269
  }
1228
1270
 
1229
1271
  if (containerType === 'blockquote') {
1230
- const closeIndex = figureWithCaptionCore(tokens, opt, figureNumberState, TokenConstructor, captionState, containerType, rRange.start + 1)
1231
- const fallbackIndex = rCaption.name ? rRange.end : n
1232
- nextIndex = Math.max(nextIndex, (typeof closeIndex === 'number' ? closeIndex : fallbackIndex) + 1)
1272
+ const nestedContentStart = rRange.start + (shouldWrap ? 3 : 1)
1273
+ const closeIndex = figureWithCaptionCore(
1274
+ tokens,
1275
+ opt,
1276
+ TokenConstructor,
1277
+ captionState,
1278
+ containerType,
1279
+ nestedContentStart,
1280
+ )
1281
+ nextIndex = Math.max(nextIndex, closeIndex + 1)
1233
1282
  }
1234
1283
 
1235
1284
  n = nextIndex
@@ -1238,7 +1287,11 @@ const figureWithCaptionCore = (tokens, opt, figureNumberState, TokenConstructor,
1238
1287
  }
1239
1288
 
1240
1289
  const mditFigureWithPCaption = (md, option) => {
1241
- let opt = {
1290
+ if (md[installedKey]) return
1291
+ if (hasOwnOption(option, 'setFigureNumber')) {
1292
+ throw new Error('setFigureNumber is not supported by figure-with-p-caption; use autoLabelNumber or autoLabelNumberSets')
1293
+ }
1294
+ const opt = {
1242
1295
  // Caption languages delegated to p-captions.
1243
1296
  languages: ['en', 'ja'],
1244
1297
  preferredLanguages: null, // compatibility tie-break for generated fallback labels; prefer env.locale / env.preferredLocales per render
@@ -1271,7 +1324,8 @@ const mditFigureWithPCaption = (md, option) => {
1271
1324
 
1272
1325
  // --- numbering controls ---
1273
1326
  autoLabelNumber: false, // shorthand for enabling numbering for both img/table unless autoLabelNumberSets is provided explicitly
1274
- autoLabelNumberSets: [], // preferred; supports ['img'], ['table'], or both
1327
+ autoLabelNumberSets: [], // preferred; supports img/table/code/samp/video marks
1328
+ autoLabelNumberPolicy: undefined, // omitted: automatic Chapter/Appendix scope; explicit null: document-wide numbering
1275
1329
 
1276
1330
  // --- caption text formatting (delegated to p7d-markdown-it-p-captions) ---
1277
1331
  hasNumClass: false,
@@ -1287,16 +1341,20 @@ const mditFigureWithPCaption = (md, option) => {
1287
1341
  labelClassFollowsFigure: false,
1288
1342
  figureToLabelClassMap: null,
1289
1343
  }
1290
- const hasExplicitAutoLabelNumberSets = option && Object.prototype.hasOwnProperty.call(option, 'autoLabelNumberSets')
1291
- const hasExplicitImageOnlyParagraphWithoutCaption = option && Object.prototype.hasOwnProperty.call(option, 'imageOnlyParagraphWithoutCaption')
1292
- const hasExplicitFigureClassThatWrapsIframeTypeBlockquote = option && Object.prototype.hasOwnProperty.call(option, 'figureClassThatWrapsIframeTypeBlockquote')
1293
- const hasExplicitFigureClassThatWrapsSlides = option && Object.prototype.hasOwnProperty.call(option, 'figureClassThatWrapsSlides')
1294
- const hasExplicitLabelClassFollowsFigure = option && Object.prototype.hasOwnProperty.call(option, 'labelClassFollowsFigure')
1344
+ const hasExplicitAutoLabelNumberSets = hasOwnOption(option, 'autoLabelNumberSets')
1345
+ const hasExplicitImageOnlyParagraphWithoutCaption = hasOwnOption(option, 'imageOnlyParagraphWithoutCaption')
1346
+ const hasExplicitFigureClassThatWrapsIframeTypeBlockquote = hasOwnOption(option, 'figureClassThatWrapsIframeTypeBlockquote')
1347
+ const hasExplicitFigureClassThatWrapsSlides = hasOwnOption(option, 'figureClassThatWrapsSlides')
1348
+ const hasExplicitLabelClassFollowsFigure = hasOwnOption(option, 'labelClassFollowsFigure')
1295
1349
  if (option) Object.assign(opt, option)
1350
+ if (Array.isArray(opt.removeUnnumberedLabelExceptMarks)) {
1351
+ opt.removeUnnumberedLabelExceptMarks = opt.removeUnnumberedLabelExceptMarks.map(
1352
+ canonicalizeCaptionNumberingMark,
1353
+ )
1354
+ }
1296
1355
  opt.imageOnlyParagraphWithoutCaption = hasExplicitImageOnlyParagraphWithoutCaption
1297
1356
  ? !!opt.imageOnlyParagraphWithoutCaption
1298
1357
  : !!opt.oneImageWithoutCaption
1299
- opt.oneImageWithoutCaption = !!opt.oneImageWithoutCaption
1300
1358
  if (!hasExplicitLabelClassFollowsFigure && opt.figureToLabelClassMap) {
1301
1359
  opt.labelClassFollowsFigure = true
1302
1360
  }
@@ -1305,7 +1363,6 @@ const mditFigureWithPCaption = (md, option) => {
1305
1363
  opt.markRegState = getMarkRegStateForLanguages(opt.languages)
1306
1364
  opt.preferredLanguages = normalizePreferredLanguages(opt.preferredLanguages, opt.markRegState.languages)
1307
1365
  if (opt.preferredLanguages.length === 0) opt.preferredLanguages = null
1308
- opt.normalizedOptionLanguages = normalizePreferredLanguages(opt.languages, opt.markRegState.languages)
1309
1366
  opt.shouldResolvePreferredLanguages = needsPreferredLanguagesResolution(opt)
1310
1367
  validateFallbackCaptionLabelOption('autoAltCaption', opt.autoAltCaption, opt.markRegState)
1311
1368
  validateFallbackCaptionLabelOption('autoTitleCaption', opt.autoTitleCaption, opt.markRegState)
@@ -1315,12 +1372,27 @@ const mditFigureWithPCaption = (md, option) => {
1315
1372
  audio: !!opt.audioWithoutCaption,
1316
1373
  iframeTypeBlockquote: !!opt.iframeTypeBlockquoteWithoutCaption,
1317
1374
  }
1318
- // Normalize option shorthands now so downstream logic works with a consistent { img, table } shape.
1319
- opt.autoLabelNumberSets = normalizeAutoLabelNumberSets(opt.autoLabelNumberSets)
1320
- if (opt.autoLabelNumber && !hasExplicitAutoLabelNumberSets) {
1321
- opt.autoLabelNumberSets.img = true
1322
- opt.autoLabelNumberSets.table = true
1323
- }
1375
+ const requestedAutoLabelNumberSets = hasExplicitAutoLabelNumberSets
1376
+ ? option.autoLabelNumberSets
1377
+ : opt.autoLabelNumber
1378
+ ? ['img', 'table']
1379
+ : []
1380
+ opt.autoLabelNumberSets = normalizeAutoLabelNumberSets(requestedAutoLabelNumberSets)
1381
+ const enabledNumberingMarks = getEnabledCaptionNumberingMarks(opt.autoLabelNumberSets, opt)
1382
+ opt.normalizedAutoLabelNumberPolicy = enabledNumberingMarks.length === 0 &&
1383
+ opt.autoLabelNumberPolicy === undefined
1384
+ ? null
1385
+ : normalizeFigureCaptionNumberingPolicy(opt.autoLabelNumberPolicy)
1386
+ opt.captionNumberingPolicy = createFigureCaptionNumberingPolicy(
1387
+ enabledNumberingMarks,
1388
+ opt.normalizedAutoLabelNumberPolicy,
1389
+ opt.markRegState,
1390
+ )
1391
+ opt.unscopedNumberingContext = opt.captionNumberingPolicy && opt.normalizedAutoLabelNumberPolicy
1392
+ ? createUnscopedNumberingContext(
1393
+ getFigureCaptionNumberingPolicyState(opt.normalizedAutoLabelNumberPolicy).separator,
1394
+ )
1395
+ : null
1324
1396
  const classPrefix = buildClassPrefix(opt.classPrefix)
1325
1397
  opt.figureClassPrefix = classPrefix
1326
1398
  opt.captionClassPrefix = classPrefix
@@ -1342,8 +1414,6 @@ const mditFigureWithPCaption = (md, option) => {
1342
1414
  defaultSlideFigureClass,
1343
1415
  )
1344
1416
  }
1345
- // Precompute label-class permutations so numbering lookup doesn't rebuild arrays per caption.
1346
- opt.labelClassLookup = buildLabelClassLookup(opt)
1347
1417
  const markerList = normalizeLabelPrefixMarkers(opt.labelPrefixMarker)
1348
1418
  opt.labelPrefixMarkerReg = buildLabelPrefixMarkerRegFromMarkers(markerList)
1349
1419
  opt.cleanCaptionRegCache = new Map()
@@ -1356,10 +1426,14 @@ const mditFigureWithPCaption = (md, option) => {
1356
1426
  opt.labelPrefixMarkerWithoutLabelNextReg = null
1357
1427
  }
1358
1428
 
1359
- //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.
1429
+ // Run after markdown-it-attrs has attached paragraph attrs, but before text replacements.
1360
1430
  md.core.ruler.before('replacements', 'figure_with_caption', (state) => {
1361
1431
  figureWithCaption(state, opt)
1362
1432
  })
1433
+ Object.defineProperty(md, installedKey, {
1434
+ value: true,
1435
+ configurable: true,
1436
+ })
1363
1437
  }
1364
1438
 
1365
1439
  export default mditFigureWithPCaption