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