@peaceroad/markdown-it-figure-with-p-caption 0.17.0 → 0.19.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 +23 -16
- package/embeds/detect.js +36 -28
- package/embeds/providers.js +3 -1
- package/index.js +349 -172
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Optionally, you can auto-number image and table caption paragraphs starting from
|
|
|
18
18
|
2. Image `alt` text that `p7d-markdown-it-p-captions` recognizes as an image caption start (`Figure. `, `Figure 1. `, `図 `, `図1 `, etc.).
|
|
19
19
|
3. Image `title` attribute that matches the same labels.
|
|
20
20
|
4. Optional fallbacks (`autoAltCaption`, `autoTitleCaption`) that inject the label when the alt/title lacks one.
|
|
21
|
-
- `autoAltCaption`: `false` (default), `true`, or a string label. `true` uses locale-aware generated-label defaults from `p7d-markdown-it-p-captions`, so the label text and punctuation stay aligned with the upstream caption language data. A string
|
|
21
|
+
- `autoAltCaption`: `false` (default), `true`, or a string label. `true` uses locale-aware generated-label defaults from `p7d-markdown-it-p-captions`, so the label text and punctuation stay aligned with the upstream caption language data. A string is treated as a label stem that must be recognized by `p7d-markdown-it-p-captions`; setup throws if it cannot be parsed as an image caption label. This plugin appends the default joint/space unless the string already ends with punctuation such as `.` / `。` / `:`. Empty alt text does not generate a fallback caption.
|
|
22
22
|
- `autoTitleCaption`: same behavior but sourced from the image `title`. It stays off by default so other plugins can keep using the `title` attribute for metadata.
|
|
23
23
|
- Set `autoCaptionDetection: false` to disable the auto-caption workflow entirely.
|
|
24
24
|
- Multi-image paragraphs are still wrapped as one figure when `multipleImages: true` (default). Layout-specific classes help with styling:
|
|
@@ -41,7 +41,7 @@ Optionally, you can auto-number image and table caption paragraphs starting from
|
|
|
41
41
|
|
|
42
42
|
### Blockquote
|
|
43
43
|
|
|
44
|
-
- Captioned blockquotes (e.g.,
|
|
44
|
+
- Captioned blockquotes (e.g., `Source. A paragraph.` written immediately before or after `> ...`) become `<figure class="f-blockquote">` while keeping the original blockquote intact.
|
|
45
45
|
|
|
46
46
|
### Video & Audio
|
|
47
47
|
|
|
@@ -50,8 +50,9 @@ Optionally, you can auto-number image and table caption paragraphs starting from
|
|
|
50
50
|
|
|
51
51
|
### Embedded content by iframe
|
|
52
52
|
|
|
53
|
-
- Inline HTML `<iframe>` elements become `<figure class="f-video">` when they point to known video hosts (YouTube `youtube-nocookie.com`, Vimeo `player.vimeo.com`).
|
|
53
|
+
- Inline HTML `<iframe>` elements become `<figure class="f-video">` when they point to known video hosts (YouTube `www.youtube.com`, `youtube.com`, `www.youtube-nocookie.com`, `youtube-nocookie.com`, Vimeo `player.vimeo.com`).
|
|
54
54
|
- `<div>` wrappers are treated as iframe-type embeds only when the same HTML block contains an `<iframe ...>` tag (for example common video wrapper markup).
|
|
55
|
+
- `videoWithoutCaption` classifies an iframe (including one nested in a `<div>` wrapper) as video only when its `src` host is one of the known YouTube/Vimeo hosts. A generic iframe wrapper still requires `iframeWithoutCaption`.
|
|
55
56
|
- Blockquote-based social embeds (Twitter/X `twitter-tweet`, Mastodon `mastodon-embed`, Bluesky `bluesky-embed`, Instagram `instagram-media`, Tumblr `text-post-media`) are treated like iframe-type embeds when their class list contains one of those provider classes. Extra classes on the same blockquote do not block detection. By default they become `<figure class="f-img">` so the caption label behaves like an image label (Labels can also use quote labels). You can override that figure class with `figureClassThatWrapsIframeTypeBlockquote` or the global `allIframeTypeFigureClassName`.
|
|
56
57
|
- `p7d-markdown-it-p-captions` ships with a `Slide.` label. When you use it (for example with Speaker Deck or SlideShare iframes), the `<figure>` wrapper automatically switches to `f-slide` (or whatever you set via `figureClassThatWrapsSlides`) so slides can get their own layout. If `allIframeTypeFigureClassName` is also configured, that class takes precedence even for slides, so you get a uniform embed wrapper without touching the slide option.
|
|
57
58
|
- All other iframes fall back to `<figure class="f-iframe">` unless you override the class via `allIframeTypeFigureClassName`.
|
|
@@ -60,7 +61,8 @@ Optionally, you can auto-number image and table caption paragraphs starting from
|
|
|
60
61
|
|
|
61
62
|
- 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`.
|
|
62
63
|
- 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
|
+
- `styleProcess` controls parsing of a trailing `{...}` block from the last text token of an image-only paragraph in this plugin's own scanner. It supports simple `.class`, `#id`, bare attributes, and quoted `key="value with spaces"` / `key='value with spaces'` pairs. It is still a narrow fallback parser, not full `markdown-it-attrs` parity, and attributes already attached to paragraph tokens by `markdown-it-attrs` are still forwarded.
|
|
65
|
+
- Attribute forwarding is not sanitization. If you render untrusted Markdown, keep using an HTML sanitizer or a trusted-host policy appropriate for your application; this plugin only decides which already-parsed or narrowly parsed attributes move onto `<figure>`.
|
|
64
66
|
- Attributes attached to caption paragraphs stay on the converted `<figcaption>` token after paragraph-to-figcaption conversion.
|
|
65
67
|
|
|
66
68
|
## Behavior Customization
|
|
@@ -75,7 +77,8 @@ Optionally, you can auto-number image and table caption paragraphs starting from
|
|
|
75
77
|
|
|
76
78
|
### Wrapping without captions
|
|
77
79
|
|
|
78
|
-
- `
|
|
80
|
+
- `imageOnlyParagraphWithoutCaption`: turn valid image-only paragraphs into `<figure>` elements even when no caption paragraph/auto caption is present. This includes single-image paragraphs and, when `multipleImages` is enabled, multi-image paragraphs that receive classes such as `f-img-horizontal`, `f-img-vertical`, or `f-img-multiple`. This is independent of automatic detection.
|
|
81
|
+
- `oneImageWithoutCaption`: legacy alias for `imageOnlyParagraphWithoutCaption`. When both are provided, `imageOnlyParagraphWithoutCaption` wins.
|
|
79
82
|
- `videoWithoutCaption`, `audioWithoutCaption`, `iframeWithoutCaption`, `iframeTypeBlockquoteWithoutCaption`: wrap the respective media blocks without caption.
|
|
80
83
|
|
|
81
84
|
### Caption text helpers (delegated to `p7d-markdown-it-p-captions`)
|
|
@@ -92,16 +95,18 @@ Every option below is forwarded verbatim to `p7d-markdown-it-p-captions`, which
|
|
|
92
95
|
- `labelClassFollowsFigure`: mirror the resolved `<figure>` class onto the `figcaption` spans (`f-embed-label`, `f-embed-label-joint`, `f-embed-body`, etc.) when you want captions styled alongside the wrapper.
|
|
93
96
|
- `figureToLabelClassMap`: extend `labelClassFollowsFigure` by mapping specific figure classes (e.g., `f-embed`) to custom caption label classes such as `caption-embed caption-social` for fine-grained control. When this map is provided and `labelClassFollowsFigure` is not set explicitly, figure-following mode is enabled automatically.
|
|
94
97
|
- `labelPrefixMarker`: allow a leading marker before labels (string or array, e.g., `*Figure. ...`). Arrays are limited to two markers; extras are ignored.
|
|
98
|
+
- `languages`: optional available caption-recognition catalogs delegated to `p7d-markdown-it-p-captions` (default: `['en', 'ja']`). Most users can leave this unset. Set it only when you want to restrict or extend which labels can be recognized (for example English `Figure.` and Japanese `図 `) and which catalogs are available for generated fallback labels. It is separate from the active locale used to choose among those available catalogs.
|
|
95
99
|
- Automatic image-label fallback text and punctuation (`Figure. `, `図 `, etc.) are generated from `p7d-markdown-it-p-captions` locale metadata, not from a local hardcoded map in this plugin.
|
|
96
|
-
-
|
|
100
|
+
- Generated fallback label tie-break is resolved once per render. Prefer passing the active locale through `env.locale` or `env.preferredLocales`. Compatibility fallbacks are `preferredLanguages`, `env.preferredLanguages`, `env.lang`, and `env.language`. If none of those selects an available catalog, this plugin finally uses a cheap document-script heuristic that skips a leading hyphen-fenced frontmatter block (`---` or longer, spaces allowed before newline), then falls back to the raw `languages` order. This tie-break only affects generated fallback labels; it does not change the caption-recognition dictionaries selected by `languages`. Compatibility note: for generated fallback labels, `env.locale` / `env.preferredLocales` intentionally take precedence over the legacy `preferredLanguages` option so a shared `md` instance can render different documents with different active locales.
|
|
97
101
|
|
|
98
102
|
### Automatic numbering
|
|
99
103
|
|
|
100
104
|
- `autoLabelNumberSets`: enable numbering per media type. Pass an array such as `['img']`, `['table']`, or `['img', 'table']`.
|
|
101
105
|
- `autoLabelNumber`: shorthand for turning numbering on for both images and tables without passing the array yourself. Provide `autoLabelNumberSets` explicitly (e.g., `['img']`) when you need finer control—the explicit array always wins.
|
|
106
|
+
- Do not pass p-captions' lower-level `setFigureNumber` option to this plugin. Figure numbering is owned by `autoLabelNumber` / `autoLabelNumberSets`; `setFigureNumber` is rejected during setup to prevent two numbering systems from mutating the same caption.
|
|
102
107
|
- Counters start at `1` near the top of the document and increment sequentially per media type. Figures and tables keep independent counters even when mixed together.
|
|
103
|
-
- The counter only advances when a real caption exists (paragraph, auto-detected alt/title, or fallback text). Figures emitted solely because of `oneImageWithoutCaption` stay unnumbered.
|
|
104
|
-
- Manual numbers inside the caption text (e.g., `Figure 5.`) always win. The plugin updates its
|
|
108
|
+
- The counter only advances when a real caption exists (paragraph, auto-detected alt/title, or fallback text). Figures emitted solely because of `imageOnlyParagraphWithoutCaption` / `oneImageWithoutCaption` stay unnumbered.
|
|
109
|
+
- Manual numbers inside the caption text (e.g., `Figure 5.`) always win. The plugin reads the exact `captionDecision.number` supplied by `p7d-markdown-it-p-captions` and updates its decimal counter so the next automatic number becomes `6`. Explicit compound/alphanumeric numbers (e.g., `Figure A.` or `Figure A.5.`) are preserved without appending an automatic number and do not seed the decimal counter. This applies to captions sourced from paragraphs, auto detection, and fallback captions.
|
|
105
110
|
|
|
106
111
|
## Basic Usage
|
|
107
112
|
|
|
@@ -128,12 +133,12 @@ Auto label numbering for images and tables.
|
|
|
128
133
|
```js
|
|
129
134
|
const figureOption = {
|
|
130
135
|
// Opinionated defaults
|
|
131
|
-
|
|
136
|
+
imageOnlyParagraphWithoutCaption: true,
|
|
132
137
|
videoWithoutCaption: true,
|
|
133
138
|
audioWithoutCaption: true,
|
|
134
139
|
iframeWithoutCaption: true,
|
|
135
140
|
iframeTypeBlockquoteWithoutCaption: true,
|
|
136
|
-
removeUnnumberedLabelExceptMarks: ['blockquote'], // keep
|
|
141
|
+
removeUnnumberedLabelExceptMarks: ['blockquote'], // keep `Quote.` labels even when unnumbered
|
|
137
142
|
allIframeTypeFigureClassName: 'f-embed', // apply a uniform class to every iframe-style embed
|
|
138
143
|
autoLabelNumber: true,
|
|
139
144
|
|
|
@@ -147,7 +152,7 @@ If there is no label number, the label will also be deleted.
|
|
|
147
152
|
|
|
148
153
|
```js
|
|
149
154
|
const figureOption = {
|
|
150
|
-
|
|
155
|
+
imageOnlyParagraphWithoutCaption: true,
|
|
151
156
|
videoWithoutCaption: true,
|
|
152
157
|
audioWithoutCaption: true,
|
|
153
158
|
iframeWithoutCaption: true,
|
|
@@ -175,7 +180,7 @@ const md = mdit({ html: true }).use(mditFigureWithPCaption, figureOption)
|
|
|
175
180
|
[HTML]
|
|
176
181
|
<p><img src="figure.jpg" alt="A single cat"></p>
|
|
177
182
|
|
|
178
|
-
<!-- Above: If oneImageWithoutCaption is true, this img element has wrapped into figure element without caption. -->
|
|
183
|
+
<!-- Above: If imageOnlyParagraphWithoutCaption (or its legacy alias oneImageWithoutCaption) is true, this img element has wrapped into figure element without caption. -->
|
|
179
184
|
|
|
180
185
|
|
|
181
186
|
[Markdown]
|
|
@@ -359,6 +364,8 @@ Slide. A Speaker Deck.
|
|
|
359
364
|
|
|
360
365
|
### Auto alt/title detection
|
|
361
366
|
|
|
367
|
+
The consumed `alt` or `title` value is cleared only after the image paragraph is confirmed to be wrappable. Tight-list images and image paragraphs with trailing non-image text keep their original accessibility attributes and baseline rendering.
|
|
368
|
+
|
|
362
369
|
```
|
|
363
370
|
[Markdown]
|
|
364
371
|

|
|
@@ -440,7 +447,7 @@ A paragraph.
|
|
|
440
447
|
|
|
441
448
|
### Styles
|
|
442
449
|
|
|
443
|
-
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`.
|
|
450
|
+
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; it supports quoted values with spaces, but for broader attrs syntax support, keep using `markdown-it-attrs`.
|
|
444
451
|
|
|
445
452
|
```
|
|
446
453
|
[Markdown]
|
|
@@ -456,7 +463,7 @@ Figure. Highlighted cat.
|
|
|
456
463
|
|
|
457
464
|
### Automatic detection fallbacks
|
|
458
465
|
|
|
459
|
-
`autoCaptionDetection` combined with `autoAltCaption` / `autoTitleCaption` can still generate caption text even when the original alt/title lacks labels, as long as the alt/title body is non-empty. The corresponding attributes are cleared after conversion so the figcaption becomes the canonical source. When these fallbacks are `true`, the generated label text and punctuation come from `p7d-markdown-it-p-captions` locale metadata rather than a local hardcoded map.
|
|
466
|
+
`autoCaptionDetection` combined with `autoAltCaption` / `autoTitleCaption` can still generate caption text even when the original alt/title lacks labels, as long as the alt/title body is non-empty. The corresponding attributes are cleared after conversion so the figcaption becomes the canonical source. When these fallbacks are `true`, the generated label text and punctuation come from `p7d-markdown-it-p-captions` locale metadata rather than a local hardcoded map. When these fallbacks are strings, the string must be a label stem recognized as an image caption label by `p7d-markdown-it-p-captions`; invalid strings fail during plugin setup instead of producing a stray paragraph.
|
|
460
467
|
|
|
461
468
|
```
|
|
462
469
|
[Markdown]
|
|
@@ -498,7 +505,7 @@ $ pwd
|
|
|
498
505
|
|
|
499
506
|
### Captionless conversion toggles
|
|
500
507
|
|
|
501
|
-
If `oneImageWithoutCaption` is enabled, a single image paragraph will be wrapped with `<figure class="f-img">` even without a caption.
|
|
508
|
+
If `imageOnlyParagraphWithoutCaption` (or the legacy alias `oneImageWithoutCaption`) is enabled, a single image paragraph will be wrapped with `<figure class="f-img">` even without a caption. Multi-image image-only paragraphs can also be wrapped, in which case the normal layout classes such as `f-img-horizontal`, `f-img-vertical`, or `f-img-multiple` are used.
|
|
502
509
|
|
|
503
510
|
```
|
|
504
511
|
[Markdown]
|
|
@@ -510,7 +517,7 @@ If `oneImageWithoutCaption` is enabled, a single image paragraph will be wrapped
|
|
|
510
517
|
</figure>
|
|
511
518
|
```
|
|
512
519
|
|
|
513
|
-
If `videoWithoutCaption` is enabled,
|
|
520
|
+
If `videoWithoutCaption` is enabled, `<video>` elements and iframes pointing to known video hosts (such as `www.youtube.com`, `youtube.com`, `www.youtube-nocookie.com`, or Vimeo) will be wrapped with `<figure class="f-video">`.
|
|
514
521
|
|
|
515
522
|
```
|
|
516
523
|
[Markdown]
|
package/embeds/detect.js
CHANGED
|
@@ -6,26 +6,38 @@ import {
|
|
|
6
6
|
|
|
7
7
|
const htmlRegCache = new Map()
|
|
8
8
|
const openingClassAttrReg = /^<[^>]*?\bclass=(?:"([^"]*)"|'([^']*)')/i
|
|
9
|
-
const
|
|
10
|
-
const endBlockquoteScriptReg = /<\/blockquote> *<script[^>]*?><\/script>$/
|
|
9
|
+
const iframeSrcAttrReg = /<iframe\b[^>]*?\bsrc=(?:"([^"]*)"|'([^']*)')/i
|
|
10
|
+
const endBlockquoteScriptReg = /<\/blockquote> *<script[^>]*?><\/script>$/i
|
|
11
|
+
const targetHtmlHintReg = /<(?:video|audio|iframe|blockquote|div)\b/i
|
|
12
|
+
const blueskyEmbedHintReg = /bluesky-embed/i
|
|
13
|
+
const videoTagHintReg = /<video\b/i
|
|
14
|
+
const audioTagHintReg = /<audio\b/i
|
|
15
|
+
const iframeTagHintReg = /<iframe\b/i
|
|
16
|
+
const blockquoteTagHintReg = /<blockquote\b/i
|
|
17
|
+
const divTagHintReg = /<div\b/i
|
|
11
18
|
const iframeTagReg = /<iframe(?=[\s>])/i
|
|
12
19
|
|
|
13
20
|
const getHtmlReg = (tag) => {
|
|
14
21
|
const cached = htmlRegCache.get(tag)
|
|
15
22
|
if (cached) return cached
|
|
16
23
|
const regexStr = `^<${tag} ?[^>]*?>[\\s\\S]*?<\\/${tag}>(\\n| *?)(<script [^>]*?>(?:<\\/script>)?)? *(\\n|$)`
|
|
17
|
-
const reg = new RegExp(regexStr)
|
|
24
|
+
const reg = new RegExp(regexStr, 'i')
|
|
18
25
|
htmlRegCache.set(tag, reg)
|
|
19
26
|
return reg
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
const getHtmlDetectionHints = (content) => {
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
const source = typeof content === 'string' ? content : ''
|
|
31
|
+
const hasTargetHtmlHint = targetHtmlHintReg.test(source)
|
|
32
|
+
const hasBlueskyHint = blueskyEmbedHintReg.test(source)
|
|
33
|
+
if (!hasTargetHtmlHint && !hasBlueskyHint) {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
const hasVideoHint = videoTagHintReg.test(source)
|
|
37
|
+
const hasAudioHint = audioTagHintReg.test(source)
|
|
38
|
+
const hasIframeHint = iframeTagHintReg.test(source)
|
|
39
|
+
const hasBlockquoteHint = blockquoteTagHintReg.test(source)
|
|
40
|
+
const hasDivHint = divTagHintReg.test(source)
|
|
29
41
|
return {
|
|
30
42
|
hasBlueskyHint,
|
|
31
43
|
hasVideoHint,
|
|
@@ -33,21 +45,10 @@ const getHtmlDetectionHints = (content) => {
|
|
|
33
45
|
hasIframeHint,
|
|
34
46
|
hasBlockquoteHint,
|
|
35
47
|
hasDivHint,
|
|
36
|
-
hasIframeTag: hasIframeHint || (hasDivHint && iframeTagReg.test(
|
|
48
|
+
hasIframeTag: hasIframeHint || (hasDivHint && iframeTagReg.test(source)),
|
|
37
49
|
}
|
|
38
50
|
}
|
|
39
51
|
|
|
40
|
-
const hasAnyHtmlDetectionHint = (hints) => {
|
|
41
|
-
return !!(
|
|
42
|
-
hints.hasBlueskyHint ||
|
|
43
|
-
hints.hasVideoHint ||
|
|
44
|
-
hints.hasAudioHint ||
|
|
45
|
-
hints.hasIframeHint ||
|
|
46
|
-
hints.hasBlockquoteHint ||
|
|
47
|
-
hints.hasDivHint
|
|
48
|
-
)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
52
|
const appendHtmlBlockNewlineIfNeeded = (token, hasTag) => {
|
|
52
53
|
if ((hasTag[2] && hasTag[3] !== '\n') || (hasTag[1] !== '\n' && hasTag[2] === undefined)) {
|
|
53
54
|
token.content += '\n'
|
|
@@ -89,6 +90,13 @@ const getOpeningAttrValue = (content, reg) => {
|
|
|
89
90
|
return match[1] || match[2] || ''
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
const getAttrValue = (content, reg) => {
|
|
94
|
+
if (typeof content !== 'string') return ''
|
|
95
|
+
const match = content.match(reg)
|
|
96
|
+
if (!match) return ''
|
|
97
|
+
return match[1] || match[2] || ''
|
|
98
|
+
}
|
|
99
|
+
|
|
92
100
|
const hasKnownBlockquoteEmbedClass = (content) => {
|
|
93
101
|
const classAttr = getOpeningAttrValue(content, openingClassAttrReg)
|
|
94
102
|
if (!classAttr) return false
|
|
@@ -105,14 +113,14 @@ const hasKnownBlockquoteEmbedClass = (content) => {
|
|
|
105
113
|
}
|
|
106
114
|
|
|
107
115
|
const isKnownVideoIframe = (content) => {
|
|
108
|
-
const src =
|
|
116
|
+
const src = getAttrValue(content, iframeSrcAttrReg)
|
|
109
117
|
if (!src || src.slice(0, 8).toLowerCase() !== 'https://') return false
|
|
110
118
|
const slashIndex = src.indexOf('/', 8)
|
|
111
119
|
const host = (slashIndex === -1 ? src.slice(8) : src.slice(8, slashIndex)).toLowerCase()
|
|
112
120
|
return VIDEO_IFRAME_HOSTS.has(host)
|
|
113
121
|
}
|
|
114
122
|
|
|
115
|
-
const detectHtmlTagCandidate = (tokens, token, startIndex, detector, hints
|
|
123
|
+
const detectHtmlTagCandidate = (tokens, token, startIndex, detector, hints) => {
|
|
116
124
|
if (detector.requiresIframeTag && !hints.hasIframeTag) return ''
|
|
117
125
|
const hasTagHint = !!(detector.hintKey && hints[detector.hintKey])
|
|
118
126
|
const allowBlueskyFallback = detector.candidate === 'blockquote' && hints.hasBlueskyHint
|
|
@@ -122,9 +130,6 @@ const detectHtmlTagCandidate = (tokens, token, startIndex, detector, hints, resu
|
|
|
122
130
|
if (!hasTag && !isBlueskyFallback) return ''
|
|
123
131
|
if (hasTag) {
|
|
124
132
|
appendHtmlBlockNewlineIfNeeded(token, hasTag)
|
|
125
|
-
if (detector.treatAsVideoIframe) {
|
|
126
|
-
result.isVideoIframe = true
|
|
127
|
-
}
|
|
128
133
|
return detector.matchedTag || detector.candidate
|
|
129
134
|
}
|
|
130
135
|
consumeBlockquoteEmbedScript(tokens, token, startIndex)
|
|
@@ -136,13 +141,16 @@ const resolveHtmlWrapWithoutCaption = (matchedTag, result, htmlWrapWithoutCaptio
|
|
|
136
141
|
if (matchedTag === 'blockquote') {
|
|
137
142
|
return !!(result.isIframeTypeBlockquote && htmlWrapWithoutCaption.iframeTypeBlockquote)
|
|
138
143
|
}
|
|
144
|
+
if (matchedTag === 'iframe' && result.isVideoIframe) {
|
|
145
|
+
return !!(htmlWrapWithoutCaption.video || htmlWrapWithoutCaption.iframe)
|
|
146
|
+
}
|
|
139
147
|
return !!htmlWrapWithoutCaption[matchedTag]
|
|
140
148
|
}
|
|
141
149
|
|
|
142
150
|
export const detectHtmlFigureCandidate = (tokens, token, startIndex, htmlWrapWithoutCaption) => {
|
|
143
151
|
if (!token || token.type !== 'html_block') return null
|
|
144
152
|
const hints = getHtmlDetectionHints(token.content)
|
|
145
|
-
if (!
|
|
153
|
+
if (!hints) return null
|
|
146
154
|
|
|
147
155
|
const result = {
|
|
148
156
|
isVideoIframe: false,
|
|
@@ -151,7 +159,7 @@ export const detectHtmlFigureCandidate = (tokens, token, startIndex, htmlWrapWit
|
|
|
151
159
|
|
|
152
160
|
let matchedTag = ''
|
|
153
161
|
for (let i = 0; i < HTML_EMBED_CANDIDATES.length; i++) {
|
|
154
|
-
matchedTag = detectHtmlTagCandidate(tokens, token, startIndex, HTML_EMBED_CANDIDATES[i], hints
|
|
162
|
+
matchedTag = detectHtmlTagCandidate(tokens, token, startIndex, HTML_EMBED_CANDIDATES[i], hints)
|
|
155
163
|
if (matchedTag) break
|
|
156
164
|
}
|
|
157
165
|
if (!matchedTag) return null
|
package/embeds/providers.js
CHANGED
|
@@ -9,7 +9,6 @@ export const HTML_EMBED_CANDIDATES = Object.freeze([
|
|
|
9
9
|
hintKey: 'hasDivHint',
|
|
10
10
|
requiresIframeTag: true,
|
|
11
11
|
matchedTag: 'iframe',
|
|
12
|
-
treatAsVideoIframe: true,
|
|
13
12
|
},
|
|
14
13
|
])
|
|
15
14
|
|
|
@@ -22,6 +21,9 @@ export const BLOCKQUOTE_EMBED_CLASS_NAMES = new Set([
|
|
|
22
21
|
])
|
|
23
22
|
|
|
24
23
|
export const VIDEO_IFRAME_HOSTS = new Set([
|
|
24
|
+
'www.youtube.com',
|
|
25
|
+
'youtube.com',
|
|
25
26
|
'www.youtube-nocookie.com',
|
|
27
|
+
'youtube-nocookie.com',
|
|
26
28
|
'player.vimeo.com',
|
|
27
29
|
])
|
package/index.js
CHANGED
|
@@ -13,14 +13,18 @@ import { detectHtmlFigureCandidate } from './embeds/detect.js'
|
|
|
13
13
|
const imageAttrsReg = /^ *\{(.*?)\} *$/
|
|
14
14
|
const classAttrReg = /^\./
|
|
15
15
|
const idAttrReg = /^#/
|
|
16
|
-
const attrParseReg = /^(.*?)="?(.*)"?$/
|
|
17
16
|
const sampLangReg = /^ *(?:samp|shell|console)(?:(?= )|$)/
|
|
18
17
|
const asciiLabelReg = /^[A-Za-z]/
|
|
18
|
+
const attrNameReg = /^[^\s=]+$/
|
|
19
|
+
const labelTrailingJointReg = /[.\u3002\uff0e::]\s*$/
|
|
19
20
|
const CHECK_TYPE_TOKEN_MAP = {
|
|
20
21
|
table_open: 'table',
|
|
21
22
|
pre_open: 'pre',
|
|
22
23
|
blockquote_open: 'blockquote',
|
|
23
24
|
}
|
|
25
|
+
const hasOwnOption = (option, name) => !!(
|
|
26
|
+
option && Object.prototype.hasOwnProperty.call(option, name)
|
|
27
|
+
)
|
|
24
28
|
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
25
29
|
const normalizeLanguageCode = (value) => {
|
|
26
30
|
if (value === null || value === undefined) return ''
|
|
@@ -29,28 +33,47 @@ const normalizeLanguageCode = (value) => {
|
|
|
29
33
|
const separatorIndex = normalized.search(/[-_]/)
|
|
30
34
|
return separatorIndex === -1 ? normalized : normalized.slice(0, separatorIndex)
|
|
31
35
|
}
|
|
36
|
+
const appendAvailableLanguage = (target, lang, availableLanguages) => {
|
|
37
|
+
if (!lang) return false
|
|
38
|
+
if (availableLanguages.indexOf(lang) === -1) return false
|
|
39
|
+
if (target.indexOf(lang) !== -1) return false
|
|
40
|
+
target.push(lang)
|
|
41
|
+
return true
|
|
42
|
+
}
|
|
32
43
|
const normalizePreferredLanguages = (value, availableLanguages) => {
|
|
33
44
|
if (!Array.isArray(availableLanguages) || availableLanguages.length === 0) return []
|
|
34
|
-
const source = typeof value === 'string' ? [value] : (Array.isArray(value) ? value : [])
|
|
35
|
-
if (source.length === 0) return []
|
|
36
|
-
const allowed = new Set(availableLanguages)
|
|
37
45
|
const languages = []
|
|
38
|
-
|
|
46
|
+
if (typeof value === 'string') {
|
|
47
|
+
appendAvailableLanguage(languages, normalizeLanguageCode(value), availableLanguages)
|
|
48
|
+
return languages
|
|
49
|
+
}
|
|
50
|
+
const source = Array.isArray(value) ? value : []
|
|
51
|
+
if (source.length === 0) return languages
|
|
39
52
|
for (let i = 0; i < source.length; i++) {
|
|
40
53
|
const lang = normalizeLanguageCode(source[i])
|
|
41
|
-
|
|
42
|
-
seen.add(lang)
|
|
43
|
-
languages.push(lang)
|
|
54
|
+
appendAvailableLanguage(languages, lang, availableLanguages)
|
|
44
55
|
}
|
|
45
56
|
return languages
|
|
46
57
|
}
|
|
47
|
-
const
|
|
48
|
-
if (!
|
|
58
|
+
const prioritizeLanguages = (languages, preferredLanguages) => {
|
|
59
|
+
if (!Array.isArray(languages) || languages.length === 0) return []
|
|
60
|
+
if (typeof preferredLanguages === 'string') {
|
|
61
|
+
if (!preferredLanguages || languages.indexOf(preferredLanguages) === -1) return languages
|
|
62
|
+
if (languages[0] === preferredLanguages) return languages
|
|
63
|
+
const prioritized = [preferredLanguages]
|
|
64
|
+
for (let i = 0; i < languages.length; i++) {
|
|
65
|
+
appendAvailableLanguage(prioritized, languages[i], languages)
|
|
66
|
+
}
|
|
67
|
+
return prioritized
|
|
68
|
+
}
|
|
69
|
+
if (!Array.isArray(preferredLanguages) || preferredLanguages.length === 0) return languages
|
|
49
70
|
const prioritized = []
|
|
50
|
-
|
|
71
|
+
for (let i = 0; i < preferredLanguages.length; i++) {
|
|
72
|
+
appendAvailableLanguage(prioritized, preferredLanguages[i], languages)
|
|
73
|
+
}
|
|
74
|
+
if (prioritized.length === 0) return languages
|
|
51
75
|
for (let i = 0; i < languages.length; i++) {
|
|
52
|
-
|
|
53
|
-
prioritized.push(languages[i])
|
|
76
|
+
appendAvailableLanguage(prioritized, languages[i], languages)
|
|
54
77
|
}
|
|
55
78
|
return prioritized
|
|
56
79
|
}
|
|
@@ -129,34 +152,49 @@ const resolvePreferredLanguagesForState = (state, opt) => {
|
|
|
129
152
|
) ? opt.markRegState.languages : []
|
|
130
153
|
if (availableLanguages.length === 0) return []
|
|
131
154
|
|
|
132
|
-
const explicitPreferred = opt && Array.isArray(opt.preferredLanguages)
|
|
133
|
-
? opt.preferredLanguages
|
|
134
|
-
: []
|
|
135
|
-
if (explicitPreferred.length > 0) return explicitPreferred
|
|
136
|
-
|
|
137
155
|
const optionLanguages = opt && Array.isArray(opt.normalizedOptionLanguages)
|
|
138
156
|
? opt.normalizedOptionLanguages
|
|
139
157
|
: []
|
|
140
158
|
const baseLanguages = optionLanguages.length > 0 ? optionLanguages : availableLanguages
|
|
141
159
|
const env = state && state.env ? state.env : null
|
|
142
|
-
const envPreferred = normalizePreferredLanguages(env && env.preferredLanguages, availableLanguages)
|
|
143
|
-
if (envPreferred.length > 0) return envPreferred
|
|
144
160
|
|
|
145
|
-
const
|
|
161
|
+
const envLocale = normalizeLanguageCode(env && env.locale)
|
|
162
|
+
if (envLocale && baseLanguages.indexOf(envLocale) !== -1) {
|
|
163
|
+
return prioritizeLanguages(baseLanguages, envLocale)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const envPreferredLocales = normalizePreferredLanguages(env && env.preferredLocales, baseLanguages)
|
|
167
|
+
if (envPreferredLocales.length > 0) {
|
|
168
|
+
return prioritizeLanguages(baseLanguages, envPreferredLocales)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const explicitPreferred = opt && Array.isArray(opt.preferredLanguages)
|
|
172
|
+
? opt.preferredLanguages
|
|
173
|
+
: []
|
|
174
|
+
if (explicitPreferred.length > 0) {
|
|
175
|
+
return prioritizeLanguages(baseLanguages, explicitPreferred)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const envPreferred = normalizePreferredLanguages(env && env.preferredLanguages, baseLanguages)
|
|
179
|
+
if (envPreferred.length > 0) {
|
|
180
|
+
return prioritizeLanguages(baseLanguages, envPreferred)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const envLanguage = normalizeLanguageCode(env && (env.preferredLanguage || env.lang || env.language))
|
|
146
184
|
if (envLanguage && baseLanguages.indexOf(envLanguage) !== -1) {
|
|
147
|
-
return
|
|
185
|
+
return prioritizeLanguages(baseLanguages, envLanguage)
|
|
148
186
|
}
|
|
149
187
|
|
|
150
188
|
const detectedLanguage = detectDocumentPrimaryLanguage(state && state.src ? state.src : '', baseLanguages)
|
|
151
189
|
if (detectedLanguage) {
|
|
152
|
-
return
|
|
190
|
+
return prioritizeLanguages(baseLanguages, detectedLanguage)
|
|
153
191
|
}
|
|
154
192
|
return baseLanguages
|
|
155
193
|
}
|
|
156
194
|
const needsPreferredLanguagesResolution = (opt) => {
|
|
157
195
|
if (!opt || !opt.markRegState || !Array.isArray(opt.markRegState.languages)) return false
|
|
196
|
+
if (!opt.autoCaptionDetection) return false
|
|
158
197
|
if (opt.markRegState.languages.length <= 1) return false
|
|
159
|
-
if (Array.isArray(opt.preferredLanguages) && opt.preferredLanguages.length > 0) return false
|
|
160
198
|
return opt.autoAltCaption === true || opt.autoTitleCaption === true
|
|
161
199
|
}
|
|
162
200
|
const normalizeOptionalClassName = (value) => {
|
|
@@ -190,10 +228,62 @@ const getLabelPrefixMarkerMatch = (inlineToken, markerReg) => {
|
|
|
190
228
|
return match[0]
|
|
191
229
|
}
|
|
192
230
|
|
|
193
|
-
const
|
|
231
|
+
const splitImageAttrParts = (raw) => {
|
|
194
232
|
if (raw === null || raw === undefined) return null
|
|
233
|
+
const parts = []
|
|
234
|
+
let current = ''
|
|
235
|
+
let quote = ''
|
|
236
|
+
let escaped = false
|
|
237
|
+
for (let i = 0; i < raw.length; i++) {
|
|
238
|
+
const ch = raw[i]
|
|
239
|
+
if (quote) {
|
|
240
|
+
current += ch
|
|
241
|
+
if (escaped) {
|
|
242
|
+
escaped = false
|
|
243
|
+
continue
|
|
244
|
+
}
|
|
245
|
+
if (ch === '\\') {
|
|
246
|
+
escaped = true
|
|
247
|
+
continue
|
|
248
|
+
}
|
|
249
|
+
if (ch === quote) {
|
|
250
|
+
quote = ''
|
|
251
|
+
}
|
|
252
|
+
continue
|
|
253
|
+
}
|
|
254
|
+
if (ch === '"' || ch === "'") {
|
|
255
|
+
quote = ch
|
|
256
|
+
current += ch
|
|
257
|
+
continue
|
|
258
|
+
}
|
|
259
|
+
if (ch === ' ') {
|
|
260
|
+
if (current) {
|
|
261
|
+
parts.push(current)
|
|
262
|
+
current = ''
|
|
263
|
+
}
|
|
264
|
+
continue
|
|
265
|
+
}
|
|
266
|
+
current += ch
|
|
267
|
+
}
|
|
268
|
+
if (quote) return null
|
|
269
|
+
if (current) parts.push(current)
|
|
270
|
+
return parts
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const unquoteAttrValue = (value) => {
|
|
274
|
+
if (typeof value !== 'string' || value.length < 2) return value || ''
|
|
275
|
+
const first = value[0]
|
|
276
|
+
const last = value[value.length - 1]
|
|
277
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
278
|
+
return value.slice(1, -1).replace(/\\(["'\\])/g, '$1')
|
|
279
|
+
}
|
|
280
|
+
return value
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const parseImageAttrs = (raw) => {
|
|
284
|
+
const parts = splitImageAttrParts(raw)
|
|
285
|
+
if (!parts || parts.length === 0) return null
|
|
195
286
|
const attrs = []
|
|
196
|
-
const parts = raw.split(/ +/)
|
|
197
287
|
for (let i = 0; i < parts.length; i++) {
|
|
198
288
|
let entry = parts[i]
|
|
199
289
|
if (!entry) continue
|
|
@@ -202,9 +292,15 @@ const parseImageAttrs = (raw) => {
|
|
|
202
292
|
} else if (idAttrReg.test(entry)) {
|
|
203
293
|
entry = entry.replace(idAttrReg, 'id=')
|
|
204
294
|
}
|
|
205
|
-
const
|
|
206
|
-
if (
|
|
207
|
-
|
|
295
|
+
const equalIndex = entry.indexOf('=')
|
|
296
|
+
if (equalIndex === -1) {
|
|
297
|
+
if (!attrNameReg.test(entry)) return null
|
|
298
|
+
attrs.push([entry, ''])
|
|
299
|
+
continue
|
|
300
|
+
}
|
|
301
|
+
const name = entry.slice(0, equalIndex)
|
|
302
|
+
if (!name || !attrNameReg.test(name)) return null
|
|
303
|
+
attrs.push([name, unquoteAttrValue(entry.slice(equalIndex + 1))])
|
|
208
304
|
}
|
|
209
305
|
return attrs
|
|
210
306
|
}
|
|
@@ -214,7 +310,7 @@ const normalizeAutoLabelNumberSets = (value) => {
|
|
|
214
310
|
if (!value) return normalized
|
|
215
311
|
if (Array.isArray(value)) {
|
|
216
312
|
for (const entry of value) {
|
|
217
|
-
if (
|
|
313
|
+
if (entry === 'img' || entry === 'table') normalized[entry] = true
|
|
218
314
|
}
|
|
219
315
|
return normalized
|
|
220
316
|
}
|
|
@@ -299,6 +395,14 @@ const getImageAltText = (token) => {
|
|
|
299
395
|
|
|
300
396
|
const getImageTitleText = (token) => getTokenAttr(token, 'title')
|
|
301
397
|
|
|
398
|
+
const getFallbackStringLabelJoint = (label) => {
|
|
399
|
+
if (!label) return ''
|
|
400
|
+
if (labelTrailingJointReg.test(label)) {
|
|
401
|
+
return asciiLabelReg.test(label) ? ' ' : ''
|
|
402
|
+
}
|
|
403
|
+
return asciiLabelReg.test(label) ? '. ' : ' '
|
|
404
|
+
}
|
|
405
|
+
|
|
302
406
|
const buildCaptionWithFallback = (text, fallbackOption, mark, markRegState, preferredLanguages) => {
|
|
303
407
|
const trimmedText = (text || '').trim()
|
|
304
408
|
if (!fallbackOption) return ''
|
|
@@ -315,7 +419,19 @@ const buildCaptionWithFallback = (text, fallbackOption, mark, markRegState, pref
|
|
|
315
419
|
if (generatedDefaults) {
|
|
316
420
|
return label + (generatedDefaults.joint || '') + (generatedDefaults.space || '') + trimmedText
|
|
317
421
|
}
|
|
318
|
-
return label + (
|
|
422
|
+
return label + getFallbackStringLabelJoint(label) + trimmedText
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const validateFallbackCaptionLabelOption = (optionName, fallbackOption, markRegState) => {
|
|
426
|
+
if (typeof fallbackOption !== 'string') return
|
|
427
|
+
const sampleCaption = buildCaptionWithFallback('caption', fallbackOption, 'img', markRegState, null)
|
|
428
|
+
const analysis = analyzeCaptionStart(sampleCaption, {
|
|
429
|
+
markRegState,
|
|
430
|
+
preferredMark: 'img',
|
|
431
|
+
})
|
|
432
|
+
if (!analysis || analysis.mark !== 'img' || analysis.kind !== 'caption') {
|
|
433
|
+
throw new Error(`${optionName} must be a string label recognized as an image caption by p7d-markdown-it-p-captions: ${fallbackOption}`)
|
|
434
|
+
}
|
|
319
435
|
}
|
|
320
436
|
|
|
321
437
|
const createAutoCaptionParagraph = (captionText, TokenConstructor) => {
|
|
@@ -343,12 +459,18 @@ const getCaptionInlineToken = (tokens, range, caption) => {
|
|
|
343
459
|
}
|
|
344
460
|
|
|
345
461
|
const hasClassName = (classAttr, className) => {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
462
|
+
if (!classAttr || !className) return false
|
|
463
|
+
let index = 0
|
|
464
|
+
while (index < classAttr.length) {
|
|
465
|
+
index = classAttr.indexOf(className, index)
|
|
466
|
+
if (index === -1) return false
|
|
467
|
+
const end = index + className.length
|
|
468
|
+
const beforeBoundary = index === 0 || classAttr.charCodeAt(index - 1) <= 0x20
|
|
469
|
+
const afterBoundary = end >= classAttr.length || classAttr.charCodeAt(end) <= 0x20
|
|
470
|
+
if (beforeBoundary && afterBoundary) return true
|
|
471
|
+
index = end
|
|
472
|
+
}
|
|
473
|
+
return false
|
|
352
474
|
}
|
|
353
475
|
|
|
354
476
|
const hasAnyClassName = (classAttr, classNames) => {
|
|
@@ -387,29 +509,18 @@ const updateInlineTokenContent = (inlineToken, originalText, newText) => {
|
|
|
387
509
|
inlineToken.content.slice(index + originalText.length)
|
|
388
510
|
}
|
|
389
511
|
|
|
390
|
-
const
|
|
512
|
+
const parsePositiveInteger = (text) => {
|
|
391
513
|
if (typeof text !== 'string' || text.length === 0) return null
|
|
392
|
-
let end = text.length - 1
|
|
393
|
-
while (end >= 0 && text.charCodeAt(end) === 0x20) end--
|
|
394
|
-
if (end < 0) return null
|
|
395
|
-
const lastCode = text.charCodeAt(end)
|
|
396
|
-
if (lastCode < 0x30 || lastCode > 0x39) return null
|
|
397
|
-
let start = end
|
|
398
|
-
while (start >= 0) {
|
|
399
|
-
const code = text.charCodeAt(start)
|
|
400
|
-
if (code < 0x30 || code > 0x39) break
|
|
401
|
-
start--
|
|
402
|
-
}
|
|
403
514
|
let value = 0
|
|
404
|
-
let
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
515
|
+
for (let i = 0; i < text.length; i++) {
|
|
516
|
+
const code = text.charCodeAt(i)
|
|
517
|
+
if (code < 0x30 || code > 0x39) return null
|
|
518
|
+
value = value * 10 + code - 0x30
|
|
408
519
|
}
|
|
409
520
|
return value
|
|
410
521
|
}
|
|
411
522
|
|
|
412
|
-
const ensureAutoFigureNumbering = (tokens, range, caption, figureNumberState, opt) => {
|
|
523
|
+
const ensureAutoFigureNumbering = (tokens, range, caption, figureNumberState, sp, opt) => {
|
|
413
524
|
const captionType = caption.name === 'img' ? 'img' : (caption.name === 'table' ? 'table' : '')
|
|
414
525
|
if (!captionType) return
|
|
415
526
|
if (!shouldApplyLabelNumbering(captionType, opt)) return
|
|
@@ -418,19 +529,12 @@ const ensureAutoFigureNumbering = (tokens, range, caption, figureNumberState, op
|
|
|
418
529
|
const labelTextToken = getInlineLabelTextToken(inlineToken, captionType, opt)
|
|
419
530
|
if (!labelTextToken || typeof labelTextToken.content !== 'string') return
|
|
420
531
|
const originalText = labelTextToken.content
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
if (code >= 0x30 && code <= 0x39) {
|
|
426
|
-
const explicitValue = parseTrailingPositiveInteger(originalText)
|
|
427
|
-
if (explicitValue !== null) {
|
|
428
|
-
if (explicitValue > (figureNumberState[captionType] || 0)) {
|
|
429
|
-
figureNumberState[captionType] = explicitValue
|
|
430
|
-
}
|
|
431
|
-
return
|
|
432
|
-
}
|
|
532
|
+
if (sp && sp.captionDecision && sp.captionDecision.hasExplicitNumber) {
|
|
533
|
+
const explicitValue = parsePositiveInteger(sp.captionDecision.number)
|
|
534
|
+
if (explicitValue !== null && explicitValue > (figureNumberState[captionType] || 0)) {
|
|
535
|
+
figureNumberState[captionType] = explicitValue
|
|
433
536
|
}
|
|
537
|
+
return
|
|
434
538
|
}
|
|
435
539
|
figureNumberState[captionType] = (figureNumberState[captionType] || 0) + 1
|
|
436
540
|
const baseLabel = originalText.trim()
|
|
@@ -453,89 +557,87 @@ const matchAutoCaptionText = (text, opt, preferredMark = 'img') => {
|
|
|
453
557
|
return ''
|
|
454
558
|
}
|
|
455
559
|
|
|
456
|
-
const getAutoCaptionFromImage = (imageToken, opt) => {
|
|
457
|
-
if (!opt.
|
|
458
|
-
if (!opt.autoAltCaption && !opt.autoTitleCaption && !(opt.markRegState && opt.markRegState.markReg && opt.markRegState.markReg.img)) return ''
|
|
560
|
+
const getAutoCaptionFromImage = (imageToken, opt, preferredLanguages) => {
|
|
561
|
+
if (!opt.autoAltCaption && !opt.autoTitleCaption && !(opt.markRegState && opt.markRegState.markReg && opt.markRegState.markReg.img)) return null
|
|
459
562
|
|
|
460
563
|
const altText = getImageAltText(imageToken)
|
|
461
564
|
let caption = matchAutoCaptionText(altText, opt)
|
|
462
565
|
if (caption) {
|
|
463
|
-
|
|
464
|
-
return caption
|
|
566
|
+
return { text: caption, source: 'alt' }
|
|
465
567
|
}
|
|
466
|
-
if (
|
|
568
|
+
if (opt.autoAltCaption) {
|
|
467
569
|
const altForFallback = altText || ''
|
|
468
|
-
const fallbackCaption = buildCaptionWithFallback(altForFallback, opt.autoAltCaption, 'img', opt.markRegState,
|
|
469
|
-
if (fallbackCaption && imageToken) {
|
|
470
|
-
clearImageAltAttr(imageToken)
|
|
471
|
-
}
|
|
570
|
+
const fallbackCaption = buildCaptionWithFallback(altForFallback, opt.autoAltCaption, 'img', opt.markRegState, preferredLanguages)
|
|
472
571
|
caption = fallbackCaption
|
|
473
572
|
}
|
|
474
|
-
if (caption) return caption
|
|
573
|
+
if (caption) return { text: caption, source: 'alt' }
|
|
475
574
|
|
|
476
575
|
const titleText = getImageTitleText(imageToken)
|
|
477
576
|
caption = matchAutoCaptionText(titleText, opt)
|
|
478
577
|
if (caption) {
|
|
479
|
-
|
|
480
|
-
return caption
|
|
578
|
+
return { text: caption, source: 'title' }
|
|
481
579
|
}
|
|
482
|
-
if (
|
|
580
|
+
if (opt.autoTitleCaption) {
|
|
483
581
|
const titleForFallback = titleText || ''
|
|
484
|
-
const fallbackCaption = buildCaptionWithFallback(titleForFallback, opt.autoTitleCaption, 'img', opt.markRegState,
|
|
485
|
-
if (fallbackCaption && imageToken) {
|
|
486
|
-
clearImageTitleAttr(imageToken)
|
|
487
|
-
}
|
|
582
|
+
const fallbackCaption = buildCaptionWithFallback(titleForFallback, opt.autoTitleCaption, 'img', opt.markRegState, preferredLanguages)
|
|
488
583
|
caption = fallbackCaption
|
|
489
584
|
}
|
|
490
|
-
return caption
|
|
585
|
+
return caption ? { text: caption, source: 'title' } : null
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const consumeAutoCaptionSource = (imageToken, autoCaption) => {
|
|
589
|
+
if (!imageToken || !autoCaption) return
|
|
590
|
+
if (autoCaption.source === 'alt') {
|
|
591
|
+
clearImageAltAttr(imageToken)
|
|
592
|
+
} else if (autoCaption.source === 'title') {
|
|
593
|
+
clearImageTitleAttr(imageToken)
|
|
594
|
+
}
|
|
491
595
|
}
|
|
492
596
|
|
|
493
597
|
const checkPrevCaption = (tokens, n, caption, sp, opt, captionState) => {
|
|
494
|
-
if(n < 3) return
|
|
495
|
-
const captionStartToken = tokens[n-3]
|
|
496
|
-
const captionInlineToken = tokens[n-2]
|
|
497
|
-
const captionEndToken = tokens[n-1]
|
|
598
|
+
if (n < 3) return
|
|
599
|
+
const captionStartToken = tokens[n - 3]
|
|
600
|
+
const captionInlineToken = tokens[n - 2]
|
|
601
|
+
const captionEndToken = tokens[n - 1]
|
|
498
602
|
if (captionStartToken === undefined || captionEndToken === undefined) return
|
|
499
603
|
if (captionStartToken.type !== 'paragraph_open' || captionEndToken.type !== 'paragraph_close') return
|
|
500
|
-
setCaptionParagraph(n-3, captionState, caption, null, sp, opt)
|
|
604
|
+
setCaptionParagraph(n - 3, captionState, caption, null, sp, opt)
|
|
501
605
|
const captionName = sp && sp.captionDecision ? sp.captionDecision.mark : ''
|
|
502
|
-
if(!captionName) {
|
|
606
|
+
if (!captionName) {
|
|
503
607
|
if (opt.labelPrefixMarkerWithoutLabelPrevReg) {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
608
|
+
const markerMatch = getLabelPrefixMarkerMatch(captionInlineToken, opt.labelPrefixMarkerWithoutLabelPrevReg)
|
|
609
|
+
if (markerMatch) {
|
|
610
|
+
stripLabelPrefixMarker(captionInlineToken, markerMatch)
|
|
611
|
+
caption.isPrev = true
|
|
612
|
+
}
|
|
509
613
|
}
|
|
510
614
|
return
|
|
511
615
|
}
|
|
512
616
|
caption.name = captionName
|
|
513
617
|
caption.isPrev = true
|
|
514
|
-
return
|
|
515
618
|
}
|
|
516
619
|
|
|
517
620
|
const checkNextCaption = (tokens, en, caption, sp, opt, captionState) => {
|
|
518
|
-
if (en +
|
|
519
|
-
const captionStartToken = tokens[en+1]
|
|
520
|
-
const captionInlineToken = tokens[en+2]
|
|
521
|
-
const captionEndToken = tokens[en+3]
|
|
621
|
+
if (en + 3 >= tokens.length) return
|
|
622
|
+
const captionStartToken = tokens[en + 1]
|
|
623
|
+
const captionInlineToken = tokens[en + 2]
|
|
624
|
+
const captionEndToken = tokens[en + 3]
|
|
522
625
|
if (captionStartToken === undefined || captionEndToken === undefined) return
|
|
523
626
|
if (captionStartToken.type !== 'paragraph_open' || captionEndToken.type !== 'paragraph_close') return
|
|
524
|
-
setCaptionParagraph(en+1, captionState, caption, null, sp, opt)
|
|
627
|
+
setCaptionParagraph(en + 1, captionState, caption, null, sp, opt)
|
|
525
628
|
const captionName = sp && sp.captionDecision ? sp.captionDecision.mark : ''
|
|
526
|
-
if(!captionName) {
|
|
629
|
+
if (!captionName) {
|
|
527
630
|
if (opt.labelPrefixMarkerWithoutLabelNextReg) {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
631
|
+
const markerMatch = getLabelPrefixMarkerMatch(captionInlineToken, opt.labelPrefixMarkerWithoutLabelNextReg)
|
|
632
|
+
if (markerMatch) {
|
|
633
|
+
stripLabelPrefixMarker(captionInlineToken, markerMatch)
|
|
634
|
+
caption.isNext = true
|
|
635
|
+
}
|
|
533
636
|
}
|
|
534
637
|
return
|
|
535
638
|
}
|
|
536
639
|
caption.name = captionName
|
|
537
640
|
caption.isNext = true
|
|
538
|
-
return
|
|
539
641
|
}
|
|
540
642
|
|
|
541
643
|
const cleanCaptionTokenAttrs = (token, captionName, opt) => {
|
|
@@ -598,59 +700,115 @@ const changePrevCaptionPosition = (tokens, n, caption, opt) => {
|
|
|
598
700
|
const captionStartToken = tokens[n-3]
|
|
599
701
|
const captionInlineToken = tokens[n-2]
|
|
600
702
|
const captionEndToken = tokens[n-1]
|
|
703
|
+
const figureBaseLevel = getTokenLevel(tokens[n])
|
|
601
704
|
|
|
602
705
|
cleanCaptionTokenAttrs(captionStartToken, caption.name, opt)
|
|
603
706
|
captionStartToken.type = 'figcaption_open'
|
|
604
707
|
captionStartToken.tag = 'figcaption'
|
|
708
|
+
captionStartToken.block = true
|
|
709
|
+
captionStartToken.level = figureBaseLevel + 1
|
|
710
|
+
captionInlineToken.level = figureBaseLevel + 2
|
|
605
711
|
captionEndToken.type = 'figcaption_close'
|
|
606
712
|
captionEndToken.tag = 'figcaption'
|
|
713
|
+
captionEndToken.block = true
|
|
714
|
+
captionEndToken.level = figureBaseLevel + 1
|
|
607
715
|
tokens.splice(n + 2, 0, captionStartToken, captionInlineToken, captionEndToken)
|
|
608
716
|
tokens.splice(n-3, 3)
|
|
609
|
-
return true
|
|
610
717
|
}
|
|
611
718
|
|
|
612
719
|
const changeNextCaptionPosition = (tokens, en, caption, opt) => {
|
|
613
|
-
const captionStartToken = tokens[en+
|
|
614
|
-
const captionInlineToken = tokens[en+
|
|
615
|
-
const captionEndToken = tokens[en+
|
|
720
|
+
const captionStartToken = tokens[en+1]
|
|
721
|
+
const captionInlineToken = tokens[en+2]
|
|
722
|
+
const captionEndToken = tokens[en+3]
|
|
723
|
+
const figureBaseLevel = getTokenLevel(tokens[en])
|
|
616
724
|
cleanCaptionTokenAttrs(captionStartToken, caption.name, opt)
|
|
617
725
|
captionStartToken.type = 'figcaption_open'
|
|
618
726
|
captionStartToken.tag = 'figcaption'
|
|
727
|
+
captionStartToken.block = true
|
|
728
|
+
captionStartToken.level = figureBaseLevel + 1
|
|
729
|
+
captionInlineToken.level = figureBaseLevel + 2
|
|
619
730
|
captionEndToken.type = 'figcaption_close'
|
|
620
731
|
captionEndToken.tag = 'figcaption'
|
|
732
|
+
captionEndToken.block = true
|
|
733
|
+
captionEndToken.level = figureBaseLevel + 1
|
|
621
734
|
tokens.splice(en, 0, captionStartToken, captionInlineToken, captionEndToken)
|
|
622
|
-
tokens.splice(en+
|
|
623
|
-
|
|
735
|
+
tokens.splice(en+4, 3)
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const getTokenMap = (token) => {
|
|
739
|
+
return token && Array.isArray(token.map) && token.map.length === 2 ? token.map : null
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const findNearestMapInRange = (tokens, start, end, step) => {
|
|
743
|
+
let i = start
|
|
744
|
+
while (step > 0 ? i <= end : i >= end) {
|
|
745
|
+
const map = getTokenMap(tokens[i])
|
|
746
|
+
if (map) return map
|
|
747
|
+
i += step
|
|
748
|
+
}
|
|
749
|
+
return null
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const getRangeMap = (tokens, start, end) => {
|
|
753
|
+
const startMap = getTokenMap(tokens[start]) || findNearestMapInRange(tokens, start, end, 1)
|
|
754
|
+
if (!startMap) return null
|
|
755
|
+
const endMap = getTokenMap(tokens[end]) || findNearestMapInRange(tokens, end, start, -1) || startMap
|
|
756
|
+
const startLine = startMap[0]
|
|
757
|
+
const endLine = Math.max(startMap[1], endMap[1])
|
|
758
|
+
if (typeof startLine !== 'number' || typeof endLine !== 'number' || endLine < startLine) {
|
|
759
|
+
return [startMap[0], startMap[1]]
|
|
760
|
+
}
|
|
761
|
+
return [startLine, endLine]
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const getTokenLevel = (token, fallback = 0) => {
|
|
765
|
+
return token && typeof token.level === 'number' ? token.level : fallback
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const adjustTokenLevels = (tokens, start, end, delta) => {
|
|
769
|
+
if (!delta) return
|
|
770
|
+
for (let i = start; i <= end; i++) {
|
|
771
|
+
const token = tokens[i]
|
|
772
|
+
if (token && typeof token.level === 'number') {
|
|
773
|
+
token.level += delta
|
|
774
|
+
}
|
|
775
|
+
}
|
|
624
776
|
}
|
|
625
777
|
|
|
626
778
|
const wrapWithFigure = (tokens, range, checkTokenTagName, caption, replaceInsteadOfWrap, sp, opt, TokenConstructor) => {
|
|
627
779
|
let n = range.start
|
|
628
780
|
let en = range.end
|
|
781
|
+
const baseLevel = getTokenLevel(tokens[n])
|
|
782
|
+
const childLevel = baseLevel + 1
|
|
629
783
|
const figureStartToken = new TokenConstructor('figure_open', 'figure', 1)
|
|
630
784
|
const figureClassName = sp.figureClassName || resolveFigureClassName(checkTokenTagName, sp, opt)
|
|
631
785
|
figureStartToken.attrSet('class', figureClassName)
|
|
786
|
+
figureStartToken.block = true
|
|
787
|
+
figureStartToken.level = baseLevel
|
|
632
788
|
|
|
633
789
|
if (opt.roleDocExample && (checkTokenTagName === 'pre-code' || checkTokenTagName === 'pre-samp')) {
|
|
634
790
|
figureStartToken.attrSet('role', 'doc-example')
|
|
635
791
|
}
|
|
636
792
|
const figureEndToken = new TokenConstructor('figure_close', 'figure', -1)
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
if (rangeStartMap) {
|
|
644
|
-
figureStartToken.map = [rangeStartMap[0], rangeStartMap[1]]
|
|
645
|
-
}
|
|
646
|
-
if (rangeEndMap) {
|
|
647
|
-
figureEndToken.map = [rangeEndMap[0], rangeEndMap[1]]
|
|
793
|
+
figureEndToken.block = true
|
|
794
|
+
figureEndToken.level = baseLevel
|
|
795
|
+
const rangeMap = getRangeMap(tokens, n, en)
|
|
796
|
+
if (rangeMap) {
|
|
797
|
+
figureStartToken.map = [rangeMap[0], rangeMap[1]]
|
|
798
|
+
figureEndToken.map = [rangeMap[0], rangeMap[1]]
|
|
648
799
|
}
|
|
649
800
|
const createBreakToken = () => {
|
|
650
801
|
const breakToken = new TokenConstructor('text', '', 0)
|
|
651
802
|
breakToken.content = '\n'
|
|
803
|
+
breakToken.level = childLevel
|
|
652
804
|
return breakToken
|
|
653
805
|
}
|
|
806
|
+
const createEmptyTextToken = () => {
|
|
807
|
+
const emptyToken = new TokenConstructor('text', '', 0)
|
|
808
|
+
emptyToken.content = ''
|
|
809
|
+
emptyToken.level = childLevel
|
|
810
|
+
return emptyToken
|
|
811
|
+
}
|
|
654
812
|
if (caption.name === 'img') {
|
|
655
813
|
const joinAttrs = (attrs) => {
|
|
656
814
|
if (!attrs || attrs.length === 0) return
|
|
@@ -665,24 +823,23 @@ const wrapWithFigure = (tokens, range, checkTokenTagName, caption, replaceInstea
|
|
|
665
823
|
joinAttrs(tokens[n].attrs)
|
|
666
824
|
}
|
|
667
825
|
if (replaceInsteadOfWrap) {
|
|
668
|
-
tokens.splice(en, 1, createBreakToken(), figureEndToken
|
|
669
|
-
tokens.splice(n, 1, figureStartToken,
|
|
826
|
+
tokens.splice(en, 1, createBreakToken(), figureEndToken)
|
|
827
|
+
tokens.splice(n, 1, figureStartToken, createEmptyTextToken())
|
|
670
828
|
en = en + 2
|
|
671
829
|
} else {
|
|
672
|
-
tokens
|
|
673
|
-
tokens.splice(
|
|
830
|
+
adjustTokenLevels(tokens, n, en, 1)
|
|
831
|
+
tokens.splice(en+1, 0, figureEndToken)
|
|
832
|
+
tokens.splice(n, 0, figureStartToken, createEmptyTextToken())
|
|
674
833
|
en = en + 3
|
|
675
834
|
}
|
|
676
835
|
range.start = n
|
|
677
836
|
range.end = en
|
|
678
|
-
return
|
|
679
837
|
}
|
|
680
838
|
|
|
681
839
|
const checkCaption = (tokens, n, en, caption, sp, opt, captionState) => {
|
|
682
840
|
checkPrevCaption(tokens, n, caption, sp, opt, captionState)
|
|
683
841
|
if (caption.isPrev) return
|
|
684
842
|
checkNextCaption(tokens, en, caption, sp, opt, captionState)
|
|
685
|
-
return
|
|
686
843
|
}
|
|
687
844
|
|
|
688
845
|
const getNestedContainerType = (token) => {
|
|
@@ -786,7 +943,7 @@ const hasLeadingImageChild = (token) => {
|
|
|
786
943
|
const detectImageParagraph = (nextToken, n, caption, sp, opt) => {
|
|
787
944
|
const multipleImagesEnabled = !!opt.multipleImages
|
|
788
945
|
const styleProcessEnabled = !!opt.styleProcess
|
|
789
|
-
const
|
|
946
|
+
const allowImageParagraphWithoutCaption = !!opt.imageOnlyParagraphWithoutCaption
|
|
790
947
|
const children = nextToken.children
|
|
791
948
|
const imageToken = children[0]
|
|
792
949
|
const childrenLength = children.length
|
|
@@ -794,6 +951,7 @@ const detectImageParagraph = (nextToken, n, caption, sp, opt) => {
|
|
|
794
951
|
let isMultipleImagesHorizontal = true
|
|
795
952
|
let isMultipleImagesVertical = true
|
|
796
953
|
let isValid = true
|
|
954
|
+
let trailingAttrToken = null
|
|
797
955
|
caption.name = 'img'
|
|
798
956
|
if (childrenLength === 1) {
|
|
799
957
|
return {
|
|
@@ -801,7 +959,7 @@ const detectImageParagraph = (nextToken, n, caption, sp, opt) => {
|
|
|
801
959
|
tagName: 'img',
|
|
802
960
|
en: n + 2,
|
|
803
961
|
replaceInsteadOfWrap: true,
|
|
804
|
-
wrapWithoutCaption:
|
|
962
|
+
wrapWithoutCaption: allowImageParagraphWithoutCaption,
|
|
805
963
|
canWrap: true,
|
|
806
964
|
imageToken,
|
|
807
965
|
}
|
|
@@ -825,13 +983,13 @@ const detectImageParagraph = (nextToken, n, caption, sp, opt) => {
|
|
|
825
983
|
const imageAttrs = rawContent.match(imageAttrsReg)
|
|
826
984
|
if (imageAttrs) {
|
|
827
985
|
const parsedAttrs = parseImageAttrs(imageAttrs[1])
|
|
828
|
-
if (parsedAttrs
|
|
986
|
+
if (parsedAttrs) {
|
|
829
987
|
for (let i = 0; i < parsedAttrs.length; i++) {
|
|
830
988
|
sp.attrs.push(parsedAttrs[i])
|
|
831
989
|
}
|
|
832
|
-
|
|
990
|
+
trailingAttrToken = child
|
|
991
|
+
break
|
|
833
992
|
}
|
|
834
|
-
break
|
|
835
993
|
}
|
|
836
994
|
}
|
|
837
995
|
if (typeof rawContent === 'string' && rawContent.trim()) {
|
|
@@ -867,12 +1025,6 @@ const detectImageParagraph = (nextToken, n, caption, sp, opt) => {
|
|
|
867
1025
|
} else {
|
|
868
1026
|
caption.nameSuffix = '-multiple'
|
|
869
1027
|
}
|
|
870
|
-
for (let i = 0; i < childrenLength; i++) {
|
|
871
|
-
const child = children[i]
|
|
872
|
-
if (isOnlySpacesText(child)) {
|
|
873
|
-
child.content = ''
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
1028
|
}
|
|
877
1029
|
const en = n + 2
|
|
878
1030
|
let tagName = 'img'
|
|
@@ -882,9 +1034,25 @@ const detectImageParagraph = (nextToken, n, caption, sp, opt) => {
|
|
|
882
1034
|
tagName,
|
|
883
1035
|
en,
|
|
884
1036
|
replaceInsteadOfWrap: true,
|
|
885
|
-
wrapWithoutCaption: isValid &&
|
|
1037
|
+
wrapWithoutCaption: isValid && allowImageParagraphWithoutCaption,
|
|
886
1038
|
canWrap: isValid,
|
|
887
1039
|
imageToken,
|
|
1040
|
+
inlineToken: nextToken,
|
|
1041
|
+
trailingAttrToken,
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
const applyImageParagraphTransform = (detection) => {
|
|
1046
|
+
if (!detection || detection.type !== 'image') return
|
|
1047
|
+
if (detection.trailingAttrToken) {
|
|
1048
|
+
detection.trailingAttrToken.content = ''
|
|
1049
|
+
}
|
|
1050
|
+
if (!detection.inlineToken || detection.tagName === 'img') return
|
|
1051
|
+
const children = detection.inlineToken.children
|
|
1052
|
+
for (let i = 0; i < children.length; i++) {
|
|
1053
|
+
if (isOnlySpacesText(children[i])) {
|
|
1054
|
+
children[i].content = ''
|
|
1055
|
+
}
|
|
888
1056
|
}
|
|
889
1057
|
}
|
|
890
1058
|
|
|
@@ -894,16 +1062,19 @@ const figureWithCaption = (state, opt) => {
|
|
|
894
1062
|
table: 0,
|
|
895
1063
|
}
|
|
896
1064
|
|
|
897
|
-
const captionState = {
|
|
1065
|
+
const captionState = {
|
|
1066
|
+
tokens: state.tokens,
|
|
1067
|
+
Token: state.Token,
|
|
1068
|
+
preferredLanguages: opt.preferredLanguages,
|
|
1069
|
+
}
|
|
898
1070
|
const shouldResolvePreferredLanguages = !!(
|
|
899
1071
|
opt.shouldResolvePreferredLanguages &&
|
|
900
1072
|
sourceMayNeedPreferredLanguages(state)
|
|
901
1073
|
)
|
|
902
|
-
const renderOpt = shouldResolvePreferredLanguages ? Object.create(opt) : opt
|
|
903
1074
|
if (shouldResolvePreferredLanguages) {
|
|
904
|
-
|
|
1075
|
+
captionState.preferredLanguages = resolvePreferredLanguagesForState(state, opt)
|
|
905
1076
|
}
|
|
906
|
-
figureWithCaptionCore(state.tokens,
|
|
1077
|
+
figureWithCaptionCore(state.tokens, opt, figureNumberState, state.Token, captionState, null, 0)
|
|
907
1078
|
}
|
|
908
1079
|
|
|
909
1080
|
const figureWithCaptionCore = (tokens, opt, figureNumberState, TokenConstructor, captionState, parentType = null, startIndex = 0) => {
|
|
@@ -984,9 +1155,9 @@ const figureWithCaptionCore = (tokens, opt, figureNumberState, TokenConstructor,
|
|
|
984
1155
|
applyCaptionDrivenFigureClass(rCaption, rSp, opt)
|
|
985
1156
|
|
|
986
1157
|
let hasCaption = rCaption.isPrev || rCaption.isNext
|
|
987
|
-
let pendingAutoCaption =
|
|
1158
|
+
let pendingAutoCaption = null
|
|
988
1159
|
if (!hasCaption && detection.type === 'image' && opt.autoCaptionDetection) {
|
|
989
|
-
pendingAutoCaption = getAutoCaptionFromImage(detection.imageToken, opt)
|
|
1160
|
+
pendingAutoCaption = getAutoCaptionFromImage(detection.imageToken, opt, captionState.preferredLanguages)
|
|
990
1161
|
if (pendingAutoCaption) {
|
|
991
1162
|
hasCaption = true
|
|
992
1163
|
}
|
|
@@ -1002,21 +1173,18 @@ const figureWithCaptionCore = (tokens, opt, figureNumberState, TokenConstructor,
|
|
|
1002
1173
|
continue
|
|
1003
1174
|
}
|
|
1004
1175
|
|
|
1005
|
-
let shouldWrap =
|
|
1006
|
-
if (detection.type === 'html') {
|
|
1007
|
-
shouldWrap =
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
shouldWrap = false
|
|
1012
|
-
}
|
|
1013
|
-
} else {
|
|
1014
|
-
shouldWrap = detection.canWrap !== false && hasCaption
|
|
1176
|
+
let shouldWrap = hasCaption
|
|
1177
|
+
if (detection.type === 'html' || detection.type === 'image') {
|
|
1178
|
+
shouldWrap = shouldWrap || detection.wrapWithoutCaption
|
|
1179
|
+
}
|
|
1180
|
+
if (detection.type === 'image' && token.hidden === true) {
|
|
1181
|
+
shouldWrap = false
|
|
1015
1182
|
}
|
|
1016
1183
|
|
|
1017
1184
|
if (shouldWrap) {
|
|
1018
1185
|
if (pendingAutoCaption) {
|
|
1019
|
-
|
|
1186
|
+
consumeAutoCaptionSource(detection.imageToken, pendingAutoCaption)
|
|
1187
|
+
const captionTokens = createAutoCaptionParagraph(pendingAutoCaption.text, TokenConstructor)
|
|
1020
1188
|
tokens.splice(rRange.start, 0, ...captionTokens)
|
|
1021
1189
|
const insertedLength = captionTokens.length
|
|
1022
1190
|
rRange.start += insertedLength
|
|
@@ -1025,7 +1193,8 @@ const figureWithCaptionCore = (tokens, opt, figureNumberState, TokenConstructor,
|
|
|
1025
1193
|
checkCaption(tokens, rRange.start, rRange.end, rCaption, rSp, opt, captionState)
|
|
1026
1194
|
applyCaptionDrivenFigureClass(rCaption, rSp, opt)
|
|
1027
1195
|
}
|
|
1028
|
-
|
|
1196
|
+
applyImageParagraphTransform(detection)
|
|
1197
|
+
ensureAutoFigureNumbering(tokens, rRange, rCaption, figureNumberState, rSp, opt)
|
|
1029
1198
|
wrapWithFigure(tokens, rRange, detection.tagName, rCaption, detection.replaceInsteadOfWrap, rSp, opt, TokenConstructor)
|
|
1030
1199
|
}
|
|
1031
1200
|
|
|
@@ -1044,8 +1213,6 @@ const figureWithCaptionCore = (tokens, opt, figureNumberState, TokenConstructor,
|
|
|
1044
1213
|
} else if (rCaption.isNext) {
|
|
1045
1214
|
changeNextCaptionPosition(tokens, en, rCaption, opt)
|
|
1046
1215
|
nextIndex = en + 4
|
|
1047
|
-
} else {
|
|
1048
|
-
nextIndex = en + 1
|
|
1049
1216
|
}
|
|
1050
1217
|
}
|
|
1051
1218
|
|
|
@@ -1061,17 +1228,18 @@ const figureWithCaptionCore = (tokens, opt, figureNumberState, TokenConstructor,
|
|
|
1061
1228
|
}
|
|
1062
1229
|
|
|
1063
1230
|
const mditFigureWithPCaption = (md, option) => {
|
|
1064
|
-
|
|
1231
|
+
const opt = {
|
|
1065
1232
|
// Caption languages delegated to p-captions.
|
|
1066
1233
|
languages: ['en', 'ja'],
|
|
1067
|
-
preferredLanguages: null, //
|
|
1234
|
+
preferredLanguages: null, // compatibility tie-break for generated fallback labels; prefer env.locale / env.preferredLocales per render
|
|
1068
1235
|
|
|
1069
1236
|
// --- figure-wrapper behavior ---
|
|
1070
1237
|
classPrefix: 'f',
|
|
1071
1238
|
figureClassThatWrapsIframeTypeBlockquote: null,
|
|
1072
1239
|
figureClassThatWrapsSlides: null,
|
|
1073
1240
|
styleProcess: true,
|
|
1074
|
-
|
|
1241
|
+
imageOnlyParagraphWithoutCaption: false,
|
|
1242
|
+
oneImageWithoutCaption: false, // legacy alias for imageOnlyParagraphWithoutCaption
|
|
1075
1243
|
iframeWithoutCaption: false,
|
|
1076
1244
|
videoWithoutCaption: false,
|
|
1077
1245
|
audioWithoutCaption: false,
|
|
@@ -1109,11 +1277,18 @@ const mditFigureWithPCaption = (md, option) => {
|
|
|
1109
1277
|
labelClassFollowsFigure: false,
|
|
1110
1278
|
figureToLabelClassMap: null,
|
|
1111
1279
|
}
|
|
1112
|
-
const hasExplicitAutoLabelNumberSets =
|
|
1113
|
-
const
|
|
1114
|
-
const
|
|
1115
|
-
const
|
|
1280
|
+
const hasExplicitAutoLabelNumberSets = hasOwnOption(option, 'autoLabelNumberSets')
|
|
1281
|
+
const hasExplicitImageOnlyParagraphWithoutCaption = hasOwnOption(option, 'imageOnlyParagraphWithoutCaption')
|
|
1282
|
+
const hasExplicitFigureClassThatWrapsIframeTypeBlockquote = hasOwnOption(option, 'figureClassThatWrapsIframeTypeBlockquote')
|
|
1283
|
+
const hasExplicitFigureClassThatWrapsSlides = hasOwnOption(option, 'figureClassThatWrapsSlides')
|
|
1284
|
+
const hasExplicitLabelClassFollowsFigure = hasOwnOption(option, 'labelClassFollowsFigure')
|
|
1285
|
+
if (hasOwnOption(option, 'setFigureNumber')) {
|
|
1286
|
+
throw new Error('setFigureNumber is not supported by figure-with-p-caption; use autoLabelNumber or autoLabelNumberSets')
|
|
1287
|
+
}
|
|
1116
1288
|
if (option) Object.assign(opt, option)
|
|
1289
|
+
opt.imageOnlyParagraphWithoutCaption = hasExplicitImageOnlyParagraphWithoutCaption
|
|
1290
|
+
? !!opt.imageOnlyParagraphWithoutCaption
|
|
1291
|
+
: !!opt.oneImageWithoutCaption
|
|
1117
1292
|
if (!hasExplicitLabelClassFollowsFigure && opt.figureToLabelClassMap) {
|
|
1118
1293
|
opt.labelClassFollowsFigure = true
|
|
1119
1294
|
}
|
|
@@ -1124,6 +1299,8 @@ const mditFigureWithPCaption = (md, option) => {
|
|
|
1124
1299
|
if (opt.preferredLanguages.length === 0) opt.preferredLanguages = null
|
|
1125
1300
|
opt.normalizedOptionLanguages = normalizePreferredLanguages(opt.languages, opt.markRegState.languages)
|
|
1126
1301
|
opt.shouldResolvePreferredLanguages = needsPreferredLanguagesResolution(opt)
|
|
1302
|
+
validateFallbackCaptionLabelOption('autoAltCaption', opt.autoAltCaption, opt.markRegState)
|
|
1303
|
+
validateFallbackCaptionLabelOption('autoTitleCaption', opt.autoTitleCaption, opt.markRegState)
|
|
1127
1304
|
opt.htmlWrapWithoutCaption = {
|
|
1128
1305
|
iframe: !!opt.iframeWithoutCaption,
|
|
1129
1306
|
video: !!opt.videoWithoutCaption,
|
|
@@ -1171,7 +1348,7 @@ const mditFigureWithPCaption = (md, option) => {
|
|
|
1171
1348
|
opt.labelPrefixMarkerWithoutLabelNextReg = null
|
|
1172
1349
|
}
|
|
1173
1350
|
|
|
1174
|
-
//
|
|
1351
|
+
// Run after markdown-it-attrs has attached paragraph attrs, but before text replacements.
|
|
1175
1352
|
md.core.ruler.before('replacements', 'figure_with_caption', (state) => {
|
|
1176
1353
|
figureWithCaption(state, opt)
|
|
1177
1354
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peaceroad/markdown-it-figure-with-p-caption",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "A markdown-it plugin. For a paragraph with only one image, a table or code block or blockquote, and by writing a caption paragraph immediately before or after, they are converted into the figure element with the figcaption element.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -20,16 +20,16 @@
|
|
|
20
20
|
"url": "https://github.com/peaceroad/p7d-markdown-it-figure-with-p-caption/issues"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@peaceroad/markdown-it-cjk-breaks-mod": "^0.1.
|
|
24
|
-
"@peaceroad/markdown-it-renderer-fence": "^0.
|
|
25
|
-
"@peaceroad/markdown-it-renderer-image": "^0.
|
|
26
|
-
"@peaceroad/markdown-it-strong-ja": "^0.9.
|
|
23
|
+
"@peaceroad/markdown-it-cjk-breaks-mod": "^0.1.14",
|
|
24
|
+
"@peaceroad/markdown-it-renderer-fence": "^0.8.0",
|
|
25
|
+
"@peaceroad/markdown-it-renderer-image": "^0.16.0",
|
|
26
|
+
"@peaceroad/markdown-it-strong-ja": "^0.9.4",
|
|
27
27
|
"highlight.js": "^11.11.1",
|
|
28
|
-
"markdown-it": "^14.
|
|
29
|
-
"markdown-it-attrs": "^
|
|
28
|
+
"markdown-it": "^14.3.0",
|
|
29
|
+
"markdown-it-attrs": "^5.0.0"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"p7d-markdown-it-p-captions": "0.
|
|
32
|
+
"p7d-markdown-it-p-captions": "0.24.0"
|
|
33
33
|
},
|
|
34
34
|
"files": [
|
|
35
35
|
"index.js",
|