├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── index.d.ts ├── index.js ├── jest.config.js ├── package.json ├── src ├── __tests__ │ ├── prefill-embed-test.tsx │ ├── prefill-lang-test.tsx │ ├── strip-indent-test.ts │ └── use-codepen-embed-test.tsx ├── components │ ├── prefill-embed.tsx │ └── prefill-lang.tsx ├── config.ts ├── index.ts ├── lib │ ├── strip-indent.ts │ └── use-codepen-embed.ts ├── test-setup.ts ├── types │ └── lang.ts └── util │ └── render-string.ts ├── tsconfig.json ├── tslint.json ├── webpack.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "bracketSpacing": false 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Alex Zaworski 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React CodePen Prefill Embed 2 | 3 | An unofficial, TypeScript-powered React wrapper around CodePen's very-official [Prefill Embed API.](https://blog.codepen.io/documentation/prefill-embeds/) 4 | 5 | ## Is this for me? 6 | 7 | From CodePen's docs: 8 | 9 | > Here's why Prefill Embeds are useful: say you have a blog or documentation site displaying important bits of code within `
` tags. Perhaps you need to show a bit of HTML, CSS, or JavaScript.
 10 | 
 11 | > ...
 12 | 
 13 | > CodePen Prefill Embeds are designed to render that code for you in a safe and isolated environment to help your users see, understand, and play with it more easily.
 14 | 
 15 | This wrapper might be a good fit for you if you're already using React to power something like a blog (maybe with [Gatsby](https://www.gatsbyjs.org/) or [Next.js](https://nextjs.org/)) and don't wanna fuss about with things like [escaping markup](https://blog.codepen.io/documentation/prefill-embeds/#what-could-go-wrong-2).
 16 | 
 17 | ## Installation
 18 | 
 19 | ### From your favorite package registry
 20 | 
 21 | - `yarn add react-codepen-prefill-embed`
 22 | - `npm install --save react-codepen-prefill-embed`
 23 | 
 24 | ```
 25 | import {
 26 |   PrefillEmbed,
 27 |   PrefillLang,
 28 |   useCodePenEmbed,
 29 |   stripIndent
 30 | } = from 'react-codepen-prefill-embed';
 31 | // ...
 32 | ```
 33 | 
 34 | ### From a CDN
 35 | 
 36 | ```html
 37 | 
 41 | 
 42 | 
 43 | 
 44 | 
 45 | 
 49 | 
 50 | 
 51 | 
 52 | 
 61 | ```
 62 | 
 63 | ### Use on CodePen
 64 | 
 65 | [Here's a template](https://codepen.io/pen?template=e0925944542ee1f8bb7d108f2015def1) that has all the scripts loaded for you.
 66 | 
 67 | ## Usage example
 68 | 
 69 | ```jsx
 70 | const App = () => {
 71 |   useCodePenEmbed();
 72 |   return (
 73 |     
 84 |       }
 85 |       scripts={[
 86 |         'https://unpkg.com/react@16.8.6/umd/react.development.js',
 87 |         'https://unpkg.com/react-dom@16.8.6/umd/react-dom.development.js',
 88 |       ]}
 89 |       stylesheets={['https://unpkg.com/normalize.css@8.0.1/normalize.css']}
 90 |     >
 91 |       
 92 |         {stripIndent`
 93 |             
94 | `} 95 |
96 | 97 | {stripIndent` 98 | $bg: #eee; 99 | body { 100 | background: $bg; 101 | } 102 | `} 103 | 104 | 105 | {stripIndent` 106 | const App = () =>

Hello

; 107 | ReactDOM.render( 108 | , 109 | document.getElementById('root') 110 | ); 111 | `} 112 |
113 |
114 | ); 115 | }; 116 | ``` 117 | 118 | ## Components 119 | 120 | ### `` 121 | 122 | The root of your embed. This is where you set Pen-specific settings like description or theme. 123 | 124 | #### Props 125 | 126 | [Type annotations here](/src/components/prefill-embed.tsx) if you prefer. Any props not listed will be passed to the wrapping `
` element. All `` props are optional. 127 | 128 | ##### Related to the Pen: 129 | 130 | | Prop | Type | Default | Description | 131 | | ----------- | ------ | ------- | ------------------------------------------------------------ | 132 | | penTitle | string | -- | Title of your Pen | 133 | | description | string | -- | Description of your Pen | 134 | | tags | array | -- | Tags for your Pen. Max 5. | 135 | | htmlClasses | array | -- | Classes to add to the html element of your Pen | 136 | | stylesheets | array | -- | Stylesheets to include as external resources | 137 | | scripts | array | -- | Scripts to include as external resources | 138 | | head | node | -- | Content for the `` of the document | 139 | | prefillData | object | -- | Any additional keys/values you want passed to `data-prefill` | 140 | 141 | ##### Related to the embed: 142 | 143 | | Prop | Type | Default | Description | 144 | | ----------- | ------ | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 145 | | embedHeight | string | -- | Height of the generated iframe | 146 | | themeId | string | -- | [Theme](https://blog.codepen.io/documentation/features/embedded-pens/#embed-themes-1) id to use in the embed | 147 | | editable | bool | false | Whether or not the embed should be editable | 148 | | defaultTabs | array | ['result'] | Default open tab(s) | 149 | | className | string | 'codepen' | Class for the wrapping `
`. If you change this, you'll be responsible for [invoking the prefill API](https://blog.codepen.io/documentation/prefill-embeds/#enhance-pre-blocks-into-editable-embeds-on-click-or-on-any-other-event-3) | 150 | 151 | ### `` 152 | 153 | Used as children of ``— your HTML, CSS, and JS tabs. 154 | 155 | #### Props 156 | 157 | `lang` is the only prop, everything else is passed to the wrapping `
` element.
158 | 
159 | | Prop | Type   | Default | Required | Description                                |
160 | | ---- | ------ | ------- | -------- | ------------------------------------------ |
161 | | lang | string | --      | ⚠️       | What kinda code is it? Support table below |
162 | 
163 | ##### Supported languages
164 | 
165 | HTML: `html`, `slim`, `haml`, `markdown`, `pug`
166 | 
167 | JS: `js`, `babel`, `typescript`, `coffeescript`, `livescript`
168 | 
169 | CSS: `css`, `scss`, `sass`, `less`, `stylus`
170 | 
171 | ## Hooks and Utilities
172 | 
173 | ### `usePrefillEmbed`
174 | 
175 | A [hook](https://reactjs.org/docs/hooks-overview.html) to load CodePen's prefill API. This may be useful if you don't want to deal with adding a CDN-only script to your build process, but isn't recommended in the browser.
176 | 
177 | The script is loaded from `https://static.codepen.io/assets/embed/ei.js` and is `async` by default.
178 | 
179 | #### Options
180 | 
181 | | Option      | Type    | Default | Description                                |
182 | | ----------- | ------- | ------- | ------------------------------------------ |
183 | | async       | boolean | true    | Load the script with the `async` attribute |
184 | | srcOverride | string  | --      | Override the source of the loaded script   |
185 | 
186 | ### `stripIndent`
187 | 
188 | Multi-line strings are easiest to work with in [template literals.](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals) This can cause headaches when dealing with indented JSX code, however— consider this:
189 | 
190 | ```jsx
191 | 
192 |   
193 |     
194 |       {`
195 |         
196 | Hello 197 |
198 | `} 199 |
200 | 201 |
202 | ``` 203 | 204 | Which will yield the following markup in your Pen: 205 | 206 | ```text 207 |
208 | Hello 209 |
210 | ``` 211 | 212 | In order for whitespace to look normal you'd need to do something like this: 213 | 214 | ```jsx 215 | 216 | 217 | 218 | {`
219 | Hello 220 |
`} 221 |
222 | 223 |
224 | ``` 225 | 226 | And that's a bummer. Instead, you can use the provided [template tag](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#Tagged_templates) to strip leading indents and leading/trailing newlines. 227 | 228 | Whitespace to strip is determined based on the first line of text after removing a leading newline. Tabs or spaces should both be fine so long as you're consistent. 229 | 230 | ```jsx 231 | 232 | 233 | 234 | {stripIndent` 235 |
236 | Hello 237 |
238 | `} 239 |
240 | 241 |
242 | ``` 243 | 244 | ...would become: 245 | 246 | ``` 247 |
248 | Hello 249 |
250 | ``` 251 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import types from './dist/cjs'; 2 | export default types; 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/cjs'); 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | setupFilesAfterEnv: ['src/test-setup.ts'], 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-codepen-prefill-embed", 3 | "version": "0.0.2", 4 | "description": "A React wrapper for CodePen's prefill embed API", 5 | "main": "index.js", 6 | "files": [ 7 | "dist" 8 | ], 9 | "types": "./dist/cjs/index.d.ts", 10 | "author": "Alex Zaworski (https://zawor.ski)", 11 | "license": "MIT", 12 | "peerDependencies": { 13 | "react": ">=16.8", 14 | "react-dom": ">=16.8" 15 | }, 16 | "scripts": { 17 | "test": "jest", 18 | "build": "rm -rf ./dist && webpack" 19 | }, 20 | "sideEffects": false, 21 | "devDependencies": { 22 | "@babel/core": "^7.5.5", 23 | "@babel/preset-env": "^7.5.5", 24 | "@types/enzyme": "^3.9.4", 25 | "@types/enzyme-adapter-react-16": "^1.0.5", 26 | "@types/jest": "^24.0.15", 27 | "@types/jsdom": "^12.2.4", 28 | "@types/node": "^12.0.10", 29 | "@types/prettier": "^1.16.4", 30 | "@types/react": "^16.8.22", 31 | "@types/react-dom": "^16.8.4", 32 | "babel-loader": "^8.0.6", 33 | "enzyme": "^3.10.0", 34 | "enzyme-adapter-react-16": "^1.14.0", 35 | "jest": "^24.8.0", 36 | "jsdom": "^15.1.1", 37 | "react": "^16.8.6", 38 | "react-dom": "^16.8.6", 39 | "ts-jest": "^24.0.2", 40 | "ts-loader": "^6.0.4", 41 | "tslint": "^5.18.0", 42 | "tslint-config-prettier": "^1.18.0", 43 | "tslint-loader": "^3.5.4", 44 | "typescript": "^3.5.2", 45 | "webpack": "^4.34.0", 46 | "webpack-cli": "^3.3.4" 47 | }, 48 | "dependencies": {} 49 | } 50 | -------------------------------------------------------------------------------- /src/__tests__/prefill-embed-test.tsx: -------------------------------------------------------------------------------- 1 | import {shallow} from 'enzyme'; 2 | import * as React from 'react'; 3 | 4 | import PrefillEmbed from '../components/prefill-embed'; 5 | 6 | const embed = shallow( 7 | } 13 | stylesheets={['some-file.css']} 14 | scripts={['first-style-sheet.css', 'another-style-sheet.css']} 15 | embedHeight="400" 16 | editable 17 | defaultTabs={['js', 'result']} 18 | themeId="1234" 19 | prefillData={{someCustomJSON: 'foo'}} 20 | data-custom-thing="foo" 21 | /> 22 | ); 23 | 24 | const expectedJSON = { 25 | description: 'This tests some stuff', 26 | head: '', 27 | html_classes: ['a-class'], 28 | scripts: ['first-style-sheet.css', 'another-style-sheet.css'], 29 | someCustomJSON: 'foo', 30 | stylesheets: ['some-file.css'], 31 | tags: ['react'], 32 | title: 'A test', 33 | }; 34 | 35 | const expectedData = { 36 | 'data-custom-thing': 'foo', 37 | 'data-default-tab': 'js,result', 38 | 'data-editable': 'true', 39 | 'data-height': '400', 40 | 'data-theme-id': '1234', 41 | }; 42 | 43 | describe('PrefillLang component', () => { 44 | const embedProps = embed.props(); 45 | const parsedPrefillProp = JSON.parse(embedProps['data-prefill']); 46 | 47 | const jsonValuePairs = Object.keys(expectedJSON).map((key: keyof object) => { 48 | return [ 49 | key, 50 | JSON.stringify(parsedPrefillProp[key]), 51 | JSON.stringify(expectedJSON[key]), 52 | ]; 53 | }); 54 | 55 | const dataValuePairs = Object.keys(expectedData).map( 56 | (attribute: keyof object) => { 57 | return [attribute, embedProps[attribute], expectedData[attribute]]; 58 | } 59 | ); 60 | 61 | describe('Generates expected prefill JSON', () => { 62 | test.each(jsonValuePairs)('%s', (_key, actualVal, expectedVal) => { 63 | expect(actualVal).toBe(expectedVal); 64 | }); 65 | }); 66 | 67 | describe('Generates expected data-* attributes', () => { 68 | test.each(dataValuePairs)('%s', (_key, actualVal, expectedVal) => { 69 | expect(actualVal).toBe(expectedVal); 70 | }); 71 | }); 72 | 73 | describe('Renders default props', () => { 74 | const noPropsEmbed = shallow(); 75 | expect(noPropsEmbed.prop('className')).toBe('codepen'); 76 | expect(noPropsEmbed.prop('data-default-tab')).toBe('result'); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/__tests__/prefill-lang-test.tsx: -------------------------------------------------------------------------------- 1 | import {shallow} from 'enzyme'; 2 | import * as React from 'react'; 3 | 4 | import PrefillLang from '../components/prefill-lang'; 5 | 6 | describe('PrefillLang component', () => { 7 | it('Renders a single
 element', () => {
 8 |     const wrapper = shallow(
 9 |       
10 |         
Inner pre
11 |
12 | ); 13 | expect(wrapper.find('pre')).toHaveLength(1); 14 | }); 15 | 16 | it('Escapes html in children', () => { 17 | const wrapper = shallow( 18 | 19 |

hello world

20 |
21 | ); 22 | expect(wrapper.html()).toMatch('<p>'); 23 | }); 24 | 25 | it('Supports unknown props', () => { 26 | const wrapper = shallow( 27 | 28 | {`body: {color: red;}`} 29 | 30 | ); 31 | expect(wrapper.find('[data-options-autoprefixer="true"]')).toHaveLength(1); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/__tests__/strip-indent-test.ts: -------------------------------------------------------------------------------- 1 | import stripIndent from '../lib/strip-indent'; 2 | 3 | // Mostly declaring these up here 'cause working with 4 | // whitespace in template tags suuuuucks 5 | 6 | const inputSpaces = stripIndent` 7 |
8 | Hello 9 |
10 | `; 11 | 12 | const expectedSpaces = `
13 | Hello 14 |
`; 15 | 16 | const inputTabs = stripIndent` 17 | \t
18 | \t\tHello 19 | \t
20 | `; 21 | 22 | const expectedTabs = `
23 | \tHello 24 |
`; 25 | 26 | const noIndentInput = stripIndent` 27 |
28 | Hello 29 |
30 | `; 31 | 32 | describe('Strip whitespace', () => { 33 | it('Works with spaces', () => { 34 | expect(inputSpaces).toBe(expectedSpaces); 35 | }); 36 | 37 | it('Works with tabs', () => { 38 | expect(inputTabs).toBe(expectedTabs); 39 | }); 40 | 41 | it('Does nothing if indentation is not detected', () => { 42 | expect(noIndentInput).toBe(expectedSpaces); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/__tests__/use-codepen-embed-test.tsx: -------------------------------------------------------------------------------- 1 | import {mount} from 'enzyme'; 2 | import {JSDOM} from 'jsdom'; 3 | import * as React from 'react'; 4 | 5 | import {EMBED_SCRIPT_ID} from '../config'; 6 | import useCodePenEmbed from '../lib/use-codepen-embed'; 7 | 8 | const FAKE_SRC = 'http://foo.bar'; 9 | 10 | const ComponentWithHook = () => { 11 | useCodePenEmbed(); 12 | return
hello
; 13 | }; 14 | 15 | const ComponentWithHookOptions = () => { 16 | useCodePenEmbed({async: false, srcOverride: FAKE_SRC}); 17 | return
hello
; 18 | }; 19 | 20 | beforeEach(() => { 21 | const jsdom = new JSDOM(); 22 | const {window} = jsdom; 23 | (global as any).window = window; 24 | (global as any).document = window.document; 25 | }); 26 | 27 | describe('useCodePenEmbed hook', () => { 28 | it('Adds a script tag to the document body', () => { 29 | mount(); 30 | const scripts = document.body.querySelectorAll(`#${EMBED_SCRIPT_ID}`); 31 | expect(scripts.length).toBe(1); 32 | }); 33 | 34 | it('Only appends a single script tag', () => { 35 | mount(); 36 | mount(); 37 | const scripts = document.querySelectorAll(`#${EMBED_SCRIPT_ID}`); 38 | expect(scripts.length).toBe(1); 39 | }); 40 | 41 | it('Uses async by default', () => { 42 | mount(); 43 | const script = document.querySelector('script'); 44 | expect(script.getAttribute('async')).toBe('true'); 45 | }); 46 | 47 | it('Respects srcOverride option', () => { 48 | mount(); 49 | const script = document.querySelector('script'); 50 | expect(script.getAttribute('src')).toBe(FAKE_SRC); 51 | }); 52 | 53 | it('Respects async option', () => { 54 | mount(); 55 | const script = document.querySelector('script'); 56 | expect(script.hasAttribute('async')).toBe(false); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/components/prefill-embed.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import Lang from '../types/lang'; 4 | import renderString from '../util/render-string'; 5 | 6 | interface PrefillEmbedProps extends React.HTMLAttributes { 7 | penTitle?: string; 8 | embedHeight?: string; 9 | themeId?: string; 10 | editable?: boolean; 11 | description?: string; 12 | tags?: [string?, string?, string?, string?, string?]; 13 | htmlClasses?: string[]; 14 | stylesheets?: string[]; 15 | scripts?: string[]; 16 | head?: React.ReactNode; 17 | defaultTabs?: [Lang?, 'result'?] | ['result'?, Lang?]; 18 | prefillData?: object; 19 | } 20 | 21 | const PrefillEmbed: React.FunctionComponent = ({ 22 | penTitle, 23 | embedHeight, 24 | themeId, 25 | editable, 26 | description, 27 | tags, 28 | htmlClasses, 29 | stylesheets, 30 | scripts, 31 | head, 32 | defaultTabs = ['result'], 33 | prefillData, 34 | children, 35 | className = 'codepen', 36 | ...embedProps 37 | }) => { 38 | const prefillJSON = JSON.stringify({ 39 | description, 40 | head: renderString(head), 41 | html_classes: htmlClasses, 42 | scripts, 43 | stylesheets, 44 | tags, 45 | title: penTitle, 46 | ...prefillData, 47 | }); 48 | 49 | return ( 50 |
59 | {children} 60 |
61 | ); 62 | }; 63 | 64 | export default PrefillEmbed; 65 | -------------------------------------------------------------------------------- /src/components/prefill-lang.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import Lang from '../types/lang'; 4 | import renderString from '../util/render-string'; 5 | 6 | export interface PrefillLangProps extends React.HTMLAttributes { 7 | lang: Lang; 8 | } 9 | 10 | const PrefillLang: React.FunctionComponent = ({ 11 | lang, 12 | children, 13 | ...rest 14 | }) => { 15 | return ( 16 |
17 |       {renderString(children)}
18 |     
19 | ); 20 | }; 21 | 22 | export default PrefillLang; 23 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export const EMBED_SCRIPT_SRC = 'https://static.codepen.io/assets/embed/ei.js'; 2 | export const EMBED_SCRIPT_ID = '__react-codepen-prefill-embed'; 3 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import PrefillEmbed from './components/prefill-embed'; 2 | import PrefillLang from './components/prefill-lang'; 3 | import stripIndent from './lib/strip-indent'; 4 | import useCodePenEmbed from './lib/use-codepen-embed'; 5 | 6 | export {PrefillEmbed, PrefillLang, stripIndent, useCodePenEmbed}; 7 | -------------------------------------------------------------------------------- /src/lib/strip-indent.ts: -------------------------------------------------------------------------------- 1 | const stripIndent = (strings: TemplateStringsArray): string => { 2 | const string = strings.join(''); 3 | const lines = string 4 | .replace(/^\n/, '') 5 | .replace(/\n$/, '') 6 | .split('\n'); 7 | 8 | const firstLineWhitespace = lines[0].match(/^\s+/); 9 | if (!firstLineWhitespace) return lines.join('\n'); 10 | 11 | const stringToReplace = firstLineWhitespace[0]; 12 | const replaceRE = new RegExp(`^${stringToReplace}`); 13 | return lines.map(line => line.replace(replaceRE, '')).join('\n'); 14 | }; 15 | 16 | export default stripIndent; 17 | -------------------------------------------------------------------------------- /src/lib/use-codepen-embed.ts: -------------------------------------------------------------------------------- 1 | import {useEffect} from 'react'; 2 | import {EMBED_SCRIPT_ID, EMBED_SCRIPT_SRC} from '../config'; 3 | 4 | interface HookOptions { 5 | async: boolean; 6 | srcOverride?: string; 7 | } 8 | 9 | const defaultOptions: HookOptions = { 10 | async: true, 11 | }; 12 | 13 | const useCodePenEmbed = (userOptions?: HookOptions): void => { 14 | const options = {...defaultOptions, ...userOptions}; 15 | useEffect(() => { 16 | /** 17 | * Checking the window here in case someone loads the script 18 | * themselves and tries to use the hook regardless. 19 | * 20 | * Won't save us if someone adds the script manually and 21 | * the hook is called before it loads but meh. 22 | */ 23 | const existsInWindow = window.hasOwnProperty('__CPEmbed'); 24 | const existsInDOM = Boolean(document.getElementById(EMBED_SCRIPT_ID)); 25 | const exists = existsInWindow || existsInDOM; 26 | 27 | if (exists) return; 28 | 29 | const s = document.createElement('script'); 30 | const {async, srcOverride} = options; 31 | 32 | if (async) s.setAttribute('async', 'true'); 33 | s.setAttribute('src', srcOverride || EMBED_SCRIPT_SRC); 34 | s.setAttribute('id', EMBED_SCRIPT_ID); 35 | 36 | document.body.appendChild(s); 37 | }); 38 | }; 39 | 40 | export default useCodePenEmbed; 41 | -------------------------------------------------------------------------------- /src/test-setup.ts: -------------------------------------------------------------------------------- 1 | import {configure} from 'enzyme'; 2 | import * as Adapter from 'enzyme-adapter-react-16'; 3 | 4 | configure({adapter: new Adapter()}); 5 | -------------------------------------------------------------------------------- /src/types/lang.ts: -------------------------------------------------------------------------------- 1 | type LangHTML = 'html' | 'slim' | 'haml' | 'markdown' | 'pug'; 2 | type LangCSS = 'css' | 'scss' | 'sass' | 'less' | 'stylus'; 3 | type LangJS = 'js' | 'babel' | 'typescript' | 'coffeescript' | 'livescript'; 4 | 5 | type Lang = LangHTML | LangCSS | LangJS; 6 | 7 | export default Lang; 8 | -------------------------------------------------------------------------------- /src/util/render-string.ts: -------------------------------------------------------------------------------- 1 | import {ReactElement} from 'react'; 2 | import {renderToStaticMarkup} from 'react-dom/server'; 3 | 4 | const renderString = (node: React.ReactNode): string => { 5 | return typeof node === 'string' 6 | ? node 7 | : renderToStaticMarkup(node as ReactElement); 8 | }; 9 | 10 | export default renderString; 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/cjs", 4 | "noImplicitAny": true, 5 | "moduleResolution": "node", 6 | "target": "es6", 7 | "module": "es6", 8 | "jsx": "react", 9 | "declaration": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true 12 | }, 13 | "include": ["./src"], 14 | "exclude": ["./src/__tests__", "./src/test-setup.ts", "./src/config.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-config-prettier"], 3 | "rules": { 4 | "quotemark": [true, "single", "jsx-double"], 5 | "trailing-comma": [false], 6 | "curly": [true, "ignore-same-line"], 7 | "interface-name": [true, "never-prefix"], 8 | "object-literal-sort-keys": [true], 9 | "variable-name": [true, "allow-leading-underscore", "allow-pascal-case"] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const shared = { 4 | mode: 'production', 5 | entry: { 6 | index: './src/index.ts', 7 | }, 8 | module: { 9 | rules: [ 10 | { 11 | test: /\.tsx$/, 12 | enforce: 'pre', 13 | use: [ 14 | { 15 | loader: 'tslint-loader', 16 | }, 17 | ], 18 | }, 19 | { 20 | test: /\.tsx?$/, 21 | use: 'ts-loader', 22 | }, 23 | ], 24 | }, 25 | resolve: { 26 | extensions: ['.ts', '.tsx', '.js'], 27 | }, 28 | }; 29 | 30 | module.exports = [ 31 | { 32 | target: 'node', 33 | output: { 34 | path: path.resolve(__dirname, 'dist/cjs'), 35 | filename: '[name].js', 36 | libraryTarget: 'commonjs2', 37 | }, 38 | externals: { 39 | react: 'react', 40 | 'react-dom/server': 'react-dom/server', 41 | }, 42 | ...shared, 43 | }, 44 | { 45 | output: { 46 | path: path.resolve(__dirname, 'dist/umd'), 47 | filename: '[name].js', 48 | libraryTarget: 'umd', 49 | library: 'ReactCodePenPrefillEmbed', 50 | }, 51 | externals: { 52 | react: 'React', 53 | 'react-dom/server': 'ReactDOMServer', 54 | }, 55 | ...shared, 56 | }, 57 | ]; 58 | --------------------------------------------------------------------------------