@readme/markdown 14.10.2 → 14.10.3

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.
@@ -2,6 +2,8 @@
2
2
  /* eslint-disable react/jsx-props-no-spreading */
3
3
  import React from 'react';
4
4
 
5
+ import { normalizeYouTubeUrlToEmbedUrl } from './normalizeYouTubeUrl';
6
+
5
7
  interface FaviconProps {
6
8
  alt?: string;
7
9
  src: string;
@@ -72,7 +74,8 @@ const Embed = ({
72
74
  !html && !explicitOptOut && url && typeOfEmbed && IFRAME_DERIVABLE_TYPES.has(typeOfEmbed);
73
75
 
74
76
  if (iframe || renderTypeAsIframe) {
75
- return <iframe {...spreadAttrs} src={url} style={iframeStyle} title={title} />;
77
+ const src = normalizeYouTubeUrlToEmbedUrl(url);
78
+ return <iframe {...spreadAttrs} src={src} style={iframeStyle} title={title} />;
76
79
  }
77
80
 
78
81
  if (!providerUrl && url)
@@ -0,0 +1,41 @@
1
+ // YouTube watch/share URLs (youtube.com/watch?v=ID, youtu.be/ID) set X-Frame-Options
2
+ // and refuse to be framed. The player only allows embedding via youtube.com/embed/ID.
3
+ const YOUTUBE_HOSTS = new Set(['youtube.com', 'www.youtube.com', 'm.youtube.com', 'youtu.be', 'www.youtu.be', 'm.youtu.be']);
4
+
5
+ // /embed/ accepts `start` (integer seconds), not the watch-page `t` value which may carry units (e.g. 596s, 1h2m3s).
6
+ const toStartSeconds = (timestamp: string): string | null => {
7
+ const [, hours, minutes, seconds] = timestamp.match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s?)?$/) ?? [];
8
+ const total = (Number(hours) || 0) * 3600 + (Number(minutes) || 0) * 60 + (Number(seconds) || 0);
9
+ return total > 0 ? String(total) : null;
10
+ };
11
+
12
+ const extractVideoId = (parsed: URL): string | null => {
13
+ if (parsed.hostname.endsWith('youtu.be')) return parsed.pathname.slice(1).split('/')[0] || null;
14
+ if (parsed.pathname.startsWith('/watch')) return parsed.searchParams.get('v');
15
+ if (parsed.pathname.startsWith('/shorts/')) return parsed.pathname.slice('/shorts/'.length).split('/')[0] || null;
16
+ return null;
17
+ };
18
+
19
+ // Most youtube links we get from the browser are watch URLs, like https://www.youtube.com/watch?v=dQw4w9WgXcQ
20
+ // These /watch URLs won't work in an iframe, so we need to convert them to the embed URL if iframe is used
21
+ export const normalizeYouTubeUrlToEmbedUrl = (url: string): string => {
22
+ let parsed: URL;
23
+ try {
24
+ parsed = new URL(url);
25
+ } catch {
26
+ return url;
27
+ }
28
+
29
+ if (!YOUTUBE_HOSTS.has(parsed.hostname)) return url;
30
+ if (parsed.pathname.startsWith('/embed/')) return url;
31
+
32
+ const videoId = extractVideoId(parsed);
33
+ if (!videoId) return url;
34
+
35
+ const embed = new URL(`https://www.youtube.com/embed/${videoId}`);
36
+ const timestamp = parsed.searchParams.get('start') || parsed.searchParams.get('t');
37
+ const start = timestamp ? toStartSeconds(timestamp) : null;
38
+ if (start) embed.searchParams.set('start', start);
39
+
40
+ return embed.toString();
41
+ };
@@ -0,0 +1 @@
1
+ export declare const normalizeYouTubeUrlToEmbedUrl: (url: string) => string;
@@ -13,12 +13,13 @@ declare module 'micromark-util-types' {
13
13
  * self-closing `<Component />`). Prevents CommonMark from fragmenting them
14
14
  * across multiple HTML / paragraph nodes.
15
15
  *
16
- * **Text (inline)** — registers only for lowercase tags with brace attrs
17
- * (e.g. `Start <a href={url}>here</a> end`). Picks them up during inline
18
- * parsing so they render inline inside their paragraph, then are rewritten
19
- * to `mdxJsxTextElement` by the `components/inline-html` transformer.
20
- * PascalCase is intentionally flow-only; ReadMe's custom components are
21
- * authored as block-level elements.
16
+ * **Text (inline)** — registers for lowercase tags and inline PascalCase
17
+ * components (Anchor, Glossary) that carry brace attrs (e.g.
18
+ * `Start <a href={url}>here</a> end`, `<Anchor href={url}>x</Anchor>`). Picks
19
+ * them up during inline parsing so they render inline inside their paragraph,
20
+ * then are rewritten to `mdxJsxTextElement` by the `components/inline-html`
21
+ * transformer. All other PascalCase is flow-only; ReadMe's custom components
22
+ * are authored as block-level elements.
22
23
  *
23
24
  * Excludes tags handled by dedicated tokenizers: Table, HTMLBlock, Glossary,
24
25
  * Anchor.
package/dist/main.js CHANGED
@@ -12060,10 +12060,55 @@ const Columns = ({ children, layout = 'auto' }) => {
12060
12060
  };
12061
12061
  /* harmony default export */ const components_Columns = (Columns);
12062
12062
 
12063
+ ;// ./components/Embed/normalizeYouTubeUrl.ts
12064
+ // YouTube watch/share URLs (youtube.com/watch?v=ID, youtu.be/ID) set X-Frame-Options
12065
+ // and refuse to be framed. The player only allows embedding via youtube.com/embed/ID.
12066
+ const YOUTUBE_HOSTS = new Set(['youtube.com', 'www.youtube.com', 'm.youtube.com', 'youtu.be', 'www.youtu.be', 'm.youtu.be']);
12067
+ // /embed/ accepts `start` (integer seconds), not the watch-page `t` value which may carry units (e.g. 596s, 1h2m3s).
12068
+ const toStartSeconds = (timestamp) => {
12069
+ const [, hours, minutes, seconds] = timestamp.match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s?)?$/) ?? [];
12070
+ const total = (Number(hours) || 0) * 3600 + (Number(minutes) || 0) * 60 + (Number(seconds) || 0);
12071
+ return total > 0 ? String(total) : null;
12072
+ };
12073
+ const extractVideoId = (parsed) => {
12074
+ if (parsed.hostname.endsWith('youtu.be'))
12075
+ return parsed.pathname.slice(1).split('/')[0] || null;
12076
+ if (parsed.pathname.startsWith('/watch'))
12077
+ return parsed.searchParams.get('v');
12078
+ if (parsed.pathname.startsWith('/shorts/'))
12079
+ return parsed.pathname.slice('/shorts/'.length).split('/')[0] || null;
12080
+ return null;
12081
+ };
12082
+ // Most youtube links we get from the browser are watch URLs, like https://www.youtube.com/watch?v=dQw4w9WgXcQ
12083
+ // These /watch URLs won't work in an iframe, so we need to convert them to the embed URL if iframe is used
12084
+ const normalizeYouTubeUrlToEmbedUrl = (url) => {
12085
+ let parsed;
12086
+ try {
12087
+ parsed = new URL(url);
12088
+ }
12089
+ catch {
12090
+ return url;
12091
+ }
12092
+ if (!YOUTUBE_HOSTS.has(parsed.hostname))
12093
+ return url;
12094
+ if (parsed.pathname.startsWith('/embed/'))
12095
+ return url;
12096
+ const videoId = extractVideoId(parsed);
12097
+ if (!videoId)
12098
+ return url;
12099
+ const embed = new URL(`https://www.youtube.com/embed/${videoId}`);
12100
+ const timestamp = parsed.searchParams.get('start') || parsed.searchParams.get('t');
12101
+ const start = timestamp ? toStartSeconds(timestamp) : null;
12102
+ if (start)
12103
+ embed.searchParams.set('start', start);
12104
+ return embed.toString();
12105
+ };
12106
+
12063
12107
  ;// ./components/Embed/index.tsx
12064
12108
  /* eslint-disable no-param-reassign */
12065
12109
  /* eslint-disable react/jsx-props-no-spreading */
12066
12110
 
12111
+
12067
12112
  const Favicon = ({ src, alt = 'favicon', ...attr }) => (external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_default().createElement("img", { ...attr, alt: alt, height: "14", src: src, width: "14" }));
12068
12113
  const IFRAME_DERIVABLE_TYPES = new Set(['youtube', 'jsfiddle', 'pdf']);
12069
12114
  // HTML width/height attrs accept bare numbers (interpreted as px), but CSS does not, convert.
@@ -12097,7 +12142,8 @@ const Embed = ({ lazy = true, url, html, providerName, providerUrl, title, ifram
12097
12142
  // Fall back to a direct iframe for URL-derivable embed types when html is missing.
12098
12143
  const renderTypeAsIframe = !html && !explicitOptOut && url && typeOfEmbed && IFRAME_DERIVABLE_TYPES.has(typeOfEmbed);
12099
12144
  if (iframe || renderTypeAsIframe) {
12100
- return external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_default().createElement("iframe", { ...spreadAttrs, src: url, style: iframeStyle, title: title });
12145
+ const src = normalizeYouTubeUrlToEmbedUrl(url);
12146
+ return external_amd_react_commonjs_react_commonjs2_react_root_React_umd_react_default().createElement("iframe", { ...spreadAttrs, src: src, style: iframeStyle, title: title });
12101
12147
  }
12102
12148
  if (!providerUrl && url)
12103
12149
  providerUrl = new URL(url).hostname
@@ -100750,6 +100796,12 @@ const types_types = /** @type {const} */ ({
100750
100796
 
100751
100797
 
100752
100798
 
100799
+
100800
+ // Raw tags (type-1: pre/script/style/textarea) and block tags (type-6: div,
100801
+ // section, …) always start a block, so they stay flow even with trailing
100802
+ // content. Other lowercase tags (i, span, …) follow the type-7 rule and only
100803
+ // stay flow when nothing trails the close tag.
100804
+ const htmlFlowTagNames = new Set([...htmlRawNames, ...htmlBlockNames]);
100753
100805
  const syntax_nonLazyContinuationStart = {
100754
100806
  tokenize: syntax_tokenizeNonLazyContinuationStart,
100755
100807
  partial: true,
@@ -100786,11 +100838,12 @@ const mdxComponentTextConstruct = {
100786
100838
  * lowercase HTML tags that carry at least one `{…}` attribute expression.
100787
100839
  * Multi-line, concrete, `afterClose` consumes the rest of the line.
100788
100840
  *
100789
- * **Text** — runs inside paragraphs / inline context. Claims *only* lowercase
100790
- * tags with brace attributes (PascalCase is intentionally flow-only, matching
100791
- * how ReadMe's custom components are authored). Aborts on line endings (inline
100792
- * constructs don't span lines) and exits immediately after `</tag>` so the
100793
- * paragraph's inline parser picks up the trailing text.
100841
+ * **Text** — runs inside paragraphs / inline context. Claims lowercase tags and
100842
+ * inline PascalCase components (`INLINE_COMPONENT_TAGS` Anchor, Glossary), both
100843
+ * gated on at least one `{…}` brace attribute. All other PascalCase stays
100844
+ * flow-only, matching how ReadMe's custom components are authored. Aborts on line
100845
+ * endings (inline constructs don't span lines) and exits immediately after
100846
+ * `</tag>` so the paragraph's inline parser picks up the trailing text.
100794
100847
  */
100795
100848
  function createTokenize(mode) {
100796
100849
  const isFlow = mode === 'flow';
@@ -100802,8 +100855,9 @@ function createTokenize(mode) {
100802
100855
  let closingTagName = '';
100803
100856
  // For lowercase tags we only want to claim the block if it uses JSX
100804
100857
  // attribute expression syntax (`attr={...}`). Plain HTML should fall
100805
- // through to CommonMark html-flow. Uppercase tags are always claimed
100806
- // (flow only PascalCase is not accepted in text mode).
100858
+ // through to CommonMark html-flow. Flow mode claims any PascalCase block
100859
+ // component; text mode claims only inline PascalCase components
100860
+ // (INLINE_COMPONENT_TAGS — Anchor, Glossary), also brace-gated.
100807
100861
  let isLowercaseTag = false;
100808
100862
  let sawBraceAttr = false;
100809
100863
  // Code span tracking
@@ -100814,6 +100868,9 @@ function createTokenize(mode) {
100814
100868
  let fenceLength = 0;
100815
100869
  let fenceCloseLength = 0;
100816
100870
  let atLineStart = false;
100871
+ // True once this construct consumes any line ending; lets `afterClose`
100872
+ // treat only single-line lowercase tags as inline candidates.
100873
+ let sawLineEnding = false;
100817
100874
  // Bail when the opener line has unmatched tag-like tokens in its body.
100818
100875
  // `<Foo>_<Bar>.csv` leaves opens > closes; matched shapes like
100819
100876
  // `<Callout>x <strong>y</strong>` balance to 0. Without this,
@@ -100964,13 +101021,13 @@ function createTokenize(mode) {
100964
101021
  }
100965
101022
  // ── Tag name parsing ───────────────────────────────────────────────────
100966
101023
  function tagNameFirst(code) {
100967
- // Uppercase A-Z → PascalCase MDX component. Flow-only PascalCase
100968
- // components are block elements in ReadMe's authoring model.
101024
+ // Uppercase A-Z → PascalCase MDX component. Flow mode claims block
101025
+ // components; text mode only claims inline components (Anchor, Glossary),
101026
+ // which is enforced once the full name is known in `tagNameRest`.
100969
101027
  if (code !== null && code >= codes.uppercaseA && code <= codes.uppercaseZ) {
100970
- if (!isFlow)
100971
- return nok(code);
100972
101028
  tagName = String.fromCharCode(code);
100973
101029
  isLowercaseTag = false;
101030
+ sawBraceAttr = false;
100974
101031
  effects.consume(code);
100975
101032
  return tagNameRest;
100976
101033
  }
@@ -100998,10 +101055,18 @@ function createTokenize(mode) {
100998
101055
  effects.consume(code);
100999
101056
  return tagNameRest;
101000
101057
  }
101001
- // Tag name complete — check exclusions
101002
- if (TOKENIZER_MDX_COMPONENT_EXCLUDED_TAGS.has(tagName)) {
101058
+ // Tag name complete — decide whether this tokenizer claims the tag.
101059
+ // Three cases: lowercase tags are always candidates (brace-gated later in
101060
+ // `afterOpenTagName`); flow-mode PascalCase claims any block component
101061
+ // except those with a dedicated tokenizer; text-mode PascalCase claims
101062
+ // only inline components (Anchor, Glossary).
101063
+ const claimable = isLowercaseTag
101064
+ ? true
101065
+ : isFlow
101066
+ ? !TOKENIZER_MDX_COMPONENT_EXCLUDED_TAGS.has(tagName)
101067
+ : INLINE_COMPONENT_TAGS.has(tagName);
101068
+ if (!claimable)
101003
101069
  return nok(code);
101004
- }
101005
101070
  depth = 1;
101006
101071
  return afterOpenTagName(code);
101007
101072
  }
@@ -101009,6 +101074,10 @@ function createTokenize(mode) {
101009
101074
  function afterOpenTagName(code) {
101010
101075
  if (code === null)
101011
101076
  return nok(code);
101077
+ // Everything except a flow-mode PascalCase block component must carry a
101078
+ // `{…}` brace attribute to be claimed; plain HTML falls through to
101079
+ // CommonMark.
101080
+ const requiresBraceAttr = isLowercaseTag || !isFlow;
101012
101081
  if (markdownLineEnding(code)) {
101013
101082
  if (!isFlow)
101014
101083
  return nok(code);
@@ -101017,14 +101086,14 @@ function createTokenize(mode) {
101017
101086
  }
101018
101087
  // Self-closing />
101019
101088
  if (code === codes.slash) {
101020
- if (isLowercaseTag && !sawBraceAttr)
101089
+ if (requiresBraceAttr && !sawBraceAttr)
101021
101090
  return nok(code);
101022
101091
  effects.consume(code);
101023
101092
  return selfCloseGt;
101024
101093
  }
101025
101094
  // End of opening tag
101026
101095
  if (code === codes.greaterThan) {
101027
- if (isLowercaseTag && !sawBraceAttr)
101096
+ if (requiresBraceAttr && !sawBraceAttr)
101028
101097
  return nok(code);
101029
101098
  effects.consume(code);
101030
101099
  onOpenerLine = isFlow;
@@ -101088,6 +101157,7 @@ function createTokenize(mode) {
101088
101157
  return effects.check(syntax_nonLazyContinuationStart, openTagContinuationNonLazy, continuationAfter)(code);
101089
101158
  }
101090
101159
  function openTagContinuationNonLazy(code) {
101160
+ sawLineEnding = true;
101091
101161
  effects.enter(types_types.lineEnding);
101092
101162
  effects.consume(code);
101093
101163
  effects.exit(types_types.lineEnding);
@@ -101211,6 +101281,7 @@ function createTokenize(mode) {
101211
101281
  return effects.check(syntax_nonLazyContinuationStart, fencedCodeContinuationNonLazy, continuationAfter)(code);
101212
101282
  }
101213
101283
  function fencedCodeContinuationNonLazy(code) {
101284
+ sawLineEnding = true;
101214
101285
  effects.enter(types_types.lineEnding);
101215
101286
  effects.consume(code);
101216
101287
  effects.exit(types_types.lineEnding);
@@ -101367,6 +101438,12 @@ function createTokenize(mode) {
101367
101438
  effects.exit('mdxComponent');
101368
101439
  return ok(code);
101369
101440
  }
101441
+ // A single-line type-7 lowercase tag with content trailing its close is
101442
+ // inline, so `<i …></i> <a>…</a>` stays on one line instead of splitting
101443
+ // into separate blocks. Raw/block tags (pre, div, …) stay flow.
101444
+ if (isLowercaseTag && !sawLineEnding && !htmlFlowTagNames.has(tagName)) {
101445
+ return afterCloseInlineCandidate(code);
101446
+ }
101370
101447
  if (code === null || markdownLineEnding(code)) {
101371
101448
  effects.exit('mdxComponentData');
101372
101449
  effects.exit('mdxComponent');
@@ -101377,11 +101454,26 @@ function createTokenize(mode) {
101377
101454
  effects.consume(code);
101378
101455
  return afterClose;
101379
101456
  }
101457
+ // Whitespace-only to the line ending keeps the flow block; any other
101458
+ // trailing char means inline content, so refuse and let inline parsing run.
101459
+ function afterCloseInlineCandidate(code) {
101460
+ if (code === null || markdownLineEnding(code)) {
101461
+ effects.exit('mdxComponentData');
101462
+ effects.exit('mdxComponent');
101463
+ return ok(code);
101464
+ }
101465
+ if (markdownSpace(code)) {
101466
+ effects.consume(code);
101467
+ return afterCloseInlineCandidate;
101468
+ }
101469
+ return nok(code);
101470
+ }
101380
101471
  // ── Body continuation (line endings) ───────────────────────────────────
101381
101472
  function bodyContinuationStart(code) {
101382
101473
  return effects.check(syntax_nonLazyContinuationStart, bodyContinuationNonLazy, continuationAfter)(code);
101383
101474
  }
101384
101475
  function bodyContinuationNonLazy(code) {
101476
+ sawLineEnding = true;
101385
101477
  effects.enter(types_types.lineEnding);
101386
101478
  effects.consume(code);
101387
101479
  effects.exit(types_types.lineEnding);
@@ -101451,12 +101543,13 @@ function syntax_tokenizeNonLazyContinuationStart(effects, ok, nok) {
101451
101543
  * self-closing `<Component />`). Prevents CommonMark from fragmenting them
101452
101544
  * across multiple HTML / paragraph nodes.
101453
101545
  *
101454
- * **Text (inline)** — registers only for lowercase tags with brace attrs
101455
- * (e.g. `Start <a href={url}>here</a> end`). Picks them up during inline
101456
- * parsing so they render inline inside their paragraph, then are rewritten
101457
- * to `mdxJsxTextElement` by the `components/inline-html` transformer.
101458
- * PascalCase is intentionally flow-only; ReadMe's custom components are
101459
- * authored as block-level elements.
101546
+ * **Text (inline)** — registers for lowercase tags and inline PascalCase
101547
+ * components (Anchor, Glossary) that carry brace attrs (e.g.
101548
+ * `Start <a href={url}>here</a> end`, `<Anchor href={url}>x</Anchor>`). Picks
101549
+ * them up during inline parsing so they render inline inside their paragraph,
101550
+ * then are rewritten to `mdxJsxTextElement` by the `components/inline-html`
101551
+ * transformer. All other PascalCase is flow-only; ReadMe's custom components
101552
+ * are authored as block-level elements.
101460
101553
  *
101461
101554
  * Excludes tags handled by dedicated tokenizers: Table, HTMLBlock, Glossary,
101462
101555
  * Anchor.
@@ -101575,12 +101668,12 @@ const parsePhrasingChildren = (value, safeMode) => {
101575
101668
  return first?.type === 'paragraph' ? first.children : [];
101576
101669
  };
101577
101670
  /**
101578
- * Attempt to promote a lowercase inline `html` node to an
101579
- * `mdxJsxTextElement`. Scope is deliberately narrow: only lowercase tags
101580
- * carrying `{…}` attribute expressions (what the `mdxComponent` text
101581
- * tokenizer claims). PascalCase components even when inline — stay with
101582
- * `mdxishComponentBlocks` so they remain `mdxJsxFlowElement`, matching
101583
- * ReadMe's flow-level component authoring model.
101671
+ * Attempt to promote an inline `html` node to an `mdxJsxTextElement`. Scope
101672
+ * mirrors what the `mdxComponent` text tokenizer claims: lowercase tags and
101673
+ * inline PascalCase components (`INLINE_COMPONENT_TAGS` — Anchor, Glossary),
101674
+ * both carrying `{…}` attribute expressions. All other PascalCase components
101675
+ * stay with `mdxishComponentBlocks` so they remain `mdxJsxFlowElement`,
101676
+ * matching ReadMe's flow-level component authoring model.
101584
101677
  *
101585
101678
  * Returns the original node unchanged when it doesn't qualify.
101586
101679
  */
@@ -101589,12 +101682,11 @@ const promoteInlineHtml = (node, parseOpts, safeMode) => {
101589
101682
  if (!parsed)
101590
101683
  return node;
101591
101684
  const { tag, attributes, selfClosing, contentAfterTag = '' } = parsed;
101592
- // PascalCase stays flow-level; handled by mdxishComponentBlocks.
101593
- if (isPascalCase(tag))
101594
- return node;
101595
- // Dedicated tokenizers (Table, HTMLBlock, Glossary, Anchor) handle their
101596
- // own tags — don't hijack them here.
101597
- if (GENERIC_MDX_COMPONENT_EXCLUDED_TAGS.has(tag))
101685
+ // Non-inline PascalCase stays flow-level (handled by mdxishComponentBlocks)
101686
+ // and dedicated tokenizers (Table, HTMLBlock) own their tags. Inline
101687
+ // components (Anchor, Glossary) with expression attrs are promoted here —
101688
+ // the text tokenizer captures them as a single html node.
101689
+ if (isPascalCase(tag) && !INLINE_COMPONENT_TAGS.has(tag))
101598
101690
  return node;
101599
101691
  // Plain inline HTML (no expressions) stays as an html node so rehype-raw
101600
101692
  // parses it with parse5 as normal.
@@ -101618,14 +101710,15 @@ const promoteInlineHtml = (node, parseOpts, safeMode) => {
101618
101710
  * Runs after `mdxishComponentBlocks`, which skips paragraph-parented html
101619
101711
  * nodes and leaves them for this pass. Two producers put html nodes inside
101620
101712
  * paragraphs:
101621
- * 1. The `mdxComponent` text tokenizer, for lowercase tags with `{…}`
101622
- * attribute expressions (e.g. `<a href={url}>here</a>`).
101713
+ * 1. The `mdxComponent` text tokenizer, for lowercase tags and inline
101714
+ * PascalCase components (Anchor, Glossary) with `{}` attribute
101715
+ * expressions (e.g. `<a href={url}>here</a>`, `<Anchor href={url}>x</Anchor>`).
101623
101716
  * 2. CommonMark's built-in html-text tokenizer, for PascalCase components
101624
101717
  * (e.g. a single html node for self-closing `<C />`).
101625
101718
  *
101626
- * Eligibility mirrors `mdxishComponentBlocks`: lowercase tags only promote
101627
- * when they carry an expression attribute; plain inline HTML like
101628
- * `<a href="x">` stays as an html node for rehype-raw.
101719
+ * Eligibility mirrors `mdxishComponentBlocks`: a tag only promotes when it
101720
+ * carries an expression attribute and isn't a non-inline PascalCase component;
101721
+ * plain inline HTML like `<a href="x">` stays as an html node for rehype-raw.
101629
101722
  */
101630
101723
  const mdxishInlineMdxHtmlBlocks = (opts = {}) => tree => {
101631
101724
  const safeMode = !!opts.safeMode;