@readme/markdown 14.10.2 → 14.11.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.
@@ -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;
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ declare const utils: {
7
7
  calloutIcons: {};
8
8
  };
9
9
  export { compile, exports, FLOW_TYPES, hast, INLINE_ONLY_PARENT_TYPES, run, mdast, mdastV6, mdx, mdxish, mdxishAstProcessor, mdxishMdastToMd, mdxishTags, extractToc, migrate, mix, plain, renderMdxish, remarkPlugins, stripComments, tags, } from './lib';
10
+ export type { MdxishOpts, RenderMdxishOpts, RunOpts } from './lib';
10
11
  export { default as Owlmoji } from './lib/owlmoji';
11
12
  export { Components, utils };
12
13
  export { tailwindCompiler } from './utils/tailwind-compiler';
@@ -16,6 +16,7 @@ export { default as plain } from './plain';
16
16
  export { default as renderMdxish } from './renderMdxish';
17
17
  export type { RenderMdxishOpts } from './renderMdxish';
18
18
  export { default as run } from './run';
19
+ export type { RunOpts } from './run';
19
20
  export { default as tags } from './tags';
20
21
  export { default as mdxishTags } from './mdxishTags';
21
22
  export { default as stripComments } from './stripComments';
@@ -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.
@@ -0,0 +1 @@
1
+ export { mdxExpressionLenient } from './syntax';
@@ -0,0 +1,2 @@
1
+ import type { Extension } from 'micromark-util-types';
2
+ export declare function mdxExpressionLenient(): Extension;