@peaceroad/markdown-it-figure-with-p-caption 0.15.2 → 0.16.1
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 +11 -6
- package/index.js +327 -158
- package/package.json +10 -7
package/README.md
CHANGED
|
@@ -27,6 +27,7 @@ Optionally, you can auto-number image and table caption paragraphs starting from
|
|
|
27
27
|
- `f-img-multiple` for mixed layouts.
|
|
28
28
|
- Automatic detection inspects only the first image in the paragraph. If it yields a caption, the entire figure reuses that caption while later images keep their own `alt`/`title`.
|
|
29
29
|
- Paragraphs that contain only images also convert when they appear inside loose lists (leave blank lines between items), blockquotes, or description lists.
|
|
30
|
+
- Caption detection intentionally skips paragraphs that are the first block inside a list item (`list_item_open` immediately before the paragraph). In practice, `- Figure. ...` followed by an image in the same item is treated as plain text unless you insert another block first.
|
|
30
31
|
|
|
31
32
|
### Table
|
|
32
33
|
|
|
@@ -50,6 +51,7 @@ Optionally, you can auto-number image and table caption paragraphs starting from
|
|
|
50
51
|
### Embedded content by iframe
|
|
51
52
|
|
|
52
53
|
- Inline HTML `<iframe>` elements become `<figure class="f-video">` when they point to known video hosts (YouTube `youtube.com` / `youtube-nocookie.com`, Vimeo `player.vimeo.com`).
|
|
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).
|
|
53
55
|
- Blockquote-based social embeds (Twitter/X `twitter-tweet`, Mastodon `mastodon-embed`, Bluesky `bluesky-embed`, Instagram `instagram-media`, Tumblr `text-post-media`) are treated like iframe-type embeds when their `class` matches those providers. By default they become `<figure class="f-img">` so the caption label behaves like an image label (Labels can also use quote labels). You can override that figure class with `figureClassThatWrapsIframeTypeBlockquote` or the global `allIframeTypeFigureClassName`.
|
|
54
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.
|
|
55
57
|
- All other iframes fall back to `<figure class="f-iframe">` unless you override the class via `allIframeTypeFigureClassName`.
|
|
@@ -57,7 +59,9 @@ Optionally, you can auto-number image and table caption paragraphs starting from
|
|
|
57
59
|
### label span class name
|
|
58
60
|
|
|
59
61
|
- The label inside the figcaption (the `span` element used for the label) is generated by `p7d-markdown-it-p-captions`, not by this plugin. By default the class name is formed by combining `classPrefix` with the mark name, producing names such as `f-img-label`, `f-video-label`, `f-blockquote-label`, and `f-slide-label`.
|
|
60
|
-
- With `markdown-it-attrs`,
|
|
62
|
+
- With `markdown-it-attrs`, attributes attached to image-only paragraphs (for example ` {.foo #bar}`) are forwarded to the generated `<figure>`.
|
|
63
|
+
- `styleProcess` controls parsing of a trailing `{...}` block from the last text token of an image-only paragraph in this plugin's own scanner. It is a narrow fallback parser, not full `markdown-it-attrs` parity, and attributes already attached to paragraph tokens by `markdown-it-attrs` are still forwarded.
|
|
64
|
+
- Attributes attached to caption paragraphs stay on the converted `<figcaption>` token after paragraph-to-figcaption conversion.
|
|
61
65
|
|
|
62
66
|
## Behavior Customization
|
|
63
67
|
|
|
@@ -67,6 +71,7 @@ Optionally, you can auto-number image and table caption paragraphs starting from
|
|
|
67
71
|
- `figureClassThatWrapsIframeTypeBlockquote`: override the class used when blockquote-based embeds (Twitter, Mastodon, Bluesky) are wrapped.
|
|
68
72
|
- `figureClassThatWrapsSlides`: override the class assigned when a caption paragraph uses the `Slide.` label.
|
|
69
73
|
- `classPrefix` (default `f`) controls the CSS namespace for every figure (`f-img`, `f-table`, etc.) so you can align with existing styles.
|
|
74
|
+
- Wrapper/class-prefix options are trimmed during setup; whitespace-only values fall back to the default class for that option.
|
|
70
75
|
|
|
71
76
|
### Wrapping without captions
|
|
72
77
|
|
|
@@ -85,7 +90,7 @@ Every option below is forwarded verbatim to `p7d-markdown-it-p-captions`, which
|
|
|
85
90
|
- `wrapCaptionBody`: wrap the non-label caption text in a span element.
|
|
86
91
|
- `hasNumClass`: add a class attribute to label span element if it has a label number.
|
|
87
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.
|
|
88
|
-
- `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.
|
|
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.
|
|
89
94
|
- `labelPrefixMarker`: allow a leading marker before labels (string or array, e.g., `*Figure. ...`). Arrays are limited to two markers; extras are ignored.
|
|
90
95
|
|
|
91
96
|
### Automatic numbering
|
|
@@ -433,13 +438,13 @@ A paragraph.
|
|
|
433
438
|
|
|
434
439
|
### Styles
|
|
435
440
|
|
|
436
|
-
This example uses `classPrefix: 'custom'` and leaves `styleProcess: true` so a trailing `{.notice}` block moves onto the `<figure>` wrapper.
|
|
441
|
+
This example uses `classPrefix: 'custom'` and leaves `styleProcess: true` so a trailing `{.notice}` block moves onto the `<figure>` wrapper. This fallback only handles the final trailing attrs block on an image-only paragraph; for broader attrs syntax support, keep using `markdown-it-attrs`.
|
|
437
442
|
|
|
438
443
|
```
|
|
439
444
|
[Markdown]
|
|
440
|
-
Figure. Highlighted cat.
|
|
445
|
+
Figure. Highlighted cat.
|
|
441
446
|
|
|
442
|
-

|
|
447
|
+
 {.notice}
|
|
443
448
|
[HTML]
|
|
444
449
|
<figure class="custom-img notice">
|
|
445
450
|
<figcaption><span class="custom-img-label">Figure<span class="custom-img-label-joint">.</span></span> Highlighted cat.</figcaption>
|
|
@@ -577,7 +582,7 @@ Video. Custom embed.
|
|
|
577
582
|
</figure>
|
|
578
583
|
```
|
|
579
584
|
|
|
580
|
-
Need matching caption classes too?
|
|
585
|
+
Need matching caption classes too? Use `labelClassFollowsFigure` (and optionally `figureToLabelClassMap`) so the `figcaption` spans inherit the embed class you just applied (e.g., `f-embed-label`, `f-embed-label-joint`). If `figureToLabelClassMap` is provided, figure-following mode is enabled automatically unless `labelClassFollowsFigure` is set explicitly.
|
|
581
586
|
|
|
582
587
|
|
|
583
588
|
### Caption markers
|
package/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
setCaptionParagraph,
|
|
3
|
+
getMarkRegStateForLanguages,
|
|
4
|
+
} from 'p7d-markdown-it-p-captions'
|
|
2
5
|
|
|
3
6
|
const htmlRegCache = new Map()
|
|
4
|
-
const cleanCaptionRegCache = new Map()
|
|
5
7
|
const blueskyEmbedReg = /^<blockquote class="bluesky-embed"[^]*?>[\s\S]*?$/
|
|
6
8
|
const videoIframeReg = /^<[^>]*? src="https:\/\/(?:www.youtube-nocookie.com|player.vimeo.com)\//i
|
|
7
9
|
const classNameReg = /^<[^>]*? class="(twitter-tweet|instagram-media|text-post-media|bluesky-embed|mastodon-embed)"/
|
|
@@ -11,22 +13,58 @@ const idAttrReg = /^#/
|
|
|
11
13
|
const attrParseReg = /^(.*?)="?(.*)"?$/
|
|
12
14
|
const sampLangReg = /^ *(?:samp|shell|console)(?:(?= )|$)/
|
|
13
15
|
const endBlockquoteScriptReg = /<\/blockquote> *<script[^>]*?><\/script>$/
|
|
14
|
-
const
|
|
16
|
+
const iframeTagReg = /<iframe(?=[\s>])/i
|
|
15
17
|
const asciiLabelReg = /^[A-Za-z]/
|
|
16
|
-
const trailingDigitsReg = /(\d+)\s*$/
|
|
17
18
|
const CHECK_TYPE_TOKEN_MAP = {
|
|
18
19
|
table_open: 'table',
|
|
19
20
|
pre_open: 'pre',
|
|
20
21
|
blockquote_open: 'blockquote',
|
|
21
22
|
}
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
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: '図' }
|
|
27
38
|
|
|
28
39
|
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
29
|
-
const
|
|
40
|
+
const normalizeOptionalClassName = (value) => {
|
|
41
|
+
if (value === null || value === undefined) return ''
|
|
42
|
+
const normalized = String(value).trim()
|
|
43
|
+
return normalized || ''
|
|
44
|
+
}
|
|
45
|
+
const buildClassPrefix = (value) => {
|
|
46
|
+
const normalized = normalizeOptionalClassName(value)
|
|
47
|
+
return normalized ? normalized + '-' : ''
|
|
48
|
+
}
|
|
49
|
+
const normalizeClassOptionWithFallback = (value, fallbackValue) => {
|
|
50
|
+
const normalized = normalizeOptionalClassName(value)
|
|
51
|
+
return normalized || fallbackValue
|
|
52
|
+
}
|
|
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
|
+
}
|
|
30
68
|
const normalizeLabelPrefixMarkers = (value) => {
|
|
31
69
|
if (typeof value === 'string') {
|
|
32
70
|
return value ? [value] : []
|
|
@@ -228,31 +266,28 @@ const isSentenceBoundaryChar = (char) => {
|
|
|
228
266
|
return char === '.' || char === '!' || char === '?' || char === '。' || char === '!' || char === '?'
|
|
229
267
|
}
|
|
230
268
|
|
|
231
|
-
const getAutoFallbackLabel = (text
|
|
232
|
-
const type = captionType === 'table' ? 'table' : 'img'
|
|
269
|
+
const getAutoFallbackLabel = (text) => {
|
|
233
270
|
const lang = detectCaptionLanguage(text)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
return defaults.en || defaults.ja || ''
|
|
271
|
+
if (lang === 'ja') return fallbackLabelDefaults.ja || fallbackLabelDefaults.en || ''
|
|
272
|
+
return fallbackLabelDefaults.en || fallbackLabelDefaults.ja || ''
|
|
237
273
|
}
|
|
238
274
|
|
|
239
|
-
const getPersistedFallbackLabel = (text,
|
|
240
|
-
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
fallbackState[type] = resolved
|
|
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
|
|
245
280
|
return resolved
|
|
246
281
|
}
|
|
247
282
|
|
|
248
|
-
const buildCaptionWithFallback = (text, fallbackOption,
|
|
283
|
+
const buildCaptionWithFallback = (text, fallbackOption, fallbackState) => {
|
|
249
284
|
const trimmedText = (text || '').trim()
|
|
250
285
|
if (!fallbackOption) return ''
|
|
251
286
|
let label = ''
|
|
252
287
|
if (typeof fallbackOption === 'string') {
|
|
253
288
|
label = fallbackOption.trim()
|
|
254
289
|
} else if (fallbackOption === true) {
|
|
255
|
-
label = getPersistedFallbackLabel(trimmedText,
|
|
290
|
+
label = getPersistedFallbackLabel(trimmedText, fallbackState)
|
|
256
291
|
}
|
|
257
292
|
if (!label) return trimmedText
|
|
258
293
|
const isAsciiLabel = asciiLabelReg.test(label)
|
|
@@ -331,6 +366,28 @@ const updateInlineTokenContent = (inlineToken, originalText, newText) => {
|
|
|
331
366
|
inlineToken.content.slice(index + originalText.length)
|
|
332
367
|
}
|
|
333
368
|
|
|
369
|
+
const parseTrailingPositiveInteger = (text) => {
|
|
370
|
+
if (typeof text !== 'string' || text.length === 0) return null
|
|
371
|
+
let end = text.length - 1
|
|
372
|
+
while (end >= 0 && text.charCodeAt(end) === 0x20) end--
|
|
373
|
+
if (end < 0) return null
|
|
374
|
+
const lastCode = text.charCodeAt(end)
|
|
375
|
+
if (lastCode < 0x30 || lastCode > 0x39) return null
|
|
376
|
+
let start = end
|
|
377
|
+
while (start >= 0) {
|
|
378
|
+
const code = text.charCodeAt(start)
|
|
379
|
+
if (code < 0x30 || code > 0x39) break
|
|
380
|
+
start--
|
|
381
|
+
}
|
|
382
|
+
let value = 0
|
|
383
|
+
let digitBase = 1
|
|
384
|
+
for (let i = end; i > start; i--) {
|
|
385
|
+
value += (text.charCodeAt(i) - 0x30) * digitBase
|
|
386
|
+
digitBase *= 10
|
|
387
|
+
}
|
|
388
|
+
return value
|
|
389
|
+
}
|
|
390
|
+
|
|
334
391
|
const ensureAutoFigureNumbering = (tokens, range, caption, figureNumberState, opt) => {
|
|
335
392
|
const captionType = caption.name === 'img' ? 'img' : (caption.name === 'table' ? 'table' : '')
|
|
336
393
|
if (!captionType) return
|
|
@@ -345,10 +402,9 @@ const ensureAutoFigureNumbering = (tokens, range, caption, figureNumberState, op
|
|
|
345
402
|
if (end >= 0) {
|
|
346
403
|
const code = originalText.charCodeAt(end)
|
|
347
404
|
if (code >= 0x30 && code <= 0x39) {
|
|
348
|
-
const
|
|
349
|
-
if (
|
|
350
|
-
|
|
351
|
-
if (!Number.isNaN(explicitValue) && explicitValue > (figureNumberState[captionType] || 0)) {
|
|
405
|
+
const explicitValue = parseTrailingPositiveInteger(originalText)
|
|
406
|
+
if (explicitValue !== null) {
|
|
407
|
+
if (explicitValue > (figureNumberState[captionType] || 0)) {
|
|
352
408
|
figureNumberState[captionType] = explicitValue
|
|
353
409
|
}
|
|
354
410
|
return
|
|
@@ -364,27 +420,27 @@ const ensureAutoFigureNumbering = (tokens, range, caption, figureNumberState, op
|
|
|
364
420
|
updateInlineTokenContent(inlineToken, originalText, newLabelText)
|
|
365
421
|
}
|
|
366
422
|
|
|
423
|
+
const matchAutoCaptionText = (text, reg) => {
|
|
424
|
+
if (!text || !reg) return ''
|
|
425
|
+
const trimmed = text.trim()
|
|
426
|
+
if (trimmed && reg.test(trimmed)) return trimmed
|
|
427
|
+
return ''
|
|
428
|
+
}
|
|
429
|
+
|
|
367
430
|
const getAutoCaptionFromImage = (imageToken, opt, fallbackLabelState) => {
|
|
431
|
+
const imgCaptionMarkReg = opt && opt.imgCaptionMarkReg ? opt.imgCaptionMarkReg : null
|
|
368
432
|
if (!opt.autoCaptionDetection) return ''
|
|
369
433
|
if (!imgCaptionMarkReg && !opt.autoAltCaption && !opt.autoTitleCaption) return ''
|
|
370
|
-
const tryMatch = (text) => {
|
|
371
|
-
if (!text) return ''
|
|
372
|
-
const trimmed = text.trim()
|
|
373
|
-
if (trimmed && imgCaptionMarkReg && imgCaptionMarkReg.test(trimmed)) {
|
|
374
|
-
return trimmed
|
|
375
|
-
}
|
|
376
|
-
return ''
|
|
377
|
-
}
|
|
378
434
|
|
|
379
435
|
const altText = getImageAltText(imageToken)
|
|
380
|
-
let caption =
|
|
436
|
+
let caption = matchAutoCaptionText(altText, imgCaptionMarkReg)
|
|
381
437
|
if (caption) {
|
|
382
438
|
clearImageAltAttr(imageToken)
|
|
383
439
|
return caption
|
|
384
440
|
}
|
|
385
441
|
if (!caption && opt.autoAltCaption) {
|
|
386
442
|
const altForFallback = altText || ''
|
|
387
|
-
caption = buildCaptionWithFallback(altForFallback, opt.autoAltCaption,
|
|
443
|
+
caption = buildCaptionWithFallback(altForFallback, opt.autoAltCaption, fallbackLabelState)
|
|
388
444
|
if (imageToken) {
|
|
389
445
|
clearImageAltAttr(imageToken)
|
|
390
446
|
}
|
|
@@ -392,14 +448,14 @@ const getAutoCaptionFromImage = (imageToken, opt, fallbackLabelState) => {
|
|
|
392
448
|
if (caption) return caption
|
|
393
449
|
|
|
394
450
|
const titleText = getImageTitleText(imageToken)
|
|
395
|
-
caption =
|
|
451
|
+
caption = matchAutoCaptionText(titleText, imgCaptionMarkReg)
|
|
396
452
|
if (caption) {
|
|
397
453
|
clearImageTitleAttr(imageToken)
|
|
398
454
|
return caption
|
|
399
455
|
}
|
|
400
456
|
if (!caption && opt.autoTitleCaption) {
|
|
401
457
|
const titleForFallback = titleText || ''
|
|
402
|
-
caption = buildCaptionWithFallback(titleForFallback, opt.autoTitleCaption,
|
|
458
|
+
caption = buildCaptionWithFallback(titleForFallback, opt.autoTitleCaption, fallbackLabelState)
|
|
403
459
|
if (imageToken) {
|
|
404
460
|
clearImageTitleAttr(imageToken)
|
|
405
461
|
}
|
|
@@ -408,21 +464,118 @@ const getAutoCaptionFromImage = (imageToken, opt, fallbackLabelState) => {
|
|
|
408
464
|
}
|
|
409
465
|
|
|
410
466
|
const getHtmlReg = (tag) => {
|
|
411
|
-
|
|
467
|
+
const cached = htmlRegCache.get(tag)
|
|
468
|
+
if (cached) return cached
|
|
412
469
|
const regexStr = `^<${tag} ?[^>]*?>[\\s\\S]*?<\\/${tag}>(\\n| *?)(<script [^>]*?>(?:<\\/script>)?)? *(\\n|$)`
|
|
413
470
|
const reg = new RegExp(regexStr)
|
|
414
471
|
htmlRegCache.set(tag, reg)
|
|
415
472
|
return reg
|
|
416
473
|
}
|
|
417
474
|
|
|
418
|
-
const
|
|
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
|
+
const checkPrevCaption = (tokens, n, caption, sp, opt, captionState) => {
|
|
419
572
|
if(n < 3) return caption
|
|
420
573
|
const captionStartToken = tokens[n-3]
|
|
421
574
|
const captionInlineToken = tokens[n-2]
|
|
422
575
|
const captionEndToken = tokens[n-1]
|
|
423
576
|
if (captionStartToken === undefined || captionEndToken === undefined) return
|
|
424
577
|
if (captionStartToken.type !== 'paragraph_open' || captionEndToken.type !== 'paragraph_close') return
|
|
425
|
-
setCaptionParagraph(n-3, captionState, caption,
|
|
578
|
+
setCaptionParagraph(n-3, captionState, caption, null, sp, opt)
|
|
426
579
|
const captionName = sp && sp.captionDecision ? sp.captionDecision.mark : ''
|
|
427
580
|
if(!captionName) {
|
|
428
581
|
if (opt.labelPrefixMarkerWithoutLabelPrevReg) {
|
|
@@ -439,14 +592,14 @@ const checkPrevCaption = (tokens, n, caption, fNum, sp, opt, captionState) => {
|
|
|
439
592
|
return
|
|
440
593
|
}
|
|
441
594
|
|
|
442
|
-
const checkNextCaption = (tokens, en, caption,
|
|
595
|
+
const checkNextCaption = (tokens, en, caption, sp, opt, captionState) => {
|
|
443
596
|
if (en + 2 > tokens.length) return
|
|
444
597
|
const captionStartToken = tokens[en+1]
|
|
445
598
|
const captionInlineToken = tokens[en+2]
|
|
446
599
|
const captionEndToken = tokens[en+3]
|
|
447
600
|
if (captionStartToken === undefined || captionEndToken === undefined) return
|
|
448
601
|
if (captionStartToken.type !== 'paragraph_open' || captionEndToken.type !== 'paragraph_close') return
|
|
449
|
-
setCaptionParagraph(en+1, captionState, caption,
|
|
602
|
+
setCaptionParagraph(en+1, captionState, caption, null, sp, opt)
|
|
450
603
|
const captionName = sp && sp.captionDecision ? sp.captionDecision.mark : ''
|
|
451
604
|
if(!captionName) {
|
|
452
605
|
if (opt.labelPrefixMarkerWithoutLabelNextReg) {
|
|
@@ -468,10 +621,13 @@ const cleanCaptionTokenAttrs = (token, captionName, opt) => {
|
|
|
468
621
|
const prefix = opt.captionClassPrefix || ''
|
|
469
622
|
const targetClass = prefix + captionName
|
|
470
623
|
if (!targetClass) return
|
|
471
|
-
|
|
624
|
+
const cleanCaptionRegCache = opt.cleanCaptionRegCache
|
|
625
|
+
let reg = cleanCaptionRegCache && cleanCaptionRegCache.get(targetClass)
|
|
472
626
|
if (!reg) {
|
|
473
627
|
reg = new RegExp('(?:^|\\s)' + escapeRegExp(targetClass) + '(?=\\s|$)', 'g')
|
|
474
|
-
cleanCaptionRegCache
|
|
628
|
+
if (cleanCaptionRegCache) {
|
|
629
|
+
cleanCaptionRegCache.set(targetClass, reg)
|
|
630
|
+
}
|
|
475
631
|
}
|
|
476
632
|
for (let i = token.attrs.length - 1; i >= 0; i--) {
|
|
477
633
|
if (token.attrs[i][0] === 'class') {
|
|
@@ -568,28 +724,31 @@ const wrapWithFigure = (tokens, range, checkTokenTagName, caption, replaceInstea
|
|
|
568
724
|
if (rangeEndMap) {
|
|
569
725
|
figureEndToken.map = [rangeEndMap[0], rangeEndMap[1]]
|
|
570
726
|
}
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
const attr = sp.attrs[i]
|
|
576
|
-
figureStartToken.attrJoin(attr[0], attr[1])
|
|
577
|
-
}
|
|
727
|
+
const createBreakToken = () => {
|
|
728
|
+
const breakToken = new TokenConstructor('text', '', 0)
|
|
729
|
+
breakToken.content = '\n'
|
|
730
|
+
return breakToken
|
|
578
731
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
732
|
+
if (caption.name === 'img') {
|
|
733
|
+
const joinAttrs = (attrs) => {
|
|
734
|
+
if (!attrs || attrs.length === 0) return
|
|
735
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
736
|
+
const attr = attrs[i]
|
|
737
|
+
figureStartToken.attrJoin(attr[0], attr[1])
|
|
738
|
+
}
|
|
584
739
|
}
|
|
740
|
+
// `styleProcess` should keep working even when markdown-it-attrs is absent.
|
|
741
|
+
if (opt.styleProcess) joinAttrs(sp.attrs)
|
|
742
|
+
// Forward attrs already materialized by markdown-it-attrs on the image paragraph.
|
|
743
|
+
joinAttrs(tokens[n].attrs)
|
|
585
744
|
}
|
|
586
745
|
if (replaceInsteadOfWrap) {
|
|
587
|
-
tokens.splice(en, 1,
|
|
588
|
-
tokens.splice(n, 1, figureStartToken,
|
|
746
|
+
tokens.splice(en, 1, createBreakToken(), figureEndToken, createBreakToken())
|
|
747
|
+
tokens.splice(n, 1, figureStartToken, createBreakToken())
|
|
589
748
|
en = en + 2
|
|
590
749
|
} else {
|
|
591
|
-
tokens.splice(en+1, 0, figureEndToken,
|
|
592
|
-
tokens.splice(n, 0, figureStartToken,
|
|
750
|
+
tokens.splice(en+1, 0, figureEndToken, createBreakToken())
|
|
751
|
+
tokens.splice(n, 0, figureStartToken, createBreakToken())
|
|
593
752
|
en = en + 3
|
|
594
753
|
}
|
|
595
754
|
range.start = n
|
|
@@ -597,10 +756,10 @@ const wrapWithFigure = (tokens, range, checkTokenTagName, caption, replaceInstea
|
|
|
597
756
|
return
|
|
598
757
|
}
|
|
599
758
|
|
|
600
|
-
const checkCaption = (tokens, n, en, caption,
|
|
601
|
-
checkPrevCaption(tokens, n, caption,
|
|
759
|
+
const checkCaption = (tokens, n, en, caption, sp, opt, captionState) => {
|
|
760
|
+
checkPrevCaption(tokens, n, caption, sp, opt, captionState)
|
|
602
761
|
if (caption.isPrev) return
|
|
603
|
-
checkNextCaption(tokens, en, caption,
|
|
762
|
+
checkNextCaption(tokens, en, caption, sp, opt, captionState)
|
|
604
763
|
return
|
|
605
764
|
}
|
|
606
765
|
|
|
@@ -695,60 +854,16 @@ const detectFenceToken = (token, n, caption) => {
|
|
|
695
854
|
|
|
696
855
|
const detectHtmlBlockToken = (tokens, token, n, caption, sp, opt) => {
|
|
697
856
|
if (!token || token.type !== 'html_block') return null
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
const hasBlueskyEmbed = hasBlueskyHint && blueskyEmbedReg.test(content)
|
|
857
|
+
const hints = getHtmlDetectionHints(token.content)
|
|
858
|
+
if (!hasAnyHtmlDetectionHint(hints)) return null
|
|
701
859
|
let matchedTag = ''
|
|
702
|
-
for (let i = 0; i <
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
const lookupTag = treatDivAsIframe ? 'div' : candidate
|
|
706
|
-
const hasTagHint = content.indexOf('<' + lookupTag) !== -1
|
|
707
|
-
if (!hasTagHint && !(candidate === 'blockquote' && hasBlueskyEmbed)) continue
|
|
708
|
-
const hasTag = hasTagHint ? content.match(getHtmlReg(lookupTag)) : null
|
|
709
|
-
const isBlueskyBlockquote = !hasTag && hasBlueskyEmbed && candidate === 'blockquote'
|
|
710
|
-
if (!(hasTag || isBlueskyBlockquote)) continue
|
|
711
|
-
if (hasTag) {
|
|
712
|
-
if ((hasTag[2] && hasTag[3] !== '\n') || (hasTag[1] !== '\n' && hasTag[2] === undefined)) {
|
|
713
|
-
token.content += '\n'
|
|
714
|
-
}
|
|
715
|
-
matchedTag = treatDivAsIframe ? 'iframe' : candidate
|
|
716
|
-
if (treatDivAsIframe) {
|
|
717
|
-
sp.isVideoIframe = true
|
|
718
|
-
}
|
|
719
|
-
} else {
|
|
720
|
-
let addedCont = ''
|
|
721
|
-
let j = n + 1
|
|
722
|
-
while (j < tokens.length) {
|
|
723
|
-
const nextToken = tokens[j]
|
|
724
|
-
if (nextToken.type === 'inline' && endBlockquoteScriptReg.test(nextToken.content)) {
|
|
725
|
-
addedCont += nextToken.content + '\n'
|
|
726
|
-
if (tokens[j + 1] && tokens[j + 1].type === 'paragraph_close') {
|
|
727
|
-
tokens.splice(j + 1, 1)
|
|
728
|
-
}
|
|
729
|
-
nextToken.content = ''
|
|
730
|
-
if (nextToken.children) {
|
|
731
|
-
for (let k = 0; k < nextToken.children.length; k++) {
|
|
732
|
-
nextToken.children[k].content = ''
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
break
|
|
736
|
-
}
|
|
737
|
-
if (nextToken.type === 'paragraph_open') {
|
|
738
|
-
addedCont += '\n'
|
|
739
|
-
tokens.splice(j, 1)
|
|
740
|
-
continue
|
|
741
|
-
}
|
|
742
|
-
j++
|
|
743
|
-
}
|
|
744
|
-
token.content += addedCont
|
|
745
|
-
matchedTag = 'blockquote'
|
|
746
|
-
}
|
|
747
|
-
break
|
|
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
|
|
748
863
|
}
|
|
749
864
|
if (!matchedTag) return null
|
|
750
865
|
if (matchedTag === 'blockquote') {
|
|
751
|
-
if (
|
|
866
|
+
if (isIframeTypeEmbedBlockquote(token.content)) {
|
|
752
867
|
sp.isIframeTypeBlockquote = true
|
|
753
868
|
} else {
|
|
754
869
|
return null
|
|
@@ -758,16 +873,7 @@ const detectHtmlBlockToken = (tokens, token, n, caption, sp, opt) => {
|
|
|
758
873
|
sp.isVideoIframe = true
|
|
759
874
|
}
|
|
760
875
|
caption.name = matchedTag
|
|
761
|
-
|
|
762
|
-
if (matchedTag === 'iframe' && opt.iframeWithoutCaption) {
|
|
763
|
-
wrapWithoutCaption = true
|
|
764
|
-
} else if (matchedTag === 'video' && opt.videoWithoutCaption) {
|
|
765
|
-
wrapWithoutCaption = true
|
|
766
|
-
} else if (matchedTag === 'audio' && opt.audioWithoutCaption) {
|
|
767
|
-
wrapWithoutCaption = true
|
|
768
|
-
} else if (matchedTag === 'blockquote' && sp.isIframeTypeBlockquote && opt.iframeTypeBlockquoteWithoutCaption) {
|
|
769
|
-
wrapWithoutCaption = true
|
|
770
|
-
}
|
|
876
|
+
const wrapWithoutCaption = resolveHtmlWrapWithoutCaption(matchedTag, sp, opt)
|
|
771
877
|
return {
|
|
772
878
|
type: 'html',
|
|
773
879
|
tagName: matchedTag,
|
|
@@ -778,23 +884,54 @@ const detectHtmlBlockToken = (tokens, token, n, caption, sp, opt) => {
|
|
|
778
884
|
}
|
|
779
885
|
}
|
|
780
886
|
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
887
|
+
const hasLeadingImageChild = (token) => {
|
|
888
|
+
return !!(token &&
|
|
889
|
+
token.type === 'inline' &&
|
|
890
|
+
token.children &&
|
|
891
|
+
token.children.length > 0 &&
|
|
892
|
+
token.children[0] &&
|
|
893
|
+
token.children[0].type === 'image')
|
|
894
|
+
}
|
|
785
895
|
|
|
896
|
+
const detectImageParagraph = (nextToken, n, caption, sp, opt) => {
|
|
897
|
+
const multipleImagesEnabled = !!opt.multipleImages
|
|
898
|
+
const styleProcessEnabled = !!opt.styleProcess
|
|
899
|
+
const allowSingleImageWithoutCaption = !!opt.oneImageWithoutCaption
|
|
900
|
+
const children = nextToken.children
|
|
901
|
+
const imageToken = children[0]
|
|
902
|
+
const childrenLength = children.length
|
|
786
903
|
let imageNum = 1
|
|
787
904
|
let isMultipleImagesHorizontal = true
|
|
788
905
|
let isMultipleImagesVertical = true
|
|
789
906
|
let isValid = true
|
|
790
907
|
caption.name = 'img'
|
|
791
|
-
|
|
792
|
-
|
|
908
|
+
if (childrenLength === 1) {
|
|
909
|
+
return {
|
|
910
|
+
type: 'image',
|
|
911
|
+
tagName: 'img',
|
|
912
|
+
en: n + 2,
|
|
913
|
+
replaceInsteadOfWrap: true,
|
|
914
|
+
wrapWithoutCaption: allowSingleImageWithoutCaption,
|
|
915
|
+
canWrap: true,
|
|
916
|
+
imageToken,
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
if (!multipleImagesEnabled && childrenLength > 2) {
|
|
920
|
+
return {
|
|
921
|
+
type: 'image',
|
|
922
|
+
tagName: 'img',
|
|
923
|
+
en: n + 2,
|
|
924
|
+
replaceInsteadOfWrap: true,
|
|
925
|
+
wrapWithoutCaption: false,
|
|
926
|
+
canWrap: false,
|
|
927
|
+
imageToken,
|
|
928
|
+
}
|
|
929
|
+
}
|
|
793
930
|
for (let childIndex = 1; childIndex < childrenLength; childIndex++) {
|
|
794
931
|
const child = children[childIndex]
|
|
795
932
|
if (childIndex === childrenLength - 1 && child.type === 'text') {
|
|
796
933
|
const rawContent = child.content
|
|
797
|
-
if (
|
|
934
|
+
if (styleProcessEnabled && rawContent && rawContent.indexOf('{') !== -1 && rawContent.indexOf('}') !== -1) {
|
|
798
935
|
const imageAttrs = rawContent.match(imageAttrsReg)
|
|
799
936
|
if (imageAttrs) {
|
|
800
937
|
const parsedAttrs = parseImageAttrs(imageAttrs[1])
|
|
@@ -802,6 +939,7 @@ const detectImageParagraph = (tokens, token, nextToken, n, caption, sp, opt) =>
|
|
|
802
939
|
for (let i = 0; i < parsedAttrs.length; i++) {
|
|
803
940
|
sp.attrs.push(parsedAttrs[i])
|
|
804
941
|
}
|
|
942
|
+
child.content = ''
|
|
805
943
|
}
|
|
806
944
|
break
|
|
807
945
|
}
|
|
@@ -812,7 +950,7 @@ const detectImageParagraph = (tokens, token, nextToken, n, caption, sp, opt) =>
|
|
|
812
950
|
break
|
|
813
951
|
}
|
|
814
952
|
|
|
815
|
-
if (!
|
|
953
|
+
if (!multipleImagesEnabled) {
|
|
816
954
|
isValid = false
|
|
817
955
|
break
|
|
818
956
|
}
|
|
@@ -831,7 +969,7 @@ const detectImageParagraph = (tokens, token, nextToken, n, caption, sp, opt) =>
|
|
|
831
969
|
isValid = false
|
|
832
970
|
break
|
|
833
971
|
}
|
|
834
|
-
if (isValid && imageNum > 1 &&
|
|
972
|
+
if (isValid && imageNum > 1 && multipleImagesEnabled) {
|
|
835
973
|
if (isMultipleImagesHorizontal) {
|
|
836
974
|
caption.nameSuffix = '-horizontal'
|
|
837
975
|
} else if (isMultipleImagesVertical) {
|
|
@@ -854,18 +992,13 @@ const detectImageParagraph = (tokens, token, nextToken, n, caption, sp, opt) =>
|
|
|
854
992
|
tagName,
|
|
855
993
|
en,
|
|
856
994
|
replaceInsteadOfWrap: true,
|
|
857
|
-
wrapWithoutCaption: isValid &&
|
|
995
|
+
wrapWithoutCaption: isValid && allowSingleImageWithoutCaption,
|
|
858
996
|
canWrap: isValid,
|
|
859
|
-
imageToken
|
|
997
|
+
imageToken,
|
|
860
998
|
}
|
|
861
999
|
}
|
|
862
1000
|
|
|
863
1001
|
const figureWithCaption = (state, opt) => {
|
|
864
|
-
let fNum = {
|
|
865
|
-
img: 0,
|
|
866
|
-
table: 0,
|
|
867
|
-
}
|
|
868
|
-
|
|
869
1002
|
const figureNumberState = {
|
|
870
1003
|
img: 0,
|
|
871
1004
|
table: 0,
|
|
@@ -873,14 +1006,13 @@ const figureWithCaption = (state, opt) => {
|
|
|
873
1006
|
|
|
874
1007
|
const fallbackLabelState = {
|
|
875
1008
|
img: null,
|
|
876
|
-
table: null,
|
|
877
1009
|
}
|
|
878
1010
|
|
|
879
1011
|
const captionState = { tokens: state.tokens, Token: state.Token }
|
|
880
|
-
figureWithCaptionCore(state.tokens, opt,
|
|
1012
|
+
figureWithCaptionCore(state.tokens, opt, figureNumberState, fallbackLabelState, state.Token, captionState, null, 0)
|
|
881
1013
|
}
|
|
882
1014
|
|
|
883
|
-
const figureWithCaptionCore = (tokens, opt,
|
|
1015
|
+
const figureWithCaptionCore = (tokens, opt, figureNumberState, fallbackLabelState, TokenConstructor, captionState, parentType = null, startIndex = 0) => {
|
|
884
1016
|
const rRange = { start: startIndex, end: startIndex }
|
|
885
1017
|
const rCaption = {
|
|
886
1018
|
name: '', nameSuffix: '', isPrev: false, isNext: false
|
|
@@ -898,7 +1030,7 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
|
|
|
898
1030
|
const containerType = getNestedContainerType(token)
|
|
899
1031
|
|
|
900
1032
|
if (containerType && containerType !== 'blockquote') {
|
|
901
|
-
const closeIndex = figureWithCaptionCore(tokens, opt,
|
|
1033
|
+
const closeIndex = figureWithCaptionCore(tokens, opt, figureNumberState, fallbackLabelState, TokenConstructor, captionState, containerType, n + 1)
|
|
902
1034
|
n = (typeof closeIndex === 'number' ? closeIndex : n) + 1
|
|
903
1035
|
continue
|
|
904
1036
|
}
|
|
@@ -912,11 +1044,13 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
|
|
|
912
1044
|
const tokenType = token.type
|
|
913
1045
|
const blockType = CHECK_TYPE_TOKEN_MAP[tokenType]
|
|
914
1046
|
if (tokenType === 'paragraph_open') {
|
|
915
|
-
resetRangeState(rRange, n)
|
|
916
|
-
resetCaptionState(rCaption)
|
|
917
|
-
resetSpecialState(rSp)
|
|
918
1047
|
const nextToken = tokens[n + 1]
|
|
919
|
-
|
|
1048
|
+
if (hasLeadingImageChild(nextToken)) {
|
|
1049
|
+
resetRangeState(rRange, n)
|
|
1050
|
+
resetCaptionState(rCaption)
|
|
1051
|
+
resetSpecialState(rSp)
|
|
1052
|
+
detection = detectImageParagraph(nextToken, n, rCaption, rSp, opt)
|
|
1053
|
+
}
|
|
920
1054
|
} else if (tokenType === 'html_block') {
|
|
921
1055
|
resetRangeState(rRange, n)
|
|
922
1056
|
resetCaptionState(rCaption)
|
|
@@ -936,7 +1070,7 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
|
|
|
936
1070
|
|
|
937
1071
|
if (!detection) {
|
|
938
1072
|
if (containerType === 'blockquote') {
|
|
939
|
-
const closeIndex = figureWithCaptionCore(tokens, opt,
|
|
1073
|
+
const closeIndex = figureWithCaptionCore(tokens, opt, figureNumberState, fallbackLabelState, TokenConstructor, captionState, containerType, n + 1)
|
|
940
1074
|
n = (typeof closeIndex === 'number' ? closeIndex : n) + 1
|
|
941
1075
|
} else {
|
|
942
1076
|
n++
|
|
@@ -947,7 +1081,7 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
|
|
|
947
1081
|
rRange.end = detection.en
|
|
948
1082
|
|
|
949
1083
|
rSp.figureClassName = resolveFigureClassName(detection.tagName, rSp, opt)
|
|
950
|
-
checkCaption(tokens, rRange.start, rRange.end, rCaption,
|
|
1084
|
+
checkCaption(tokens, rRange.start, rRange.end, rCaption, rSp, opt, captionState)
|
|
951
1085
|
applyCaptionDrivenFigureClass(rCaption, rSp, opt)
|
|
952
1086
|
|
|
953
1087
|
let hasCaption = rCaption.isPrev || rCaption.isNext
|
|
@@ -962,7 +1096,7 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
|
|
|
962
1096
|
if (detection.canWrap === false) {
|
|
963
1097
|
let nextIndex = rRange.end + 1
|
|
964
1098
|
if (containerType === 'blockquote') {
|
|
965
|
-
const closeIndex = figureWithCaptionCore(tokens, opt,
|
|
1099
|
+
const closeIndex = figureWithCaptionCore(tokens, opt, figureNumberState, fallbackLabelState, TokenConstructor, captionState, containerType, rRange.start + 1)
|
|
966
1100
|
nextIndex = Math.max(nextIndex, (typeof closeIndex === 'number' ? closeIndex : rRange.end) + 1)
|
|
967
1101
|
}
|
|
968
1102
|
n = nextIndex
|
|
@@ -989,7 +1123,7 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
|
|
|
989
1123
|
rRange.start += insertedLength
|
|
990
1124
|
rRange.end += insertedLength
|
|
991
1125
|
n += insertedLength
|
|
992
|
-
checkCaption(tokens, rRange.start, rRange.end, rCaption,
|
|
1126
|
+
checkCaption(tokens, rRange.start, rRange.end, rCaption, rSp, opt, captionState)
|
|
993
1127
|
applyCaptionDrivenFigureClass(rCaption, rSp, opt)
|
|
994
1128
|
}
|
|
995
1129
|
ensureAutoFigureNumbering(tokens, rRange, rCaption, figureNumberState, opt)
|
|
@@ -1017,7 +1151,7 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
|
|
|
1017
1151
|
}
|
|
1018
1152
|
|
|
1019
1153
|
if (containerType === 'blockquote') {
|
|
1020
|
-
const closeIndex = figureWithCaptionCore(tokens, opt,
|
|
1154
|
+
const closeIndex = figureWithCaptionCore(tokens, opt, figureNumberState, fallbackLabelState, TokenConstructor, captionState, containerType, rRange.start + 1)
|
|
1021
1155
|
const fallbackIndex = rCaption.name ? rRange.end : n
|
|
1022
1156
|
nextIndex = Math.max(nextIndex, (typeof closeIndex === 'number' ? closeIndex : fallbackIndex) + 1)
|
|
1023
1157
|
}
|
|
@@ -1029,11 +1163,14 @@ const figureWithCaptionCore = (tokens, opt, fNum, figureNumberState, fallbackLab
|
|
|
1029
1163
|
|
|
1030
1164
|
const mditFigureWithPCaption = (md, option) => {
|
|
1031
1165
|
let opt = {
|
|
1166
|
+
// Caption languages delegated to p-captions.
|
|
1167
|
+
languages: ['en', 'ja'],
|
|
1168
|
+
|
|
1032
1169
|
// --- figure-wrapper behavior ---
|
|
1033
1170
|
classPrefix: 'f',
|
|
1034
1171
|
figureClassThatWrapsIframeTypeBlockquote: null,
|
|
1035
1172
|
figureClassThatWrapsSlides: null,
|
|
1036
|
-
styleProcess
|
|
1173
|
+
styleProcess: true,
|
|
1037
1174
|
oneImageWithoutCaption: false,
|
|
1038
1175
|
iframeWithoutCaption: false,
|
|
1039
1176
|
videoWithoutCaption: false,
|
|
@@ -1047,7 +1184,7 @@ const mditFigureWithPCaption = (md, option) => {
|
|
|
1047
1184
|
// Applies only to the first image within an image-only paragraph (even when multipleImages is true).
|
|
1048
1185
|
// Priority: caption paragraphs (before/after) > alt text > title attribute; auto detection only runs when no paragraph caption exists.
|
|
1049
1186
|
autoCaptionDetection: true,
|
|
1050
|
-
autoAltCaption: false, // allow alt text (when matching markReg.img
|
|
1187
|
+
autoAltCaption: false, // allow alt text (when matching markReg.img) to build captions automatically
|
|
1051
1188
|
autoTitleCaption: false, // same as above but reads from the title attribute when alt isn't usable
|
|
1052
1189
|
|
|
1053
1190
|
// --- label prefix marker helpers ---
|
|
@@ -1069,11 +1206,30 @@ const mditFigureWithPCaption = (md, option) => {
|
|
|
1069
1206
|
removeUnnumberedLabelExceptMarks: [],
|
|
1070
1207
|
removeMarkNameInCaptionClass: false,
|
|
1071
1208
|
wrapCaptionBody: false,
|
|
1209
|
+
labelClassFollowsFigure: false,
|
|
1210
|
+
figureToLabelClassMap: null,
|
|
1072
1211
|
}
|
|
1073
1212
|
const hasExplicitAutoLabelNumberSets = option && Object.prototype.hasOwnProperty.call(option, 'autoLabelNumberSets')
|
|
1074
1213
|
const hasExplicitFigureClassThatWrapsIframeTypeBlockquote = option && Object.prototype.hasOwnProperty.call(option, 'figureClassThatWrapsIframeTypeBlockquote')
|
|
1075
1214
|
const hasExplicitFigureClassThatWrapsSlides = option && Object.prototype.hasOwnProperty.call(option, 'figureClassThatWrapsSlides')
|
|
1215
|
+
const hasExplicitLabelClassFollowsFigure = option && Object.prototype.hasOwnProperty.call(option, 'labelClassFollowsFigure')
|
|
1076
1216
|
if (option) Object.assign(opt, option)
|
|
1217
|
+
if (!hasExplicitLabelClassFollowsFigure && opt.figureToLabelClassMap) {
|
|
1218
|
+
opt.labelClassFollowsFigure = true
|
|
1219
|
+
}
|
|
1220
|
+
opt.classPrefix = normalizeOptionalClassName(opt.classPrefix)
|
|
1221
|
+
opt.allIframeTypeFigureClassName = normalizeOptionalClassName(opt.allIframeTypeFigureClassName)
|
|
1222
|
+
opt.languages = normalizeLanguages(opt.languages)
|
|
1223
|
+
opt.markRegState = getMarkRegStateForLanguages(opt.languages)
|
|
1224
|
+
opt.imgCaptionMarkReg = opt.markRegState && opt.markRegState.markReg
|
|
1225
|
+
? opt.markRegState.markReg.img
|
|
1226
|
+
: null
|
|
1227
|
+
opt.htmlWrapWithoutCaption = {
|
|
1228
|
+
iframe: !!opt.iframeWithoutCaption,
|
|
1229
|
+
video: !!opt.videoWithoutCaption,
|
|
1230
|
+
audio: !!opt.audioWithoutCaption,
|
|
1231
|
+
iframeTypeBlockquote: !!opt.iframeTypeBlockquoteWithoutCaption,
|
|
1232
|
+
}
|
|
1077
1233
|
// Normalize option shorthands now so downstream logic works with a consistent { img, table } shape.
|
|
1078
1234
|
opt.autoLabelNumberSets = normalizeAutoLabelNumberSets(opt.autoLabelNumberSets)
|
|
1079
1235
|
if (opt.autoLabelNumber && !hasExplicitAutoLabelNumberSets) {
|
|
@@ -1083,16 +1239,29 @@ const mditFigureWithPCaption = (md, option) => {
|
|
|
1083
1239
|
const classPrefix = buildClassPrefix(opt.classPrefix)
|
|
1084
1240
|
opt.figureClassPrefix = classPrefix
|
|
1085
1241
|
opt.captionClassPrefix = classPrefix
|
|
1242
|
+
const defaultIframeTypeBlockquoteClass = classPrefix + 'img'
|
|
1243
|
+
const defaultSlideFigureClass = classPrefix + 'slide'
|
|
1086
1244
|
if (!hasExplicitFigureClassThatWrapsIframeTypeBlockquote) {
|
|
1087
|
-
opt.figureClassThatWrapsIframeTypeBlockquote =
|
|
1245
|
+
opt.figureClassThatWrapsIframeTypeBlockquote = defaultIframeTypeBlockquoteClass
|
|
1246
|
+
} else {
|
|
1247
|
+
opt.figureClassThatWrapsIframeTypeBlockquote = normalizeClassOptionWithFallback(
|
|
1248
|
+
opt.figureClassThatWrapsIframeTypeBlockquote,
|
|
1249
|
+
defaultIframeTypeBlockquoteClass,
|
|
1250
|
+
)
|
|
1088
1251
|
}
|
|
1089
1252
|
if (!hasExplicitFigureClassThatWrapsSlides) {
|
|
1090
|
-
opt.figureClassThatWrapsSlides =
|
|
1253
|
+
opt.figureClassThatWrapsSlides = defaultSlideFigureClass
|
|
1254
|
+
} else {
|
|
1255
|
+
opt.figureClassThatWrapsSlides = normalizeClassOptionWithFallback(
|
|
1256
|
+
opt.figureClassThatWrapsSlides,
|
|
1257
|
+
defaultSlideFigureClass,
|
|
1258
|
+
)
|
|
1091
1259
|
}
|
|
1092
1260
|
// Precompute label-class permutations so numbering lookup doesn't rebuild arrays per caption.
|
|
1093
1261
|
opt.labelClassLookup = buildLabelClassLookup(opt)
|
|
1094
1262
|
const markerList = normalizeLabelPrefixMarkers(opt.labelPrefixMarker)
|
|
1095
1263
|
opt.labelPrefixMarkerReg = buildLabelPrefixMarkerRegFromList(markerList)
|
|
1264
|
+
opt.cleanCaptionRegCache = new Map()
|
|
1096
1265
|
if (opt.allowLabelPrefixMarkerWithoutLabel === true) {
|
|
1097
1266
|
const markerPair = resolveLabelPrefixMarkerPair(markerList)
|
|
1098
1267
|
opt.labelPrefixMarkerWithoutLabelPrevReg = buildLabelPrefixMarkerRegFromList(markerPair.prev)
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peaceroad/markdown-it-figure-with-p-caption",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
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",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"test": "node test/test.js"
|
|
8
|
+
"test": "node test/test.js",
|
|
9
|
+
"test:p-captions": "node test/test-p-captions.js",
|
|
10
|
+
"test:all": "npm run test && npm run test:p-captions",
|
|
11
|
+
"perf": "node test/performance/benchmark.js"
|
|
9
12
|
},
|
|
10
13
|
"repository": {
|
|
11
14
|
"type": "git",
|
|
@@ -17,16 +20,16 @@
|
|
|
17
20
|
"url": "https://github.com/peaceroad/p7d-markdown-it-figure-with-p-caption/issues"
|
|
18
21
|
},
|
|
19
22
|
"devDependencies": {
|
|
20
|
-
"@peaceroad/markdown-it-cjk-breaks-mod": "^0.1.
|
|
21
|
-
"@peaceroad/markdown-it-renderer-fence": "^0.
|
|
22
|
-
"@peaceroad/markdown-it-renderer-image": "^0.
|
|
23
|
-
"@peaceroad/markdown-it-strong-ja": "^0.
|
|
23
|
+
"@peaceroad/markdown-it-cjk-breaks-mod": "^0.1.10",
|
|
24
|
+
"@peaceroad/markdown-it-renderer-fence": "^0.6.1",
|
|
25
|
+
"@peaceroad/markdown-it-renderer-image": "^0.13.0",
|
|
26
|
+
"@peaceroad/markdown-it-strong-ja": "^0.9.0",
|
|
24
27
|
"highlight.js": "^11.11.1",
|
|
25
28
|
"markdown-it": "^14.1.0",
|
|
26
29
|
"markdown-it-attrs": "^4.3.1"
|
|
27
30
|
},
|
|
28
31
|
"dependencies": {
|
|
29
|
-
"p7d-markdown-it-p-captions": "^0.
|
|
32
|
+
"p7d-markdown-it-p-captions": "^0.21.0"
|
|
30
33
|
},
|
|
31
34
|
"files": [
|
|
32
35
|
"index.js",
|