├── .eslintignore ├── .prettierignore ├── .gitignore ├── .prettierrc ├── src ├── index.ts ├── use-script.ts ├── interfaces.ts ├── default-providers.ts ├── component.tsx └── utils.ts ├── tsconfig.json ├── LICENCE ├── rollup.config.js ├── .eslintrc.json ├── package.json ├── PLUGINS.md └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | react-tiny-oembed*.tgz -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "arrowParens": "avoid", 6 | "tabWidth": 4, 7 | "semi": false 8 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './component' 2 | 3 | export * from './interfaces' 4 | export * from './utils' 5 | 6 | export { default as defaultProviders } from './default-providers' 7 | export { default as useScript } from './use-script' 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "lib": [ 5 | "ESNext", 6 | "DOM" 7 | ], 8 | "module": "ESNext", 9 | "declaration": false, 10 | "noEmitOnError": true, 11 | "checkJs": false, 12 | "preserveSymlinks": true, 13 | "outDir": "lib", 14 | "strict": true, 15 | "jsx": "react", 16 | "moduleResolution": "Node", 17 | "allowSyntheticDefaultImports": true, 18 | "esModuleInterop": true 19 | }, 20 | "include": [ 21 | "src/index.ts" 22 | ], 23 | "exclude": [ 24 | "node_modules", 25 | "lib", 26 | "**/__tests__/*" 27 | ] 28 | } -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2020-99999999899 muzam1l (https://github.com/muzam1l) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /src/use-script.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import type { DependencyList } from 'react' 3 | 4 | type Attributes = { 5 | [key: string]: string 6 | } 7 | 8 | export default function useScript(html: string, attributes?: Attributes, deps?: DependencyList): void { 9 | // track which scripts are currently added with hook 10 | let scripts: HTMLScriptElement[] = [] 11 | 12 | useEffect(() => { 13 | const div = document.createElement('div') 14 | div.innerHTML = html 15 | div.querySelectorAll('script').forEach(doc => { 16 | const script = document.createElement('script') 17 | 18 | // copy attributes 19 | Array.from(doc.attributes).forEach(attr => { 20 | if (attr.nodeName !== 'id') script.setAttribute(attr.nodeName, attr.nodeValue || '') 21 | }) 22 | // copy innerHTML 23 | script.innerHTML = doc.innerHTML 24 | 25 | // override attributes with attributes from props 26 | // attributes = attributes || {}; 27 | Object.entries(attributes || {}).forEach(entry => { 28 | const [key, value] = entry 29 | script.setAttribute(key, value) 30 | }) 31 | 32 | document.body.appendChild(script) 33 | scripts = scripts.concat(script) 34 | }) 35 | return () => { 36 | scripts.forEach(script => { 37 | document.body.removeChild(script) 38 | }) 39 | scripts = [] 40 | } 41 | }, deps || [html]) 42 | } 43 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from '@rollup/plugin-node-resolve' 2 | import commonjs from '@rollup/plugin-commonjs' 3 | // import typescript from '@rollup/plugin-typescript'; // HAD PROBLEMS 4 | import typescript from 'rollup-plugin-typescript2' // REPLACEMENT 5 | import { terser } from 'rollup-plugin-terser' 6 | import dts from 'rollup-plugin-dts' // For concatenated dts 7 | import filesize from 'rollup-plugin-filesize' 8 | import pkg from './package.json' 9 | 10 | const EXTERNAL = [...new Set([...Object.keys(pkg.dependencies), ...Object.keys(pkg.peerDependencies)])] 11 | 12 | export default [ 13 | // CommonJS (for Node) and ES module (for bundlers) build. 14 | // (We could have three entries in the configuration array 15 | // instead of two, but it's quicker to generate multiple 16 | // builds from a single configuration where possible, using 17 | // an array for the `output` option, where we can specify 18 | // `file` and `format` for each target) 19 | { 20 | input: 'src/index.ts', 21 | plugins: [ 22 | nodeResolve(), // so Rollup can find external modules 23 | commonjs(), // so Rollup can convert external modules to an ES module 24 | typescript(), // so Rollup can convert TypeScript to JavaScript 25 | terser(), // minify 26 | filesize(), 27 | ], 28 | external: EXTERNAL, 29 | output: [ 30 | // { file: pkg.main, format: 'cjs', exports: 'auto', }, 31 | // { file: pkg.module, format: 'es', exports: 'auto', }, 32 | { name: 'Embed', file: pkg.main, format: 'umd', exports: 'auto' }, 33 | ], 34 | }, 35 | // For concatenated dts 36 | { 37 | input: 'src/index.ts', 38 | output: [{ file: 'lib/index.d.ts', format: 'es' }], 39 | plugins: [dts()], 40 | }, 41 | ] 42 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true, 4 | "browser": true 5 | }, 6 | "extends": [ 7 | "plugin:react/recommended", 8 | "airbnb", 9 | "prettier", 10 | "prettier/react" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaFeatures": { 15 | "jsx": true 16 | }, 17 | "ecmaVersion": 12, 18 | "sourceType": "module" 19 | }, 20 | "plugins": [ 21 | "react", 22 | "@typescript-eslint" 23 | ], 24 | "settings": { 25 | "import/resolver": { 26 | "node": { 27 | "extensions": [ 28 | ".ts", 29 | ".tsx", 30 | ".js", 31 | ".jsx" 32 | ], 33 | "moduleDirectory": [ 34 | "node_modules", 35 | "src/" 36 | ] 37 | } 38 | } 39 | }, 40 | "rules": { 41 | "react/jsx-filename-extension": [ 42 | 1, 43 | { 44 | "extensions": [ 45 | ".tsx" 46 | ] 47 | } 48 | ], 49 | "import/extensions": [ 50 | "error", 51 | "ignorePackages", 52 | { 53 | "js": "never", 54 | "jsx": "never", 55 | "ts": "never", 56 | "tsx": "never" 57 | } 58 | ], 59 | // note you must disable the base rule as it can report incorrect errors 60 | "no-use-before-define": "off", 61 | "@typescript-eslint/no-use-before-define": [ 62 | "error", 63 | { 64 | "functions": false 65 | } 66 | ], 67 | "react/prop-types": "off", // Since we do not use prop-types 68 | "react/require-default-props": "off" // Since we do not use prop-types 69 | } 70 | } -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import type { ReactElement, CSSProperties, ComponentType } from 'react' 3 | 4 | export interface EmbedProps { 5 | url: string 6 | proxy?: string 7 | style?: CSSProperties 8 | options?: Partial 9 | providers?: any[] 10 | FallbackElement?: ReactElement 11 | LoadingFallbackElement?: ReactElement 12 | ImgComponent?: ComponentType<{ responce?: PhotoEmbedResponce }> 13 | LinkComponent?: ComponentType<{ responce?: LinkEmbedResponce }> 14 | } 15 | 16 | export interface CommonEmbedResponce { 17 | type: 'rich' | 'video' | 'link' | 'photo' | string 18 | version: string 19 | thumbnail_url?: string 20 | thumbnail_width?: number 21 | thumbnail_height?: number 22 | title?: string 23 | author_name?: string 24 | author_url?: string 25 | provider_name?: string 26 | provider_url?: string 27 | referrer?: string 28 | cache_age?: number 29 | [extras: string]: any 30 | } 31 | export interface PhotoEmbedResponce extends CommonEmbedResponce { 32 | type: 'photo' 33 | url: string 34 | width: number 35 | height: number 36 | } 37 | export interface VideoEmbedResponce extends CommonEmbedResponce { 38 | type: 'video' 39 | html: string 40 | width: number 41 | height: number 42 | } 43 | export interface RichEmbedResponce extends CommonEmbedResponce { 44 | type: 'rich' 45 | html: string 46 | width: number 47 | height: number 48 | } 49 | 50 | export interface LinkEmbedResponce extends CommonEmbedResponce { 51 | type: 'link' 52 | } 53 | 54 | export type EmbedResponce = PhotoEmbedResponce | VideoEmbedResponce | RichEmbedResponce | LinkEmbedResponce 55 | 56 | export interface EmbedRequestOptions { 57 | /** url of resource website */ 58 | url: string 59 | maxwidth?: number 60 | maxheight?: number 61 | format?: 'json' 62 | [extras: string]: any 63 | } 64 | 65 | // eslint-disable-next-line no-unused-vars 66 | export type GetReponceType = (options: EmbedRequestOptions) => EmbedResponce 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-tiny-oembed", 3 | "version": "1.1.0", 4 | "description": "Oembed compliant tiny react component for embedding content from websites", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "build": "npm run format && npm run lint && rollup -c", 10 | "format": "prettier --write \"src/**/*.ts\" \"src/**/*.tsx\"", 11 | "lint": "eslint \"src/**\"", 12 | "prepare": "npm run build" 13 | }, 14 | "files": [ 15 | "lib/**/*" 16 | ], 17 | "keywords": [ 18 | "oembed", 19 | "embed", 20 | "tiny", 21 | "react", 22 | "hooks", 23 | "iframe" 24 | ], 25 | "author": "muzam1l https://github.com/muzam1l", 26 | "license": "MIT", 27 | "homepage": "https://github.com/muzam1l/react-tiny-oembed", 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/muzam1l/react-tiny-oembed.git" 31 | }, 32 | "devDependencies": { 33 | "@rollup/plugin-commonjs": "^16.0.0", 34 | "@rollup/plugin-node-resolve": "^10.0.0", 35 | "@types/minimatch": "^3.0.3", 36 | "@types/react": "^16.9.56", 37 | "@typescript-eslint/eslint-plugin": "^4.8.1", 38 | "@typescript-eslint/parser": "^4.8.1", 39 | "eslint": "^7.13.0", 40 | "eslint-config-airbnb": "^18.2.1", 41 | "eslint-config-prettier": "^6.15.0", 42 | "eslint-plugin-import": "^2.22.1", 43 | "eslint-plugin-jsx-a11y": "^6.4.1", 44 | "eslint-plugin-react": "^7.21.5", 45 | "eslint-plugin-react-hooks": "^4.2.0", 46 | "prettier": "^2.1.2", 47 | "react": ">=16.8.0", 48 | "react-dom": ">=16.8.0", 49 | "rollup": "^2.33.3", 50 | "rollup-plugin-dts": "^1.4.14", 51 | "rollup-plugin-filesize": "^9.1.1", 52 | "rollup-plugin-terser": "^7.0.2", 53 | "rollup-plugin-typescript2": "^0.29.0", 54 | "tslib": "^2.0.3", 55 | "typescript": "^4.0.5" 56 | }, 57 | "dependencies": { 58 | "axios": "^0.21.1", 59 | "minimatch": "^3.0.4" 60 | }, 61 | "peerDependencies": { 62 | "react": ">=16.8.0", 63 | "react-dom": ">=16.8.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/default-providers.ts: -------------------------------------------------------------------------------- 1 | const providers: any[] = [ 2 | { 3 | provider_name: 'YouTube', 4 | provider_url: 'https://www.youtube.com/', 5 | endpoints: [ 6 | { 7 | schemes: ['https://*.youtube.com/watch*', 'https://*.youtube.com/v/*', 'https://youtu.be/*'], 8 | url: 'https://www.youtube.com/oembed', 9 | discovery: true, 10 | }, 11 | ], 12 | }, 13 | { 14 | provider_name: 'Reddit', 15 | provider_url: 'https://reddit.com/', 16 | endpoints: [ 17 | { 18 | schemes: ['https://reddit.com/r/*/comments/*/*', 'https://www.reddit.com/r/*/comments/*/*'], 19 | url: 'https://www.reddit.com/oembed', 20 | }, 21 | ], 22 | }, 23 | { 24 | provider_name: 'Flickr', 25 | provider_url: 'https://www.flickr.com/', 26 | endpoints: [ 27 | { 28 | schemes: [ 29 | 'http://*.flickr.com/photos/*', 30 | 'http://flic.kr/p/*', 31 | 'https://*.flickr.com/photos/*', 32 | 'https://flic.kr/p/*', 33 | ], 34 | url: 'https://www.flickr.com/services/oembed/', 35 | discovery: true, 36 | }, 37 | ], 38 | }, 39 | { 40 | provider_name: 'Vimeo', 41 | provider_url: 'https://vimeo.com/', 42 | endpoints: [ 43 | { 44 | schemes: [ 45 | 'https://vimeo.com/*', 46 | 'https://vimeo.com/album/*/video/*', 47 | 'https://vimeo.com/channels/*/*', 48 | 'https://vimeo.com/groups/*/videos/*', 49 | 'https://vimeo.com/ondemand/*/*', 50 | 'https://player.vimeo.com/video/*', 51 | ], 52 | url: 'https://vimeo.com/api/oembed.{format}', 53 | discovery: true, 54 | }, 55 | ], 56 | }, 57 | { 58 | provider_name: 'SoundCloud', 59 | provider_url: 'http://soundcloud.com/', 60 | endpoints: [ 61 | { 62 | schemes: ['http://soundcloud.com/*', 'https://soundcloud.com/*', 'https://soundcloud.app.goog.gl/*'], 63 | url: 'https://soundcloud.com/oembed', 64 | }, 65 | ], 66 | }, 67 | { 68 | provider_name: 'Twitter', 69 | provider_url: 'http://www.twitter.com/', 70 | endpoints: [ 71 | { 72 | schemes: [ 73 | 'https://twitter.com/*/status/*', 74 | 'https://*.twitter.com/*/status/*', 75 | 'https://twitter.com/*/moments/*', 76 | 'https://*.twitter.com/*/moments/*', 77 | ], 78 | url: 'https://publish.twitter.com/oembed', 79 | }, 80 | ], 81 | }, 82 | { 83 | provider_name: 'GIPHY', 84 | provider_url: 'https://giphy.com', 85 | endpoints: [ 86 | { 87 | schemes: ['https://giphy.com/gifs/*', 'http://gph.is/*', 'https://media.giphy.com/media/*/giphy.gif'], 88 | url: 'https://giphy.com/services/oembed', 89 | discovery: true, 90 | }, 91 | ], 92 | }, 93 | ] 94 | export default providers 95 | -------------------------------------------------------------------------------- /PLUGINS.md: -------------------------------------------------------------------------------- 1 | # Why Plugins 2 | 3 | Now we break our top rule, no exceptions, or at least twist it a bit. While i do believe every site should support oembed instead of having custom method, but there are some who don't support it and some important ones like _Github_. 4 | 5 | # Proxy servers 6 | 7 | But this component still understands _only_ oembed, so the recommended way for those sites is to create a oembed-proxy server, and referencing that in custom provider. Provider objects are self explanatory, important parts of it are `endpoints.schemes` which defines patterns of urls to match and uses _globs_ matched by [minimatch](https://github.com/isaacs/minimatch) and `endpoints.url` which defines oembed compatible url, see [https://oembed.com](https://oembed.com/) 8 | 9 | Example of custom provider would look like this 10 | 11 | ```json 12 | { 13 | "provider_name": "My Provider", 14 | "provider_url": "https://myurl.com/", 15 | "endpoints": [{ 16 | "schemes": [ 17 | "https://github.com/**" 18 | ], 19 | "url": "https://proxyurl.com/{raw_url}", 20 | "discovery": true 21 | }] 22 | }, 23 | ``` 24 | 25 | > Note: `{raw_url}` and `{url}` are placeholders that can be present in `endpoints.url` to specify where the url will be substituted, `{url}` will be replaced by url-encoded url and must be decoded on server accordingly, if both are omitted, url will simply be appended. 26 | 27 | # Override 28 | 29 | Some sites would just give you an embed code, contaning anchor and script tags etc, so to use that you can use this override api to execute that code. 30 | 31 | For that, your provider object must have `url` value set to `OVERRIDE` and `getResponce` function, which is expected to return the `oembed` reponce, given the options. 32 | 33 | Below is the example of plugin object for *Tenor* gifs. 34 | 35 | ```javascript 36 | { 37 | provider_name: 'Tenor Gifs by muzam1l', 38 | provider_url: 'https://github.com/muzam1l', 39 | endpoints: [ 40 | { 41 | schemes: ['https://tenor.com/view/**'], 42 | url: 'OVERRIDE', 43 | }, 44 | ], 45 | getResponce: (options: EmbedRequestOptions) => { 46 | const { url, width = '100%', aspectRatio = '1.0', caption = '' } = options 47 | const match = url.match(/-(?\d+)$/) 48 | const postId = match?.groups?.id 49 | return { 50 | type: 'rich', 51 | version: '1.0.0', 52 | html: `
53 | ${caption} 54 |
55 | `, 56 | } 57 | }, 58 | } 59 | ``` 60 | 61 | `getResponce` has following signature 62 | 63 | ```typescript 64 | type GetReponceType = (options: EmbedRequestOptions) => EmbedResponce 65 | ``` 66 | 67 | Check out interfaces [here](./src/interfaces.ts) 68 | 69 | 70 | # Axios Interceptors 71 | 72 | You can also use `requestInterceptor` and `responceInterceptor` fields in Provider object, which should be [axios](https://github.com/axios/axios) interceptor functions whose goal is to convert request into oembed compatible request and responce into oembed compatible responce, [github-gist](https://github.com/muzam1l/oembed-github-gist) is one such example. -------------------------------------------------------------------------------- /src/component.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import type { ReactNode } from 'react' 3 | 4 | import { requestEmbed } from './utils' 5 | import useScript from './use-script' 6 | import type { EmbedProps, EmbedResponce } from './interfaces' 7 | 8 | export default function Embed({ 9 | url, 10 | proxy, 11 | style, 12 | options, 13 | providers, 14 | ImgComponent, 15 | LinkComponent, 16 | FallbackElement, 17 | LoadingFallbackElement, 18 | }: EmbedProps) { 19 | const [data, setData] = useState(undefined) 20 | const [status, setStatus] = useState<'done' | 'idle' | 'loading' | 'error'>('idle') 21 | const [html, setHtml] = useState('') 22 | 23 | useScript(html, { defer: '' }) 24 | 25 | // make a request 26 | async function fetchEmbed() { 27 | try { 28 | setStatus('loading') 29 | const res = await requestEmbed(proxy, providers, { 30 | url, 31 | maxwidth: 700, 32 | maxheight: 500, 33 | align: 'center', // for twitter 34 | ...options, 35 | format: 'json', // only supported format 36 | }) 37 | if (!res) throw Error('Nill embed responce') 38 | setStatus('done') 39 | setData(res) 40 | if (res.html) setHtml(res.html) 41 | } catch (err) { 42 | // eslint-disable-next-line 43 | console.error('Error', err) 44 | setStatus('error') 45 | } 46 | } 47 | 48 | useEffect(() => { 49 | if (status === 'idle') fetchEmbed() 50 | }, [status]) 51 | 52 | const Link = ( 53 | 54 | {url} 55 | 56 | ) 57 | 58 | let CustomNode: ReactNode 59 | if (data && !data.html) { 60 | if (data.type === 'photo') { 61 | if (ImgComponent) { 62 | CustomNode = 63 | } else CustomNode = 64 | } else if (data.type === 'link') { 65 | if (LinkComponent) { 66 | CustomNode = 67 | } else 68 | CustomNode = ( 69 | 70 | {url} 71 | 72 | ) 73 | } 74 | } 75 | if (status === 'loading' || status === 'idle') return LoadingFallbackElement || Link 76 | if (status === 'error') return FallbackElement || Link 77 | return ( 78 | 79 | {CustomNode} 80 | {/* eslint-disable-next-line */} 81 | {html && } 82 | 106 | 107 | ) 108 | } 109 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | /* eslint-disable no-param-reassign */ 3 | import minimatch from 'minimatch' 4 | import axios from 'axios' 5 | import defaultProviders from './default-providers' 6 | 7 | import type { EmbedRequestOptions, EmbedResponce, GetReponceType } from './interfaces' 8 | /** 9 | * 10 | * @param endpoint Like `https:\/\/www.youtube.com\/` 11 | * @param proxy Like `https:\/\/cors-anywhere.heroku.app/{raw_url}`, {raw_url | url} is placeholder for url, {raw_url} isn't encoded whhere as {url} is 12 | * @param options params to the embed request 13 | */ 14 | export async function requestEmbed( 15 | proxy: string | undefined, 16 | providers: any[] | undefined, 17 | options: EmbedRequestOptions, 18 | ): Promise { 19 | // guess oembed url from resource url 20 | const { base_url, requestInterceptor, responceInterceptor, getResponce } = getEndpoint(options.url, providers) 21 | if (!base_url) throw Error('Invalid url: cannot guess oembed endpoint') 22 | 23 | if (base_url === 'OVERRIDE') return getResponce?.(options) 24 | 25 | const proxied_url = makeUrl(base_url, proxy) 26 | 27 | // axios instance to apply interceptors on 28 | const instance = axios.create() 29 | 30 | // apply interceptors 31 | if (requestInterceptor) instance.interceptors.request.use(requestInterceptor as any) 32 | if (responceInterceptor) instance.interceptors.response.use(responceInterceptor as any) 33 | 34 | // send a request 35 | const responce = await instance.get(proxied_url, { 36 | params: options, 37 | }) 38 | 39 | return responce.data 40 | } 41 | 42 | /** 43 | * gets the oembed endpoint url from providers list 44 | * @param url // resource url for identifications 45 | * @param providers // if undefined uses default ones, but [] would mean not to use default ones 46 | */ 47 | export function getEndpoint( 48 | url: string, 49 | providers: any[] | undefined, 50 | ): { 51 | base_url?: string 52 | requestInterceptor?: Function 53 | responceInterceptor?: Function 54 | // eslint-disable-next-line no-unused-vars 55 | getResponce?: GetReponceType 56 | } { 57 | let base_url: string | undefined 58 | let requestInterceptor: Function | undefined 59 | let responceInterceptor: Function | undefined 60 | let getResponce: GetReponceType | undefined 61 | providers = providers || defaultProviders 62 | 63 | providers.forEach((provider: any) => { 64 | let selected: any 65 | /* for providers with endpoints.length > 1, vl pick last match, (see instagram oembed provider) */ 66 | provider.endpoints.forEach((endpoint: any) => { 67 | if (isMatch(url, endpoint.schemes)) selected = endpoint 68 | }) 69 | 70 | if (selected) { 71 | base_url = selected.url 72 | ;({ requestInterceptor, responceInterceptor, getResponce } = provider) 73 | } 74 | }) 75 | 76 | return { 77 | base_url, 78 | requestInterceptor, 79 | responceInterceptor, 80 | getResponce, 81 | } 82 | } 83 | 84 | /** 85 | * Constructs url from oembed endpoint and proxy 86 | * @param base_url 87 | * @param proxy 88 | */ 89 | function makeUrl(base_url: string, proxy: string | undefined): string { 90 | // remove trailing slash 91 | if (base_url.endsWith('/')) base_url = base_url.slice(0, -1) 92 | // replace {format} placeholder 93 | const format = /\{format\}/gi 94 | if (base_url.match(format)) { 95 | base_url = base_url.replace(format, 'json') 96 | } 97 | 98 | // return if no proxy is present 99 | if (!proxy) return base_url 100 | 101 | const url = /\{url\}/gi 102 | const raw_url = /\{raw_url\}/gi 103 | 104 | // replace {url} with base_url in proxy and return 105 | if (proxy.match(raw_url)) return proxy.replace(raw_url, base_url) 106 | 107 | // replace {raw_url} with url encoded base_url and return 108 | if (proxy.match(url)) { 109 | base_url = encodeURIComponent(base_url) 110 | return proxy.replace(url, base_url) 111 | } 112 | 113 | // else just append the url 114 | if (proxy.endsWith('/')) proxy = proxy.slice(0, -1) 115 | return `${proxy}/${base_url}` 116 | } 117 | 118 | function isMatch(url: string, schemes: string[]): boolean { 119 | const lvl1 = Boolean(schemes.find(scheme => minimatch(url, scheme, { nocase: true }))) 120 | if (lvl1) return lvl1 121 | 122 | // search again with * replaced with ** (see soundclound provider) 123 | return Boolean(schemes.find(scheme => minimatch(url, scheme.replace(/\*/g, '**'), { nocase: true }))) 124 | } 125 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Tiny Oembed [![version](https://img.shields.io/npm/v/react-tiny-oembed.svg)](https://www.npmjs.com/package/react-tiny-oembed) [![npm bundle](https://img.shields.io/bundlephobia/minzip/react-tiny-oembed?label=npm%20bundle)](https://www.npmjs.com/package/react-tiny-oembed) [![gzip size](https://img.badgesize.io/https://github.com/muzam1l/react-tiny-oembed/releases/download/1.1.0/index.js?compression=gzip&label=gzip)](https://github.com/muzam1l/react-tiny-oembed/releases) 2 | 3 | React component for embedding content from sites going [oEembed](https://oembed.com/) way and only[1] _oembed_ way. Just give it a URL and it will do the rest, no more paying for widgets! 4 | 5 | The motivation behind this component is the admiration of _oembed_, an opensource standard with a unified way of embedding content from all supported sites, instead of having different methods for every site. 6 | 7 | > [1] _However sites not supporting _oembed_ for now can also be embedded using _oembed_ wrapper proxies and interceptors, see `Plugins` below_ 8 | 9 | ## Installation 10 | 11 | ```bash 12 | npm install react-tiny-oembed 13 | ``` 14 | 15 | _requires React 16.8 or higher_ 16 | 17 | ## Basic usage 18 | 19 | ```jsx 20 | import Embed from 'react-tiny-oembed' 21 | 22 | function App() { 23 | ... 24 | 25 | 29 | } 30 | ``` 31 | 32 | > A note on the proxy: most of the sites do not have cors enabled, so cors proxy is necessary in most cases. 33 | > The above-used proxy is just for demonstration and is slow and highly rate-limited, so provide your own proxy. You can host [Cors anywhere](https://github.com/Rob--W/cors-anywhere) on your own node server and use that. 34 | 35 | By default only _YouTube_, _Reddit_, _Flickr_, _Vimeo_, _SoundCloud_, _Twitter_, _GIPHY_ are enabled, to add more or reduce even default ones, see `providers` prop below. 36 | 37 | ## Props 38 | 39 | You can pass multiple props to the `Embed` component, typings can be imported as named exports. 40 | 41 | - `options`: `EmbedRequestOptions` 42 | 43 | An object containing _oembed_ options, these fields are used as query params to oembed provider, these include general options like `maxwidth` and `maxheight` and also some site-specific options. Below are some of the default ones used. 44 | 45 | | value | type | default | description | 46 | | ----------- | ------ | -------- | ----------------------------------------------------------------------------------------------------- | 47 | | `maxwidth` | number | 700 | maximum width of the iframe or image rendered by the provider. Note that this is separate from the outer container | 48 | | `maxheight` | number | 400 | similar to the `maxwidth` | 49 | | `align` | string | 'center' | for [twitter](https://developer.twitter.com/en/docs/twitter-for-websites/timelines/guides/oembed-api) | 50 | 51 | - `style`: `CSSProperties` 52 | 53 | Styles applied to outer container, container also has `__embed` class so you can use that too, by default it has `100%` width and `700px` max width. 54 | 55 | - `FallbackElement` and `LoadingFallbackElement`: `ReactElement` 56 | 57 | By default the given URL is shown as an anchor tag (external) for states like _loading_, _error_, etc. However, you can pass your own elements like 58 | 59 | ```jsx 60 | 66 | ``` 67 | 68 | - `ImgComponent`: `ComponentType<{ responce?: PhotoEmbedResponce }>` 69 | 70 | While most sites will render some good-looking widgets, some sites like _Giphy_ will just render a plain image. Images are displayed plain, without any styling, you might want to have your own custom component for images. That component will receive the `responce` prop as _oembed_ `responce` object. For example, you can access `src` via `responce.url`. 71 | 72 | ```jsx 73 | function CustomImg({ responce }) { 74 | return
75 |

Image from {responce.provider_name}

76 | {responce.author_name} 77 |
78 | } 79 | 80 | ... 81 | 85 | ``` 86 | 87 | _similar is for `LinkComponent` but I did not see any site returning just link!_ 88 | 89 | - `providers` ⭐ 90 | 91 | Default providers are just a handful, you have hundreds to choose from. This prop can be used to enable (or reduce) support for individual sites. It expects an array of [`Provider`](https://oembed.com/providers.json) objects which define matching patterns for links, embedding URLs or interceptors to add to. 92 | 93 | Say you want to extend support for more sites, go to [https://oembed.com/providers.json](https://oembed.com/providers.json), choose a provider object and pass that to this prop. Say we pick the first one, _TwoThreeHQ_, we will use it like this. 94 | 95 | ```jsx 96 | import Embed, { defaultProviders } from 'react-oembed' 97 | 98 | const TwoThreeHQ = { 99 | "provider_name": "23HQ", 100 | "provider_url": "http:\/\/www.23hq.com", 101 | "endpoints": [ 102 | { 103 | "schemes": [ 104 | "http:\/\/www.23hq.com\/*\/photo\/*" 105 | ], 106 | "url": "http:\/\/www.23hq.com\/23\/oembed" 107 | } 108 | ] 109 | } 110 | 111 | ... 112 | 113 | 118 | ``` 119 | 120 | > Note: Passing the `providers` list overrides the default one, so you need to pass `defaultProviders` to have them too. 121 | 122 | > Note: Providers like _Instagram_ and _Facebook_ require developer keys too, so pass them in the `options` prop above (testing TBD). 123 | 124 | If you want to filter out even the default ones, you can 125 | 126 | ```js 127 | const providers = defaultProviders.filter( 128 | p => p.provider_name === 'Vimeo' || p.provider_name === 'SoundCloud' 129 | ) 130 | ``` 131 | 132 | For sites not supporting _oembed_ but see `Plugins` section below. 133 | 134 | ## Plugins 135 | 136 | - [github-gist](https://github.com/muzam1l/oembed-github-gist) - Github gist sample plugin for react-tiny-oembed without a proxy server. 137 | - ...others 138 | 139 | For authoring plugins see [PLUGINS](./PLUGINS.md) 140 | 141 | ## Contributing 142 | 143 | Contributions welcome! 144 | --------------------------------------------------------------------------------