@peaceroad/markdown-it-figure-with-p-caption 0.16.1 → 0.18.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,42 +1,198 @@
1
1
  import {
2
+ analyzeCaptionStart,
3
+ buildLabelClassLookup,
4
+ buildLabelPrefixMarkerRegFromMarkers,
5
+ getGeneratedLabelDefaults,
6
+ normalizeLabelPrefixMarkers,
2
7
  setCaptionParagraph,
3
8
  getMarkRegStateForLanguages,
9
+ stripLabelPrefixMarker,
4
10
  } from 'p7d-markdown-it-p-captions'
11
+ import { detectHtmlFigureCandidate } from './embeds/detect.js'
5
12
 
6
- const htmlRegCache = new Map()
7
- const blueskyEmbedReg = /^<blockquote class="bluesky-embed"[^]*?>[\s\S]*?$/
8
- const videoIframeReg = /^<[^>]*? src="https:\/\/(?:www.youtube-nocookie.com|player.vimeo.com)\//i
9
- const classNameReg = /^<[^>]*? class="(twitter-tweet|instagram-media|text-post-media|bluesky-embed|mastodon-embed)"/
10
13
  const imageAttrsReg = /^ *\{(.*?)\} *$/
11
14
  const classAttrReg = /^\./
12
15
  const idAttrReg = /^#/
13
- const attrParseReg = /^(.*?)="?(.*)"?$/
14
16
  const sampLangReg = /^ *(?:samp|shell|console)(?:(?= )|$)/
15
- const endBlockquoteScriptReg = /<\/blockquote> *<script[^>]*?><\/script>$/
16
- const iframeTagReg = /<iframe(?=[\s>])/i
17
17
  const asciiLabelReg = /^[A-Za-z]/
18
+ const attrNameReg = /^[^\s=]+$/
19
+ const labelTrailingJointReg = /[.\u3002\uff0e::]\s*$/
18
20
  const CHECK_TYPE_TOKEN_MAP = {
19
21
  table_open: 'table',
20
22
  pre_open: 'pre',
21
23
  blockquote_open: 'blockquote',
22
24
  }
23
- const HTML_TAG_DETECTORS = [
24
- { candidate: 'video', lookupTag: 'video', hintKey: 'hasVideoHint' },
25
- { candidate: 'audio', lookupTag: 'audio', hintKey: 'hasAudioHint' },
26
- { candidate: 'iframe', lookupTag: 'iframe', hintKey: 'hasIframeHint' },
27
- { candidate: 'blockquote', lookupTag: 'blockquote', hintKey: 'hasBlockquoteHint' },
28
- {
29
- candidate: 'div',
30
- lookupTag: 'div',
31
- hintKey: 'hasDivHint',
32
- requiresIframeTag: true,
33
- matchedTag: 'iframe',
34
- setVideoIframe: true,
35
- },
36
- ]
37
- const fallbackLabelDefaults = { en: 'Figure', ja: '図' }
38
-
39
25
  const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
26
+ const normalizeLanguageCode = (value) => {
27
+ if (value === null || value === undefined) return ''
28
+ const normalized = String(value).trim().toLowerCase()
29
+ if (!normalized) return ''
30
+ const separatorIndex = normalized.search(/[-_]/)
31
+ return separatorIndex === -1 ? normalized : normalized.slice(0, separatorIndex)
32
+ }
33
+ 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
37
+ target.push(lang)
38
+ return true
39
+ }
40
+ const normalizePreferredLanguages = (value, availableLanguages) => {
41
+ if (!Array.isArray(availableLanguages) || availableLanguages.length === 0) return []
42
+ const languages = []
43
+ if (typeof value === 'string') {
44
+ appendAvailableLanguage(languages, normalizeLanguageCode(value), availableLanguages)
45
+ return languages
46
+ }
47
+ const source = Array.isArray(value) ? value : []
48
+ if (source.length === 0) return languages
49
+ for (let i = 0; i < source.length; i++) {
50
+ const lang = normalizeLanguageCode(source[i])
51
+ appendAvailableLanguage(languages, lang, availableLanguages)
52
+ }
53
+ return languages
54
+ }
55
+ const prioritizeLanguages = (languages, preferredLanguages) => {
56
+ if (!Array.isArray(languages) || languages.length === 0) return []
57
+ if (typeof preferredLanguages === 'string') {
58
+ if (!preferredLanguages || languages.indexOf(preferredLanguages) === -1) return languages
59
+ if (languages[0] === preferredLanguages) return languages
60
+ const prioritized = [preferredLanguages]
61
+ for (let i = 0; i < languages.length; i++) {
62
+ appendAvailableLanguage(prioritized, languages[i], languages)
63
+ }
64
+ return prioritized
65
+ }
66
+ if (!Array.isArray(preferredLanguages) || preferredLanguages.length === 0) return languages
67
+ const prioritized = []
68
+ for (let i = 0; i < preferredLanguages.length; i++) {
69
+ appendAvailableLanguage(prioritized, preferredLanguages[i], languages)
70
+ }
71
+ if (prioritized.length === 0) return languages
72
+ for (let i = 0; i < languages.length; i++) {
73
+ appendAvailableLanguage(prioritized, languages[i], languages)
74
+ }
75
+ return prioritized
76
+ }
77
+ const isAsciiAlphaCode = (code) => {
78
+ return (code >= 0x41 && code <= 0x5a) || (code >= 0x61 && code <= 0x7a)
79
+ }
80
+ const isJapaneseCharCode = (code) => {
81
+ return (
82
+ (code >= 0x3040 && code <= 0x30ff) ||
83
+ (code >= 0x31f0 && code <= 0x31ff) ||
84
+ (code >= 0x4e00 && code <= 0x9fff) ||
85
+ (code >= 0xff66 && code <= 0xff9f)
86
+ )
87
+ }
88
+ const isHyphenFenceLine = (src, lineStart) => {
89
+ if (typeof src !== 'string' || lineStart < 0 || lineStart >= src.length) return 0
90
+ let index = lineStart
91
+ let hyphenCount = 0
92
+ while (index < src.length && src.charCodeAt(index) === 0x2d) {
93
+ hyphenCount++
94
+ index++
95
+ }
96
+ if (hyphenCount < 3) return 0
97
+ while (index < src.length && src.charCodeAt(index) === 0x20) {
98
+ index++
99
+ }
100
+ if (index >= src.length || src.charCodeAt(index) !== 0x0a) return 0
101
+ return hyphenCount
102
+ }
103
+ const skipLeadingFrontmatter = (src) => {
104
+ if (typeof src !== 'string' || isHyphenFenceLine(src, 0) === 0) return src
105
+ let lineStart = src.indexOf('\n')
106
+ if (lineStart === -1) return src
107
+ lineStart++
108
+ while (lineStart < src.length) {
109
+ if (isHyphenFenceLine(src, lineStart) > 0) {
110
+ const nextLineStart = src.indexOf('\n', lineStart)
111
+ if (nextLineStart === -1) return ''
112
+ return src.slice(nextLineStart + 1)
113
+ }
114
+ const nextLineStart = src.indexOf('\n', lineStart)
115
+ if (nextLineStart === -1) break
116
+ lineStart = nextLineStart + 1
117
+ }
118
+ return src
119
+ }
120
+ const detectDocumentPrimaryLanguage = (src, availableLanguages) => {
121
+ if (!src || availableLanguages.indexOf('ja') === -1) return ''
122
+ const body = skipLeadingFrontmatter(src)
123
+ const limit = Math.min(body.length, 8192)
124
+ let japaneseCount = 0
125
+ let asciiAlphaCount = 0
126
+ for (let i = 0; i < limit; i++) {
127
+ const code = body.charCodeAt(i)
128
+ if (isJapaneseCharCode(code)) {
129
+ japaneseCount++
130
+ continue
131
+ }
132
+ if (isAsciiAlphaCode(code)) {
133
+ asciiAlphaCount++
134
+ }
135
+ }
136
+ if (japaneseCount === 0) return ''
137
+ if (asciiAlphaCount === 0) return 'ja'
138
+ return japaneseCount * 2 >= asciiAlphaCount ? 'ja' : ''
139
+ }
140
+ const sourceMayNeedPreferredLanguages = (state) => {
141
+ const src = state && typeof state.src === 'string' ? state.src : ''
142
+ return src.indexOf('![') !== -1
143
+ }
144
+ const resolvePreferredLanguagesForState = (state, opt) => {
145
+ const availableLanguages = (
146
+ opt &&
147
+ opt.markRegState &&
148
+ Array.isArray(opt.markRegState.languages)
149
+ ) ? opt.markRegState.languages : []
150
+ if (availableLanguages.length === 0) return []
151
+
152
+ const optionLanguages = opt && Array.isArray(opt.normalizedOptionLanguages)
153
+ ? opt.normalizedOptionLanguages
154
+ : []
155
+ const baseLanguages = optionLanguages.length > 0 ? optionLanguages : availableLanguages
156
+ const env = state && state.env ? state.env : null
157
+
158
+ const envLocale = normalizeLanguageCode(env && env.locale)
159
+ if (envLocale && baseLanguages.indexOf(envLocale) !== -1) {
160
+ return prioritizeLanguages(baseLanguages, envLocale)
161
+ }
162
+
163
+ const envPreferredLocales = normalizePreferredLanguages(env && env.preferredLocales, baseLanguages)
164
+ if (envPreferredLocales.length > 0) {
165
+ return prioritizeLanguages(baseLanguages, envPreferredLocales)
166
+ }
167
+
168
+ const explicitPreferred = opt && Array.isArray(opt.preferredLanguages)
169
+ ? opt.preferredLanguages
170
+ : []
171
+ if (explicitPreferred.length > 0) {
172
+ return prioritizeLanguages(baseLanguages, explicitPreferred)
173
+ }
174
+
175
+ const envPreferred = normalizePreferredLanguages(env && env.preferredLanguages, baseLanguages)
176
+ if (envPreferred.length > 0) {
177
+ return prioritizeLanguages(baseLanguages, envPreferred)
178
+ }
179
+
180
+ const envLanguage = normalizeLanguageCode(env && (env.preferredLanguage || env.lang || env.language))
181
+ if (envLanguage && baseLanguages.indexOf(envLanguage) !== -1) {
182
+ return prioritizeLanguages(baseLanguages, envLanguage)
183
+ }
184
+
185
+ const detectedLanguage = detectDocumentPrimaryLanguage(state && state.src ? state.src : '', baseLanguages)
186
+ if (detectedLanguage) {
187
+ return prioritizeLanguages(baseLanguages, detectedLanguage)
188
+ }
189
+ return baseLanguages
190
+ }
191
+ const needsPreferredLanguagesResolution = (opt) => {
192
+ if (!opt || !opt.markRegState || !Array.isArray(opt.markRegState.languages)) return false
193
+ if (opt.markRegState.languages.length <= 1) return false
194
+ return opt.autoAltCaption === true || opt.autoTitleCaption === true
195
+ }
40
196
  const normalizeOptionalClassName = (value) => {
41
197
  if (value === null || value === undefined) return ''
42
198
  const normalized = String(value).trim()
@@ -50,36 +206,6 @@ const normalizeClassOptionWithFallback = (value, fallbackValue) => {
50
206
  const normalized = normalizeOptionalClassName(value)
51
207
  return normalized || fallbackValue
52
208
  }
53
- const normalizeLanguages = (value) => {
54
- if (!Array.isArray(value)) return ['en', 'ja']
55
- const normalized = []
56
- const seen = new Set()
57
- for (let i = 0; i < value.length; i++) {
58
- const lang = value[i]
59
- if (typeof lang !== 'string') continue
60
- const trimmed = lang.trim()
61
- if (!trimmed || seen.has(trimmed)) continue
62
- seen.add(trimmed)
63
- normalized.push(trimmed)
64
- }
65
- if (normalized.length === 0) return ['en', 'ja']
66
- return normalized
67
- }
68
- const normalizeLabelPrefixMarkers = (value) => {
69
- if (typeof value === 'string') {
70
- return value ? [value] : []
71
- }
72
- if (Array.isArray(value)) {
73
- const normalized = value.map(entry => String(entry)).filter(Boolean)
74
- return normalized.length > 2 ? normalized.slice(0, 2) : normalized
75
- }
76
- return []
77
- }
78
- const buildLabelPrefixMarkerRegFromList = (markers) => {
79
- if (!markers || markers.length === 0) return null
80
- const pattern = markers.map(escapeRegExp).join('|')
81
- return new RegExp('^(?:' + pattern + ')(?:[ \\t ]+)?')
82
- }
83
209
  const resolveLabelPrefixMarkerPair = (markers) => {
84
210
  if (!markers || markers.length === 0) return { prev: [], next: [] }
85
211
  if (markers.length === 1) {
@@ -87,26 +213,6 @@ const resolveLabelPrefixMarkerPair = (markers) => {
87
213
  }
88
214
  return { prev: [markers[0]], next: [markers[1]] }
89
215
  }
90
- const stripLeadingPrefix = (text, prefix) => {
91
- if (typeof text !== 'string' || !text || !prefix) return text
92
- if (text.startsWith(prefix)) return text.slice(prefix.length)
93
- return text
94
- }
95
- const stripLabelPrefixMarkerFromInline = (inlineToken, markerText) => {
96
- if (!inlineToken || !markerText) return
97
- if (typeof inlineToken.content === 'string') {
98
- inlineToken.content = stripLeadingPrefix(inlineToken.content, markerText)
99
- }
100
- if (inlineToken.children && inlineToken.children.length) {
101
- for (let i = 0; i < inlineToken.children.length; i++) {
102
- const child = inlineToken.children[i]
103
- if (child && child.type === 'text' && typeof child.content === 'string') {
104
- child.content = stripLeadingPrefix(child.content, markerText)
105
- break
106
- }
107
- }
108
- }
109
- }
110
216
  const getLabelPrefixMarkerMatch = (inlineToken, markerReg) => {
111
217
  if (!markerReg || !inlineToken || inlineToken.type !== 'inline') return null
112
218
  const content = typeof inlineToken.content === 'string' ? inlineToken.content : ''
@@ -118,10 +224,62 @@ const getLabelPrefixMarkerMatch = (inlineToken, markerReg) => {
118
224
  return match[0]
119
225
  }
120
226
 
121
- const parseImageAttrs = (raw) => {
227
+ const splitImageAttrParts = (raw) => {
122
228
  if (raw === null || raw === undefined) return null
229
+ const parts = []
230
+ let current = ''
231
+ let quote = ''
232
+ let escaped = false
233
+ for (let i = 0; i < raw.length; i++) {
234
+ const ch = raw[i]
235
+ if (quote) {
236
+ current += ch
237
+ if (escaped) {
238
+ escaped = false
239
+ continue
240
+ }
241
+ if (ch === '\\') {
242
+ escaped = true
243
+ continue
244
+ }
245
+ if (ch === quote) {
246
+ quote = ''
247
+ }
248
+ continue
249
+ }
250
+ if (ch === '"' || ch === "'") {
251
+ quote = ch
252
+ current += ch
253
+ continue
254
+ }
255
+ if (ch === ' ') {
256
+ if (current) {
257
+ parts.push(current)
258
+ current = ''
259
+ }
260
+ continue
261
+ }
262
+ current += ch
263
+ }
264
+ if (quote) return null
265
+ if (current) parts.push(current)
266
+ return parts
267
+ }
268
+
269
+ const unquoteAttrValue = (value) => {
270
+ if (typeof value !== 'string' || value.length < 2) return value || ''
271
+ const first = value[0]
272
+ const last = value[value.length - 1]
273
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
274
+ return value.slice(1, -1).replace(/\\(["'\\])/g, '$1')
275
+ }
276
+ return value
277
+ }
278
+
279
+ const parseImageAttrs = (raw) => {
280
+ const parts = splitImageAttrParts(raw)
281
+ if (!parts || parts.length === 0) return null
123
282
  const attrs = []
124
- const parts = raw.split(/ +/)
125
283
  for (let i = 0; i < parts.length; i++) {
126
284
  let entry = parts[i]
127
285
  if (!entry) continue
@@ -130,9 +288,15 @@ const parseImageAttrs = (raw) => {
130
288
  } else if (idAttrReg.test(entry)) {
131
289
  entry = entry.replace(idAttrReg, 'id=')
132
290
  }
133
- const imageAttr = entry.match(attrParseReg)
134
- if (!imageAttr || !imageAttr[1]) continue
135
- attrs.push([imageAttr[1], imageAttr[2]])
291
+ const equalIndex = entry.indexOf('=')
292
+ if (equalIndex === -1) {
293
+ if (!attrNameReg.test(entry)) return null
294
+ attrs.push([entry, ''])
295
+ continue
296
+ }
297
+ const name = entry.slice(0, equalIndex)
298
+ if (!name || !attrNameReg.test(name)) return null
299
+ attrs.push([name, unquoteAttrValue(entry.slice(equalIndex + 1))])
136
300
  }
137
301
  return attrs
138
302
  }
@@ -149,20 +313,6 @@ const normalizeAutoLabelNumberSets = (value) => {
149
313
  return normalized
150
314
  }
151
315
 
152
- const buildLabelClassLookup = (opt) => {
153
- const classPrefix = opt.classPrefix ? opt.classPrefix + '-' : ''
154
- const defaultClasses = [classPrefix + 'label']
155
- const withType = (type) => {
156
- if (opt.removeMarkNameInCaptionClass) return defaultClasses
157
- return [classPrefix + type + '-label', ...defaultClasses]
158
- }
159
- return {
160
- img: withType('img'),
161
- table: withType('table'),
162
- default: defaultClasses,
163
- }
164
- }
165
-
166
316
  const shouldApplyLabelNumbering = (captionType, opt) => {
167
317
  const setting = opt.autoLabelNumberSets
168
318
  if (!setting) return false
@@ -241,60 +391,43 @@ const getImageAltText = (token) => {
241
391
 
242
392
  const getImageTitleText = (token) => getTokenAttr(token, 'title')
243
393
 
244
- const detectCaptionLanguage = (text) => {
245
- const target = (text || '').trim()
246
- if (!target) return 'en'
247
- for (let i = 0; i < target.length; i++) {
248
- const char = target[i]
249
- const code = target.charCodeAt(i)
250
- if (isJapaneseCharCode(code)) return 'ja'
251
- if (isSentenceBoundaryChar(char) || char === '\n') break
394
+ const getFallbackStringLabelJoint = (label) => {
395
+ if (!label) return ''
396
+ if (labelTrailingJointReg.test(label)) {
397
+ return asciiLabelReg.test(label) ? ' ' : ''
252
398
  }
253
- return 'en'
254
- }
255
-
256
- const isJapaneseCharCode = (code) => {
257
- return (
258
- (code >= 0x3040 && code <= 0x30ff) || // Hiragana + Katakana
259
- (code >= 0x31f0 && code <= 0x31ff) || // Katakana extensions
260
- (code >= 0x4e00 && code <= 0x9fff) || // CJK Unified Ideographs
261
- (code >= 0xff66 && code <= 0xff9f) // Half-width Katakana
262
- )
399
+ return asciiLabelReg.test(label) ? '. ' : ' '
263
400
  }
264
401
 
265
- const isSentenceBoundaryChar = (char) => {
266
- return char === '.' || char === '!' || char === '?' || char === '。' || char === '!' || char === '?'
267
- }
268
-
269
- const getAutoFallbackLabel = (text) => {
270
- const lang = detectCaptionLanguage(text)
271
- if (lang === 'ja') return fallbackLabelDefaults.ja || fallbackLabelDefaults.en || ''
272
- return fallbackLabelDefaults.en || fallbackLabelDefaults.ja || ''
273
- }
274
-
275
- const getPersistedFallbackLabel = (text, fallbackState) => {
276
- if (!fallbackState) return getAutoFallbackLabel(text)
277
- if (fallbackState.img) return fallbackState.img
278
- const resolved = getAutoFallbackLabel(text)
279
- fallbackState.img = resolved
280
- return resolved
281
- }
282
-
283
- const buildCaptionWithFallback = (text, fallbackOption, fallbackState) => {
402
+ const buildCaptionWithFallback = (text, fallbackOption, mark, markRegState, preferredLanguages) => {
284
403
  const trimmedText = (text || '').trim()
285
404
  if (!fallbackOption) return ''
405
+ if (!trimmedText) return ''
286
406
  let label = ''
407
+ let generatedDefaults = null
287
408
  if (typeof fallbackOption === 'string') {
288
409
  label = fallbackOption.trim()
289
410
  } else if (fallbackOption === true) {
290
- label = getPersistedFallbackLabel(trimmedText, fallbackState)
411
+ generatedDefaults = getGeneratedLabelDefaults(mark, trimmedText, markRegState, preferredLanguages)
412
+ label = generatedDefaults && generatedDefaults.label ? generatedDefaults.label : ''
413
+ }
414
+ if (!label) return fallbackOption === true ? '' : trimmedText
415
+ if (generatedDefaults) {
416
+ return label + (generatedDefaults.joint || '') + (generatedDefaults.space || '') + trimmedText
291
417
  }
292
- if (!label) return trimmedText
293
- const isAsciiLabel = asciiLabelReg.test(label)
294
- if (!trimmedText) {
295
- return isAsciiLabel ? label + '.' : label
418
+ return label + getFallbackStringLabelJoint(label) + trimmedText
419
+ }
420
+
421
+ const validateFallbackCaptionLabelOption = (optionName, fallbackOption, markRegState) => {
422
+ if (typeof fallbackOption !== 'string') return
423
+ const sampleCaption = buildCaptionWithFallback('caption', fallbackOption, 'img', markRegState, null)
424
+ const analysis = analyzeCaptionStart(sampleCaption, {
425
+ markRegState,
426
+ preferredMark: 'img',
427
+ })
428
+ if (!analysis || analysis.mark !== 'img' || analysis.kind !== 'caption') {
429
+ throw new Error(`${optionName} must be a string label recognized as an image caption by p7d-markdown-it-p-captions: ${fallbackOption}`)
296
430
  }
297
- return label + (isAsciiLabel ? '. ' : ' ') + trimmedText
298
431
  }
299
432
 
300
433
  const createAutoCaptionParagraph = (captionText, TokenConstructor) => {
@@ -322,12 +455,18 @@ const getCaptionInlineToken = (tokens, range, caption) => {
322
455
  }
323
456
 
324
457
  const hasClassName = (classAttr, className) => {
325
- const index = classAttr.indexOf(className)
326
- if (index === -1) return false
327
- const end = index + className.length
328
- if (index > 0 && classAttr.charCodeAt(index - 1) > 0x20) return false
329
- if (end < classAttr.length && classAttr.charCodeAt(end) > 0x20) return false
330
- return true
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
331
470
  }
332
471
 
333
472
  const hasAnyClassName = (classAttr, classNames) => {
@@ -420,154 +559,55 @@ const ensureAutoFigureNumbering = (tokens, range, caption, figureNumberState, op
420
559
  updateInlineTokenContent(inlineToken, originalText, newLabelText)
421
560
  }
422
561
 
423
- const matchAutoCaptionText = (text, reg) => {
424
- if (!text || !reg) return ''
562
+ const matchAutoCaptionText = (text, opt, preferredMark = 'img') => {
563
+ if (!text || !opt || !opt.markRegState) return ''
425
564
  const trimmed = text.trim()
426
- if (trimmed && reg.test(trimmed)) return trimmed
565
+ if (!trimmed) return ''
566
+ const analysis = analyzeCaptionStart(trimmed, {
567
+ markRegState: opt.markRegState,
568
+ preferredMark,
569
+ })
570
+ if (analysis) return trimmed
427
571
  return ''
428
572
  }
429
573
 
430
- const getAutoCaptionFromImage = (imageToken, opt, fallbackLabelState) => {
431
- const imgCaptionMarkReg = opt && opt.imgCaptionMarkReg ? opt.imgCaptionMarkReg : null
574
+ const getAutoCaptionFromImage = (imageToken, opt) => {
432
575
  if (!opt.autoCaptionDetection) return ''
433
- if (!imgCaptionMarkReg && !opt.autoAltCaption && !opt.autoTitleCaption) return ''
576
+ if (!opt.autoAltCaption && !opt.autoTitleCaption && !(opt.markRegState && opt.markRegState.markReg && opt.markRegState.markReg.img)) return ''
434
577
 
435
578
  const altText = getImageAltText(imageToken)
436
- let caption = matchAutoCaptionText(altText, imgCaptionMarkReg)
579
+ let caption = matchAutoCaptionText(altText, opt)
437
580
  if (caption) {
438
581
  clearImageAltAttr(imageToken)
439
582
  return caption
440
583
  }
441
584
  if (!caption && opt.autoAltCaption) {
442
585
  const altForFallback = altText || ''
443
- caption = buildCaptionWithFallback(altForFallback, opt.autoAltCaption, fallbackLabelState)
444
- if (imageToken) {
586
+ const fallbackCaption = buildCaptionWithFallback(altForFallback, opt.autoAltCaption, 'img', opt.markRegState, opt.preferredLanguages)
587
+ if (fallbackCaption && imageToken) {
445
588
  clearImageAltAttr(imageToken)
446
589
  }
590
+ caption = fallbackCaption
447
591
  }
448
592
  if (caption) return caption
449
593
 
450
594
  const titleText = getImageTitleText(imageToken)
451
- caption = matchAutoCaptionText(titleText, imgCaptionMarkReg)
595
+ caption = matchAutoCaptionText(titleText, opt)
452
596
  if (caption) {
453
597
  clearImageTitleAttr(imageToken)
454
598
  return caption
455
599
  }
456
600
  if (!caption && opt.autoTitleCaption) {
457
601
  const titleForFallback = titleText || ''
458
- caption = buildCaptionWithFallback(titleForFallback, opt.autoTitleCaption, fallbackLabelState)
459
- if (imageToken) {
602
+ const fallbackCaption = buildCaptionWithFallback(titleForFallback, opt.autoTitleCaption, 'img', opt.markRegState, opt.preferredLanguages)
603
+ if (fallbackCaption && imageToken) {
460
604
  clearImageTitleAttr(imageToken)
461
605
  }
606
+ caption = fallbackCaption
462
607
  }
463
608
  return caption
464
609
  }
465
610
 
466
- const getHtmlReg = (tag) => {
467
- const cached = htmlRegCache.get(tag)
468
- if (cached) return cached
469
- const regexStr = `^<${tag} ?[^>]*?>[\\s\\S]*?<\\/${tag}>(\\n| *?)(<script [^>]*?>(?:<\\/script>)?)? *(\\n|$)`
470
- const reg = new RegExp(regexStr)
471
- htmlRegCache.set(tag, reg)
472
- return reg
473
- }
474
-
475
- const getHtmlDetectionHints = (content) => {
476
- const hasBlueskyHint = content.indexOf('bluesky-embed') !== -1
477
- const hasVideoHint = content.indexOf('<video') !== -1
478
- const hasAudioHint = content.indexOf('<audio') !== -1
479
- const hasIframeHint = content.indexOf('<iframe') !== -1
480
- const hasBlockquoteHint = content.indexOf('<blockquote') !== -1
481
- const hasDivHint = content.indexOf('<div') !== -1
482
- return {
483
- hasBlueskyHint,
484
- hasVideoHint,
485
- hasAudioHint,
486
- hasIframeHint,
487
- hasBlockquoteHint,
488
- hasDivHint,
489
- hasIframeTag: hasIframeHint || (hasDivHint && iframeTagReg.test(content)),
490
- hasBlueskyEmbed: hasBlueskyHint && blueskyEmbedReg.test(content),
491
- }
492
- }
493
-
494
- const hasAnyHtmlDetectionHint = (hints) => {
495
- return !!(
496
- hints.hasBlueskyHint ||
497
- hints.hasVideoHint ||
498
- hints.hasAudioHint ||
499
- hints.hasIframeHint ||
500
- hints.hasBlockquoteHint ||
501
- hints.hasDivHint
502
- )
503
- }
504
-
505
- const appendHtmlBlockNewlineIfNeeded = (token, hasTag) => {
506
- if ((hasTag[2] && hasTag[3] !== '\n') || (hasTag[1] !== '\n' && hasTag[2] === undefined)) {
507
- token.content += '\n'
508
- }
509
- }
510
-
511
- const consumeBlockquoteEmbedScript = (tokens, token, startIndex) => {
512
- let addedCont = ''
513
- let j = startIndex + 1
514
- while (j < tokens.length) {
515
- const nextToken = tokens[j]
516
- if (nextToken.type === 'inline' && endBlockquoteScriptReg.test(nextToken.content)) {
517
- addedCont += nextToken.content + '\n'
518
- if (tokens[j + 1] && tokens[j + 1].type === 'paragraph_close') {
519
- tokens.splice(j + 1, 1)
520
- }
521
- nextToken.content = ''
522
- if (nextToken.children) {
523
- for (let k = 0; k < nextToken.children.length; k++) {
524
- nextToken.children[k].content = ''
525
- }
526
- }
527
- break
528
- }
529
- if (nextToken.type === 'paragraph_open') {
530
- addedCont += '\n'
531
- tokens.splice(j, 1)
532
- continue
533
- }
534
- j++
535
- }
536
- token.content += addedCont
537
- }
538
-
539
- const detectHtmlTagCandidate = (tokens, token, startIndex, detector, hints, sp) => {
540
- if (detector.requiresIframeTag && !hints.hasIframeTag) return ''
541
- const hasTagHint = !!(detector.hintKey && hints[detector.hintKey])
542
- const allowBlueskyFallback = detector.candidate === 'blockquote' && hints.hasBlueskyEmbed
543
- if (!hasTagHint && !allowBlueskyFallback) return ''
544
- const hasTag = hasTagHint ? token.content.match(getHtmlReg(detector.lookupTag)) : null
545
- const isBlueskyBlockquote = detector.candidate === 'blockquote' && !hasTag && hints.hasBlueskyEmbed
546
- if (!hasTag && !isBlueskyBlockquote) return ''
547
- if (hasTag) {
548
- appendHtmlBlockNewlineIfNeeded(token, hasTag)
549
- if (detector.setVideoIframe) {
550
- sp.isVideoIframe = true
551
- }
552
- return detector.matchedTag || detector.candidate
553
- }
554
- consumeBlockquoteEmbedScript(tokens, token, startIndex)
555
- return 'blockquote'
556
- }
557
-
558
- const isIframeTypeEmbedBlockquote = (content) => {
559
- return content.indexOf('class="') !== -1 && classNameReg.test(content)
560
- }
561
-
562
- const resolveHtmlWrapWithoutCaption = (matchedTag, sp, opt) => {
563
- const htmlWrapWithoutCaption = opt.htmlWrapWithoutCaption
564
- if (!htmlWrapWithoutCaption) return false
565
- if (matchedTag === 'blockquote') {
566
- return !!(sp.isIframeTypeBlockquote && htmlWrapWithoutCaption.iframeTypeBlockquote)
567
- }
568
- return !!htmlWrapWithoutCaption[matchedTag]
569
- }
570
-
571
611
  const checkPrevCaption = (tokens, n, caption, sp, opt, captionState) => {
572
612
  if(n < 3) return caption
573
613
  const captionStartToken = tokens[n-3]
@@ -579,11 +619,11 @@ const checkPrevCaption = (tokens, n, caption, sp, opt, captionState) => {
579
619
  const captionName = sp && sp.captionDecision ? sp.captionDecision.mark : ''
580
620
  if(!captionName) {
581
621
  if (opt.labelPrefixMarkerWithoutLabelPrevReg) {
582
- const markerMatch = getLabelPrefixMarkerMatch(captionInlineToken, opt.labelPrefixMarkerWithoutLabelPrevReg)
583
- if (markerMatch) {
584
- stripLabelPrefixMarkerFromInline(captionInlineToken, markerMatch)
585
- caption.isPrev = true
586
- }
622
+ const markerMatch = getLabelPrefixMarkerMatch(captionInlineToken, opt.labelPrefixMarkerWithoutLabelPrevReg)
623
+ if (markerMatch) {
624
+ stripLabelPrefixMarker(captionInlineToken, markerMatch)
625
+ caption.isPrev = true
626
+ }
587
627
  }
588
628
  return
589
629
  }
@@ -603,11 +643,11 @@ const checkNextCaption = (tokens, en, caption, sp, opt, captionState) => {
603
643
  const captionName = sp && sp.captionDecision ? sp.captionDecision.mark : ''
604
644
  if(!captionName) {
605
645
  if (opt.labelPrefixMarkerWithoutLabelNextReg) {
606
- const markerMatch = getLabelPrefixMarkerMatch(captionInlineToken, opt.labelPrefixMarkerWithoutLabelNextReg)
607
- if (markerMatch) {
608
- stripLabelPrefixMarkerFromInline(captionInlineToken, markerMatch)
609
- caption.isNext = true
610
- }
646
+ const markerMatch = getLabelPrefixMarkerMatch(captionInlineToken, opt.labelPrefixMarkerWithoutLabelNextReg)
647
+ if (markerMatch) {
648
+ stripLabelPrefixMarker(captionInlineToken, markerMatch)
649
+ caption.isNext = true
650
+ }
611
651
  }
612
652
  return
613
653
  }
@@ -676,59 +716,117 @@ const changePrevCaptionPosition = (tokens, n, caption, opt) => {
676
716
  const captionStartToken = tokens[n-3]
677
717
  const captionInlineToken = tokens[n-2]
678
718
  const captionEndToken = tokens[n-1]
719
+ const figureBaseLevel = getTokenLevel(tokens[n])
679
720
 
680
721
  cleanCaptionTokenAttrs(captionStartToken, caption.name, opt)
681
722
  captionStartToken.type = 'figcaption_open'
682
723
  captionStartToken.tag = 'figcaption'
724
+ captionStartToken.block = true
725
+ captionStartToken.level = figureBaseLevel + 1
726
+ captionInlineToken.level = figureBaseLevel + 2
683
727
  captionEndToken.type = 'figcaption_close'
684
728
  captionEndToken.tag = 'figcaption'
729
+ captionEndToken.block = true
730
+ captionEndToken.level = figureBaseLevel + 1
685
731
  tokens.splice(n + 2, 0, captionStartToken, captionInlineToken, captionEndToken)
686
732
  tokens.splice(n-3, 3)
687
733
  return true
688
734
  }
689
735
 
690
736
  const changeNextCaptionPosition = (tokens, en, caption, opt) => {
691
- const captionStartToken = tokens[en+2] // +1: text node for figure.
692
- const captionInlineToken = tokens[en+3]
693
- const captionEndToken = tokens[en+4]
737
+ const captionStartToken = tokens[en+1]
738
+ const captionInlineToken = tokens[en+2]
739
+ const captionEndToken = tokens[en+3]
740
+ const figureBaseLevel = getTokenLevel(tokens[en])
694
741
  cleanCaptionTokenAttrs(captionStartToken, caption.name, opt)
695
742
  captionStartToken.type = 'figcaption_open'
696
743
  captionStartToken.tag = 'figcaption'
744
+ captionStartToken.block = true
745
+ captionStartToken.level = figureBaseLevel + 1
746
+ captionInlineToken.level = figureBaseLevel + 2
697
747
  captionEndToken.type = 'figcaption_close'
698
748
  captionEndToken.tag = 'figcaption'
749
+ captionEndToken.block = true
750
+ captionEndToken.level = figureBaseLevel + 1
699
751
  tokens.splice(en, 0, captionStartToken, captionInlineToken, captionEndToken)
700
- tokens.splice(en+5, 3)
752
+ tokens.splice(en+4, 3)
701
753
  return true
702
754
  }
703
755
 
756
+ const getTokenMap = (token) => {
757
+ return token && Array.isArray(token.map) && token.map.length === 2 ? token.map : null
758
+ }
759
+
760
+ const findNearestMapInRange = (tokens, start, end, step) => {
761
+ let i = start
762
+ while (step > 0 ? i <= end : i >= end) {
763
+ const map = getTokenMap(tokens[i])
764
+ if (map) return map
765
+ i += step
766
+ }
767
+ return null
768
+ }
769
+
770
+ const getRangeMap = (tokens, start, end) => {
771
+ const startMap = getTokenMap(tokens[start]) || findNearestMapInRange(tokens, start, end, 1)
772
+ if (!startMap) return null
773
+ const endMap = getTokenMap(tokens[end]) || findNearestMapInRange(tokens, end, start, -1) || startMap
774
+ const startLine = startMap[0]
775
+ 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
+ return [startLine, endLine]
780
+ }
781
+
782
+ const getTokenLevel = (token, fallback = 0) => {
783
+ return token && typeof token.level === 'number' ? token.level : fallback
784
+ }
785
+
786
+ const adjustTokenLevels = (tokens, start, end, delta) => {
787
+ if (!delta) return
788
+ for (let i = start; i <= end; i++) {
789
+ const token = tokens[i]
790
+ if (token && typeof token.level === 'number') {
791
+ token.level += delta
792
+ }
793
+ }
794
+ }
795
+
704
796
  const wrapWithFigure = (tokens, range, checkTokenTagName, caption, replaceInsteadOfWrap, sp, opt, TokenConstructor) => {
705
797
  let n = range.start
706
798
  let en = range.end
799
+ const baseLevel = getTokenLevel(tokens[n])
800
+ const childLevel = baseLevel + 1
707
801
  const figureStartToken = new TokenConstructor('figure_open', 'figure', 1)
708
802
  const figureClassName = sp.figureClassName || resolveFigureClassName(checkTokenTagName, sp, opt)
709
803
  figureStartToken.attrSet('class', figureClassName)
804
+ figureStartToken.block = true
805
+ figureStartToken.level = baseLevel
710
806
 
711
807
  if (opt.roleDocExample && (checkTokenTagName === 'pre-code' || checkTokenTagName === 'pre-samp')) {
712
808
  figureStartToken.attrSet('role', 'doc-example')
713
809
  }
714
810
  const figureEndToken = new TokenConstructor('figure_close', 'figure', -1)
715
- const rangeStartMap = tokens[n] && Array.isArray(tokens[n].map) && tokens[n].map.length === 2
716
- ? tokens[n].map
717
- : null
718
- const rangeEndMap = tokens[en] && Array.isArray(tokens[en].map) && tokens[en].map.length === 2
719
- ? tokens[en].map
720
- : rangeStartMap
721
- if (rangeStartMap) {
722
- figureStartToken.map = [rangeStartMap[0], rangeStartMap[1]]
723
- }
724
- if (rangeEndMap) {
725
- figureEndToken.map = [rangeEndMap[0], rangeEndMap[1]]
811
+ figureEndToken.block = true
812
+ figureEndToken.level = baseLevel
813
+ const rangeMap = getRangeMap(tokens, n, en)
814
+ if (rangeMap) {
815
+ figureStartToken.map = [rangeMap[0], rangeMap[1]]
816
+ figureEndToken.map = [rangeMap[0], rangeMap[1]]
726
817
  }
727
818
  const createBreakToken = () => {
728
819
  const breakToken = new TokenConstructor('text', '', 0)
729
820
  breakToken.content = '\n'
821
+ breakToken.level = childLevel
730
822
  return breakToken
731
823
  }
824
+ const createEmptyTextToken = () => {
825
+ const emptyToken = new TokenConstructor('text', '', 0)
826
+ emptyToken.content = ''
827
+ emptyToken.level = childLevel
828
+ return emptyToken
829
+ }
732
830
  if (caption.name === 'img') {
733
831
  const joinAttrs = (attrs) => {
734
832
  if (!attrs || attrs.length === 0) return
@@ -743,12 +841,13 @@ const wrapWithFigure = (tokens, range, checkTokenTagName, caption, replaceInstea
743
841
  joinAttrs(tokens[n].attrs)
744
842
  }
745
843
  if (replaceInsteadOfWrap) {
746
- tokens.splice(en, 1, createBreakToken(), figureEndToken, createBreakToken())
747
- tokens.splice(n, 1, figureStartToken, createBreakToken())
844
+ tokens.splice(en, 1, createBreakToken(), figureEndToken)
845
+ tokens.splice(n, 1, figureStartToken, createEmptyTextToken())
748
846
  en = en + 2
749
847
  } else {
750
- tokens.splice(en+1, 0, figureEndToken, createBreakToken())
751
- tokens.splice(n, 0, figureStartToken, createBreakToken())
848
+ adjustTokenLevels(tokens, n, en, 1)
849
+ tokens.splice(en+1, 0, figureEndToken)
850
+ tokens.splice(n, 0, figureStartToken, createEmptyTextToken())
752
851
  en = en + 3
753
852
  }
754
853
  range.start = n
@@ -852,38 +951,6 @@ const detectFenceToken = (token, n, caption) => {
852
951
  }
853
952
  }
854
953
 
855
- const detectHtmlBlockToken = (tokens, token, n, caption, sp, opt) => {
856
- if (!token || token.type !== 'html_block') return null
857
- const hints = getHtmlDetectionHints(token.content)
858
- if (!hasAnyHtmlDetectionHint(hints)) return null
859
- let matchedTag = ''
860
- for (let i = 0; i < HTML_TAG_DETECTORS.length; i++) {
861
- matchedTag = detectHtmlTagCandidate(tokens, token, n, HTML_TAG_DETECTORS[i], hints, sp)
862
- if (matchedTag) break
863
- }
864
- if (!matchedTag) return null
865
- if (matchedTag === 'blockquote') {
866
- if (isIframeTypeEmbedBlockquote(token.content)) {
867
- sp.isIframeTypeBlockquote = true
868
- } else {
869
- return null
870
- }
871
- }
872
- if (matchedTag === 'iframe' && videoIframeReg.test(token.content)) {
873
- sp.isVideoIframe = true
874
- }
875
- caption.name = matchedTag
876
- const wrapWithoutCaption = resolveHtmlWrapWithoutCaption(matchedTag, sp, opt)
877
- return {
878
- type: 'html',
879
- tagName: matchedTag,
880
- en: n,
881
- replaceInsteadOfWrap: false,
882
- wrapWithoutCaption,
883
- canWrap: true,
884
- }
885
- }
886
-
887
954
  const hasLeadingImageChild = (token) => {
888
955
  return !!(token &&
889
956
  token.type === 'inline' &&
@@ -896,7 +963,7 @@ const hasLeadingImageChild = (token) => {
896
963
  const detectImageParagraph = (nextToken, n, caption, sp, opt) => {
897
964
  const multipleImagesEnabled = !!opt.multipleImages
898
965
  const styleProcessEnabled = !!opt.styleProcess
899
- const allowSingleImageWithoutCaption = !!opt.oneImageWithoutCaption
966
+ const allowImageParagraphWithoutCaption = !!opt.imageOnlyParagraphWithoutCaption
900
967
  const children = nextToken.children
901
968
  const imageToken = children[0]
902
969
  const childrenLength = children.length
@@ -911,7 +978,7 @@ const detectImageParagraph = (nextToken, n, caption, sp, opt) => {
911
978
  tagName: 'img',
912
979
  en: n + 2,
913
980
  replaceInsteadOfWrap: true,
914
- wrapWithoutCaption: allowSingleImageWithoutCaption,
981
+ wrapWithoutCaption: allowImageParagraphWithoutCaption,
915
982
  canWrap: true,
916
983
  imageToken,
917
984
  }
@@ -935,13 +1002,13 @@ const detectImageParagraph = (nextToken, n, caption, sp, opt) => {
935
1002
  const imageAttrs = rawContent.match(imageAttrsReg)
936
1003
  if (imageAttrs) {
937
1004
  const parsedAttrs = parseImageAttrs(imageAttrs[1])
938
- if (parsedAttrs && parsedAttrs.length) {
1005
+ if (parsedAttrs) {
939
1006
  for (let i = 0; i < parsedAttrs.length; i++) {
940
1007
  sp.attrs.push(parsedAttrs[i])
941
1008
  }
942
1009
  child.content = ''
1010
+ break
943
1011
  }
944
- break
945
1012
  }
946
1013
  }
947
1014
  if (typeof rawContent === 'string' && rawContent.trim()) {
@@ -992,7 +1059,7 @@ const detectImageParagraph = (nextToken, n, caption, sp, opt) => {
992
1059
  tagName,
993
1060
  en,
994
1061
  replaceInsteadOfWrap: true,
995
- wrapWithoutCaption: isValid && allowSingleImageWithoutCaption,
1062
+ wrapWithoutCaption: isValid && allowImageParagraphWithoutCaption,
996
1063
  canWrap: isValid,
997
1064
  imageToken,
998
1065
  }
@@ -1004,15 +1071,19 @@ const figureWithCaption = (state, opt) => {
1004
1071
  table: 0,
1005
1072
  }
1006
1073
 
1007
- const fallbackLabelState = {
1008
- img: null,
1009
- }
1010
-
1011
1074
  const captionState = { tokens: state.tokens, Token: state.Token }
1012
- figureWithCaptionCore(state.tokens, opt, figureNumberState, fallbackLabelState, state.Token, captionState, null, 0)
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)
1082
+ }
1083
+ figureWithCaptionCore(state.tokens, renderOpt, figureNumberState, state.Token, captionState, null, 0)
1013
1084
  }
1014
1085
 
1015
- const figureWithCaptionCore = (tokens, opt, figureNumberState, fallbackLabelState, TokenConstructor, captionState, parentType = null, startIndex = 0) => {
1086
+ const figureWithCaptionCore = (tokens, opt, figureNumberState, TokenConstructor, captionState, parentType = null, startIndex = 0) => {
1016
1087
  const rRange = { start: startIndex, end: startIndex }
1017
1088
  const rCaption = {
1018
1089
  name: '', nameSuffix: '', isPrev: false, isNext: false
@@ -1030,7 +1101,7 @@ const figureWithCaptionCore = (tokens, opt, figureNumberState, fallbackLabelStat
1030
1101
  const containerType = getNestedContainerType(token)
1031
1102
 
1032
1103
  if (containerType && containerType !== 'blockquote') {
1033
- const closeIndex = figureWithCaptionCore(tokens, opt, figureNumberState, fallbackLabelState, TokenConstructor, captionState, containerType, n + 1)
1104
+ const closeIndex = figureWithCaptionCore(tokens, opt, figureNumberState, TokenConstructor, captionState, containerType, n + 1)
1034
1105
  n = (typeof closeIndex === 'number' ? closeIndex : n) + 1
1035
1106
  continue
1036
1107
  }
@@ -1055,7 +1126,12 @@ const figureWithCaptionCore = (tokens, opt, figureNumberState, fallbackLabelStat
1055
1126
  resetRangeState(rRange, n)
1056
1127
  resetCaptionState(rCaption)
1057
1128
  resetSpecialState(rSp)
1058
- detection = detectHtmlBlockToken(tokens, token, n, rCaption, rSp, opt)
1129
+ detection = detectHtmlFigureCandidate(tokens, token, n, opt.htmlWrapWithoutCaption)
1130
+ if (detection) {
1131
+ rCaption.name = detection.tagName
1132
+ rSp.isVideoIframe = !!detection.isVideoIframe
1133
+ rSp.isIframeTypeBlockquote = !!detection.isIframeTypeBlockquote
1134
+ }
1059
1135
  } else if (tokenType === 'fence') {
1060
1136
  resetRangeState(rRange, n)
1061
1137
  resetCaptionState(rCaption)
@@ -1070,7 +1146,7 @@ const figureWithCaptionCore = (tokens, opt, figureNumberState, fallbackLabelStat
1070
1146
 
1071
1147
  if (!detection) {
1072
1148
  if (containerType === 'blockquote') {
1073
- const closeIndex = figureWithCaptionCore(tokens, opt, figureNumberState, fallbackLabelState, TokenConstructor, captionState, containerType, n + 1)
1149
+ const closeIndex = figureWithCaptionCore(tokens, opt, figureNumberState, TokenConstructor, captionState, containerType, n + 1)
1074
1150
  n = (typeof closeIndex === 'number' ? closeIndex : n) + 1
1075
1151
  } else {
1076
1152
  n++
@@ -1087,7 +1163,7 @@ const figureWithCaptionCore = (tokens, opt, figureNumberState, fallbackLabelStat
1087
1163
  let hasCaption = rCaption.isPrev || rCaption.isNext
1088
1164
  let pendingAutoCaption = ''
1089
1165
  if (!hasCaption && detection.type === 'image' && opt.autoCaptionDetection) {
1090
- pendingAutoCaption = getAutoCaptionFromImage(detection.imageToken, opt, fallbackLabelState)
1166
+ pendingAutoCaption = getAutoCaptionFromImage(detection.imageToken, opt)
1091
1167
  if (pendingAutoCaption) {
1092
1168
  hasCaption = true
1093
1169
  }
@@ -1096,7 +1172,7 @@ const figureWithCaptionCore = (tokens, opt, figureNumberState, fallbackLabelStat
1096
1172
  if (detection.canWrap === false) {
1097
1173
  let nextIndex = rRange.end + 1
1098
1174
  if (containerType === 'blockquote') {
1099
- const closeIndex = figureWithCaptionCore(tokens, opt, figureNumberState, fallbackLabelState, TokenConstructor, captionState, containerType, rRange.start + 1)
1175
+ const closeIndex = figureWithCaptionCore(tokens, opt, figureNumberState, TokenConstructor, captionState, containerType, rRange.start + 1)
1100
1176
  nextIndex = Math.max(nextIndex, (typeof closeIndex === 'number' ? closeIndex : rRange.end) + 1)
1101
1177
  }
1102
1178
  n = nextIndex
@@ -1151,7 +1227,7 @@ const figureWithCaptionCore = (tokens, opt, figureNumberState, fallbackLabelStat
1151
1227
  }
1152
1228
 
1153
1229
  if (containerType === 'blockquote') {
1154
- const closeIndex = figureWithCaptionCore(tokens, opt, figureNumberState, fallbackLabelState, TokenConstructor, captionState, containerType, rRange.start + 1)
1230
+ const closeIndex = figureWithCaptionCore(tokens, opt, figureNumberState, TokenConstructor, captionState, containerType, rRange.start + 1)
1155
1231
  const fallbackIndex = rCaption.name ? rRange.end : n
1156
1232
  nextIndex = Math.max(nextIndex, (typeof closeIndex === 'number' ? closeIndex : fallbackIndex) + 1)
1157
1233
  }
@@ -1165,13 +1241,15 @@ const mditFigureWithPCaption = (md, option) => {
1165
1241
  let opt = {
1166
1242
  // Caption languages delegated to p-captions.
1167
1243
  languages: ['en', 'ja'],
1244
+ preferredLanguages: null, // compatibility tie-break for generated fallback labels; prefer env.locale / env.preferredLocales per render
1168
1245
 
1169
1246
  // --- figure-wrapper behavior ---
1170
1247
  classPrefix: 'f',
1171
1248
  figureClassThatWrapsIframeTypeBlockquote: null,
1172
1249
  figureClassThatWrapsSlides: null,
1173
1250
  styleProcess: true,
1174
- oneImageWithoutCaption: false,
1251
+ imageOnlyParagraphWithoutCaption: false,
1252
+ oneImageWithoutCaption: false, // legacy alias for imageOnlyParagraphWithoutCaption
1175
1253
  iframeWithoutCaption: false,
1176
1254
  videoWithoutCaption: false,
1177
1255
  audioWithoutCaption: false,
@@ -1210,20 +1288,27 @@ const mditFigureWithPCaption = (md, option) => {
1210
1288
  figureToLabelClassMap: null,
1211
1289
  }
1212
1290
  const hasExplicitAutoLabelNumberSets = option && Object.prototype.hasOwnProperty.call(option, 'autoLabelNumberSets')
1291
+ const hasExplicitImageOnlyParagraphWithoutCaption = option && Object.prototype.hasOwnProperty.call(option, 'imageOnlyParagraphWithoutCaption')
1213
1292
  const hasExplicitFigureClassThatWrapsIframeTypeBlockquote = option && Object.prototype.hasOwnProperty.call(option, 'figureClassThatWrapsIframeTypeBlockquote')
1214
1293
  const hasExplicitFigureClassThatWrapsSlides = option && Object.prototype.hasOwnProperty.call(option, 'figureClassThatWrapsSlides')
1215
1294
  const hasExplicitLabelClassFollowsFigure = option && Object.prototype.hasOwnProperty.call(option, 'labelClassFollowsFigure')
1216
1295
  if (option) Object.assign(opt, option)
1296
+ opt.imageOnlyParagraphWithoutCaption = hasExplicitImageOnlyParagraphWithoutCaption
1297
+ ? !!opt.imageOnlyParagraphWithoutCaption
1298
+ : !!opt.oneImageWithoutCaption
1299
+ opt.oneImageWithoutCaption = !!opt.oneImageWithoutCaption
1217
1300
  if (!hasExplicitLabelClassFollowsFigure && opt.figureToLabelClassMap) {
1218
1301
  opt.labelClassFollowsFigure = true
1219
1302
  }
1220
1303
  opt.classPrefix = normalizeOptionalClassName(opt.classPrefix)
1221
1304
  opt.allIframeTypeFigureClassName = normalizeOptionalClassName(opt.allIframeTypeFigureClassName)
1222
- opt.languages = normalizeLanguages(opt.languages)
1223
1305
  opt.markRegState = getMarkRegStateForLanguages(opt.languages)
1224
- opt.imgCaptionMarkReg = opt.markRegState && opt.markRegState.markReg
1225
- ? opt.markRegState.markReg.img
1226
- : null
1306
+ opt.preferredLanguages = normalizePreferredLanguages(opt.preferredLanguages, opt.markRegState.languages)
1307
+ if (opt.preferredLanguages.length === 0) opt.preferredLanguages = null
1308
+ opt.normalizedOptionLanguages = normalizePreferredLanguages(opt.languages, opt.markRegState.languages)
1309
+ opt.shouldResolvePreferredLanguages = needsPreferredLanguagesResolution(opt)
1310
+ validateFallbackCaptionLabelOption('autoAltCaption', opt.autoAltCaption, opt.markRegState)
1311
+ validateFallbackCaptionLabelOption('autoTitleCaption', opt.autoTitleCaption, opt.markRegState)
1227
1312
  opt.htmlWrapWithoutCaption = {
1228
1313
  iframe: !!opt.iframeWithoutCaption,
1229
1314
  video: !!opt.videoWithoutCaption,
@@ -1260,12 +1345,12 @@ const mditFigureWithPCaption = (md, option) => {
1260
1345
  // Precompute label-class permutations so numbering lookup doesn't rebuild arrays per caption.
1261
1346
  opt.labelClassLookup = buildLabelClassLookup(opt)
1262
1347
  const markerList = normalizeLabelPrefixMarkers(opt.labelPrefixMarker)
1263
- opt.labelPrefixMarkerReg = buildLabelPrefixMarkerRegFromList(markerList)
1348
+ opt.labelPrefixMarkerReg = buildLabelPrefixMarkerRegFromMarkers(markerList)
1264
1349
  opt.cleanCaptionRegCache = new Map()
1265
1350
  if (opt.allowLabelPrefixMarkerWithoutLabel === true) {
1266
1351
  const markerPair = resolveLabelPrefixMarkerPair(markerList)
1267
- opt.labelPrefixMarkerWithoutLabelPrevReg = buildLabelPrefixMarkerRegFromList(markerPair.prev)
1268
- opt.labelPrefixMarkerWithoutLabelNextReg = buildLabelPrefixMarkerRegFromList(markerPair.next)
1352
+ opt.labelPrefixMarkerWithoutLabelPrevReg = buildLabelPrefixMarkerRegFromMarkers(markerPair.prev)
1353
+ opt.labelPrefixMarkerWithoutLabelNextReg = buildLabelPrefixMarkerRegFromMarkers(markerPair.next)
1269
1354
  } else {
1270
1355
  opt.labelPrefixMarkerWithoutLabelPrevReg = null
1271
1356
  opt.labelPrefixMarkerWithoutLabelNextReg = null