@qkix/strapi-plugin-better-blocks 0.23.0 → 0.23.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.
@@ -0,0 +1,87 @@
1
+ /**
2
+ * How another Strapi plugin adds a block type to the Better Blocks editor.
3
+ *
4
+ * Unlike the renderers, which take their registrations as a prop, the editor
5
+ * keeps a module-level registry. That is not an oversight: the Strapi admin is
6
+ * a single browser application whose plugins register once at boot, exactly
7
+ * like `app.customFields.register`. There is no second document being edited in
8
+ * another request to leak into.
9
+ *
10
+ * Registering is the *only* supported way in. Adding a block by editing this
11
+ * package is how the editor, both renderers and the validator ended up with
12
+ * four separate hardcoded lists of block types in the first place.
13
+ */
14
+ /// <reference types="react" />
15
+ import type { BlockDefinition } from '@qkix/better-blocks-core';
16
+ import type { Editor } from 'slate';
17
+ import type { MessageDescriptor } from 'react-intl';
18
+ import type { RenderElementProps } from 'slate-react';
19
+ import type { CSSProperties } from 'styled-components';
20
+ /**
21
+ * A block type contributed by another package.
22
+ *
23
+ * Extends the core {@link BlockDefinition}, so a package writes `type` and
24
+ * `content` once and the same object teaches the validator, the migrator and
25
+ * both renderers about the block.
26
+ */
27
+ export type EditorBlockDefinition = BlockDefinition & {
28
+ /** Draws the block inside the editor. Slate calls this with its element props. */
29
+ renderElement: (props: RenderElementProps) => React.JSX.Element;
30
+ /**
31
+ * Icon for the Insert menu and the slash command. Required to appear in
32
+ * either — a block with no icon is still rendered, just not offered.
33
+ */
34
+ icon?: React.ComponentType;
35
+ /** Menu label. A plain string is fine; a descriptor gets translated. */
36
+ label?: MessageDescriptor | string;
37
+ /**
38
+ * Inserts an empty block of this type at the selection. Required for the
39
+ * block to appear in the Insert menu or the slash command, since without it
40
+ * there is nothing for those to do.
41
+ */
42
+ insert?: (editor: Editor) => void;
43
+ /** Text that converts into this block as you type, e.g. '```mermaid'. */
44
+ snippets?: string[];
45
+ /**
46
+ * Anything `isVoid` cannot express — custom normalization, paste handling,
47
+ * key bindings. Applied when the editor is created.
48
+ */
49
+ withEditor?: (editor: Editor) => Editor;
50
+ /** Nudges the drag handle down, for blocks whose first line is not text. */
51
+ dragHandleTopMargin?: CSSProperties['marginTop'];
52
+ };
53
+ /**
54
+ * Adds a block type to the editor. Call from a Strapi plugin's `register()`.
55
+ *
56
+ * Throws on the two mistakes that would otherwise surface much later as a block
57
+ * that mysteriously does not appear: shadowing a built-in type, and registering
58
+ * the same type twice.
59
+ */
60
+ export declare function registerBlock(definition: EditorBlockDefinition): void;
61
+ /** Every registered block, in registration order. */
62
+ export declare function getRegisteredBlocks(): EditorBlockDefinition[];
63
+ /**
64
+ * Whether the block can be offered in the Insert menu and the slash command.
65
+ *
66
+ * All three are needed: something to draw, something to name it, and something
67
+ * to do when it is picked. A block missing any of them still renders in a
68
+ * document, it just is not offered as a thing to add.
69
+ */
70
+ export declare function isOfferedInMenus(definition: EditorBlockDefinition): boolean;
71
+ /**
72
+ * The block's menu label as react-intl wants it. A plain string becomes the
73
+ * default message under a generated id, so it shows as written until someone
74
+ * ships a translation for it.
75
+ */
76
+ export declare function blockLabelDescriptor(definition: EditorBlockDefinition): MessageDescriptor;
77
+ /** Empties the registry. Exists for tests; the admin never needs it. */
78
+ export declare function clearRegisteredBlocks(): void;
79
+ /**
80
+ * Teaches Slate about the registered blocks.
81
+ *
82
+ * Void-ness is derived from the declared content model rather than asked for
83
+ * separately: a block that renders from its own attributes is exactly a Slate
84
+ * void, and getting that wrong is the difference between a working block and an
85
+ * uneditable one.
86
+ */
87
+ export declare function withRegisteredBlocks(editor: Editor): Editor;
@@ -2,7 +2,7 @@ import { Editor } from 'slate';
2
2
  import { type BlocksStore } from '../BlocksEditor';
3
3
  import { createTableCell, createTableRow, insertTable } from './tableOperations';
4
4
  /**
5
- * Protects the table's shape from Slate's generic editing behaviours (deleting
5
+ * Protects the table's shape from Slate's generic editing behaviors (deleting
6
6
  * across cells, normalizing rows away, converting a cell to a paragraph, …).
7
7
  *
8
8
  * Structural edits are performed by tableOperations, which lifts the guard via
@@ -3,7 +3,7 @@ import { type AspectRatio, type EmbedProvider } from '../utils/types';
3
3
  export declare const EMBED_FRAME_HOSTS: string[];
4
4
  /** Human-readable provider name for a URL, e.g. "YouTube". */
5
5
  export declare const getProviderLabel: (provider?: EmbedProvider) => string;
6
- /** Detected provider for a share URL, or `generic` when unrecognised. */
6
+ /** Detected provider for a share URL, or `generic` when unrecognized. */
7
7
  export declare const detectProvider: (url: string) => EmbedProvider;
8
8
  /** Iframe `src` derived from a share URL, or null when not derivable. */
9
9
  export declare const getEmbedSrc: (url: string) => string | null;
@@ -32,7 +32,7 @@ export interface IframeError {
32
32
  */
33
33
  export declare const sanitizeIframe: (input: string) => SanitizedIframe | IframeError;
34
34
  /**
35
- * Build the iframe markup for a recognised share URL. Returns null when the URL
35
+ * Build the iframe markup for a recognized share URL. Returns null when the URL
36
36
  * isn't one of the known providers — the author then has to use embed-code mode.
37
37
  */
38
38
  export declare const buildUrlEmbed: (url: string, title?: string) => SanitizedIframe | null;
@@ -85,6 +85,6 @@ export declare const cellsInRange: (grid: TableGrid, range: GridRange) => GridCe
85
85
  * linear, so everything between r2c1 and r3c2 includes the whole tail of row 2
86
86
  * — taking the bounding box of every touched cell would turn a 2x2 pick into a
87
87
  * full-width 2x5 one. Treating anchor and focus as opposite corners gives the
88
- * 2D behaviour authors expect from a table.
88
+ * 2D behavior authors expect from a table.
89
89
  */
90
90
  export declare const getSelectedRange: (editor: Editor, grid: TableGrid) => GridRange | null;
@@ -29,10 +29,18 @@ type NonSelectorBlockKey = 'list-item' | 'link' | 'horizontal-line' | 'table' |
29
29
  declare const selectorBlockKeys: readonly ["paragraph", "heading-one", "heading-two", "heading-three", "heading-four", "heading-five", "heading-six", "list-ordered", "list-unordered", "list-todo", "image", "audio", "video", "embed", "quote", "code", "math", "diagram", "callout", "details", "button", "social-embed"];
30
30
  type SelectorBlockKey = (typeof selectorBlockKeys)[number];
31
31
  declare const isSelectorBlockKey: (key: unknown) => key is "image" | "audio" | "math" | "diagram" | "callout" | "details" | "button" | "social-embed" | "embed" | "video" | "code" | "paragraph" | "heading-one" | "heading-two" | "heading-three" | "heading-four" | "heading-five" | "heading-six" | "list-ordered" | "list-unordered" | "list-todo" | "quote";
32
+ /**
33
+ * The built-in keys are spelled out so `blocks.paragraph` stays typed, while
34
+ * the index signature leaves room for the types other packages register — see
35
+ * `registerBlock`. Without it, every consumer that walks the store would have
36
+ * to widen at the call site.
37
+ */
32
38
  type BlocksStore = {
33
39
  [K in SelectorBlockKey]: SelectorBlock;
34
40
  } & {
35
41
  [K in NonSelectorBlockKey]: NonSelectorBlock;
42
+ } & {
43
+ [key: string]: SelectorBlock | NonSelectorBlock;
36
44
  };
37
45
  interface BlocksEditorContextValue {
38
46
  blocks: BlocksStore;
@@ -20,7 +20,7 @@
20
20
  * scroll (defeating the purpose on macOS). We therefore do NOT set
21
21
  * `scrollbar-color`; Firefox falls back to its own always-visible bar.
22
22
  * - The track gets a real background so the channel stays visible even when the
23
- * content fits and the thumb spans the full height; the colours are
23
+ * content fits and the thumb spans the full height; the colors are
24
24
  * mid-range neutrals that contrast with the menu background (`neutral0`) in
25
25
  * both the light and dark themes.
26
26
  * - `!important` / `display: block` are required to beat the design-system
@@ -85,7 +85,7 @@ export interface ImageElement extends CustomElement {
85
85
  }
86
86
  export type AudioAlignment = 'left' | 'center' | 'right' | 'none';
87
87
  export type AudioPreload = 'none' | 'metadata' | 'auto';
88
- /** Player behaviour flags, mirrored 1:1 onto the HTML5 `<audio>` element. */
88
+ /** Player behavior flags, mirrored 1:1 onto the HTML5 `<audio>` element. */
89
89
  export interface AudioPlayerSettings {
90
90
  autoplay: boolean;
91
91
  loop: boolean;
@@ -95,7 +95,7 @@ export interface AudioPlayerSettings {
95
95
  /**
96
96
  * Audio block. A void block referencing a Strapi Media Library asset (or an
97
97
  * external URL). Stores the file metadata plus a display title, caption, player
98
- * behaviour flags and alignment. The frontend renderer turns this into a native
98
+ * behavior flags and alignment. The frontend renderer turns this into a native
99
99
  * HTML5 `<audio>` player — see issue #43 for the renderer contract.
100
100
  */
101
101
  export interface AudioElement extends CustomElement {
@@ -271,7 +271,7 @@ export declare const isButtonNode: (element: CustomElement) => element is Button
271
271
  export type SocialPlatform = 'twitter' | 'instagram' | 'facebook' | 'tiktok' | 'linkedin' | 'pinterest';
272
272
  export type SocialAlignment = 'left' | 'center' | 'right';
273
273
  /**
274
- * Normalised oEmbed payload fetched from the platform via the plugin's server
274
+ * Normalized oEmbed payload fetched from the platform via the plugin's server
275
275
  * oEmbed proxy (`GET /better-blocks/oembed`). Stored on the node so the frontend
276
276
  * renderer can render the embed without itself calling the platform at runtime.
277
277
  */
@@ -353,7 +353,7 @@ export interface EmbedElement extends CustomElement {
353
353
  export declare const isEmbedNode: (element: CustomElement) => element is EmbedElement;
354
354
  /** Video hosting providers the video block understands. */
355
355
  export type VideoProvider = 'local' | 'mux' | 'api-video' | 'cloudinary' | 'custom';
356
- /** Player behaviour flags, mirrored onto the HTML5 `<video>` element. */
356
+ /** Player behavior flags, mirrored onto the HTML5 `<video>` element. */
357
357
  export interface VideoPlayerSettings {
358
358
  autoplay: boolean;
359
359
  loop: boolean;
@@ -38,7 +38,7 @@ type UseDragAndDropReturn<E extends Element = HTMLElement> = [
38
38
  ];
39
39
  /**
40
40
  * A utility hook abstracting the general drag and drop hooks from react-dnd.
41
- * Centralising the same behaviours and by default offering keyboard support.
41
+ * Centralising the same behaviors and by default offering keyboard support.
42
42
  */
43
43
  declare const useDragAndDrop: <TIndex extends number | number[], TItem extends {
44
44
  [key: string]: unknown;
@@ -8,3 +8,10 @@ declare const _default: {
8
8
  }[]>;
9
9
  };
10
10
  export default _default;
11
+ /**
12
+ * The extension surface. Another Strapi plugin adds a block type by calling
13
+ * `registerBlock` from its own `register()`, which runs before any editor is
14
+ * mounted.
15
+ */
16
+ export { registerBlock, getRegisteredBlocks, clearRegisteredBlocks, } from './blockRegistry';
17
+ export type { EditorBlockDefinition } from './blockRegistry';
@@ -1 +1 @@
1
- "use strict";const g=({strapi:t})=>{},k=({strapi:t})=>{},y=({strapi:t})=>{t.customFields.register({name:"better-blocks",plugin:"better-blocks",type:"json",inputSize:{default:12,isResizable:!1}})},w={default:{details:{defaultSummary:"Click to expand",style:"github"},button:{defaultStyle:{backgroundColor:"#4945ff",textColor:"#ffffff",borderRadius:"4px",padding:"12px 24px",fontSize:"16px",fontWeight:"600"},presets:{primary:{backgroundColor:"#4945ff",textColor:"#ffffff",border:"none"},secondary:{backgroundColor:"#dcdce4",textColor:"#32324d",border:"none"},outline:{backgroundColor:"transparent",textColor:"#4945ff",border:"2px solid #4945ff"},filled:{backgroundColor:"#32324d",textColor:"#ffffff",border:"none"}}},social:{enabled:!0,platforms:["twitter","instagram","facebook","tiktok","linkedin","pinterest"],cache:!0,cacheTTL:86400,instagram:{accessToken:void 0},facebook:{accessToken:void 0}}},validator(t={}){const o=t?.details?.style;if(o&&o!=="github"&&o!=="custom")throw new Error(`[better-blocks] config.details.style must be "github" or "custom" (received "${o}")`)}},C={},v=({strapi:t})=>({index(o){o.body=t.plugin("better-blocks").service("service").getWelcomeMessage()},getConfig(o){o.body={details:t.plugin("better-blocks").config("details",{defaultSummary:"Click to expand",style:"github"}),button:t.plugin("better-blocks").config("button",{defaultStyle:{backgroundColor:"#4945ff",textColor:"#ffffff",borderRadius:"4px",padding:"12px 24px",fontSize:"16px",fontWeight:"600"},presets:{primary:{backgroundColor:"#4945ff",textColor:"#ffffff",border:"none"},secondary:{backgroundColor:"#dcdce4",textColor:"#32324d",border:"none"},outline:{backgroundColor:"transparent",textColor:"#4945ff",border:"2px solid #4945ff"},filled:{backgroundColor:"#32324d",textColor:"#ffffff",border:"none"}}}),social:(()=>{const e=t.plugin("better-blocks").config("social",{enabled:!0,platforms:["twitter","instagram","facebook","tiktok","linkedin","pinterest"]});return{enabled:e.enabled!==!1,platforms:e.platforms??[],instagramConfigured:!!e.instagram?.accessToken,facebookConfigured:!!e.facebook?.accessToken}})()}}}),x=["twitter","instagram","facebook","tiktok","linkedin","pinterest"],$=({strapi:t})=>({async fetch(o){const{url:e,platform:i}=o.query;if(!e||typeof e!="string")return o.badRequest('Missing required "url" query parameter.');const r=i&&x.includes(i)?i:void 0;try{o.body=await t.plugin("better-blocks").service("oembed").fetch(e,r)}catch(n){const a=n instanceof Error?n.message:"oEmbed request failed.";return o.badRequest(a)}}}),T={controller:v,oembed:$},E={},R={},L=[{method:"GET",path:"/",handler:"controller.index",config:{policies:[]}}],_=[{method:"GET",path:"/config",handler:"controller.getConfig",config:{policies:[]}},{method:"GET",path:"/oembed",handler:"oembed.fetch",config:{policies:["admin::isAuthenticatedAdmin"]}}],S={"content-api":{type:"content-api",routes:L},admin:{type:"admin",routes:_}},U=({strapi:t})=>({getWelcomeMessage(){return"Welcome to Strapi 🚀"}}),I=[{platform:"twitter",re:/(?:twitter\.com|x\.com)\/[^/]+\/status(?:es)?\/\d+/i},{platform:"instagram",re:/instagram\.com\/(?:p|reel|tv)\//i},{platform:"tiktok",re:/tiktok\.com\/@[^/]+\/video\/\d+/i},{platform:"facebook",re:/facebook\.com\//i},{platform:"linkedin",re:/linkedin\.com\//i},{platform:"pinterest",re:/(?:pinterest\.[a-z.]+|pin\.it)\//i}],b=t=>{const o=I.find(({re:e})=>e.test(t));return o?o.platform:null},A=(t,o,e)=>{const i=encodeURIComponent(o);switch(t){case"twitter":return`https://publish.twitter.com/oembed?url=${i}&omit_script=true&dnt=true`;case"tiktok":return`https://www.tiktok.com/oembed?url=${i}`;case"pinterest":return`https://www.pinterest.com/oembed.json?url=${i}`;case"instagram":{const r=e.instagram?.accessToken;return r?`https://graph.facebook.com/v17.0/instagram_oembed?url=${i}&access_token=${encodeURIComponent(r)}&omitscript=true`:null}case"facebook":{const r=e.facebook?.accessToken;return r?`https://graph.facebook.com/v17.0/oembed_post?url=${i}&access_token=${encodeURIComponent(r)}&omitscript=true`:null}case"linkedin":return null;default:return null}},P=async t=>{if(!/^https?:\/\/pin\.it\//i.test(t))return t;try{const e=(await fetch(t,{redirect:"follow"})).url||t;return/pinterest\.[a-z.]+\/pin\//i.test(e)?e:t}catch{return t}},q=t=>{const o=t.match(/(?:urn:li:)?(ugcPost|activity|share)[:-](\d{10,})/i);if(!o)return null;const i=`urn:li:${o[1].toLowerCase()==="ugcpost"?"ugcPost":o[1].toLowerCase()}:${o[2]}`,r=`https://www.linkedin.com/embed/feed/update/${encodeURIComponent(i)}`;return{platform:"linkedin",url:t,html:`<iframe src="${r}" height="600" width="100%" frameborder="0" allowfullscreen title="Embedded LinkedIn post" loading="lazy"></iframe>`,providerName:"LinkedIn",width:504,height:600}},z=t=>t.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi,"").trim(),M=(t,o,e)=>({platform:t,url:o,html:typeof e.html=="string"&&z(e.html)||null,title:typeof e.title=="string"?e.title:void 0,author:typeof e.author_name=="string"?e.author_name:void 0,authorUrl:typeof e.author_url=="string"?e.author_url:void 0,thumbnailUrl:typeof e.thumbnail_url=="string"?e.thumbnail_url:void 0,providerName:typeof e.provider_name=="string"?e.provider_name:void 0,width:typeof e.width=="number"?e.width:void 0,height:typeof e.height=="number"?e.height:void 0}),h=new Map,W=({strapi:t})=>{const o=()=>t.plugin("better-blocks").config("social",{});return{detectPlatform:b,async fetch(e,i){const r=o();if(r.enabled===!1)throw new Error("Social embeds are disabled in plugin config.");const n=i??b(e);if(!n)throw new Error(`Unsupported or unrecognised social URL: ${e}`);const a=r.platforms;if(Array.isArray(a)&&!a.includes(n))throw new Error(`Platform "${n}" is not enabled in plugin config.`);const f=r.cache!==!1,u=(r.cacheTTL??86400)*1e3,p=`${n}:${e}`;if(f){const c=h.get(p);if(c&&c.expires>Date.now())return c.data}let s=null;if(n==="linkedin"){if(s=q(e),!s)throw new Error("Could not derive a LinkedIn embed from this URL. Paste the post embed code instead.")}else{const c=await P(e),m=A(n,c,r);if(!m)throw new Error(`oEmbed for "${n}" requires an access token. Configure config.social.${n}.accessToken, or paste the embed code instead.`);const l=await fetch(m,{headers:{Accept:"application/json"}});if(!l.ok)throw new Error(`oEmbed request to ${n} failed (HTTP ${l.status}).`);const d=await l.json();if(typeof d.error=="string")throw new Error(`${n} could not embed this URL: ${d.error}`);if(s=M(n,c,d),!s.html)throw new Error(`${n} returned no embed markup for this URL. Check the post is public, or paste the embed code instead.`)}return f&&u>0&&h.set(p,{expires:Date.now()+u,data:s}),s}}},j={service:U,oembed:W},D={register:y,bootstrap:g,destroy:k,config:w,controllers:T,routes:S,services:j,contentTypes:C,policies:R,middlewares:E};module.exports=D;
1
+ "use strict";const k=({strapi:o})=>{},w=({strapi:o})=>{},y=({strapi:o})=>{o.customFields.register({name:"better-blocks",plugin:"better-blocks",type:"json",inputSize:{default:12,isResizable:!1}})},C={default:{details:{defaultSummary:"Click to expand",style:"github"},button:{defaultStyle:{backgroundColor:"#4945ff",textColor:"#ffffff",borderRadius:"4px",padding:"12px 24px",fontSize:"16px",fontWeight:"600"},presets:{primary:{backgroundColor:"#4945ff",textColor:"#ffffff",border:"none"},secondary:{backgroundColor:"#dcdce4",textColor:"#32324d",border:"none"},outline:{backgroundColor:"transparent",textColor:"#4945ff",border:"2px solid #4945ff"},filled:{backgroundColor:"#32324d",textColor:"#ffffff",border:"none"}}},social:{enabled:!0,platforms:["twitter","instagram","facebook","tiktok","linkedin","pinterest"],cache:!0,cacheTTL:86400,cacheMaxEntries:500,instagram:{accessToken:void 0},facebook:{accessToken:void 0}}},validator(o={}){const t=o?.details?.style;if(t&&t!=="github"&&t!=="custom")throw new Error(`[better-blocks] config.details.style must be "github" or "custom" (received "${t}")`)}},v={},x=({strapi:o})=>({index(t){t.body=o.plugin("better-blocks").service("service").getWelcomeMessage()},getConfig(t){t.body={details:o.plugin("better-blocks").config("details",{defaultSummary:"Click to expand",style:"github"}),button:o.plugin("better-blocks").config("button",{defaultStyle:{backgroundColor:"#4945ff",textColor:"#ffffff",borderRadius:"4px",padding:"12px 24px",fontSize:"16px",fontWeight:"600"},presets:{primary:{backgroundColor:"#4945ff",textColor:"#ffffff",border:"none"},secondary:{backgroundColor:"#dcdce4",textColor:"#32324d",border:"none"},outline:{backgroundColor:"transparent",textColor:"#4945ff",border:"2px solid #4945ff"},filled:{backgroundColor:"#32324d",textColor:"#ffffff",border:"none"}}}),social:(()=>{const e=o.plugin("better-blocks").config("social",{enabled:!0,platforms:["twitter","instagram","facebook","tiktok","linkedin","pinterest"]});return{enabled:e.enabled!==!1,platforms:e.platforms??[],instagramConfigured:!!e.instagram?.accessToken,facebookConfigured:!!e.facebook?.accessToken}})()}}}),E=["twitter","instagram","facebook","tiktok","linkedin","pinterest"],$=({strapi:o})=>({async fetch(t){const{url:e,platform:r}=t.query;if(!e||typeof e!="string")return t.badRequest('Missing required "url" query parameter.');const n=r&&E.includes(r)?r:void 0;try{t.body=await o.plugin("better-blocks").service("oembed").fetch(e,n)}catch(s){const c=s instanceof Error?s.message:"oEmbed request failed.";return t.badRequest(c)}}}),T={controller:x,oembed:$},R={},_={},L=[{method:"GET",path:"/",handler:"controller.index",config:{policies:[]}}],S=[{method:"GET",path:"/config",handler:"controller.getConfig",config:{policies:[]}},{method:"GET",path:"/oembed",handler:"oembed.fetch",config:{policies:["admin::isAuthenticatedAdmin"]}}],z={"content-api":{type:"content-api",routes:L},admin:{type:"admin",routes:S}},A=({strapi:o})=>({getWelcomeMessage(){return"Welcome to Strapi 🚀"}}),U=({max:o})=>{if(o<1)throw new Error(`ttlCache: max must be at least 1 (received ${o})`);const t=new Map;return{get(e){const r=t.get(e);if(r){if(r.expires<=Date.now()){t.delete(e);return}return t.delete(e),t.set(e,r),r.value}},set(e,r,n){if(t.delete(e),t.size>=o){const s=Date.now();for(const[c,i]of t)i.expires<=s&&t.delete(c)}for(;t.size>=o;){const s=t.keys().next();if(s.done)break;t.delete(s.value)}t.set(e,{expires:Date.now()+n,value:r})},delete:e=>t.delete(e),clear:()=>t.clear(),get size(){return t.size}}},I=[{platform:"twitter",re:/(?:twitter\.com|x\.com)\/[^/]+\/status(?:es)?\/\d+/i},{platform:"instagram",re:/instagram\.com\/(?:p|reel|tv)\//i},{platform:"tiktok",re:/tiktok\.com\/@[^/]+\/video\/\d+/i},{platform:"facebook",re:/facebook\.com\//i},{platform:"linkedin",re:/linkedin\.com\//i},{platform:"pinterest",re:/(?:pinterest\.[a-z.]+|pin\.it)\//i}],g=o=>{const t=I.find(({re:e})=>e.test(o));return t?t.platform:null},M=(o,t,e)=>{const r=encodeURIComponent(t);switch(o){case"twitter":return`https://publish.twitter.com/oembed?url=${r}&omit_script=true&dnt=true`;case"tiktok":return`https://www.tiktok.com/oembed?url=${r}`;case"pinterest":return`https://www.pinterest.com/oembed.json?url=${r}`;case"instagram":{const n=e.instagram?.accessToken;return n?`https://graph.facebook.com/v17.0/instagram_oembed?url=${r}&access_token=${encodeURIComponent(n)}&omitscript=true`:null}case"facebook":{const n=e.facebook?.accessToken;return n?`https://graph.facebook.com/v17.0/oembed_post?url=${r}&access_token=${encodeURIComponent(n)}&omitscript=true`:null}case"linkedin":return null;default:return null}},P=async o=>{if(!/^https?:\/\/pin\.it\//i.test(o))return o;try{const e=(await fetch(o,{redirect:"follow"})).url||o;return/pinterest\.[a-z.]+\/pin\//i.test(e)?e:o}catch{return o}},q=o=>{const t=o.match(/(?:urn:li:)?(ugcPost|activity|share)[:-](\d{10,})/i);if(!t)return null;const r=`urn:li:${t[1].toLowerCase()==="ugcpost"?"ugcPost":t[1].toLowerCase()}:${t[2]}`,n=`https://www.linkedin.com/embed/feed/update/${encodeURIComponent(r)}`;return{platform:"linkedin",url:o,html:`<iframe src="${n}" height="600" width="100%" frameborder="0" allowfullscreen title="Embedded LinkedIn post" loading="lazy"></iframe>`,providerName:"LinkedIn",width:504,height:600}},D=o=>o.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi,"").trim(),W=(o,t,e)=>({platform:o,url:t,html:typeof e.html=="string"&&D(e.html)||null,title:typeof e.title=="string"?e.title:void 0,author:typeof e.author_name=="string"?e.author_name:void 0,authorUrl:typeof e.author_url=="string"?e.author_url:void 0,thumbnailUrl:typeof e.thumbnail_url=="string"?e.thumbnail_url:void 0,providerName:typeof e.provider_name=="string"?e.provider_name:void 0,width:typeof e.width=="number"?e.width:void 0,height:typeof e.height=="number"?e.height:void 0}),j=500,F=({strapi:o})=>{const t=()=>o.plugin("better-blocks").config("social",{});let e=null;const r=()=>(e||(e=U({max:t().cacheMaxEntries??j})),e);return{detectPlatform:g,clearCache:()=>e?.clear(),async fetch(n,s){const c=t();if(c.enabled===!1)throw new Error("Social embeds are disabled in plugin config.");const i=s??g(n);if(!i)throw new Error(`Unsupported or unrecognized social URL: ${n}`);const u=c.platforms;if(Array.isArray(u)&&!u.includes(i))throw new Error(`Platform "${i}" is not enabled in plugin config.`);const p=c.cache!==!1,m=(c.cacheTTL??86400)*1e3,b=`${i}:${n}`;if(p){const l=r().get(b);if(l)return l}let a=null;if(i==="linkedin"){if(a=q(n),!a)throw new Error("Could not derive a LinkedIn embed from this URL. Paste the post embed code instead.")}else{const l=await P(n),h=M(i,l,c);if(!h)throw new Error(`oEmbed for "${i}" requires an access token. Configure config.social.${i}.accessToken, or paste the embed code instead.`);const d=await fetch(h,{headers:{Accept:"application/json"}});if(!d.ok)throw new Error(`oEmbed request to ${i} failed (HTTP ${d.status}).`);const f=await d.json();if(typeof f.error=="string")throw new Error(`${i} could not embed this URL: ${f.error}`);if(a=W(i,l,f),!a.html)throw new Error(`${i} returned no embed markup for this URL. Check the post is public, or paste the embed code instead.`)}return p&&m>0&&r().set(b,a,m),a}}},N={service:A,oembed:F},G={register:y,bootstrap:k,destroy:w,config:C,controllers:T,routes:z,services:N,contentTypes:v,policies:_,middlewares:R};module.exports=G;