@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 +8 -6
- package/embeds/detect.js +178 -0
- package/embeds/providers.js +27 -0
- package/index.js +210 -310
- package/package.json +3 -2
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
|
|
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`
|
|
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
|
|
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
|
|
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
|
|
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]
|
package/embeds/detect.js
ADDED
|
@@ -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
|
|
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
|
-
|
|
311
|
+
generatedDefaults = getGeneratedLabelDefaults(mark, trimmedText, markRegState, preferredLanguages)
|
|
312
|
+
label = generatedDefaults && generatedDefaults.label ? generatedDefaults.label : ''
|
|
291
313
|
}
|
|
292
|
-
if (!label) return trimmedText
|
|
293
|
-
|
|
294
|
-
|
|
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 + (
|
|
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,
|
|
424
|
-
if (!text || !
|
|
444
|
+
const matchAutoCaptionText = (text, opt, preferredMark = 'img') => {
|
|
445
|
+
if (!text || !opt || !opt.markRegState) return ''
|
|
425
446
|
const trimmed = text.trim()
|
|
426
|
-
if (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
|
|
431
|
-
const imgCaptionMarkReg = opt && opt.imgCaptionMarkReg ? opt.imgCaptionMarkReg : null
|
|
456
|
+
const getAutoCaptionFromImage = (imageToken, opt) => {
|
|
432
457
|
if (!opt.autoCaptionDetection) return ''
|
|
433
|
-
if (!
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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 =
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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.
|
|
1225
|
-
|
|
1226
|
-
|
|
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 =
|
|
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 =
|
|
1268
|
-
opt.labelPrefixMarkerWithoutLabelNextReg =
|
|
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.
|
|
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": "
|
|
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
|
]
|