@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.
@@ -7,14 +7,15 @@ import type { Plugin } from 'unified';
7
7
  * Runs after `mdxishComponentBlocks`, which skips paragraph-parented html
8
8
  * nodes and leaves them for this pass. Two producers put html nodes inside
9
9
  * paragraphs:
10
- * 1. The `mdxComponent` text tokenizer, for lowercase tags with `{…}`
11
- * attribute expressions (e.g. `<a href={url}>here</a>`).
10
+ * 1. The `mdxComponent` text tokenizer, for lowercase tags and inline
11
+ * PascalCase components (Anchor, Glossary) with `{}` attribute
12
+ * expressions (e.g. `<a href={url}>here</a>`, `<Anchor href={url}>x</Anchor>`).
12
13
  * 2. CommonMark's built-in html-text tokenizer, for PascalCase components
13
14
  * (e.g. a single html node for self-closing `<C />`).
14
15
  *
15
- * Eligibility mirrors `mdxishComponentBlocks`: lowercase tags only promote
16
- * when they carry an expression attribute; plain inline HTML like
17
- * `<a href="x">` stays as an html node for rehype-raw.
16
+ * Eligibility mirrors `mdxishComponentBlocks`: a tag only promotes when it
17
+ * carries an expression attribute and isn't a non-inline PascalCase component;
18
+ * plain inline HTML like `<a href="x">` stays as an html node for rehype-raw.
18
19
  */
19
20
  declare const mdxishInlineMdxHtmlBlocks: Plugin<[{
20
21
  safeMode?: boolean;
@@ -86655,10 +86655,55 @@ const Columns = ({ children, layout = 'auto' }) => {
86655
86655
  };
86656
86656
  /* harmony default export */ const components_Columns = (Columns);
86657
86657
 
86658
+ ;// ./components/Embed/normalizeYouTubeUrl.ts
86659
+ // YouTube watch/share URLs (youtube.com/watch?v=ID, youtu.be/ID) set X-Frame-Options
86660
+ // and refuse to be framed. The player only allows embedding via youtube.com/embed/ID.
86661
+ const YOUTUBE_HOSTS = new Set(['youtube.com', 'www.youtube.com', 'm.youtube.com', 'youtu.be', 'www.youtu.be', 'm.youtu.be']);
86662
+ // /embed/ accepts `start` (integer seconds), not the watch-page `t` value which may carry units (e.g. 596s, 1h2m3s).
86663
+ const toStartSeconds = (timestamp) => {
86664
+ const [, hours, minutes, seconds] = timestamp.match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s?)?$/) ?? [];
86665
+ const total = (Number(hours) || 0) * 3600 + (Number(minutes) || 0) * 60 + (Number(seconds) || 0);
86666
+ return total > 0 ? String(total) : null;
86667
+ };
86668
+ const extractVideoId = (parsed) => {
86669
+ if (parsed.hostname.endsWith('youtu.be'))
86670
+ return parsed.pathname.slice(1).split('/')[0] || null;
86671
+ if (parsed.pathname.startsWith('/watch'))
86672
+ return parsed.searchParams.get('v');
86673
+ if (parsed.pathname.startsWith('/shorts/'))
86674
+ return parsed.pathname.slice('/shorts/'.length).split('/')[0] || null;
86675
+ return null;
86676
+ };
86677
+ // Most youtube links we get from the browser are watch URLs, like https://www.youtube.com/watch?v=dQw4w9WgXcQ
86678
+ // These /watch URLs won't work in an iframe, so we need to convert them to the embed URL if iframe is used
86679
+ const normalizeYouTubeUrlToEmbedUrl = (url) => {
86680
+ let parsed;
86681
+ try {
86682
+ parsed = new URL(url);
86683
+ }
86684
+ catch {
86685
+ return url;
86686
+ }
86687
+ if (!YOUTUBE_HOSTS.has(parsed.hostname))
86688
+ return url;
86689
+ if (parsed.pathname.startsWith('/embed/'))
86690
+ return url;
86691
+ const videoId = extractVideoId(parsed);
86692
+ if (!videoId)
86693
+ return url;
86694
+ const embed = new URL(`https://www.youtube.com/embed/${videoId}`);
86695
+ const timestamp = parsed.searchParams.get('start') || parsed.searchParams.get('t');
86696
+ const start = timestamp ? toStartSeconds(timestamp) : null;
86697
+ if (start)
86698
+ embed.searchParams.set('start', start);
86699
+ return embed.toString();
86700
+ };
86701
+
86658
86702
  ;// ./components/Embed/index.tsx
86659
86703
  /* eslint-disable no-param-reassign */
86660
86704
  /* eslint-disable react/jsx-props-no-spreading */
86661
86705
 
86706
+
86662
86707
  const Favicon = ({ src, alt = 'favicon', ...attr }) => (external_react_default().createElement("img", { ...attr, alt: alt, height: "14", src: src, width: "14" }));
86663
86708
  const IFRAME_DERIVABLE_TYPES = new Set(['youtube', 'jsfiddle', 'pdf']);
86664
86709
  // HTML width/height attrs accept bare numbers (interpreted as px), but CSS does not, convert.
@@ -86692,7 +86737,8 @@ const Embed = ({ lazy = true, url, html, providerName, providerUrl, title, ifram
86692
86737
  // Fall back to a direct iframe for URL-derivable embed types when html is missing.
86693
86738
  const renderTypeAsIframe = !html && !explicitOptOut && url && typeOfEmbed && IFRAME_DERIVABLE_TYPES.has(typeOfEmbed);
86694
86739
  if (iframe || renderTypeAsIframe) {
86695
- return external_react_default().createElement("iframe", { ...spreadAttrs, src: url, style: iframeStyle, title: title });
86740
+ const src = normalizeYouTubeUrlToEmbedUrl(url);
86741
+ return external_react_default().createElement("iframe", { ...spreadAttrs, src: src, style: iframeStyle, title: title });
86696
86742
  }
86697
86743
  if (!providerUrl && url)
86698
86744
  providerUrl = new URL(url).hostname
@@ -119505,6 +119551,12 @@ const types_types = /** @type {const} */ ({
119505
119551
 
119506
119552
 
119507
119553
 
119554
+
119555
+ // Raw tags (type-1: pre/script/style/textarea) and block tags (type-6: div,
119556
+ // section, …) always start a block, so they stay flow even with trailing
119557
+ // content. Other lowercase tags (i, span, …) follow the type-7 rule and only
119558
+ // stay flow when nothing trails the close tag.
119559
+ const htmlFlowTagNames = new Set([...htmlRawNames, ...htmlBlockNames]);
119508
119560
  const syntax_nonLazyContinuationStart = {
119509
119561
  tokenize: syntax_tokenizeNonLazyContinuationStart,
119510
119562
  partial: true,
@@ -119541,11 +119593,12 @@ const mdxComponentTextConstruct = {
119541
119593
  * lowercase HTML tags that carry at least one `{…}` attribute expression.
119542
119594
  * Multi-line, concrete, `afterClose` consumes the rest of the line.
119543
119595
  *
119544
- * **Text** — runs inside paragraphs / inline context. Claims *only* lowercase
119545
- * tags with brace attributes (PascalCase is intentionally flow-only, matching
119546
- * how ReadMe's custom components are authored). Aborts on line endings (inline
119547
- * constructs don't span lines) and exits immediately after `</tag>` so the
119548
- * paragraph's inline parser picks up the trailing text.
119596
+ * **Text** — runs inside paragraphs / inline context. Claims lowercase tags and
119597
+ * inline PascalCase components (`INLINE_COMPONENT_TAGS` Anchor, Glossary), both
119598
+ * gated on at least one `{…}` brace attribute. All other PascalCase stays
119599
+ * flow-only, matching how ReadMe's custom components are authored. Aborts on line
119600
+ * endings (inline constructs don't span lines) and exits immediately after
119601
+ * `</tag>` so the paragraph's inline parser picks up the trailing text.
119549
119602
  */
119550
119603
  function createTokenize(mode) {
119551
119604
  const isFlow = mode === 'flow';
@@ -119557,8 +119610,9 @@ function createTokenize(mode) {
119557
119610
  let closingTagName = '';
119558
119611
  // For lowercase tags we only want to claim the block if it uses JSX
119559
119612
  // attribute expression syntax (`attr={...}`). Plain HTML should fall
119560
- // through to CommonMark html-flow. Uppercase tags are always claimed
119561
- // (flow only PascalCase is not accepted in text mode).
119613
+ // through to CommonMark html-flow. Flow mode claims any PascalCase block
119614
+ // component; text mode claims only inline PascalCase components
119615
+ // (INLINE_COMPONENT_TAGS — Anchor, Glossary), also brace-gated.
119562
119616
  let isLowercaseTag = false;
119563
119617
  let sawBraceAttr = false;
119564
119618
  // Code span tracking
@@ -119569,6 +119623,9 @@ function createTokenize(mode) {
119569
119623
  let fenceLength = 0;
119570
119624
  let fenceCloseLength = 0;
119571
119625
  let atLineStart = false;
119626
+ // True once this construct consumes any line ending; lets `afterClose`
119627
+ // treat only single-line lowercase tags as inline candidates.
119628
+ let sawLineEnding = false;
119572
119629
  // Bail when the opener line has unmatched tag-like tokens in its body.
119573
119630
  // `<Foo>_<Bar>.csv` leaves opens > closes; matched shapes like
119574
119631
  // `<Callout>x <strong>y</strong>` balance to 0. Without this,
@@ -119719,13 +119776,13 @@ function createTokenize(mode) {
119719
119776
  }
119720
119777
  // ── Tag name parsing ───────────────────────────────────────────────────
119721
119778
  function tagNameFirst(code) {
119722
- // Uppercase A-Z → PascalCase MDX component. Flow-only PascalCase
119723
- // components are block elements in ReadMe's authoring model.
119779
+ // Uppercase A-Z → PascalCase MDX component. Flow mode claims block
119780
+ // components; text mode only claims inline components (Anchor, Glossary),
119781
+ // which is enforced once the full name is known in `tagNameRest`.
119724
119782
  if (code !== null && code >= codes.uppercaseA && code <= codes.uppercaseZ) {
119725
- if (!isFlow)
119726
- return nok(code);
119727
119783
  tagName = String.fromCharCode(code);
119728
119784
  isLowercaseTag = false;
119785
+ sawBraceAttr = false;
119729
119786
  effects.consume(code);
119730
119787
  return tagNameRest;
119731
119788
  }
@@ -119753,10 +119810,18 @@ function createTokenize(mode) {
119753
119810
  effects.consume(code);
119754
119811
  return tagNameRest;
119755
119812
  }
119756
- // Tag name complete — check exclusions
119757
- if (TOKENIZER_MDX_COMPONENT_EXCLUDED_TAGS.has(tagName)) {
119813
+ // Tag name complete — decide whether this tokenizer claims the tag.
119814
+ // Three cases: lowercase tags are always candidates (brace-gated later in
119815
+ // `afterOpenTagName`); flow-mode PascalCase claims any block component
119816
+ // except those with a dedicated tokenizer; text-mode PascalCase claims
119817
+ // only inline components (Anchor, Glossary).
119818
+ const claimable = isLowercaseTag
119819
+ ? true
119820
+ : isFlow
119821
+ ? !TOKENIZER_MDX_COMPONENT_EXCLUDED_TAGS.has(tagName)
119822
+ : INLINE_COMPONENT_TAGS.has(tagName);
119823
+ if (!claimable)
119758
119824
  return nok(code);
119759
- }
119760
119825
  depth = 1;
119761
119826
  return afterOpenTagName(code);
119762
119827
  }
@@ -119764,6 +119829,10 @@ function createTokenize(mode) {
119764
119829
  function afterOpenTagName(code) {
119765
119830
  if (code === null)
119766
119831
  return nok(code);
119832
+ // Everything except a flow-mode PascalCase block component must carry a
119833
+ // `{…}` brace attribute to be claimed; plain HTML falls through to
119834
+ // CommonMark.
119835
+ const requiresBraceAttr = isLowercaseTag || !isFlow;
119767
119836
  if (markdownLineEnding(code)) {
119768
119837
  if (!isFlow)
119769
119838
  return nok(code);
@@ -119772,14 +119841,14 @@ function createTokenize(mode) {
119772
119841
  }
119773
119842
  // Self-closing />
119774
119843
  if (code === codes.slash) {
119775
- if (isLowercaseTag && !sawBraceAttr)
119844
+ if (requiresBraceAttr && !sawBraceAttr)
119776
119845
  return nok(code);
119777
119846
  effects.consume(code);
119778
119847
  return selfCloseGt;
119779
119848
  }
119780
119849
  // End of opening tag
119781
119850
  if (code === codes.greaterThan) {
119782
- if (isLowercaseTag && !sawBraceAttr)
119851
+ if (requiresBraceAttr && !sawBraceAttr)
119783
119852
  return nok(code);
119784
119853
  effects.consume(code);
119785
119854
  onOpenerLine = isFlow;
@@ -119843,6 +119912,7 @@ function createTokenize(mode) {
119843
119912
  return effects.check(syntax_nonLazyContinuationStart, openTagContinuationNonLazy, continuationAfter)(code);
119844
119913
  }
119845
119914
  function openTagContinuationNonLazy(code) {
119915
+ sawLineEnding = true;
119846
119916
  effects.enter(types_types.lineEnding);
119847
119917
  effects.consume(code);
119848
119918
  effects.exit(types_types.lineEnding);
@@ -119966,6 +120036,7 @@ function createTokenize(mode) {
119966
120036
  return effects.check(syntax_nonLazyContinuationStart, fencedCodeContinuationNonLazy, continuationAfter)(code);
119967
120037
  }
119968
120038
  function fencedCodeContinuationNonLazy(code) {
120039
+ sawLineEnding = true;
119969
120040
  effects.enter(types_types.lineEnding);
119970
120041
  effects.consume(code);
119971
120042
  effects.exit(types_types.lineEnding);
@@ -120122,6 +120193,12 @@ function createTokenize(mode) {
120122
120193
  effects.exit('mdxComponent');
120123
120194
  return ok(code);
120124
120195
  }
120196
+ // A single-line type-7 lowercase tag with content trailing its close is
120197
+ // inline, so `<i …></i> <a>…</a>` stays on one line instead of splitting
120198
+ // into separate blocks. Raw/block tags (pre, div, …) stay flow.
120199
+ if (isLowercaseTag && !sawLineEnding && !htmlFlowTagNames.has(tagName)) {
120200
+ return afterCloseInlineCandidate(code);
120201
+ }
120125
120202
  if (code === null || markdownLineEnding(code)) {
120126
120203
  effects.exit('mdxComponentData');
120127
120204
  effects.exit('mdxComponent');
@@ -120132,11 +120209,26 @@ function createTokenize(mode) {
120132
120209
  effects.consume(code);
120133
120210
  return afterClose;
120134
120211
  }
120212
+ // Whitespace-only to the line ending keeps the flow block; any other
120213
+ // trailing char means inline content, so refuse and let inline parsing run.
120214
+ function afterCloseInlineCandidate(code) {
120215
+ if (code === null || markdownLineEnding(code)) {
120216
+ effects.exit('mdxComponentData');
120217
+ effects.exit('mdxComponent');
120218
+ return ok(code);
120219
+ }
120220
+ if (markdownSpace(code)) {
120221
+ effects.consume(code);
120222
+ return afterCloseInlineCandidate;
120223
+ }
120224
+ return nok(code);
120225
+ }
120135
120226
  // ── Body continuation (line endings) ───────────────────────────────────
120136
120227
  function bodyContinuationStart(code) {
120137
120228
  return effects.check(syntax_nonLazyContinuationStart, bodyContinuationNonLazy, continuationAfter)(code);
120138
120229
  }
120139
120230
  function bodyContinuationNonLazy(code) {
120231
+ sawLineEnding = true;
120140
120232
  effects.enter(types_types.lineEnding);
120141
120233
  effects.consume(code);
120142
120234
  effects.exit(types_types.lineEnding);
@@ -120206,12 +120298,13 @@ function syntax_tokenizeNonLazyContinuationStart(effects, ok, nok) {
120206
120298
  * self-closing `<Component />`). Prevents CommonMark from fragmenting them
120207
120299
  * across multiple HTML / paragraph nodes.
120208
120300
  *
120209
- * **Text (inline)** — registers only for lowercase tags with brace attrs
120210
- * (e.g. `Start <a href={url}>here</a> end`). Picks them up during inline
120211
- * parsing so they render inline inside their paragraph, then are rewritten
120212
- * to `mdxJsxTextElement` by the `components/inline-html` transformer.
120213
- * PascalCase is intentionally flow-only; ReadMe's custom components are
120214
- * authored as block-level elements.
120301
+ * **Text (inline)** — registers for lowercase tags and inline PascalCase
120302
+ * components (Anchor, Glossary) that carry brace attrs (e.g.
120303
+ * `Start <a href={url}>here</a> end`, `<Anchor href={url}>x</Anchor>`). Picks
120304
+ * them up during inline parsing so they render inline inside their paragraph,
120305
+ * then are rewritten to `mdxJsxTextElement` by the `components/inline-html`
120306
+ * transformer. All other PascalCase is flow-only; ReadMe's custom components
120307
+ * are authored as block-level elements.
120215
120308
  *
120216
120309
  * Excludes tags handled by dedicated tokenizers: Table, HTMLBlock, Glossary,
120217
120310
  * Anchor.
@@ -120330,12 +120423,12 @@ const parsePhrasingChildren = (value, safeMode) => {
120330
120423
  return first?.type === 'paragraph' ? first.children : [];
120331
120424
  };
120332
120425
  /**
120333
- * Attempt to promote a lowercase inline `html` node to an
120334
- * `mdxJsxTextElement`. Scope is deliberately narrow: only lowercase tags
120335
- * carrying `{…}` attribute expressions (what the `mdxComponent` text
120336
- * tokenizer claims). PascalCase components even when inline — stay with
120337
- * `mdxishComponentBlocks` so they remain `mdxJsxFlowElement`, matching
120338
- * ReadMe's flow-level component authoring model.
120426
+ * Attempt to promote an inline `html` node to an `mdxJsxTextElement`. Scope
120427
+ * mirrors what the `mdxComponent` text tokenizer claims: lowercase tags and
120428
+ * inline PascalCase components (`INLINE_COMPONENT_TAGS` — Anchor, Glossary),
120429
+ * both carrying `{…}` attribute expressions. All other PascalCase components
120430
+ * stay with `mdxishComponentBlocks` so they remain `mdxJsxFlowElement`,
120431
+ * matching ReadMe's flow-level component authoring model.
120339
120432
  *
120340
120433
  * Returns the original node unchanged when it doesn't qualify.
120341
120434
  */
@@ -120344,12 +120437,11 @@ const promoteInlineHtml = (node, parseOpts, safeMode) => {
120344
120437
  if (!parsed)
120345
120438
  return node;
120346
120439
  const { tag, attributes, selfClosing, contentAfterTag = '' } = parsed;
120347
- // PascalCase stays flow-level; handled by mdxishComponentBlocks.
120348
- if (isPascalCase(tag))
120349
- return node;
120350
- // Dedicated tokenizers (Table, HTMLBlock, Glossary, Anchor) handle their
120351
- // own tags — don't hijack them here.
120352
- if (GENERIC_MDX_COMPONENT_EXCLUDED_TAGS.has(tag))
120440
+ // Non-inline PascalCase stays flow-level (handled by mdxishComponentBlocks)
120441
+ // and dedicated tokenizers (Table, HTMLBlock) own their tags. Inline
120442
+ // components (Anchor, Glossary) with expression attrs are promoted here —
120443
+ // the text tokenizer captures them as a single html node.
120444
+ if (isPascalCase(tag) && !INLINE_COMPONENT_TAGS.has(tag))
120353
120445
  return node;
120354
120446
  // Plain inline HTML (no expressions) stays as an html node so rehype-raw
120355
120447
  // parses it with parse5 as normal.
@@ -120373,14 +120465,15 @@ const promoteInlineHtml = (node, parseOpts, safeMode) => {
120373
120465
  * Runs after `mdxishComponentBlocks`, which skips paragraph-parented html
120374
120466
  * nodes and leaves them for this pass. Two producers put html nodes inside
120375
120467
  * paragraphs:
120376
- * 1. The `mdxComponent` text tokenizer, for lowercase tags with `{…}`
120377
- * attribute expressions (e.g. `<a href={url}>here</a>`).
120468
+ * 1. The `mdxComponent` text tokenizer, for lowercase tags and inline
120469
+ * PascalCase components (Anchor, Glossary) with `{}` attribute
120470
+ * expressions (e.g. `<a href={url}>here</a>`, `<Anchor href={url}>x</Anchor>`).
120378
120471
  * 2. CommonMark's built-in html-text tokenizer, for PascalCase components
120379
120472
  * (e.g. a single html node for self-closing `<C />`).
120380
120473
  *
120381
- * Eligibility mirrors `mdxishComponentBlocks`: lowercase tags only promote
120382
- * when they carry an expression attribute; plain inline HTML like
120383
- * `<a href="x">` stays as an html node for rehype-raw.
120474
+ * Eligibility mirrors `mdxishComponentBlocks`: a tag only promotes when it
120475
+ * carries an expression attribute and isn't a non-inline PascalCase component;
120476
+ * plain inline HTML like `<a href="x">` stays as an html node for rehype-raw.
120384
120477
  */
120385
120478
  const mdxishInlineMdxHtmlBlocks = (opts = {}) => tree => {
120386
120479
  const safeMode = !!opts.safeMode;