@qkix/better-blocks-react-renderer 0.18.0 → 0.18.2

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
@@ -9,10 +9,10 @@
9
9
  <a href="https://www.npmjs.com/package/@qkix/better-blocks-react-renderer">
10
10
  <img alt="npm downloads" src="https://img.shields.io/npm/dm/@qkix/better-blocks-react-renderer.svg" />
11
11
  </a>
12
- <a href="https://github.com/qkix/strapi-plugin-better-blocks/blob/main/LICENSE">
12
+ <a href="https://github.com/qkix/strapi-plugins/blob/main/LICENSE">
13
13
  <img alt="license" src="https://img.shields.io/npm/l/@qkix/better-blocks-react-renderer.svg" />
14
14
  </a>
15
- <a href="https://buymeacoffee.com/k11k">
15
+ <a href="https://buymeacoffee.com/qkix">
16
16
  <img alt="Buy Me a Coffee" src="https://img.shields.io/badge/Buy%20Me%20a%20Coffee-support-FFDD00?logo=buymeacoffee&logoColor=black" />
17
17
  </a>
18
18
  </p>
@@ -33,16 +33,17 @@
33
33
  6. [Supported Blocks](#supported-blocks)
34
34
  7. [Supported Modifiers](#supported-modifiers)
35
35
  8. [Custom Renderers](#custom-renderers)
36
- 9. [TypeScript](#typescript)
37
- 10. [Contributing](#contributing)
38
- 11. [Support this project](#support-this-project)
39
- 12. [License](#license)
36
+ 9. [Registered Block Types](#registered-block-types)
37
+ 10. [TypeScript](#typescript)
38
+ 11. [Contributing](#contributing)
39
+ 12. [Support this project](#support-this-project)
40
+ 13. [License](#license)
40
41
 
41
42
  ---
42
43
 
43
44
  ## Why?
44
45
 
45
- The official [`@strapi/blocks-react-renderer`](https://github.com/strapi/blocks-react-renderer) doesn't support the features that the [Better Blocks](https://github.com/qkix/strapi-plugin-better-blocks) plugin adds to the Strapi editor &mdash; color marks, text alignment, to-do lists, tables, media embeds, and more.
46
+ The official [`@strapi/blocks-react-renderer`](https://github.com/strapi/blocks-react-renderer) doesn't support the features that the [Better Blocks](https://github.com/qkix/strapi-plugins) plugin adds to the Strapi editor &mdash; color marks, text alignment, to-do lists, tables, media embeds, and more.
46
47
 
47
48
  This package is a **drop-in replacement** that renders all Better Blocks features out of the box &mdash; no configuration needed.
48
49
 
@@ -333,7 +334,7 @@ The markup is wrapped in a `<figure className="bb-embed align-{alignment}">` (al
333
334
 
334
335
  > **Trust boundary.** `embedHtml` is injected with `dangerouslySetInnerHTML`. The plugin sanitizes it at author time &mdash; the iframe is rebuilt from an attribute allowlist over an **https-only** `src`, with scripts, event handlers, inline styles and unknown attributes stripped, and `allow` filtered to a safe permission set &mdash; so treat your CMS content as trusted. If you accept `embed` blocks from untrusted authors, sanitize on the server before storing, or override the block (below) and render the parsed parts instead of the HTML.
335
336
 
336
- Consumers need to allow the embed hosts in their `frame-src` (and `img-src` for thumbnails) CSP directives &mdash; see the [plugin README](https://github.com/qkix/strapi-plugin-better-blocks#embed-json-shape-for-frontend-renderers) for the host list.
337
+ Consumers need to allow the embed hosts in their `frame-src` (and `img-src` for thumbnails) CSP directives &mdash; see the [plugin README](https://github.com/qkix/strapi-plugins#embed-json-shape-for-frontend-renderers) for the host list.
337
338
 
338
339
  Baseline appearance ships as inline styles plus one small stylesheet (injected only when a default embed renders, since inline styles can't reach into injected markup). Every element carries a stable `bb-embed*` class:
339
340
 
@@ -540,7 +541,7 @@ Cell `children` go through the same inline renderer as paragraphs, so marks (`bo
540
541
 
541
542
  These three blocks ship with GitHub-flavored defaults out of the box &mdash; no stylesheet to import. Each carries a stable `bb-*` class, and the styles are injected as a `<style>` tag only when the block actually appears in the content (and skipped entirely when you override the block). Everything is rethemable through CSS custom properties, so you can restyle without replacing any markup.
542
543
 
543
- The same classes and custom properties are used by the [Astro renderer](https://github.com/qkix/strapi-plugin-better-blocks/tree/main/packages/better-blocks-astro-renderer), so one shared theme covers both.
544
+ The same classes and custom properties are used by the [Astro renderer](https://github.com/qkix/strapi-plugins/tree/main/packages/better-blocks-astro-renderer), so one shared theme covers both.
544
545
 
545
546
  | Block | Default element | Custom properties |
546
547
  | ------- | ------------------------------- | ---------------------------------------------------------------------------------------- |
@@ -849,6 +850,42 @@ Override any text modifier with your own component:
849
850
  />
850
851
  ```
851
852
 
853
+ ## Registered Block Types
854
+
855
+ `blocks` overrides how a **known** block is drawn. `blockPlugins` adds a block
856
+ type this renderer has never heard of — one owned by another package, such as a
857
+ chart.
858
+
859
+ ```tsx
860
+ import { BlocksRenderer } from '@qkix/better-blocks-react-renderer';
861
+ import type { BlockPlugin } from '@qkix/better-blocks-react-renderer';
862
+
863
+ const chart: BlockPlugin = {
864
+ type: 'chart',
865
+ // 'void' (attributes only, the default), 'inline' (text), or 'blocks' (nested blocks).
866
+ content: 'void',
867
+ component: ({ node }) => <MyChart spec={node.spec} />,
868
+ };
869
+
870
+ <BlocksRenderer content={content} blockPlugins={[chart]} />;
871
+ ```
872
+
873
+ The component receives the whole `node` — this renderer does not know what
874
+ attributes the block has, which is the point — and, when the content model is
875
+ `inline` or `blocks`, its rendered `children`. A registered block works at any
876
+ depth, including inside a callout or a details.
877
+
878
+ A `BlockPlugin` is a core `BlockDefinition` plus `component`, so the same object
879
+ that teaches `validateDocument` and `migrateDocument` about the block also
880
+ teaches this renderer to draw it. See
881
+ [`@qkix/better-blocks-core`](../better-blocks-core#registering-a-block-type).
882
+
883
+ Passing plugins explicitly, rather than registering them into a global, is
884
+ deliberate: this renderer runs on servers handling concurrent requests, where
885
+ mutable module state leaks one page's registrations into another's.
886
+
887
+ A block type nobody registered renders nothing, exactly as before.
888
+
852
889
  ## TypeScript
853
890
 
854
891
  All types are exported:
@@ -902,12 +939,12 @@ import type {
902
939
  ## Contributing
903
940
 
904
941
  Contributions are welcome! This package lives in the
905
- [strapi-plugin-better-blocks](https://github.com/qkix/strapi-plugin-better-blocks)
942
+ [strapi-plugin-better-blocks](https://github.com/qkix/strapi-plugins)
906
943
  monorepo, next to the Strapi plugin and the Astro renderer. The easiest way to
907
944
  get started is with Docker:
908
945
 
909
946
  ```bash
910
- git clone https://github.com/qkix/strapi-plugin-better-blocks.git
947
+ git clone https://github.com/qkix/strapi-plugins.git
911
948
  cd strapi-plugin-better-blocks
912
949
 
913
950
  docker compose up --build
@@ -948,13 +985,13 @@ pnpm --filter @qkix/better-blocks-react-renderer test # just this one
948
985
 
949
986
  ## Community & Support
950
987
 
951
- - [GitHub Issues](https://github.com/qkix/strapi-plugin-better-blocks/issues) &mdash; Bug reports and feature requests
988
+ - [GitHub Issues](https://github.com/qkix/strapi-plugins/issues) &mdash; Bug reports and feature requests
952
989
 
953
990
  ## Support this project
954
991
 
955
992
  This package is built and maintained in my free time, and it's free for everyone. If it has saved you time on a project, you can help keep it caffeinated and actively developed:
956
993
 
957
- <a href="https://buymeacoffee.com/k11k">
994
+ <a href="https://buymeacoffee.com/qkix">
958
995
  <img alt="Buy Me a Coffee" src="https://img.shields.io/badge/Buy%20Me%20a%20Coffee-support-FFDD00?logo=buymeacoffee&logoColor=black&style=for-the-badge" />
959
996
  </a>
960
997
 
@@ -962,7 +999,7 @@ Every coffee goes toward fixing bugs, reviewing PRs, writing docs, and shipping
962
999
 
963
1000
  ## Related
964
1001
 
965
- - [@qkix/strapi-plugin-better-blocks](https://github.com/qkix/strapi-plugin-better-blocks) &mdash; Strapi plugin that extends the Blocks editor with colors, tables, to-do lists, media embeds, and more
1002
+ - [@qkix/strapi-plugin-better-blocks](https://github.com/qkix/strapi-plugins) &mdash; Strapi plugin that extends the Blocks editor with colors, tables, to-do lists, media embeds, and more
966
1003
  - [@strapi/blocks-react-renderer](https://github.com/strapi/blocks-react-renderer) &mdash; Official Strapi renderer (standard blocks only)
967
1004
 
968
1005
  ## License
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ReactNode, ComponentType, CSSProperties } from 'react';
2
- import { BlocksContent, TableCellAlign, CalloutVariant, ButtonAlignment, ButtonLink, ButtonFile, ButtonStyle, SocialPlatform, SocialEmbedOembed, SocialEmbedAlignment, AudioFile, AudioPlayer, AudioAlignment, EmbedProvider, AspectRatio, MediaAlignment, VideoProvider, VideoFile, VideoPlayer } from '@qkix/better-blocks-core';
3
- export { AspectRatio, AudioAlignment, AudioFile, AudioNode, AudioPlayer, AudioPreload, BlockNode, BlocksContent, ButtonAlignment, ButtonElement, ButtonFile, ButtonLink, ButtonStyle, CalloutNode, CalloutVariant, CodeNode, DetailsNode, DiagramNode, EmbedNode, EmbedProvider, HeadingNode, HorizontalLineNode, ImageNode, InlineNode, LinkNode, ListItemNode, ListNode, MathNode, MediaAlignment, MediaEmbedNode, ParagraphNode, QuoteNode, SocialEmbedAlignment, SocialEmbedNode, SocialEmbedOembed, SocialPlatform, TableCellAlign, TableCellNode, TableHeaderCellNode, TableNode, TableRowNode, TextAlign, TextNode, VideoFile, VideoNode, VideoPlayer, VideoProvider } from '@qkix/better-blocks-core';
2
+ import { BlockDefinition, CustomBlockNode, BlocksContent, ExtendedBlocksContent, TableCellAlign, CalloutVariant, ButtonAlignment, ButtonLink, ButtonFile, ButtonStyle, SocialPlatform, SocialEmbedOembed, SocialEmbedAlignment, AudioFile, AudioPlayer, AudioAlignment, EmbedProvider, AspectRatio, MediaAlignment, VideoProvider, VideoFile, VideoPlayer } from '@qkix/better-blocks-core';
3
+ export { AnyBlockNode, AspectRatio, AudioAlignment, AudioFile, AudioNode, AudioPlayer, AudioPreload, BlockDefinition, BlockNode, BlocksContent, ButtonAlignment, ButtonElement, ButtonFile, ButtonLink, ButtonStyle, CalloutNode, CalloutVariant, CodeNode, CustomBlockNode, DetailsNode, DiagramNode, EmbedNode, EmbedProvider, ExtendedBlocksContent, HeadingNode, HorizontalLineNode, ImageNode, InlineNode, LinkNode, ListItemNode, ListNode, MathNode, MediaAlignment, MediaEmbedNode, ParagraphNode, QuoteNode, SocialEmbedAlignment, SocialEmbedNode, SocialEmbedOembed, SocialPlatform, TableCellAlign, TableCellNode, TableHeaderCellNode, TableNode, TableRowNode, TextAlign, TextNode, VideoFile, VideoNode, VideoPlayer, VideoProvider } from '@qkix/better-blocks-core';
4
4
 
5
5
  type ModifierProps = {
6
6
  children: ReactNode;
@@ -166,10 +166,44 @@ type CustomModifiersConfig = Partial<{
166
166
  fontFamily: ComponentType<FontFamilyModifierProps>;
167
167
  fontSize: ComponentType<FontSizeModifierProps>;
168
168
  }>;
169
+ /**
170
+ * Props handed to a registered block's component.
171
+ *
172
+ * The whole node is passed rather than spread attributes, because this renderer
173
+ * does not know what attributes the block has — that is the point of it being
174
+ * registered elsewhere.
175
+ */
176
+ type CustomBlockProps = {
177
+ node: CustomBlockNode;
178
+ /**
179
+ * The rendered children, for a block whose content model is `inline` or
180
+ * `blocks`. Undefined for a `void` block, whose children are only the empty
181
+ * placeholder Slate requires.
182
+ */
183
+ children?: ReactNode;
184
+ };
185
+ /**
186
+ * A block type from another package, plus how React draws it.
187
+ *
188
+ * Extends the core {@link BlockDefinition} so one object serves validation,
189
+ * migration and rendering: a package publishes its definition once and adds
190
+ * `component` here.
191
+ */
192
+ type BlockPlugin = BlockDefinition & {
193
+ component: ComponentType<CustomBlockProps>;
194
+ };
169
195
  type BlocksRendererProps = {
170
- content: BlocksContent;
196
+ content: BlocksContent | ExtendedBlocksContent;
171
197
  blocks?: CustomBlocksConfig;
172
198
  modifiers?: CustomModifiersConfig;
199
+ /**
200
+ * Block types this renderer does not ship, supplied by another package.
201
+ * Passed explicitly rather than read from a global, so a server rendering
202
+ * concurrent requests cannot leak one page's registrations into another's.
203
+ *
204
+ * An unregistered block type renders nothing, exactly as before.
205
+ */
206
+ blockPlugins?: readonly BlockPlugin[];
173
207
  /**
174
208
  * Shiki theme for the default `code` block highlighting. Any bundled Shiki
175
209
  * theme name (e.g. `github-dark`, `github-light`, `dracula`, `nord`).
@@ -184,6 +218,6 @@ type BlocksRendererProps = {
184
218
  codeCopyButton?: boolean;
185
219
  };
186
220
 
187
- declare function BlocksRenderer({ content, blocks, modifiers, codeTheme, codeCopyButton, }: BlocksRendererProps): ReactNode;
221
+ declare function BlocksRenderer({ content, blocks, modifiers, blockPlugins, codeTheme, codeCopyButton, }: BlocksRendererProps): ReactNode;
188
222
 
189
- export { type BackgroundColorModifierProps, type BlockComponentProps, BlocksRenderer, type BlocksRendererProps, type ColorModifierProps, type CustomBlocksConfig, type CustomModifiersConfig, type FontFamilyModifierProps, type FontSizeModifierProps, type ModifierProps, type TableCellRenderProps };
223
+ export { type BackgroundColorModifierProps, type BlockComponentProps, type BlockPlugin, BlocksRenderer, type BlocksRendererProps, type ColorModifierProps, type CustomBlockProps, type CustomBlocksConfig, type CustomModifiersConfig, type FontFamilyModifierProps, type FontSizeModifierProps, type ModifierProps, type TableCellRenderProps };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ReactNode, ComponentType, CSSProperties } from 'react';
2
- import { BlocksContent, TableCellAlign, CalloutVariant, ButtonAlignment, ButtonLink, ButtonFile, ButtonStyle, SocialPlatform, SocialEmbedOembed, SocialEmbedAlignment, AudioFile, AudioPlayer, AudioAlignment, EmbedProvider, AspectRatio, MediaAlignment, VideoProvider, VideoFile, VideoPlayer } from '@qkix/better-blocks-core';
3
- export { AspectRatio, AudioAlignment, AudioFile, AudioNode, AudioPlayer, AudioPreload, BlockNode, BlocksContent, ButtonAlignment, ButtonElement, ButtonFile, ButtonLink, ButtonStyle, CalloutNode, CalloutVariant, CodeNode, DetailsNode, DiagramNode, EmbedNode, EmbedProvider, HeadingNode, HorizontalLineNode, ImageNode, InlineNode, LinkNode, ListItemNode, ListNode, MathNode, MediaAlignment, MediaEmbedNode, ParagraphNode, QuoteNode, SocialEmbedAlignment, SocialEmbedNode, SocialEmbedOembed, SocialPlatform, TableCellAlign, TableCellNode, TableHeaderCellNode, TableNode, TableRowNode, TextAlign, TextNode, VideoFile, VideoNode, VideoPlayer, VideoProvider } from '@qkix/better-blocks-core';
2
+ import { BlockDefinition, CustomBlockNode, BlocksContent, ExtendedBlocksContent, TableCellAlign, CalloutVariant, ButtonAlignment, ButtonLink, ButtonFile, ButtonStyle, SocialPlatform, SocialEmbedOembed, SocialEmbedAlignment, AudioFile, AudioPlayer, AudioAlignment, EmbedProvider, AspectRatio, MediaAlignment, VideoProvider, VideoFile, VideoPlayer } from '@qkix/better-blocks-core';
3
+ export { AnyBlockNode, AspectRatio, AudioAlignment, AudioFile, AudioNode, AudioPlayer, AudioPreload, BlockDefinition, BlockNode, BlocksContent, ButtonAlignment, ButtonElement, ButtonFile, ButtonLink, ButtonStyle, CalloutNode, CalloutVariant, CodeNode, CustomBlockNode, DetailsNode, DiagramNode, EmbedNode, EmbedProvider, ExtendedBlocksContent, HeadingNode, HorizontalLineNode, ImageNode, InlineNode, LinkNode, ListItemNode, ListNode, MathNode, MediaAlignment, MediaEmbedNode, ParagraphNode, QuoteNode, SocialEmbedAlignment, SocialEmbedNode, SocialEmbedOembed, SocialPlatform, TableCellAlign, TableCellNode, TableHeaderCellNode, TableNode, TableRowNode, TextAlign, TextNode, VideoFile, VideoNode, VideoPlayer, VideoProvider } from '@qkix/better-blocks-core';
4
4
 
5
5
  type ModifierProps = {
6
6
  children: ReactNode;
@@ -166,10 +166,44 @@ type CustomModifiersConfig = Partial<{
166
166
  fontFamily: ComponentType<FontFamilyModifierProps>;
167
167
  fontSize: ComponentType<FontSizeModifierProps>;
168
168
  }>;
169
+ /**
170
+ * Props handed to a registered block's component.
171
+ *
172
+ * The whole node is passed rather than spread attributes, because this renderer
173
+ * does not know what attributes the block has — that is the point of it being
174
+ * registered elsewhere.
175
+ */
176
+ type CustomBlockProps = {
177
+ node: CustomBlockNode;
178
+ /**
179
+ * The rendered children, for a block whose content model is `inline` or
180
+ * `blocks`. Undefined for a `void` block, whose children are only the empty
181
+ * placeholder Slate requires.
182
+ */
183
+ children?: ReactNode;
184
+ };
185
+ /**
186
+ * A block type from another package, plus how React draws it.
187
+ *
188
+ * Extends the core {@link BlockDefinition} so one object serves validation,
189
+ * migration and rendering: a package publishes its definition once and adds
190
+ * `component` here.
191
+ */
192
+ type BlockPlugin = BlockDefinition & {
193
+ component: ComponentType<CustomBlockProps>;
194
+ };
169
195
  type BlocksRendererProps = {
170
- content: BlocksContent;
196
+ content: BlocksContent | ExtendedBlocksContent;
171
197
  blocks?: CustomBlocksConfig;
172
198
  modifiers?: CustomModifiersConfig;
199
+ /**
200
+ * Block types this renderer does not ship, supplied by another package.
201
+ * Passed explicitly rather than read from a global, so a server rendering
202
+ * concurrent requests cannot leak one page's registrations into another's.
203
+ *
204
+ * An unregistered block type renders nothing, exactly as before.
205
+ */
206
+ blockPlugins?: readonly BlockPlugin[];
173
207
  /**
174
208
  * Shiki theme for the default `code` block highlighting. Any bundled Shiki
175
209
  * theme name (e.g. `github-dark`, `github-light`, `dracula`, `nord`).
@@ -184,6 +218,6 @@ type BlocksRendererProps = {
184
218
  codeCopyButton?: boolean;
185
219
  };
186
220
 
187
- declare function BlocksRenderer({ content, blocks, modifiers, codeTheme, codeCopyButton, }: BlocksRendererProps): ReactNode;
221
+ declare function BlocksRenderer({ content, blocks, modifiers, blockPlugins, codeTheme, codeCopyButton, }: BlocksRendererProps): ReactNode;
188
222
 
189
- export { type BackgroundColorModifierProps, type BlockComponentProps, BlocksRenderer, type BlocksRendererProps, type ColorModifierProps, type CustomBlocksConfig, type CustomModifiersConfig, type FontFamilyModifierProps, type FontSizeModifierProps, type ModifierProps, type TableCellRenderProps };
223
+ export { type BackgroundColorModifierProps, type BlockComponentProps, type BlockPlugin, BlocksRenderer, type BlocksRendererProps, type ColorModifierProps, type CustomBlockProps, type CustomBlocksConfig, type CustomModifiersConfig, type FontFamilyModifierProps, type FontSizeModifierProps, type ModifierProps, type TableCellRenderProps };
package/dist/index.js CHANGED
@@ -120,17 +120,17 @@ function CodeBlock({
120
120
  }) {
121
121
  const [html, setHtml] = (0, import_react.useState)(null);
122
122
  (0, import_react.useEffect)(() => {
123
- let cancelled = false;
123
+ let canceled = false;
124
124
  setHtml(null);
125
125
  if (!plainText) return;
126
126
  const lang = (0, import_better_blocks_core2.normalizeCodeLang)(language);
127
127
  loadHighlighter(theme, lang).then((highlighter) => highlighter.codeToHtml(plainText, { lang, theme })).then((highlighted) => {
128
- if (!cancelled) setHtml(highlighted);
128
+ if (!canceled) setHtml(highlighted);
129
129
  }).catch(() => {
130
- if (!cancelled) setHtml(null);
130
+ if (!canceled) setHtml(null);
131
131
  });
132
132
  return () => {
133
- cancelled = true;
133
+ canceled = true;
134
134
  };
135
135
  }, [plainText, language, theme]);
136
136
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "bb-code", children: [
@@ -157,16 +157,16 @@ var renderCount = 0;
157
157
  function MermaidDiagram({ value }) {
158
158
  const [svg, setSvg] = (0, import_react2.useState)(null);
159
159
  (0, import_react2.useEffect)(() => {
160
- let cancelled = false;
160
+ let canceled = false;
161
161
  setSvg(null);
162
162
  if (!value) return;
163
163
  loadMermaid().then((mermaid) => mermaid.render(`bb-mermaid-${renderCount++}`, value)).then(({ svg: svg2 }) => {
164
- if (!cancelled) setSvg(svg2);
164
+ if (!canceled) setSvg(svg2);
165
165
  }).catch(() => {
166
- if (!cancelled) setSvg(null);
166
+ if (!canceled) setSvg(null);
167
167
  });
168
168
  return () => {
169
- cancelled = true;
169
+ canceled = true;
170
170
  };
171
171
  }, [value]);
172
172
  if (svg) {
@@ -348,14 +348,14 @@ function SocialEmbed({
348
348
  });
349
349
  const widget = WIDGET_SCRIPTS[platform];
350
350
  if (!widget) return;
351
- let cancelled = false;
351
+ let canceled = false;
352
352
  void loadWidgetScript(widget.src, platform).then(() => {
353
353
  const container = containerRef.current;
354
- if (cancelled || !container) return;
354
+ if (canceled || !container) return;
355
355
  if (!widget.process(container)) reinjectWidgetScript(widget.src, platform);
356
356
  });
357
357
  return () => {
358
- cancelled = true;
358
+ canceled = true;
359
359
  };
360
360
  }, [platform, html]);
361
361
  const providerName = (_a = oembed == null ? void 0 : oembed.providerName) != null ? _a : PLATFORM_LABELS[platform];
@@ -415,12 +415,12 @@ function Video({
415
415
  setMuxDefined(true);
416
416
  return;
417
417
  }
418
- let cancelled = false;
418
+ let canceled = false;
419
419
  void window.customElements.whenDefined("mux-player").then(() => {
420
- if (!cancelled) setMuxDefined(true);
420
+ if (!canceled) setMuxDefined(true);
421
421
  });
422
422
  return () => {
423
- cancelled = true;
423
+ canceled = true;
424
424
  };
425
425
  }, [wantsMux]);
426
426
  const isHls = HLS_MANIFEST.test(url);
@@ -642,44 +642,63 @@ function renderTable(block, key, blocks, modifiers) {
642
642
  function isHeaderRow(row) {
643
643
  return !!row && row.children.length > 0 && row.children.every((c) => c.type === "table-header-cell");
644
644
  }
645
+ function renderCustomBlock(plugin, block, key, blocks, modifiers, options) {
646
+ const { component: Component, content = "void" } = plugin;
647
+ const children = Array.isArray(block.children) ? block.children : [];
648
+ if (content === "blocks") {
649
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Component, { node: block, children: children.map(
650
+ (child, index) => renderBlock(child, index, blocks, modifiers, options)
651
+ ) }, key);
652
+ }
653
+ if (content === "inline") {
654
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Component, { node: block, children: renderInlineContent(children, blocks, modifiers) }, key);
655
+ }
656
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(Component, { node: block }, key);
657
+ }
645
658
  function renderBlock(block, key, blocks, modifiers, options) {
646
- switch (block.type) {
659
+ var _a;
660
+ const plugin = (_a = options == null ? void 0 : options.plugins) == null ? void 0 : _a.get(block.type);
661
+ if (plugin) {
662
+ return renderCustomBlock(plugin, block, key, blocks, modifiers, options);
663
+ }
664
+ const builtIn = block;
665
+ switch (builtIn.type) {
647
666
  case "paragraph":
648
- return renderParagraph(block, key, blocks, modifiers);
667
+ return renderParagraph(builtIn, key, blocks, modifiers);
649
668
  case "heading":
650
- return renderHeading(block, key, blocks, modifiers);
669
+ return renderHeading(builtIn, key, blocks, modifiers);
651
670
  case "list":
652
- return renderList(block, key, blocks, modifiers);
671
+ return renderList(builtIn, key, blocks, modifiers);
653
672
  case "quote":
654
- return renderQuote(block, key, blocks, modifiers);
673
+ return renderQuote(builtIn, key, blocks, modifiers);
655
674
  case "code":
656
- return renderCode(block, key, blocks, options);
675
+ return renderCode(builtIn, key, blocks, options);
657
676
  case "image":
658
- return renderImage(block, key, blocks);
677
+ return renderImage(builtIn, key, blocks);
659
678
  case "horizontal-line":
660
- return renderHorizontalLine(block, key, blocks);
679
+ return renderHorizontalLine(builtIn, key, blocks);
661
680
  case "table":
662
- return renderTable(block, key, blocks, modifiers);
681
+ return renderTable(builtIn, key, blocks, modifiers);
663
682
  case "media-embed":
664
- return renderMediaEmbed(block, key, blocks);
683
+ return renderMediaEmbed(builtIn, key, blocks);
665
684
  case "embed":
666
- return renderEmbed(block, key, blocks);
685
+ return renderEmbed(builtIn, key, blocks);
667
686
  case "video":
668
- return renderVideo(block, key, blocks);
687
+ return renderVideo(builtIn, key, blocks);
669
688
  case "math":
670
- return renderMath(block, key, blocks);
689
+ return renderMath(builtIn, key, blocks);
671
690
  case "diagram":
672
- return renderDiagram(block, key, blocks);
691
+ return renderDiagram(builtIn, key, blocks);
673
692
  case "callout":
674
- return renderCallout(block, key, blocks, modifiers, options);
693
+ return renderCallout(builtIn, key, blocks, modifiers, options);
675
694
  case "details":
676
- return renderDetails(block, key, blocks, modifiers, options);
695
+ return renderDetails(builtIn, key, blocks, modifiers, options);
677
696
  case "button":
678
- return renderButton(block, key, blocks);
697
+ return renderButton(builtIn, key, blocks);
679
698
  case "social-embed":
680
- return renderSocialEmbed(block, key, blocks);
699
+ return renderSocialEmbed(builtIn, key, blocks);
681
700
  case "audio":
682
- return renderAudio(block, key, blocks);
701
+ return renderAudio(builtIn, key, blocks);
683
702
  default:
684
703
  return null;
685
704
  }
@@ -1260,6 +1279,7 @@ function BlocksRenderer({
1260
1279
  content,
1261
1280
  blocks,
1262
1281
  modifiers,
1282
+ blockPlugins,
1263
1283
  codeTheme = DEFAULT_CODE_THEME,
1264
1284
  codeCopyButton = false
1265
1285
  }) {
@@ -1271,7 +1291,11 @@ function BlocksRenderer({
1271
1291
  const needsTableCss = !(blocks == null ? void 0 : blocks.table) && contentHasBlock(content, "table");
1272
1292
  const needsQuoteCss = !(blocks == null ? void 0 : blocks.quote) && contentHasBlock(content, "quote");
1273
1293
  const needsCodeCss = !(blocks == null ? void 0 : blocks.code) && contentHasBlock(content, "code");
1274
- const options = { codeTheme, codeCopyButton };
1294
+ const options = {
1295
+ codeTheme,
1296
+ codeCopyButton,
1297
+ plugins: (blockPlugins == null ? void 0 : blockPlugins.length) ? new Map(blockPlugins.map((plugin) => [plugin.type, plugin])) : void 0
1298
+ };
1275
1299
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
1276
1300
  needsButtonCss && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("style", { children: BUTTON_HOVER_CSS }),
1277
1301
  needsEmbedCss && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("style", { children: EMBED_FRAME_CSS }),