@peaceroad/markdown-it-figure-with-p-caption 0.16.1 → 0.17.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/README.md CHANGED
@@ -15,10 +15,10 @@ Optionally, you can auto-number image and table caption paragraphs starting from
15
15
  - Pure image paragraphs (`![...](...)`) become `<figure class="f-img">` blocks as soon as a caption paragraph (previous or next) or an auto-detected caption exists.
16
16
  - Auto detection runs per image paragraph when `autoCaptionDetection` is `true` (default). The priority is:
17
17
  1. Caption paragraphs immediately before or after the image (standard syntax).
18
- 2. Image `alt` text that already matches p7d-markdown-it-p-captions label formats (`Figure. `, `Figure 1. `, `図 `,`図1 `, etc.).
18
+ 2. Image `alt` text that `p7d-markdown-it-p-captions` recognizes as an image caption start (`Figure. `, `Figure 1. `, `図 `, `図1 `, etc.).
19
19
  3. Image `title` attribute that matches the same labels.
20
20
  4. Optional fallbacks (`autoAltCaption`, `autoTitleCaption`) that inject the label when the alt/title lacks one.
21
- - `autoAltCaption`: `false` (default), `true`, or a string label. `true` inspects the first sentence of the caption text and picks `Figure` / `図` based on detected language; a string uses that label verbatim.
21
+ - `autoAltCaption`: `false` (default), `true`, or a string label. `true` uses locale-aware generated-label defaults from `p7d-markdown-it-p-captions`, so the label text and punctuation stay aligned with the upstream caption language data. A string uses that label verbatim. Empty alt text does not generate a fallback caption.
22
22
  - `autoTitleCaption`: same behavior but sourced from the image `title`. It stays off by default so other plugins can keep using the `title` attribute for metadata.
23
23
  - Set `autoCaptionDetection: false` to disable the auto-caption workflow entirely.
24
24
  - Multi-image paragraphs are still wrapped as one figure when `multipleImages: true` (default). Layout-specific classes help with styling:
@@ -50,9 +50,9 @@ Optionally, you can auto-number image and table caption paragraphs starting from
50
50
 
51
51
  ### Embedded content by iframe
52
52
 
53
- - Inline HTML `<iframe>` elements become `<figure class="f-video">` when they point to known video hosts (YouTube `youtube.com` / `youtube-nocookie.com`, Vimeo `player.vimeo.com`).
53
+ - Inline HTML `<iframe>` elements become `<figure class="f-video">` when they point to known video hosts (YouTube `youtube-nocookie.com`, Vimeo `player.vimeo.com`).
54
54
  - `<div>` wrappers are treated as iframe-type embeds only when the same HTML block contains an `<iframe ...>` tag (for example common video wrapper markup).
55
- - Blockquote-based social embeds (Twitter/X `twitter-tweet`, Mastodon `mastodon-embed`, Bluesky `bluesky-embed`, Instagram `instagram-media`, Tumblr `text-post-media`) are treated like iframe-type embeds when their `class` matches those providers. By default they become `<figure class="f-img">` so the caption label behaves like an image label (Labels can also use quote labels). You can override that figure class with `figureClassThatWrapsIframeTypeBlockquote` or the global `allIframeTypeFigureClassName`.
55
+ - Blockquote-based social embeds (Twitter/X `twitter-tweet`, Mastodon `mastodon-embed`, Bluesky `bluesky-embed`, Instagram `instagram-media`, Tumblr `text-post-media`) are treated like iframe-type embeds when their class list contains one of those provider classes. Extra classes on the same blockquote do not block detection. By default they become `<figure class="f-img">` so the caption label behaves like an image label (Labels can also use quote labels). You can override that figure class with `figureClassThatWrapsIframeTypeBlockquote` or the global `allIframeTypeFigureClassName`.
56
56
  - `p7d-markdown-it-p-captions` ships with a `Slide.` label. When you use it (for example with Speaker Deck or SlideShare iframes), the `<figure>` wrapper automatically switches to `f-slide` (or whatever you set via `figureClassThatWrapsSlides`) so slides can get their own layout. If `allIframeTypeFigureClassName` is also configured, that class takes precedence even for slides, so you get a uniform embed wrapper without touching the slide option.
57
57
  - All other iframes fall back to `<figure class="f-iframe">` unless you override the class via `allIframeTypeFigureClassName`.
58
58
 
@@ -85,13 +85,15 @@ Every option below is forwarded verbatim to `p7d-markdown-it-p-captions`, which
85
85
  - `strongFilename` / `dquoteFilename`: pull out filenames from captions using `**filename**` or `"filename"` syntax and wrap them in `<strong class="f-*-filename">`.
86
86
  - `jointSpaceUseHalfWidth`: replace full-width space between Japanese labels and caption body with half-width space.
87
87
  - `bLabel` / `strongLabel`: emphasize the label span itself.
88
- - `removeUnnumberedLabel`: drop the leading “Figure. Etext entirely when no label number is present. Use `removeUnnumberedLabelExceptMarks` to keep specific labels (e.g., `['blockquote']` keeps `Quote. `).
88
+ - `removeUnnumberedLabel`: drop the leading label entirely when no label number is present. Use `removeUnnumberedLabelExceptMarks` to keep specific labels (e.g., `['blockquote']` keeps `Quote. `).
89
89
  - `removeMarkNameInCaptionClass`: replace `.f-img-label` / `.f-table-label` with the generic `.f-label`.
90
90
  - `wrapCaptionBody`: wrap the non-label caption text in a span element.
91
91
  - `hasNumClass`: add a class attribute to label span element if it has a label number.
92
92
  - `labelClassFollowsFigure`: mirror the resolved `<figure>` class onto the `figcaption` spans (`f-embed-label`, `f-embed-label-joint`, `f-embed-body`, etc.) when you want captions styled alongside the wrapper.
93
93
  - `figureToLabelClassMap`: extend `labelClassFollowsFigure` by mapping specific figure classes (e.g., `f-embed`) to custom caption label classes such as `caption-embed caption-social` for fine-grained control. When this map is provided and `labelClassFollowsFigure` is not set explicitly, figure-following mode is enabled automatically.
94
94
  - `labelPrefixMarker`: allow a leading marker before labels (string or array, e.g., `*Figure. ...`). Arrays are limited to two markers; extras are ignored.
95
+ - Automatic image-label fallback text and punctuation (`Figure. `, `図 `, etc.) are generated from `p7d-markdown-it-p-captions` locale metadata, not from a local hardcoded map in this plugin.
96
+ - `preferredLanguages`: optional tie-break order for generated fallback labels. When omitted, this plugin derives the order once per render from `env.preferredLanguages`, `env.lang` / `env.locale`, then a cheap document-script heuristic that skips a leading hyphen-fenced frontmatter block (`---` or longer, spaces allowed before newline), and finally the raw `languages` order.
95
97
 
96
98
  ### Automatic numbering
97
99
 
@@ -454,7 +456,7 @@ Figure. Highlighted cat.
454
456
 
455
457
  ### Automatic detection fallbacks
456
458
 
457
- `autoCaptionDetection` combined with `autoAltCaption` / `autoTitleCaption` can still generate caption text even when the original alt/title lacks labels. The corresponding attributes are cleared after conversion so the figcaption becomes the canonical source.
459
+ `autoCaptionDetection` combined with `autoAltCaption` / `autoTitleCaption` can still generate caption text even when the original alt/title lacks labels, as long as the alt/title body is non-empty. The corresponding attributes are cleared after conversion so the figcaption becomes the canonical source. When these fallbacks are `true`, the generated label text and punctuation come from `p7d-markdown-it-p-captions` locale metadata rather than a local hardcoded map.
458
460
 
459
461
  ```
460
462
  [Markdown]
@@ -0,0 +1,178 @@
1
+ import {
2
+ BLOCKQUOTE_EMBED_CLASS_NAMES,
3
+ HTML_EMBED_CANDIDATES,
4
+ VIDEO_IFRAME_HOSTS,
5
+ } from './providers.js'
6
+
7
+ const htmlRegCache = new Map()
8
+ const openingClassAttrReg = /^<[^>]*?\bclass=(?:"([^"]*)"|'([^']*)')/i
9
+ const openingSrcAttrReg = /^<[^>]*?\bsrc=(?:"([^"]*)"|'([^']*)')/i
10
+ const endBlockquoteScriptReg = /<\/blockquote> *<script[^>]*?><\/script>$/
11
+ const iframeTagReg = /<iframe(?=[\s>])/i
12
+
13
+ const getHtmlReg = (tag) => {
14
+ const cached = htmlRegCache.get(tag)
15
+ if (cached) return cached
16
+ const regexStr = `^<${tag} ?[^>]*?>[\\s\\S]*?<\\/${tag}>(\\n| *?)(<script [^>]*?>(?:<\\/script>)?)? *(\\n|$)`
17
+ const reg = new RegExp(regexStr)
18
+ htmlRegCache.set(tag, reg)
19
+ return reg
20
+ }
21
+
22
+ const getHtmlDetectionHints = (content) => {
23
+ const hasBlueskyHint = content.indexOf('bluesky-embed') !== -1
24
+ const hasVideoHint = content.indexOf('<video') !== -1
25
+ const hasAudioHint = content.indexOf('<audio') !== -1
26
+ const hasIframeHint = content.indexOf('<iframe') !== -1
27
+ const hasBlockquoteHint = content.indexOf('<blockquote') !== -1
28
+ const hasDivHint = content.indexOf('<div') !== -1
29
+ return {
30
+ hasBlueskyHint,
31
+ hasVideoHint,
32
+ hasAudioHint,
33
+ hasIframeHint,
34
+ hasBlockquoteHint,
35
+ hasDivHint,
36
+ hasIframeTag: hasIframeHint || (hasDivHint && iframeTagReg.test(content)),
37
+ }
38
+ }
39
+
40
+ const hasAnyHtmlDetectionHint = (hints) => {
41
+ return !!(
42
+ hints.hasBlueskyHint ||
43
+ hints.hasVideoHint ||
44
+ hints.hasAudioHint ||
45
+ hints.hasIframeHint ||
46
+ hints.hasBlockquoteHint ||
47
+ hints.hasDivHint
48
+ )
49
+ }
50
+
51
+ const appendHtmlBlockNewlineIfNeeded = (token, hasTag) => {
52
+ if ((hasTag[2] && hasTag[3] !== '\n') || (hasTag[1] !== '\n' && hasTag[2] === undefined)) {
53
+ token.content += '\n'
54
+ }
55
+ }
56
+
57
+ const consumeBlockquoteEmbedScript = (tokens, token, startIndex) => {
58
+ let addedContent = ''
59
+ let i = startIndex + 1
60
+ while (i < tokens.length) {
61
+ const nextToken = tokens[i]
62
+ if (nextToken.type === 'inline' && endBlockquoteScriptReg.test(nextToken.content)) {
63
+ addedContent += nextToken.content + '\n'
64
+ if (tokens[i + 1] && tokens[i + 1].type === 'paragraph_close') {
65
+ tokens.splice(i + 1, 1)
66
+ }
67
+ nextToken.content = ''
68
+ if (nextToken.children) {
69
+ for (let j = 0; j < nextToken.children.length; j++) {
70
+ nextToken.children[j].content = ''
71
+ }
72
+ }
73
+ break
74
+ }
75
+ if (nextToken.type === 'paragraph_open') {
76
+ addedContent += '\n'
77
+ tokens.splice(i, 1)
78
+ continue
79
+ }
80
+ i++
81
+ }
82
+ token.content += addedContent
83
+ }
84
+
85
+ const getOpeningAttrValue = (content, reg) => {
86
+ if (typeof content !== 'string' || content.charCodeAt(0) !== 0x3c) return ''
87
+ const match = content.match(reg)
88
+ if (!match) return ''
89
+ return match[1] || match[2] || ''
90
+ }
91
+
92
+ const hasKnownBlockquoteEmbedClass = (content) => {
93
+ const classAttr = getOpeningAttrValue(content, openingClassAttrReg)
94
+ if (!classAttr) return false
95
+ let start = 0
96
+ while (start < classAttr.length) {
97
+ while (start < classAttr.length && classAttr.charCodeAt(start) <= 0x20) start++
98
+ if (start >= classAttr.length) break
99
+ let end = start + 1
100
+ while (end < classAttr.length && classAttr.charCodeAt(end) > 0x20) end++
101
+ if (BLOCKQUOTE_EMBED_CLASS_NAMES.has(classAttr.slice(start, end))) return true
102
+ start = end + 1
103
+ }
104
+ return false
105
+ }
106
+
107
+ const isKnownVideoIframe = (content) => {
108
+ const src = getOpeningAttrValue(content, openingSrcAttrReg)
109
+ if (!src || src.slice(0, 8).toLowerCase() !== 'https://') return false
110
+ const slashIndex = src.indexOf('/', 8)
111
+ const host = (slashIndex === -1 ? src.slice(8) : src.slice(8, slashIndex)).toLowerCase()
112
+ return VIDEO_IFRAME_HOSTS.has(host)
113
+ }
114
+
115
+ const detectHtmlTagCandidate = (tokens, token, startIndex, detector, hints, result) => {
116
+ if (detector.requiresIframeTag && !hints.hasIframeTag) return ''
117
+ const hasTagHint = !!(detector.hintKey && hints[detector.hintKey])
118
+ const allowBlueskyFallback = detector.candidate === 'blockquote' && hints.hasBlueskyHint
119
+ if (!hasTagHint && !allowBlueskyFallback) return ''
120
+ const hasTag = hasTagHint ? token.content.match(getHtmlReg(detector.lookupTag)) : null
121
+ const isBlueskyFallback = detector.candidate === 'blockquote' && !hasTag && hints.hasBlueskyHint
122
+ if (!hasTag && !isBlueskyFallback) return ''
123
+ if (hasTag) {
124
+ appendHtmlBlockNewlineIfNeeded(token, hasTag)
125
+ if (detector.treatAsVideoIframe) {
126
+ result.isVideoIframe = true
127
+ }
128
+ return detector.matchedTag || detector.candidate
129
+ }
130
+ consumeBlockquoteEmbedScript(tokens, token, startIndex)
131
+ return 'blockquote'
132
+ }
133
+
134
+ const resolveHtmlWrapWithoutCaption = (matchedTag, result, htmlWrapWithoutCaption) => {
135
+ if (!htmlWrapWithoutCaption) return false
136
+ if (matchedTag === 'blockquote') {
137
+ return !!(result.isIframeTypeBlockquote && htmlWrapWithoutCaption.iframeTypeBlockquote)
138
+ }
139
+ return !!htmlWrapWithoutCaption[matchedTag]
140
+ }
141
+
142
+ export const detectHtmlFigureCandidate = (tokens, token, startIndex, htmlWrapWithoutCaption) => {
143
+ if (!token || token.type !== 'html_block') return null
144
+ const hints = getHtmlDetectionHints(token.content)
145
+ if (!hasAnyHtmlDetectionHint(hints)) return null
146
+
147
+ const result = {
148
+ isVideoIframe: false,
149
+ isIframeTypeBlockquote: false,
150
+ }
151
+
152
+ let matchedTag = ''
153
+ for (let i = 0; i < HTML_EMBED_CANDIDATES.length; i++) {
154
+ matchedTag = detectHtmlTagCandidate(tokens, token, startIndex, HTML_EMBED_CANDIDATES[i], hints, result)
155
+ if (matchedTag) break
156
+ }
157
+ if (!matchedTag) return null
158
+
159
+ if (matchedTag === 'blockquote') {
160
+ if (!hasKnownBlockquoteEmbedClass(token.content)) return null
161
+ result.isIframeTypeBlockquote = true
162
+ }
163
+
164
+ if (matchedTag === 'iframe' && isKnownVideoIframe(token.content)) {
165
+ result.isVideoIframe = true
166
+ }
167
+
168
+ return {
169
+ type: 'html',
170
+ tagName: matchedTag,
171
+ en: startIndex,
172
+ replaceInsteadOfWrap: false,
173
+ wrapWithoutCaption: resolveHtmlWrapWithoutCaption(matchedTag, result, htmlWrapWithoutCaption),
174
+ canWrap: true,
175
+ isVideoIframe: result.isVideoIframe,
176
+ isIframeTypeBlockquote: result.isIframeTypeBlockquote,
177
+ }
178
+ }
@@ -0,0 +1,27 @@
1
+ export const HTML_EMBED_CANDIDATES = Object.freeze([
2
+ { candidate: 'video', lookupTag: 'video', hintKey: 'hasVideoHint' },
3
+ { candidate: 'audio', lookupTag: 'audio', hintKey: 'hasAudioHint' },
4
+ { candidate: 'iframe', lookupTag: 'iframe', hintKey: 'hasIframeHint' },
5
+ { candidate: 'blockquote', lookupTag: 'blockquote', hintKey: 'hasBlockquoteHint' },
6
+ {
7
+ candidate: 'div',
8
+ lookupTag: 'div',
9
+ hintKey: 'hasDivHint',
10
+ requiresIframeTag: true,
11
+ matchedTag: 'iframe',
12
+ treatAsVideoIframe: true,
13
+ },
14
+ ])
15
+
16
+ export const BLOCKQUOTE_EMBED_CLASS_NAMES = new Set([
17
+ 'twitter-tweet',
18
+ 'instagram-media',
19
+ 'text-post-media',
20
+ 'bluesky-embed',
21
+ 'mastodon-embed',
22
+ ])
23
+
24
+ export const VIDEO_IFRAME_HOSTS = new Set([
25
+ 'www.youtube-nocookie.com',
26
+ 'player.vimeo.com',
27
+ ])
package/index.js CHANGED
@@ -1,42 +1,164 @@
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
16
  const attrParseReg = /^(.*?)="?(.*)"?$/
14
17
  const sampLangReg = /^ *(?:samp|shell|console)(?:(?= )|$)/
15
- const endBlockquoteScriptReg = /<\/blockquote> *<script[^>]*?><\/script>$/
16
- const iframeTagReg = /<iframe(?=[\s>])/i
17
18
  const asciiLabelReg = /^[A-Za-z]/
18
19
  const CHECK_TYPE_TOKEN_MAP = {
19
20
  table_open: 'table',
20
21
  pre_open: 'pre',
21
22
  blockquote_open: 'blockquote',
22
23
  }
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
24
  const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
25
+ const normalizeLanguageCode = (value) => {
26
+ if (value === null || value === undefined) return ''
27
+ const normalized = String(value).trim().toLowerCase()
28
+ if (!normalized) return ''
29
+ const separatorIndex = normalized.search(/[-_]/)
30
+ return separatorIndex === -1 ? normalized : normalized.slice(0, separatorIndex)
31
+ }
32
+ const normalizePreferredLanguages = (value, availableLanguages) => {
33
+ if (!Array.isArray(availableLanguages) || availableLanguages.length === 0) return []
34
+ const source = typeof value === 'string' ? [value] : (Array.isArray(value) ? value : [])
35
+ if (source.length === 0) return []
36
+ const allowed = new Set(availableLanguages)
37
+ const languages = []
38
+ const seen = new Set()
39
+ for (let i = 0; i < source.length; i++) {
40
+ const lang = normalizeLanguageCode(source[i])
41
+ if (!lang || seen.has(lang) || !allowed.has(lang)) continue
42
+ seen.add(lang)
43
+ languages.push(lang)
44
+ }
45
+ return languages
46
+ }
47
+ const prioritizeLanguage = (languages, preferredLanguage) => {
48
+ if (!preferredLanguage || languages.length === 0) return languages.slice()
49
+ const prioritized = []
50
+ prioritized.push(preferredLanguage)
51
+ for (let i = 0; i < languages.length; i++) {
52
+ if (languages[i] === preferredLanguage) continue
53
+ prioritized.push(languages[i])
54
+ }
55
+ return prioritized
56
+ }
57
+ const isAsciiAlphaCode = (code) => {
58
+ return (code >= 0x41 && code <= 0x5a) || (code >= 0x61 && code <= 0x7a)
59
+ }
60
+ const isJapaneseCharCode = (code) => {
61
+ return (
62
+ (code >= 0x3040 && code <= 0x30ff) ||
63
+ (code >= 0x31f0 && code <= 0x31ff) ||
64
+ (code >= 0x4e00 && code <= 0x9fff) ||
65
+ (code >= 0xff66 && code <= 0xff9f)
66
+ )
67
+ }
68
+ const isHyphenFenceLine = (src, lineStart) => {
69
+ if (typeof src !== 'string' || lineStart < 0 || lineStart >= src.length) return 0
70
+ let index = lineStart
71
+ let hyphenCount = 0
72
+ while (index < src.length && src.charCodeAt(index) === 0x2d) {
73
+ hyphenCount++
74
+ index++
75
+ }
76
+ if (hyphenCount < 3) return 0
77
+ while (index < src.length && src.charCodeAt(index) === 0x20) {
78
+ index++
79
+ }
80
+ if (index >= src.length || src.charCodeAt(index) !== 0x0a) return 0
81
+ return hyphenCount
82
+ }
83
+ const skipLeadingFrontmatter = (src) => {
84
+ if (typeof src !== 'string' || isHyphenFenceLine(src, 0) === 0) return src
85
+ let lineStart = src.indexOf('\n')
86
+ if (lineStart === -1) return src
87
+ lineStart++
88
+ while (lineStart < src.length) {
89
+ if (isHyphenFenceLine(src, lineStart) > 0) {
90
+ const nextLineStart = src.indexOf('\n', lineStart)
91
+ if (nextLineStart === -1) return ''
92
+ return src.slice(nextLineStart + 1)
93
+ }
94
+ const nextLineStart = src.indexOf('\n', lineStart)
95
+ if (nextLineStart === -1) break
96
+ lineStart = nextLineStart + 1
97
+ }
98
+ return src
99
+ }
100
+ const detectDocumentPrimaryLanguage = (src, availableLanguages) => {
101
+ if (!src || availableLanguages.indexOf('ja') === -1) return ''
102
+ const body = skipLeadingFrontmatter(src)
103
+ const limit = Math.min(body.length, 8192)
104
+ let japaneseCount = 0
105
+ let asciiAlphaCount = 0
106
+ for (let i = 0; i < limit; i++) {
107
+ const code = body.charCodeAt(i)
108
+ if (isJapaneseCharCode(code)) {
109
+ japaneseCount++
110
+ continue
111
+ }
112
+ if (isAsciiAlphaCode(code)) {
113
+ asciiAlphaCount++
114
+ }
115
+ }
116
+ if (japaneseCount === 0) return ''
117
+ if (asciiAlphaCount === 0) return 'ja'
118
+ return japaneseCount * 2 >= asciiAlphaCount ? 'ja' : ''
119
+ }
120
+ const sourceMayNeedPreferredLanguages = (state) => {
121
+ const src = state && typeof state.src === 'string' ? state.src : ''
122
+ return src.indexOf('![') !== -1
123
+ }
124
+ const resolvePreferredLanguagesForState = (state, opt) => {
125
+ const availableLanguages = (
126
+ opt &&
127
+ opt.markRegState &&
128
+ Array.isArray(opt.markRegState.languages)
129
+ ) ? opt.markRegState.languages : []
130
+ if (availableLanguages.length === 0) return []
131
+
132
+ const explicitPreferred = opt && Array.isArray(opt.preferredLanguages)
133
+ ? opt.preferredLanguages
134
+ : []
135
+ if (explicitPreferred.length > 0) return explicitPreferred
136
+
137
+ const optionLanguages = opt && Array.isArray(opt.normalizedOptionLanguages)
138
+ ? opt.normalizedOptionLanguages
139
+ : []
140
+ const baseLanguages = optionLanguages.length > 0 ? optionLanguages : availableLanguages
141
+ const env = state && state.env ? state.env : null
142
+ const envPreferred = normalizePreferredLanguages(env && env.preferredLanguages, availableLanguages)
143
+ if (envPreferred.length > 0) return envPreferred
144
+
145
+ const envLanguage = normalizeLanguageCode(env && (env.preferredLanguage || env.lang || env.language || env.locale))
146
+ if (envLanguage && baseLanguages.indexOf(envLanguage) !== -1) {
147
+ return prioritizeLanguage(baseLanguages, envLanguage)
148
+ }
149
+
150
+ const detectedLanguage = detectDocumentPrimaryLanguage(state && state.src ? state.src : '', baseLanguages)
151
+ if (detectedLanguage) {
152
+ return prioritizeLanguage(baseLanguages, detectedLanguage)
153
+ }
154
+ return baseLanguages
155
+ }
156
+ const needsPreferredLanguagesResolution = (opt) => {
157
+ if (!opt || !opt.markRegState || !Array.isArray(opt.markRegState.languages)) return false
158
+ if (opt.markRegState.languages.length <= 1) return false
159
+ if (Array.isArray(opt.preferredLanguages) && opt.preferredLanguages.length > 0) return false
160
+ return opt.autoAltCaption === true || opt.autoTitleCaption === true
161
+ }
40
162
  const normalizeOptionalClassName = (value) => {
41
163
  if (value === null || value === undefined) return ''
42
164
  const normalized = String(value).trim()
@@ -50,36 +172,6 @@ const normalizeClassOptionWithFallback = (value, fallbackValue) => {
50
172
  const normalized = normalizeOptionalClassName(value)
51
173
  return normalized || fallbackValue
52
174
  }
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
175
  const resolveLabelPrefixMarkerPair = (markers) => {
84
176
  if (!markers || markers.length === 0) return { prev: [], next: [] }
85
177
  if (markers.length === 1) {
@@ -87,26 +179,6 @@ const resolveLabelPrefixMarkerPair = (markers) => {
87
179
  }
88
180
  return { prev: [markers[0]], next: [markers[1]] }
89
181
  }
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
182
  const getLabelPrefixMarkerMatch = (inlineToken, markerReg) => {
111
183
  if (!markerReg || !inlineToken || inlineToken.type !== 'inline') return null
112
184
  const content = typeof inlineToken.content === 'string' ? inlineToken.content : ''
@@ -149,20 +221,6 @@ const normalizeAutoLabelNumberSets = (value) => {
149
221
  return normalized
150
222
  }
151
223
 
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
224
  const shouldApplyLabelNumbering = (captionType, opt) => {
167
225
  const setting = opt.autoLabelNumberSets
168
226
  if (!setting) return false
@@ -241,60 +299,23 @@ const getImageAltText = (token) => {
241
299
 
242
300
  const getImageTitleText = (token) => getTokenAttr(token, 'title')
243
301
 
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
252
- }
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
- )
263
- }
264
-
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) => {
302
+ const buildCaptionWithFallback = (text, fallbackOption, mark, markRegState, preferredLanguages) => {
284
303
  const trimmedText = (text || '').trim()
285
304
  if (!fallbackOption) return ''
305
+ if (!trimmedText) return ''
286
306
  let label = ''
307
+ let generatedDefaults = null
287
308
  if (typeof fallbackOption === 'string') {
288
309
  label = fallbackOption.trim()
289
310
  } else if (fallbackOption === true) {
290
- label = getPersistedFallbackLabel(trimmedText, fallbackState)
311
+ generatedDefaults = getGeneratedLabelDefaults(mark, trimmedText, markRegState, preferredLanguages)
312
+ label = generatedDefaults && generatedDefaults.label ? generatedDefaults.label : ''
291
313
  }
292
- if (!label) return trimmedText
293
- const isAsciiLabel = asciiLabelReg.test(label)
294
- if (!trimmedText) {
295
- return isAsciiLabel ? label + '.' : label
314
+ if (!label) return fallbackOption === true ? '' : trimmedText
315
+ if (generatedDefaults) {
316
+ return label + (generatedDefaults.joint || '') + (generatedDefaults.space || '') + trimmedText
296
317
  }
297
- return label + (isAsciiLabel ? '. ' : ' ') + trimmedText
318
+ return label + (asciiLabelReg.test(label) ? '. ' : ' ') + trimmedText
298
319
  }
299
320
 
300
321
  const createAutoCaptionParagraph = (captionText, TokenConstructor) => {
@@ -420,154 +441,55 @@ const ensureAutoFigureNumbering = (tokens, range, caption, figureNumberState, op
420
441
  updateInlineTokenContent(inlineToken, originalText, newLabelText)
421
442
  }
422
443
 
423
- const matchAutoCaptionText = (text, reg) => {
424
- if (!text || !reg) return ''
444
+ const matchAutoCaptionText = (text, opt, preferredMark = 'img') => {
445
+ if (!text || !opt || !opt.markRegState) return ''
425
446
  const trimmed = text.trim()
426
- if (trimmed && reg.test(trimmed)) return trimmed
447
+ if (!trimmed) return ''
448
+ const analysis = analyzeCaptionStart(trimmed, {
449
+ markRegState: opt.markRegState,
450
+ preferredMark,
451
+ })
452
+ if (analysis) return trimmed
427
453
  return ''
428
454
  }
429
455
 
430
- const getAutoCaptionFromImage = (imageToken, opt, fallbackLabelState) => {
431
- const imgCaptionMarkReg = opt && opt.imgCaptionMarkReg ? opt.imgCaptionMarkReg : null
456
+ const getAutoCaptionFromImage = (imageToken, opt) => {
432
457
  if (!opt.autoCaptionDetection) return ''
433
- if (!imgCaptionMarkReg && !opt.autoAltCaption && !opt.autoTitleCaption) return ''
458
+ if (!opt.autoAltCaption && !opt.autoTitleCaption && !(opt.markRegState && opt.markRegState.markReg && opt.markRegState.markReg.img)) return ''
434
459
 
435
460
  const altText = getImageAltText(imageToken)
436
- let caption = matchAutoCaptionText(altText, imgCaptionMarkReg)
461
+ let caption = matchAutoCaptionText(altText, opt)
437
462
  if (caption) {
438
463
  clearImageAltAttr(imageToken)
439
464
  return caption
440
465
  }
441
466
  if (!caption && opt.autoAltCaption) {
442
467
  const altForFallback = altText || ''
443
- caption = buildCaptionWithFallback(altForFallback, opt.autoAltCaption, fallbackLabelState)
444
- if (imageToken) {
468
+ const fallbackCaption = buildCaptionWithFallback(altForFallback, opt.autoAltCaption, 'img', opt.markRegState, opt.preferredLanguages)
469
+ if (fallbackCaption && imageToken) {
445
470
  clearImageAltAttr(imageToken)
446
471
  }
472
+ caption = fallbackCaption
447
473
  }
448
474
  if (caption) return caption
449
475
 
450
476
  const titleText = getImageTitleText(imageToken)
451
- caption = matchAutoCaptionText(titleText, imgCaptionMarkReg)
477
+ caption = matchAutoCaptionText(titleText, opt)
452
478
  if (caption) {
453
479
  clearImageTitleAttr(imageToken)
454
480
  return caption
455
481
  }
456
482
  if (!caption && opt.autoTitleCaption) {
457
483
  const titleForFallback = titleText || ''
458
- caption = buildCaptionWithFallback(titleForFallback, opt.autoTitleCaption, fallbackLabelState)
459
- if (imageToken) {
484
+ const fallbackCaption = buildCaptionWithFallback(titleForFallback, opt.autoTitleCaption, 'img', opt.markRegState, opt.preferredLanguages)
485
+ if (fallbackCaption && imageToken) {
460
486
  clearImageTitleAttr(imageToken)
461
487
  }
488
+ caption = fallbackCaption
462
489
  }
463
490
  return caption
464
491
  }
465
492
 
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
493
  const checkPrevCaption = (tokens, n, caption, sp, opt, captionState) => {
572
494
  if(n < 3) return caption
573
495
  const captionStartToken = tokens[n-3]
@@ -579,11 +501,11 @@ const checkPrevCaption = (tokens, n, caption, sp, opt, captionState) => {
579
501
  const captionName = sp && sp.captionDecision ? sp.captionDecision.mark : ''
580
502
  if(!captionName) {
581
503
  if (opt.labelPrefixMarkerWithoutLabelPrevReg) {
582
- const markerMatch = getLabelPrefixMarkerMatch(captionInlineToken, opt.labelPrefixMarkerWithoutLabelPrevReg)
583
- if (markerMatch) {
584
- stripLabelPrefixMarkerFromInline(captionInlineToken, markerMatch)
585
- caption.isPrev = true
586
- }
504
+ const markerMatch = getLabelPrefixMarkerMatch(captionInlineToken, opt.labelPrefixMarkerWithoutLabelPrevReg)
505
+ if (markerMatch) {
506
+ stripLabelPrefixMarker(captionInlineToken, markerMatch)
507
+ caption.isPrev = true
508
+ }
587
509
  }
588
510
  return
589
511
  }
@@ -603,11 +525,11 @@ const checkNextCaption = (tokens, en, caption, sp, opt, captionState) => {
603
525
  const captionName = sp && sp.captionDecision ? sp.captionDecision.mark : ''
604
526
  if(!captionName) {
605
527
  if (opt.labelPrefixMarkerWithoutLabelNextReg) {
606
- const markerMatch = getLabelPrefixMarkerMatch(captionInlineToken, opt.labelPrefixMarkerWithoutLabelNextReg)
607
- if (markerMatch) {
608
- stripLabelPrefixMarkerFromInline(captionInlineToken, markerMatch)
609
- caption.isNext = true
610
- }
528
+ const markerMatch = getLabelPrefixMarkerMatch(captionInlineToken, opt.labelPrefixMarkerWithoutLabelNextReg)
529
+ if (markerMatch) {
530
+ stripLabelPrefixMarker(captionInlineToken, markerMatch)
531
+ caption.isNext = true
532
+ }
611
533
  }
612
534
  return
613
535
  }
@@ -852,38 +774,6 @@ const detectFenceToken = (token, n, caption) => {
852
774
  }
853
775
  }
854
776
 
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
777
  const hasLeadingImageChild = (token) => {
888
778
  return !!(token &&
889
779
  token.type === 'inline' &&
@@ -1004,15 +894,19 @@ const figureWithCaption = (state, opt) => {
1004
894
  table: 0,
1005
895
  }
1006
896
 
1007
- const fallbackLabelState = {
1008
- img: null,
1009
- }
1010
-
1011
897
  const captionState = { tokens: state.tokens, Token: state.Token }
1012
- figureWithCaptionCore(state.tokens, opt, figureNumberState, fallbackLabelState, state.Token, captionState, null, 0)
898
+ const shouldResolvePreferredLanguages = !!(
899
+ opt.shouldResolvePreferredLanguages &&
900
+ sourceMayNeedPreferredLanguages(state)
901
+ )
902
+ const renderOpt = shouldResolvePreferredLanguages ? Object.create(opt) : opt
903
+ if (shouldResolvePreferredLanguages) {
904
+ renderOpt.preferredLanguages = resolvePreferredLanguagesForState(state, opt)
905
+ }
906
+ figureWithCaptionCore(state.tokens, renderOpt, figureNumberState, state.Token, captionState, null, 0)
1013
907
  }
1014
908
 
1015
- const figureWithCaptionCore = (tokens, opt, figureNumberState, fallbackLabelState, TokenConstructor, captionState, parentType = null, startIndex = 0) => {
909
+ const figureWithCaptionCore = (tokens, opt, figureNumberState, TokenConstructor, captionState, parentType = null, startIndex = 0) => {
1016
910
  const rRange = { start: startIndex, end: startIndex }
1017
911
  const rCaption = {
1018
912
  name: '', nameSuffix: '', isPrev: false, isNext: false
@@ -1030,7 +924,7 @@ const figureWithCaptionCore = (tokens, opt, figureNumberState, fallbackLabelStat
1030
924
  const containerType = getNestedContainerType(token)
1031
925
 
1032
926
  if (containerType && containerType !== 'blockquote') {
1033
- const closeIndex = figureWithCaptionCore(tokens, opt, figureNumberState, fallbackLabelState, TokenConstructor, captionState, containerType, n + 1)
927
+ const closeIndex = figureWithCaptionCore(tokens, opt, figureNumberState, TokenConstructor, captionState, containerType, n + 1)
1034
928
  n = (typeof closeIndex === 'number' ? closeIndex : n) + 1
1035
929
  continue
1036
930
  }
@@ -1055,7 +949,12 @@ const figureWithCaptionCore = (tokens, opt, figureNumberState, fallbackLabelStat
1055
949
  resetRangeState(rRange, n)
1056
950
  resetCaptionState(rCaption)
1057
951
  resetSpecialState(rSp)
1058
- detection = detectHtmlBlockToken(tokens, token, n, rCaption, rSp, opt)
952
+ detection = detectHtmlFigureCandidate(tokens, token, n, opt.htmlWrapWithoutCaption)
953
+ if (detection) {
954
+ rCaption.name = detection.tagName
955
+ rSp.isVideoIframe = !!detection.isVideoIframe
956
+ rSp.isIframeTypeBlockquote = !!detection.isIframeTypeBlockquote
957
+ }
1059
958
  } else if (tokenType === 'fence') {
1060
959
  resetRangeState(rRange, n)
1061
960
  resetCaptionState(rCaption)
@@ -1070,7 +969,7 @@ const figureWithCaptionCore = (tokens, opt, figureNumberState, fallbackLabelStat
1070
969
 
1071
970
  if (!detection) {
1072
971
  if (containerType === 'blockquote') {
1073
- const closeIndex = figureWithCaptionCore(tokens, opt, figureNumberState, fallbackLabelState, TokenConstructor, captionState, containerType, n + 1)
972
+ const closeIndex = figureWithCaptionCore(tokens, opt, figureNumberState, TokenConstructor, captionState, containerType, n + 1)
1074
973
  n = (typeof closeIndex === 'number' ? closeIndex : n) + 1
1075
974
  } else {
1076
975
  n++
@@ -1087,7 +986,7 @@ const figureWithCaptionCore = (tokens, opt, figureNumberState, fallbackLabelStat
1087
986
  let hasCaption = rCaption.isPrev || rCaption.isNext
1088
987
  let pendingAutoCaption = ''
1089
988
  if (!hasCaption && detection.type === 'image' && opt.autoCaptionDetection) {
1090
- pendingAutoCaption = getAutoCaptionFromImage(detection.imageToken, opt, fallbackLabelState)
989
+ pendingAutoCaption = getAutoCaptionFromImage(detection.imageToken, opt)
1091
990
  if (pendingAutoCaption) {
1092
991
  hasCaption = true
1093
992
  }
@@ -1096,7 +995,7 @@ const figureWithCaptionCore = (tokens, opt, figureNumberState, fallbackLabelStat
1096
995
  if (detection.canWrap === false) {
1097
996
  let nextIndex = rRange.end + 1
1098
997
  if (containerType === 'blockquote') {
1099
- const closeIndex = figureWithCaptionCore(tokens, opt, figureNumberState, fallbackLabelState, TokenConstructor, captionState, containerType, rRange.start + 1)
998
+ const closeIndex = figureWithCaptionCore(tokens, opt, figureNumberState, TokenConstructor, captionState, containerType, rRange.start + 1)
1100
999
  nextIndex = Math.max(nextIndex, (typeof closeIndex === 'number' ? closeIndex : rRange.end) + 1)
1101
1000
  }
1102
1001
  n = nextIndex
@@ -1151,7 +1050,7 @@ const figureWithCaptionCore = (tokens, opt, figureNumberState, fallbackLabelStat
1151
1050
  }
1152
1051
 
1153
1052
  if (containerType === 'blockquote') {
1154
- const closeIndex = figureWithCaptionCore(tokens, opt, figureNumberState, fallbackLabelState, TokenConstructor, captionState, containerType, rRange.start + 1)
1053
+ const closeIndex = figureWithCaptionCore(tokens, opt, figureNumberState, TokenConstructor, captionState, containerType, rRange.start + 1)
1155
1054
  const fallbackIndex = rCaption.name ? rRange.end : n
1156
1055
  nextIndex = Math.max(nextIndex, (typeof closeIndex === 'number' ? closeIndex : fallbackIndex) + 1)
1157
1056
  }
@@ -1165,6 +1064,7 @@ const mditFigureWithPCaption = (md, option) => {
1165
1064
  let opt = {
1166
1065
  // Caption languages delegated to p-captions.
1167
1066
  languages: ['en', 'ja'],
1067
+ preferredLanguages: null, // optional tie-break order for generated fallback labels; defaults to inferred document order / languages
1168
1068
 
1169
1069
  // --- figure-wrapper behavior ---
1170
1070
  classPrefix: 'f',
@@ -1219,11 +1119,11 @@ const mditFigureWithPCaption = (md, option) => {
1219
1119
  }
1220
1120
  opt.classPrefix = normalizeOptionalClassName(opt.classPrefix)
1221
1121
  opt.allIframeTypeFigureClassName = normalizeOptionalClassName(opt.allIframeTypeFigureClassName)
1222
- opt.languages = normalizeLanguages(opt.languages)
1223
1122
  opt.markRegState = getMarkRegStateForLanguages(opt.languages)
1224
- opt.imgCaptionMarkReg = opt.markRegState && opt.markRegState.markReg
1225
- ? opt.markRegState.markReg.img
1226
- : null
1123
+ opt.preferredLanguages = normalizePreferredLanguages(opt.preferredLanguages, opt.markRegState.languages)
1124
+ if (opt.preferredLanguages.length === 0) opt.preferredLanguages = null
1125
+ opt.normalizedOptionLanguages = normalizePreferredLanguages(opt.languages, opt.markRegState.languages)
1126
+ opt.shouldResolvePreferredLanguages = needsPreferredLanguagesResolution(opt)
1227
1127
  opt.htmlWrapWithoutCaption = {
1228
1128
  iframe: !!opt.iframeWithoutCaption,
1229
1129
  video: !!opt.videoWithoutCaption,
@@ -1260,12 +1160,12 @@ const mditFigureWithPCaption = (md, option) => {
1260
1160
  // Precompute label-class permutations so numbering lookup doesn't rebuild arrays per caption.
1261
1161
  opt.labelClassLookup = buildLabelClassLookup(opt)
1262
1162
  const markerList = normalizeLabelPrefixMarkers(opt.labelPrefixMarker)
1263
- opt.labelPrefixMarkerReg = buildLabelPrefixMarkerRegFromList(markerList)
1163
+ opt.labelPrefixMarkerReg = buildLabelPrefixMarkerRegFromMarkers(markerList)
1264
1164
  opt.cleanCaptionRegCache = new Map()
1265
1165
  if (opt.allowLabelPrefixMarkerWithoutLabel === true) {
1266
1166
  const markerPair = resolveLabelPrefixMarkerPair(markerList)
1267
- opt.labelPrefixMarkerWithoutLabelPrevReg = buildLabelPrefixMarkerRegFromList(markerPair.prev)
1268
- opt.labelPrefixMarkerWithoutLabelNextReg = buildLabelPrefixMarkerRegFromList(markerPair.next)
1167
+ opt.labelPrefixMarkerWithoutLabelPrevReg = buildLabelPrefixMarkerRegFromMarkers(markerPair.prev)
1168
+ opt.labelPrefixMarkerWithoutLabelNextReg = buildLabelPrefixMarkerRegFromMarkers(markerPair.next)
1269
1169
  } else {
1270
1170
  opt.labelPrefixMarkerWithoutLabelPrevReg = null
1271
1171
  opt.labelPrefixMarkerWithoutLabelNextReg = null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peaceroad/markdown-it-figure-with-p-caption",
3
- "version": "0.16.1",
3
+ "version": "0.17.0",
4
4
  "description": "A markdown-it plugin. For a paragraph with only one image, a table or code block or blockquote, and by writing a caption paragraph immediately before or after, they are converted into the figure element with the figcaption element.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -29,10 +29,11 @@
29
29
  "markdown-it-attrs": "^4.3.1"
30
30
  },
31
31
  "dependencies": {
32
- "p7d-markdown-it-p-captions": "^0.21.0"
32
+ "p7d-markdown-it-p-captions": "0.22.0"
33
33
  },
34
34
  "files": [
35
35
  "index.js",
36
+ "embeds/",
36
37
  "README.md",
37
38
  "LICENSE"
38
39
  ]