@peaceroad/markdown-it-figure-with-p-caption 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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 uses that label verbatim. Empty alt text does not generate a fallback caption.
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., Source. A paragraph. Ewritten immediately before or after `> ...`) become `<figure class="f-blockquote">` while keeping the original blockquote intact.
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,7 +50,7 @@ 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
55
  - Blockquote-based social embeds (Twitter/X `twitter-tweet`, Mastodon `mastodon-embed`, Bluesky `bluesky-embed`, Instagram `instagram-media`, Tumblr `text-post-media`) are treated like iframe-type embeds when their class list contains one of those provider classes. Extra classes on the same blockquote do not block detection. By default they become `<figure class="f-img">` so the caption label behaves like an image label (Labels can also use quote labels). You can override that figure class with `figureClassThatWrapsIframeTypeBlockquote` or the global `allIframeTypeFigureClassName`.
56
56
  - `p7d-markdown-it-p-captions` ships with a `Slide.` label. When you use it (for example with Speaker Deck or SlideShare iframes), the `<figure>` wrapper automatically switches to `f-slide` (or whatever you set via `figureClassThatWrapsSlides`) so slides can get their own layout. If `allIframeTypeFigureClassName` is also configured, that class takes precedence even for slides, so you get a uniform embed wrapper without touching the slide option.
@@ -60,7 +60,8 @@ Optionally, you can auto-number image and table caption paragraphs starting from
60
60
 
61
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`.
62
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.
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 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.
64
+ - 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
65
  - Attributes attached to caption paragraphs stay on the converted `<figcaption>` token after paragraph-to-figcaption conversion.
65
66
 
66
67
  ## Behavior Customization
@@ -75,7 +76,8 @@ Optionally, you can auto-number image and table caption paragraphs starting from
75
76
 
76
77
  ### Wrapping without captions
77
78
 
78
- - `oneImageWithoutCaption`: turn single-image paragraphs into `<figure>` elements even when no caption paragraph/auto caption is present. This is independent of automatic detection.
79
+ - `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.
80
+ - `oneImageWithoutCaption`: legacy alias for `imageOnlyParagraphWithoutCaption`. When both are provided, `imageOnlyParagraphWithoutCaption` wins.
79
81
  - `videoWithoutCaption`, `audioWithoutCaption`, `iframeWithoutCaption`, `iframeTypeBlockquoteWithoutCaption`: wrap the respective media blocks without caption.
80
82
 
81
83
  ### Caption text helpers (delegated to `p7d-markdown-it-p-captions`)
@@ -92,15 +94,16 @@ Every option below is forwarded verbatim to `p7d-markdown-it-p-captions`, which
92
94
  - `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
95
  - `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
96
  - `labelPrefixMarker`: allow a leading marker before labels (string or array, e.g., `*Figure. ...`). Arrays are limited to two markers; extras are ignored.
97
+ - `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
98
  - Automatic image-label fallback text and punctuation (`Figure. `, `図 `, etc.) are generated from `p7d-markdown-it-p-captions` locale metadata, not from a local hardcoded map in this plugin.
96
- - `preferredLanguages`: optional tie-break order for generated fallback labels. When omitted, this plugin derives the order once per render from `env.preferredLanguages`, `env.lang` / `env.locale`, then a cheap document-script heuristic that skips a leading hyphen-fenced frontmatter block (`---` or longer, spaces allowed before newline), and finally the raw `languages` order.
99
+ - 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
100
 
98
101
  ### Automatic numbering
99
102
 
100
103
  - `autoLabelNumberSets`: enable numbering per media type. Pass an array such as `['img']`, `['table']`, or `['img', 'table']`.
101
104
  - `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.
102
105
  - 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.
106
+ - 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.
104
107
  - Manual numbers inside the caption text (e.g., `Figure 5.`) always win. The plugin updates its internal counter so the next automatic number becomes `6`. This applies to captions sourced from paragraphs, auto detection, and fallback captions.
105
108
 
106
109
  ## Basic Usage
@@ -128,12 +131,12 @@ Auto label numbering for images and tables.
128
131
  ```js
129
132
  const figureOption = {
130
133
  // Opinionated defaults
131
- oneImageWithoutCaption: true,
134
+ imageOnlyParagraphWithoutCaption: true,
132
135
  videoWithoutCaption: true,
133
136
  audioWithoutCaption: true,
134
137
  iframeWithoutCaption: true,
135
138
  iframeTypeBlockquoteWithoutCaption: true,
136
- removeUnnumberedLabelExceptMarks: ['blockquote'], // keep Quote. Elabels even when unnumbered
139
+ removeUnnumberedLabelExceptMarks: ['blockquote'], // keep `Quote.` labels even when unnumbered
137
140
  allIframeTypeFigureClassName: 'f-embed', // apply a uniform class to every iframe-style embed
138
141
  autoLabelNumber: true,
139
142
 
@@ -147,7 +150,7 @@ If there is no label number, the label will also be deleted.
147
150
 
148
151
  ```js
149
152
  const figureOption = {
150
- oneImageWithoutCaption: true,
153
+ imageOnlyParagraphWithoutCaption: true,
151
154
  videoWithoutCaption: true,
152
155
  audioWithoutCaption: true,
153
156
  iframeWithoutCaption: true,
@@ -175,7 +178,7 @@ const md = mdit({ html: true }).use(mditFigureWithPCaption, figureOption)
175
178
  [HTML]
176
179
  <p><img src="figure.jpg" alt="A single cat"></p>
177
180
 
178
- <!-- Above: If oneImageWithoutCaption is true, this img element has wrapped into figure element without caption. -->
181
+ <!-- Above: If imageOnlyParagraphWithoutCaption (or its legacy alias oneImageWithoutCaption) is true, this img element has wrapped into figure element without caption. -->
179
182
 
180
183
 
181
184
  [Markdown]
@@ -440,7 +443,7 @@ A paragraph.
440
443
 
441
444
  ### Styles
442
445
 
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`.
446
+ 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
447
 
445
448
  ```
446
449
  [Markdown]
@@ -456,7 +459,7 @@ Figure. Highlighted cat.
456
459
 
457
460
  ### Automatic detection fallbacks
458
461
 
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.
462
+ `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
463
 
461
464
  ```
462
465
  [Markdown]
@@ -498,7 +501,7 @@ $ pwd
498
501
 
499
502
  ### Captionless conversion toggles
500
503
 
501
- If `oneImageWithoutCaption` is enabled, a single image paragraph will be wrapped with `<figure class="f-img">` even without a caption.
504
+ 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
505
 
503
506
  ```
504
507
  [Markdown]
@@ -510,7 +513,7 @@ If `oneImageWithoutCaption` is enabled, a single image paragraph will be wrapped
510
513
  </figure>
511
514
  ```
512
515
 
513
- If `videoWithoutCaption` is enabled, an `iframe` pointing to a known video host (such as YouTube or video elements) will be wrapped with `<figure class="f-video">`.
516
+ 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
517
 
515
518
  ```
516
519
  [Markdown]
package/embeds/detect.js CHANGED
@@ -7,25 +7,37 @@ import {
7
7
  const htmlRegCache = new Map()
8
8
  const openingClassAttrReg = /^<[^>]*?\bclass=(?:"([^"]*)"|'([^']*)')/i
9
9
  const openingSrcAttrReg = /^<[^>]*?\bsrc=(?:"([^"]*)"|'([^']*)')/i
10
- const endBlockquoteScriptReg = /<\/blockquote> *<script[^>]*?><\/script>$/
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 hasBlueskyHint = content.indexOf('bluesky-embed') !== -1
24
- const hasVideoHint = content.indexOf('<video') !== -1
25
- const hasAudioHint = content.indexOf('<audio') !== -1
26
- const hasIframeHint = content.indexOf('<iframe') !== -1
27
- const hasBlockquoteHint = content.indexOf('<blockquote') !== -1
28
- const hasDivHint = content.indexOf('<div') !== -1
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(content)),
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'
@@ -136,13 +137,16 @@ const resolveHtmlWrapWithoutCaption = (matchedTag, result, htmlWrapWithoutCaptio
136
137
  if (matchedTag === 'blockquote') {
137
138
  return !!(result.isIframeTypeBlockquote && htmlWrapWithoutCaption.iframeTypeBlockquote)
138
139
  }
140
+ if (matchedTag === 'iframe' && result.isVideoIframe) {
141
+ return !!(htmlWrapWithoutCaption.video || htmlWrapWithoutCaption.iframe)
142
+ }
139
143
  return !!htmlWrapWithoutCaption[matchedTag]
140
144
  }
141
145
 
142
146
  export const detectHtmlFigureCandidate = (tokens, token, startIndex, htmlWrapWithoutCaption) => {
143
147
  if (!token || token.type !== 'html_block') return null
144
148
  const hints = getHtmlDetectionHints(token.content)
145
- if (!hasAnyHtmlDetectionHint(hints)) return null
149
+ if (!hints) return null
146
150
 
147
151
  const result = {
148
152
  isVideoIframe: false,
@@ -22,6 +22,9 @@ export const BLOCKQUOTE_EMBED_CLASS_NAMES = new Set([
22
22
  ])
23
23
 
24
24
  export const VIDEO_IFRAME_HOSTS = new Set([
25
+ 'www.youtube.com',
26
+ 'youtube.com',
25
27
  'www.youtube-nocookie.com',
28
+ 'youtube-nocookie.com',
26
29
  'player.vimeo.com',
27
30
  ])
package/index.js CHANGED
@@ -13,9 +13,10 @@ 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',
@@ -29,28 +30,47 @@ const normalizeLanguageCode = (value) => {
29
30
  const separatorIndex = normalized.search(/[-_]/)
30
31
  return separatorIndex === -1 ? normalized : normalized.slice(0, separatorIndex)
31
32
  }
33
+ const appendAvailableLanguage = (target, lang, availableLanguages) => {
34
+ if (!lang) return false
35
+ if (availableLanguages.indexOf(lang) === -1) return false
36
+ if (target.indexOf(lang) !== -1) return false
37
+ target.push(lang)
38
+ return true
39
+ }
32
40
  const normalizePreferredLanguages = (value, availableLanguages) => {
33
41
  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
42
  const languages = []
38
- const seen = new Set()
43
+ if (typeof value === 'string') {
44
+ appendAvailableLanguage(languages, normalizeLanguageCode(value), availableLanguages)
45
+ return languages
46
+ }
47
+ const source = Array.isArray(value) ? value : []
48
+ if (source.length === 0) return languages
39
49
  for (let i = 0; i < source.length; i++) {
40
50
  const lang = normalizeLanguageCode(source[i])
41
- if (!lang || seen.has(lang) || !allowed.has(lang)) continue
42
- seen.add(lang)
43
- languages.push(lang)
51
+ appendAvailableLanguage(languages, lang, availableLanguages)
44
52
  }
45
53
  return languages
46
54
  }
47
- const prioritizeLanguage = (languages, preferredLanguage) => {
48
- if (!preferredLanguage || languages.length === 0) return languages.slice()
55
+ const prioritizeLanguages = (languages, preferredLanguages) => {
56
+ if (!Array.isArray(languages) || languages.length === 0) return []
57
+ if (typeof preferredLanguages === 'string') {
58
+ if (!preferredLanguages || languages.indexOf(preferredLanguages) === -1) return languages
59
+ if (languages[0] === preferredLanguages) return languages
60
+ const prioritized = [preferredLanguages]
61
+ for (let i = 0; i < languages.length; i++) {
62
+ appendAvailableLanguage(prioritized, languages[i], languages)
63
+ }
64
+ return prioritized
65
+ }
66
+ if (!Array.isArray(preferredLanguages) || preferredLanguages.length === 0) return languages
49
67
  const prioritized = []
50
- prioritized.push(preferredLanguage)
68
+ for (let i = 0; i < preferredLanguages.length; i++) {
69
+ appendAvailableLanguage(prioritized, preferredLanguages[i], languages)
70
+ }
71
+ if (prioritized.length === 0) return languages
51
72
  for (let i = 0; i < languages.length; i++) {
52
- if (languages[i] === preferredLanguage) continue
53
- prioritized.push(languages[i])
73
+ appendAvailableLanguage(prioritized, languages[i], languages)
54
74
  }
55
75
  return prioritized
56
76
  }
@@ -129,34 +149,48 @@ const resolvePreferredLanguagesForState = (state, opt) => {
129
149
  ) ? opt.markRegState.languages : []
130
150
  if (availableLanguages.length === 0) return []
131
151
 
132
- const explicitPreferred = opt && Array.isArray(opt.preferredLanguages)
133
- ? opt.preferredLanguages
134
- : []
135
- if (explicitPreferred.length > 0) return explicitPreferred
136
-
137
152
  const optionLanguages = opt && Array.isArray(opt.normalizedOptionLanguages)
138
153
  ? opt.normalizedOptionLanguages
139
154
  : []
140
155
  const baseLanguages = optionLanguages.length > 0 ? optionLanguages : availableLanguages
141
156
  const env = state && state.env ? state.env : null
142
- const envPreferred = normalizePreferredLanguages(env && env.preferredLanguages, availableLanguages)
143
- if (envPreferred.length > 0) return envPreferred
144
157
 
145
- const envLanguage = normalizeLanguageCode(env && (env.preferredLanguage || env.lang || env.language || env.locale))
158
+ const envLocale = normalizeLanguageCode(env && env.locale)
159
+ if (envLocale && baseLanguages.indexOf(envLocale) !== -1) {
160
+ return prioritizeLanguages(baseLanguages, envLocale)
161
+ }
162
+
163
+ const envPreferredLocales = normalizePreferredLanguages(env && env.preferredLocales, baseLanguages)
164
+ if (envPreferredLocales.length > 0) {
165
+ return prioritizeLanguages(baseLanguages, envPreferredLocales)
166
+ }
167
+
168
+ const explicitPreferred = opt && Array.isArray(opt.preferredLanguages)
169
+ ? opt.preferredLanguages
170
+ : []
171
+ if (explicitPreferred.length > 0) {
172
+ return prioritizeLanguages(baseLanguages, explicitPreferred)
173
+ }
174
+
175
+ const envPreferred = normalizePreferredLanguages(env && env.preferredLanguages, baseLanguages)
176
+ if (envPreferred.length > 0) {
177
+ return prioritizeLanguages(baseLanguages, envPreferred)
178
+ }
179
+
180
+ const envLanguage = normalizeLanguageCode(env && (env.preferredLanguage || env.lang || env.language))
146
181
  if (envLanguage && baseLanguages.indexOf(envLanguage) !== -1) {
147
- return prioritizeLanguage(baseLanguages, envLanguage)
182
+ return prioritizeLanguages(baseLanguages, envLanguage)
148
183
  }
149
184
 
150
185
  const detectedLanguage = detectDocumentPrimaryLanguage(state && state.src ? state.src : '', baseLanguages)
151
186
  if (detectedLanguage) {
152
- return prioritizeLanguage(baseLanguages, detectedLanguage)
187
+ return prioritizeLanguages(baseLanguages, detectedLanguage)
153
188
  }
154
189
  return baseLanguages
155
190
  }
156
191
  const needsPreferredLanguagesResolution = (opt) => {
157
192
  if (!opt || !opt.markRegState || !Array.isArray(opt.markRegState.languages)) return false
158
193
  if (opt.markRegState.languages.length <= 1) return false
159
- if (Array.isArray(opt.preferredLanguages) && opt.preferredLanguages.length > 0) return false
160
194
  return opt.autoAltCaption === true || opt.autoTitleCaption === true
161
195
  }
162
196
  const normalizeOptionalClassName = (value) => {
@@ -190,10 +224,62 @@ const getLabelPrefixMarkerMatch = (inlineToken, markerReg) => {
190
224
  return match[0]
191
225
  }
192
226
 
193
- const parseImageAttrs = (raw) => {
227
+ const splitImageAttrParts = (raw) => {
194
228
  if (raw === null || raw === undefined) return null
229
+ const parts = []
230
+ let current = ''
231
+ let quote = ''
232
+ let escaped = false
233
+ for (let i = 0; i < raw.length; i++) {
234
+ const ch = raw[i]
235
+ if (quote) {
236
+ current += ch
237
+ if (escaped) {
238
+ escaped = false
239
+ continue
240
+ }
241
+ if (ch === '\\') {
242
+ escaped = true
243
+ continue
244
+ }
245
+ if (ch === quote) {
246
+ quote = ''
247
+ }
248
+ continue
249
+ }
250
+ if (ch === '"' || ch === "'") {
251
+ quote = ch
252
+ current += ch
253
+ continue
254
+ }
255
+ if (ch === ' ') {
256
+ if (current) {
257
+ parts.push(current)
258
+ current = ''
259
+ }
260
+ continue
261
+ }
262
+ current += ch
263
+ }
264
+ if (quote) return null
265
+ if (current) parts.push(current)
266
+ return parts
267
+ }
268
+
269
+ const unquoteAttrValue = (value) => {
270
+ if (typeof value !== 'string' || value.length < 2) return value || ''
271
+ const first = value[0]
272
+ const last = value[value.length - 1]
273
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
274
+ return value.slice(1, -1).replace(/\\(["'\\])/g, '$1')
275
+ }
276
+ return value
277
+ }
278
+
279
+ const parseImageAttrs = (raw) => {
280
+ const parts = splitImageAttrParts(raw)
281
+ if (!parts || parts.length === 0) return null
195
282
  const attrs = []
196
- const parts = raw.split(/ +/)
197
283
  for (let i = 0; i < parts.length; i++) {
198
284
  let entry = parts[i]
199
285
  if (!entry) continue
@@ -202,9 +288,15 @@ const parseImageAttrs = (raw) => {
202
288
  } else if (idAttrReg.test(entry)) {
203
289
  entry = entry.replace(idAttrReg, 'id=')
204
290
  }
205
- const imageAttr = entry.match(attrParseReg)
206
- if (!imageAttr || !imageAttr[1]) continue
207
- attrs.push([imageAttr[1], imageAttr[2]])
291
+ const equalIndex = entry.indexOf('=')
292
+ if (equalIndex === -1) {
293
+ if (!attrNameReg.test(entry)) return null
294
+ attrs.push([entry, ''])
295
+ continue
296
+ }
297
+ const name = entry.slice(0, equalIndex)
298
+ if (!name || !attrNameReg.test(name)) return null
299
+ attrs.push([name, unquoteAttrValue(entry.slice(equalIndex + 1))])
208
300
  }
209
301
  return attrs
210
302
  }
@@ -299,6 +391,14 @@ const getImageAltText = (token) => {
299
391
 
300
392
  const getImageTitleText = (token) => getTokenAttr(token, 'title')
301
393
 
394
+ const getFallbackStringLabelJoint = (label) => {
395
+ if (!label) return ''
396
+ if (labelTrailingJointReg.test(label)) {
397
+ return asciiLabelReg.test(label) ? ' ' : ''
398
+ }
399
+ return asciiLabelReg.test(label) ? '. ' : ' '
400
+ }
401
+
302
402
  const buildCaptionWithFallback = (text, fallbackOption, mark, markRegState, preferredLanguages) => {
303
403
  const trimmedText = (text || '').trim()
304
404
  if (!fallbackOption) return ''
@@ -315,7 +415,19 @@ const buildCaptionWithFallback = (text, fallbackOption, mark, markRegState, pref
315
415
  if (generatedDefaults) {
316
416
  return label + (generatedDefaults.joint || '') + (generatedDefaults.space || '') + trimmedText
317
417
  }
318
- return label + (asciiLabelReg.test(label) ? '. ' : ' ') + trimmedText
418
+ return label + getFallbackStringLabelJoint(label) + trimmedText
419
+ }
420
+
421
+ const validateFallbackCaptionLabelOption = (optionName, fallbackOption, markRegState) => {
422
+ if (typeof fallbackOption !== 'string') return
423
+ const sampleCaption = buildCaptionWithFallback('caption', fallbackOption, 'img', markRegState, null)
424
+ const analysis = analyzeCaptionStart(sampleCaption, {
425
+ markRegState,
426
+ preferredMark: 'img',
427
+ })
428
+ if (!analysis || analysis.mark !== 'img' || analysis.kind !== 'caption') {
429
+ throw new Error(`${optionName} must be a string label recognized as an image caption by p7d-markdown-it-p-captions: ${fallbackOption}`)
430
+ }
319
431
  }
320
432
 
321
433
  const createAutoCaptionParagraph = (captionText, TokenConstructor) => {
@@ -343,12 +455,18 @@ const getCaptionInlineToken = (tokens, range, caption) => {
343
455
  }
344
456
 
345
457
  const hasClassName = (classAttr, className) => {
346
- const index = classAttr.indexOf(className)
347
- if (index === -1) return false
348
- const end = index + className.length
349
- if (index > 0 && classAttr.charCodeAt(index - 1) > 0x20) return false
350
- if (end < classAttr.length && classAttr.charCodeAt(end) > 0x20) return false
351
- return true
458
+ if (!classAttr || !className) return false
459
+ let index = 0
460
+ while (index < classAttr.length) {
461
+ index = classAttr.indexOf(className, index)
462
+ if (index === -1) return false
463
+ const end = index + className.length
464
+ const beforeBoundary = index === 0 || classAttr.charCodeAt(index - 1) <= 0x20
465
+ const afterBoundary = end >= classAttr.length || classAttr.charCodeAt(end) <= 0x20
466
+ if (beforeBoundary && afterBoundary) return true
467
+ index = end
468
+ }
469
+ return false
352
470
  }
353
471
 
354
472
  const hasAnyClassName = (classAttr, classNames) => {
@@ -598,59 +716,117 @@ const changePrevCaptionPosition = (tokens, n, caption, opt) => {
598
716
  const captionStartToken = tokens[n-3]
599
717
  const captionInlineToken = tokens[n-2]
600
718
  const captionEndToken = tokens[n-1]
719
+ const figureBaseLevel = getTokenLevel(tokens[n])
601
720
 
602
721
  cleanCaptionTokenAttrs(captionStartToken, caption.name, opt)
603
722
  captionStartToken.type = 'figcaption_open'
604
723
  captionStartToken.tag = 'figcaption'
724
+ captionStartToken.block = true
725
+ captionStartToken.level = figureBaseLevel + 1
726
+ captionInlineToken.level = figureBaseLevel + 2
605
727
  captionEndToken.type = 'figcaption_close'
606
728
  captionEndToken.tag = 'figcaption'
729
+ captionEndToken.block = true
730
+ captionEndToken.level = figureBaseLevel + 1
607
731
  tokens.splice(n + 2, 0, captionStartToken, captionInlineToken, captionEndToken)
608
732
  tokens.splice(n-3, 3)
609
733
  return true
610
734
  }
611
735
 
612
736
  const changeNextCaptionPosition = (tokens, en, caption, opt) => {
613
- const captionStartToken = tokens[en+2] // +1: text node for figure.
614
- const captionInlineToken = tokens[en+3]
615
- const captionEndToken = tokens[en+4]
737
+ const captionStartToken = tokens[en+1]
738
+ const captionInlineToken = tokens[en+2]
739
+ const captionEndToken = tokens[en+3]
740
+ const figureBaseLevel = getTokenLevel(tokens[en])
616
741
  cleanCaptionTokenAttrs(captionStartToken, caption.name, opt)
617
742
  captionStartToken.type = 'figcaption_open'
618
743
  captionStartToken.tag = 'figcaption'
744
+ captionStartToken.block = true
745
+ captionStartToken.level = figureBaseLevel + 1
746
+ captionInlineToken.level = figureBaseLevel + 2
619
747
  captionEndToken.type = 'figcaption_close'
620
748
  captionEndToken.tag = 'figcaption'
749
+ captionEndToken.block = true
750
+ captionEndToken.level = figureBaseLevel + 1
621
751
  tokens.splice(en, 0, captionStartToken, captionInlineToken, captionEndToken)
622
- tokens.splice(en+5, 3)
752
+ tokens.splice(en+4, 3)
623
753
  return true
624
754
  }
625
755
 
756
+ const getTokenMap = (token) => {
757
+ return token && Array.isArray(token.map) && token.map.length === 2 ? token.map : null
758
+ }
759
+
760
+ const findNearestMapInRange = (tokens, start, end, step) => {
761
+ let i = start
762
+ while (step > 0 ? i <= end : i >= end) {
763
+ const map = getTokenMap(tokens[i])
764
+ if (map) return map
765
+ i += step
766
+ }
767
+ return null
768
+ }
769
+
770
+ const getRangeMap = (tokens, start, end) => {
771
+ const startMap = getTokenMap(tokens[start]) || findNearestMapInRange(tokens, start, end, 1)
772
+ if (!startMap) return null
773
+ const endMap = getTokenMap(tokens[end]) || findNearestMapInRange(tokens, end, start, -1) || startMap
774
+ const startLine = startMap[0]
775
+ const endLine = Math.max(startMap[1], endMap[1])
776
+ if (typeof startLine !== 'number' || typeof endLine !== 'number' || endLine < startLine) {
777
+ return [startMap[0], startMap[1]]
778
+ }
779
+ return [startLine, endLine]
780
+ }
781
+
782
+ const getTokenLevel = (token, fallback = 0) => {
783
+ return token && typeof token.level === 'number' ? token.level : fallback
784
+ }
785
+
786
+ const adjustTokenLevels = (tokens, start, end, delta) => {
787
+ if (!delta) return
788
+ for (let i = start; i <= end; i++) {
789
+ const token = tokens[i]
790
+ if (token && typeof token.level === 'number') {
791
+ token.level += delta
792
+ }
793
+ }
794
+ }
795
+
626
796
  const wrapWithFigure = (tokens, range, checkTokenTagName, caption, replaceInsteadOfWrap, sp, opt, TokenConstructor) => {
627
797
  let n = range.start
628
798
  let en = range.end
799
+ const baseLevel = getTokenLevel(tokens[n])
800
+ const childLevel = baseLevel + 1
629
801
  const figureStartToken = new TokenConstructor('figure_open', 'figure', 1)
630
802
  const figureClassName = sp.figureClassName || resolveFigureClassName(checkTokenTagName, sp, opt)
631
803
  figureStartToken.attrSet('class', figureClassName)
804
+ figureStartToken.block = true
805
+ figureStartToken.level = baseLevel
632
806
 
633
807
  if (opt.roleDocExample && (checkTokenTagName === 'pre-code' || checkTokenTagName === 'pre-samp')) {
634
808
  figureStartToken.attrSet('role', 'doc-example')
635
809
  }
636
810
  const figureEndToken = new TokenConstructor('figure_close', 'figure', -1)
637
- const rangeStartMap = tokens[n] && Array.isArray(tokens[n].map) && tokens[n].map.length === 2
638
- ? tokens[n].map
639
- : null
640
- const rangeEndMap = tokens[en] && Array.isArray(tokens[en].map) && tokens[en].map.length === 2
641
- ? tokens[en].map
642
- : rangeStartMap
643
- if (rangeStartMap) {
644
- figureStartToken.map = [rangeStartMap[0], rangeStartMap[1]]
645
- }
646
- if (rangeEndMap) {
647
- figureEndToken.map = [rangeEndMap[0], rangeEndMap[1]]
811
+ figureEndToken.block = true
812
+ figureEndToken.level = baseLevel
813
+ const rangeMap = getRangeMap(tokens, n, en)
814
+ if (rangeMap) {
815
+ figureStartToken.map = [rangeMap[0], rangeMap[1]]
816
+ figureEndToken.map = [rangeMap[0], rangeMap[1]]
648
817
  }
649
818
  const createBreakToken = () => {
650
819
  const breakToken = new TokenConstructor('text', '', 0)
651
820
  breakToken.content = '\n'
821
+ breakToken.level = childLevel
652
822
  return breakToken
653
823
  }
824
+ const createEmptyTextToken = () => {
825
+ const emptyToken = new TokenConstructor('text', '', 0)
826
+ emptyToken.content = ''
827
+ emptyToken.level = childLevel
828
+ return emptyToken
829
+ }
654
830
  if (caption.name === 'img') {
655
831
  const joinAttrs = (attrs) => {
656
832
  if (!attrs || attrs.length === 0) return
@@ -665,12 +841,13 @@ const wrapWithFigure = (tokens, range, checkTokenTagName, caption, replaceInstea
665
841
  joinAttrs(tokens[n].attrs)
666
842
  }
667
843
  if (replaceInsteadOfWrap) {
668
- tokens.splice(en, 1, createBreakToken(), figureEndToken, createBreakToken())
669
- tokens.splice(n, 1, figureStartToken, createBreakToken())
844
+ tokens.splice(en, 1, createBreakToken(), figureEndToken)
845
+ tokens.splice(n, 1, figureStartToken, createEmptyTextToken())
670
846
  en = en + 2
671
847
  } else {
672
- tokens.splice(en+1, 0, figureEndToken, createBreakToken())
673
- tokens.splice(n, 0, figureStartToken, createBreakToken())
848
+ adjustTokenLevels(tokens, n, en, 1)
849
+ tokens.splice(en+1, 0, figureEndToken)
850
+ tokens.splice(n, 0, figureStartToken, createEmptyTextToken())
674
851
  en = en + 3
675
852
  }
676
853
  range.start = n
@@ -786,7 +963,7 @@ const hasLeadingImageChild = (token) => {
786
963
  const detectImageParagraph = (nextToken, n, caption, sp, opt) => {
787
964
  const multipleImagesEnabled = !!opt.multipleImages
788
965
  const styleProcessEnabled = !!opt.styleProcess
789
- const allowSingleImageWithoutCaption = !!opt.oneImageWithoutCaption
966
+ const allowImageParagraphWithoutCaption = !!opt.imageOnlyParagraphWithoutCaption
790
967
  const children = nextToken.children
791
968
  const imageToken = children[0]
792
969
  const childrenLength = children.length
@@ -801,7 +978,7 @@ const detectImageParagraph = (nextToken, n, caption, sp, opt) => {
801
978
  tagName: 'img',
802
979
  en: n + 2,
803
980
  replaceInsteadOfWrap: true,
804
- wrapWithoutCaption: allowSingleImageWithoutCaption,
981
+ wrapWithoutCaption: allowImageParagraphWithoutCaption,
805
982
  canWrap: true,
806
983
  imageToken,
807
984
  }
@@ -825,13 +1002,13 @@ const detectImageParagraph = (nextToken, n, caption, sp, opt) => {
825
1002
  const imageAttrs = rawContent.match(imageAttrsReg)
826
1003
  if (imageAttrs) {
827
1004
  const parsedAttrs = parseImageAttrs(imageAttrs[1])
828
- if (parsedAttrs && parsedAttrs.length) {
1005
+ if (parsedAttrs) {
829
1006
  for (let i = 0; i < parsedAttrs.length; i++) {
830
1007
  sp.attrs.push(parsedAttrs[i])
831
1008
  }
832
1009
  child.content = ''
1010
+ break
833
1011
  }
834
- break
835
1012
  }
836
1013
  }
837
1014
  if (typeof rawContent === 'string' && rawContent.trim()) {
@@ -882,7 +1059,7 @@ const detectImageParagraph = (nextToken, n, caption, sp, opt) => {
882
1059
  tagName,
883
1060
  en,
884
1061
  replaceInsteadOfWrap: true,
885
- wrapWithoutCaption: isValid && allowSingleImageWithoutCaption,
1062
+ wrapWithoutCaption: isValid && allowImageParagraphWithoutCaption,
886
1063
  canWrap: isValid,
887
1064
  imageToken,
888
1065
  }
@@ -1064,14 +1241,15 @@ const mditFigureWithPCaption = (md, option) => {
1064
1241
  let opt = {
1065
1242
  // Caption languages delegated to p-captions.
1066
1243
  languages: ['en', 'ja'],
1067
- preferredLanguages: null, // optional tie-break order for generated fallback labels; defaults to inferred document order / languages
1244
+ preferredLanguages: null, // compatibility tie-break for generated fallback labels; prefer env.locale / env.preferredLocales per render
1068
1245
 
1069
1246
  // --- figure-wrapper behavior ---
1070
1247
  classPrefix: 'f',
1071
1248
  figureClassThatWrapsIframeTypeBlockquote: null,
1072
1249
  figureClassThatWrapsSlides: null,
1073
1250
  styleProcess: true,
1074
- oneImageWithoutCaption: false,
1251
+ imageOnlyParagraphWithoutCaption: false,
1252
+ oneImageWithoutCaption: false, // legacy alias for imageOnlyParagraphWithoutCaption
1075
1253
  iframeWithoutCaption: false,
1076
1254
  videoWithoutCaption: false,
1077
1255
  audioWithoutCaption: false,
@@ -1110,10 +1288,15 @@ const mditFigureWithPCaption = (md, option) => {
1110
1288
  figureToLabelClassMap: null,
1111
1289
  }
1112
1290
  const hasExplicitAutoLabelNumberSets = option && Object.prototype.hasOwnProperty.call(option, 'autoLabelNumberSets')
1291
+ const hasExplicitImageOnlyParagraphWithoutCaption = option && Object.prototype.hasOwnProperty.call(option, 'imageOnlyParagraphWithoutCaption')
1113
1292
  const hasExplicitFigureClassThatWrapsIframeTypeBlockquote = option && Object.prototype.hasOwnProperty.call(option, 'figureClassThatWrapsIframeTypeBlockquote')
1114
1293
  const hasExplicitFigureClassThatWrapsSlides = option && Object.prototype.hasOwnProperty.call(option, 'figureClassThatWrapsSlides')
1115
1294
  const hasExplicitLabelClassFollowsFigure = option && Object.prototype.hasOwnProperty.call(option, 'labelClassFollowsFigure')
1116
1295
  if (option) Object.assign(opt, option)
1296
+ opt.imageOnlyParagraphWithoutCaption = hasExplicitImageOnlyParagraphWithoutCaption
1297
+ ? !!opt.imageOnlyParagraphWithoutCaption
1298
+ : !!opt.oneImageWithoutCaption
1299
+ opt.oneImageWithoutCaption = !!opt.oneImageWithoutCaption
1117
1300
  if (!hasExplicitLabelClassFollowsFigure && opt.figureToLabelClassMap) {
1118
1301
  opt.labelClassFollowsFigure = true
1119
1302
  }
@@ -1124,6 +1307,8 @@ const mditFigureWithPCaption = (md, option) => {
1124
1307
  if (opt.preferredLanguages.length === 0) opt.preferredLanguages = null
1125
1308
  opt.normalizedOptionLanguages = normalizePreferredLanguages(opt.languages, opt.markRegState.languages)
1126
1309
  opt.shouldResolvePreferredLanguages = needsPreferredLanguagesResolution(opt)
1310
+ validateFallbackCaptionLabelOption('autoAltCaption', opt.autoAltCaption, opt.markRegState)
1311
+ validateFallbackCaptionLabelOption('autoTitleCaption', opt.autoTitleCaption, opt.markRegState)
1127
1312
  opt.htmlWrapWithoutCaption = {
1128
1313
  iframe: !!opt.iframeWithoutCaption,
1129
1314
  video: !!opt.videoWithoutCaption,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peaceroad/markdown-it-figure-with-p-caption",
3
- "version": "0.17.0",
3
+ "version": "0.18.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.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",
23
+ "@peaceroad/markdown-it-cjk-breaks-mod": "^0.1.11",
24
+ "@peaceroad/markdown-it-renderer-fence": "^0.7.0",
25
+ "@peaceroad/markdown-it-renderer-image": "^0.15.0",
26
+ "@peaceroad/markdown-it-strong-ja": "^0.9.1",
27
27
  "highlight.js": "^11.11.1",
28
28
  "markdown-it": "^14.1.0",
29
29
  "markdown-it-attrs": "^4.3.1"
30
30
  },
31
31
  "dependencies": {
32
- "p7d-markdown-it-p-captions": "0.22.0"
32
+ "p7d-markdown-it-p-captions": "0.23.0"
33
33
  },
34
34
  "files": [
35
35
  "index.js",