├── .gitignore ├── LICENSE ├── README.md ├── package.json └── src ├── format.js ├── get-data.js ├── index.js └── open-codepen.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2020 Yuan Chuan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # codepen-prefill 2 | 3 | The live editor of [CodePen](https://codepen.io/) is not stable for me most of the time due to the network (in China...), so I always write demos on my local machine first, 4 | then open CodePen afterwards and copy/paste into the editor. 5 | 6 | This tool is for saving this process by doing the following steps: 7 | 8 | 1. Extract *external* or *embedded* **scripts** and **styles** from a local HTML file. 9 | 2. Open a new CodePen editor with default browser. 10 | 3. Prefill each HTML/JS/CSS section and external depencencies in the editor automatically. 11 | 12 | The rest is to click the **SAVE** button. 13 | 14 | ## Installation 15 | 16 | ```bash 17 | $ npm install -g codepen-prefill 18 | ``` 19 | 20 | ## Example 21 | 22 | ```bash 23 | $ codepen-prefill index.html 24 | ``` 25 | 26 | It can be used to preview a `markdown` file quickly on CodePen: 27 | 28 | ```bash 29 | $ codepen-prefill README.md 30 | ``` 31 | 32 | Edit a JS file on CodePen: 33 | 34 | ```bash 35 | $ codepen-prefill example.js 36 | ``` 37 | Using [npx](https://blog.npmjs.org/post/162869356040/introducing-npx-an-npm-package-runner) which is a package runner bundled in `npm`: 38 | 39 | ```bash 40 | $ npx codepen-prefill index.html 41 | ``` 42 | 43 | ## Options 44 | 45 | #### --keep-embedded, --embed 46 | 47 | By default all the **embedded** scripts/styles in HTML will be put into JS/CSS sections seperatly, 48 | but they can stay with HTML using `--keep-embedded` or `--embed` option. 49 | 50 | #### --data 51 | 52 | Output the prefiled data instead of open CodePen. 53 | 54 | 55 | ## References 56 | 57 | [https://blog.codepen.io/documentation/prefill](https://blog.codepen.io/documentation/prefill/) 58 | 59 | 60 | ## Usage 61 | 62 | ``` 63 | Usage: 64 | codepen-prefill 65 | 66 | Options: 67 | --keep-embedded: Keep embedded styles/scripts inside html (alias: --embed) 68 | --data: Output the prefilled data 69 | --help: Display help info 70 | 71 | Supported filename types by extension: 72 | htm, html, md, markdown, js, ts, css, less, sass, scss, styl 73 | ``` 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codepen-prefill", 3 | "version": "1.2.0", 4 | "description": "Create new pen from local HTML/JS/CSS files with ease", 5 | "repository": { 6 | "url": "git://github.com/yuanchuan/codepen-prefill.git", 7 | "type": "git" 8 | }, 9 | "bin": { 10 | "codepen-prefill": "src/index.js" 11 | }, 12 | "scripts": { 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "author": "yuanchuan (https://yuanchuan.dev)", 16 | "homepage": "https://github.com/yuanchuan/codepen-prefill", 17 | "bugs": { 18 | "url": "https://github.com/yuanchuan/codepen-prefill/issues" 19 | }, 20 | "license": "MIT", 21 | "dependencies": { 22 | "cheerio": "^1.0.0-rc.3", 23 | "open": "^7.3.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/format.js: -------------------------------------------------------------------------------- 1 | function trimEmptyLines(input) { 2 | return input.replace(/^\n+|\n+$/g, ''); 3 | } 4 | 5 | function normalizeIndent(input) { 6 | let temp = input.replace(/^\n+/g, ''); 7 | let len = temp.length - temp.replace(/^\s+/g, '').length; 8 | let result = input.split('\n').map(n => ( 9 | n.replace(new RegExp(`^\\s{${len}}`, 'g'), '') 10 | )); 11 | return result.join('\n').trim(); 12 | } 13 | 14 | module.exports = { 15 | trimEmptyLines, 16 | normalizeIndent 17 | } 18 | -------------------------------------------------------------------------------- /src/get-data.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const cheerio = require('cheerio'); 3 | const URL = require('url'); 4 | const format = require('./format'); 5 | 6 | const EXTERNAL = 'EXTERNAL'; 7 | const LOCAL = 'LOCAL'; 8 | const EMBEDDED = 'EMBEDDED'; 9 | 10 | const LINE = '\n\n'; 11 | 12 | function getType(url) { 13 | if (typeof url === "undefined" || !url.length) return EMBEDDED; 14 | if (/^http|^\/{2}.+/.test(url)) return EXTERNAL; 15 | return LOCAL; 16 | } 17 | 18 | function getProcessor($elem) { 19 | let attr = $elem.attr('type') || ''; 20 | let [text, type = text] = String(attr).split('/'); 21 | let processors = { 22 | less: 'less', 23 | scss: 'scss', 24 | sass: 'sass', 25 | stylus: 'stylus', 26 | 27 | coffeescript: 'coffeescript', 28 | livescript: 'livescript', 29 | typescript: 'typescript', 30 | babel: 'babel', 31 | 32 | // babel for ES module 33 | module: 'babel', 34 | }; 35 | return processors[type] || ''; 36 | } 37 | 38 | function getUrl($elem) { 39 | let tagName = $elem.get(0).tagName; 40 | let name = tagName == 'script' ? 'src' : 'href'; 41 | return $elem.attr(name); 42 | } 43 | 44 | function getLocalFile(url) { 45 | try { 46 | return fs.readFileSync(url, 'utf8'); 47 | } catch (e) { 48 | return ''; 49 | } 50 | } 51 | 52 | function unique(array) { 53 | return array.filter((v, i) => array.indexOf(v) === i); 54 | } 55 | 56 | function getContent($, $elements, options) { 57 | let external = []; 58 | let embedded = []; 59 | let cssPreprocessor = 'none'; 60 | let jsPreprocessor = 'none'; 61 | let $body = $('body'); 62 | let $head = $('
').prependTo($body); 63 | 64 | $elements.each(function() { 65 | let $elem = $(this); 66 | let url = getUrl($elem); 67 | let type = getType(url); 68 | 69 | if (type === EXTERNAL) { 70 | external.push(url); 71 | $elem.remove(); 72 | } 73 | 74 | else if (type === LOCAL) { 75 | let { pathname } = URL.parse(url); 76 | let result = getLocalFile(pathname); 77 | embedded.push(result); 78 | $elem.remove(); 79 | } 80 | 81 | else if (type === EMBEDDED) { 82 | let tag = $elem.get(0).tagName; 83 | 84 | if (options.keepEmbedded) { 85 | $cloned = $elem.clone(); 86 | 87 | if (tag === 'style') { 88 | $head.append($cloned + LINE); 89 | } else { 90 | $body.append($cloned + LINE); 91 | } 92 | } else { 93 | let result = $elem.html(); 94 | if (result.length) { 95 | embedded.push(result); 96 | } 97 | let attrType = getProcessor($elem); 98 | if (attrType) { 99 | if (tag === 'style' && cssPreprocessor === 'none') { 100 | cssPreprocessor = attrType; 101 | } 102 | if (tag === 'script' && jsPreprocessor === 'none') { 103 | jsPreprocessor = attrType; 104 | } 105 | } 106 | } 107 | $elem.remove(); 108 | } 109 | }); 110 | 111 | $head.replaceWith($head.html()); 112 | 113 | return { 114 | external: unique(external).join(';'), 115 | embedded: unique(embedded).join(LINE), 116 | cssPreprocessor, 117 | jsPreprocessor 118 | } 119 | } 120 | 121 | function getEditorsFlag({ html, css, js }) { 122 | let flag = [0,0,0]; 123 | if (html.length) flag[0] = 1; 124 | if (css.length) flag[1] = 1; 125 | if (js.length) flag[2] = 1; 126 | return flag.join(''); 127 | } 128 | 129 | function getHTMLData(input, options = {}) { 130 | let $ = cheerio.load(input, { decodeEntities: false }); 131 | let $body = $('body'); 132 | 133 | let title = $('title').eq(0).text(); 134 | let description = $('meta[name="description"]').eq(0).attr('content') || ''; 135 | let $styles = $('style, link[rel="stylesheet"]'); 136 | 137 | let $scripts = $('script').filter(function() { 138 | let $elem = $(this); 139 | let processor = getProcessor($elem); 140 | if (processor) { 141 | return true; 142 | } 143 | // scripts like shader/fragment 144 | else { 145 | $body.append($elem); 146 | } 147 | }); 148 | 149 | let scriptContent = getContent($, $scripts, options); 150 | let styleContent = getContent($, $styles, options); 151 | 152 | let html = format.trimEmptyLines($body.html()); 153 | let css = format.normalizeIndent(styleContent.embedded); 154 | let js = format.normalizeIndent(format.trimEmptyLines(scriptContent.embedded)); 155 | let css_external = styleContent.external; 156 | let js_external = scriptContent.external; 157 | let js_pre_processor = scriptContent.jsPreprocessor; 158 | let css_pre_processor = styleContent.cssPreprocessor; 159 | 160 | let editors = getEditorsFlag({ html, css, js }); 161 | 162 | return { 163 | title, 164 | description, 165 | editors, 166 | html, 167 | css, 168 | js, 169 | css_external, 170 | js_external, 171 | js_pre_processor, 172 | css_pre_processor 173 | } 174 | } 175 | 176 | function getCSSData(input, options = {}) { 177 | let extname = options.extname; 178 | let preProccessors = { 179 | less: 'less', 180 | scss: 'scss', 181 | sass: 'sass', 182 | styl: 'stylus' 183 | }; 184 | return { 185 | editors: '010', 186 | css: input, 187 | css_pre_processor: preProccessors[extname] || 'none' 188 | } 189 | } 190 | 191 | function getJSData(input) { 192 | return { 193 | editors: '001', 194 | js: input 195 | } 196 | } 197 | 198 | function getTSData(input) { 199 | return { 200 | editors: '001', 201 | js: input, 202 | js_pre_processor: 'typescript' 203 | } 204 | } 205 | 206 | function getMarkdownData(input) { 207 | return { 208 | editors: '100', 209 | html: input, 210 | html_pre_processor: 'markdown' 211 | } 212 | } 213 | 214 | function getData(content, options = {}) { 215 | let data, error; 216 | let extname = String(options.extname).toLowerCase(); 217 | switch (extname) { 218 | case 'htm': 219 | case 'html': { 220 | data = getHTMLData(content, options); 221 | break; 222 | } 223 | case 'md': 224 | case 'markdown': { 225 | data = getMarkdownData(content); 226 | break; 227 | } 228 | case 'js': { 229 | data = getJSData(content); 230 | break; 231 | } 232 | case 'ts': { 233 | data = getTSData(content); 234 | break; 235 | } 236 | case 'css': 237 | case 'less': 238 | case 'scss': 239 | case 'sass': 240 | case 'styl': { 241 | data = getCSSData(content, { extname }); 242 | break; 243 | } 244 | default: { 245 | error = new Error(`Unsupported file type: ${ extname }`); 246 | } 247 | } 248 | return { data, error } 249 | } 250 | 251 | module.exports = getData; 252 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const getData = require('./get-data'); 6 | const openCodePen = require('./open-codepen'); 7 | 8 | let { inputFileName, inputOptions } = getInputArgs(); 9 | 10 | if (typeof inputFileName === 'undefined') { 11 | help(); 12 | exit(); 13 | } 14 | 15 | const source = path.resolve(inputFileName); 16 | const extname = path.extname(source).substr(1); 17 | 18 | let sourceContent = ''; 19 | try { 20 | sourceContent = fs.readFileSync(source, 'utf8'); 21 | } catch (error) { 22 | let message = error.message; 23 | if (error.code == 'ENOENT') { 24 | message = `Can not open '${ inputFileName }'.` 25 | } 26 | exit(message); 27 | } 28 | 29 | const { data, error } = getData(sourceContent, { 30 | extname, 31 | keepEmbedded: inputOptions.includes('--keep-embedded') || inputOptions.includes('--embed') 32 | }); 33 | 34 | if (error) { 35 | exit(error.message); 36 | } 37 | 38 | if (inputOptions.includes('--data')) { 39 | console.log(data); 40 | exit(); 41 | } 42 | 43 | openCodePen(data, error => { 44 | if (error) { 45 | exit(error.message); 46 | } 47 | }); 48 | 49 | function getInputArgs() { 50 | let input = process.argv.slice(2); 51 | return { 52 | inputFileName: input.find(n => !n.startsWith('--')), 53 | inputOptions: input.filter(n => n.startsWith('--')), 54 | } 55 | } 56 | 57 | function help() { 58 | console.log(` 59 | Usage: 60 | codepen 61 | 62 | Options: 63 | --keep-embedded: Keep embedded styles/scripts inside html (alias: --embed) 64 | --data: Output the prefilled data 65 | --help: Display help info 66 | 67 | Supported filename types by extension: 68 | htm, html, md, markdown, js, ts, css, less, sass, scss, styl 69 | `); 70 | } 71 | 72 | function exit(message) { 73 | if (message) { 74 | console.error('\n', message); 75 | } 76 | process.exit(); 77 | } 78 | -------------------------------------------------------------------------------- /src/open-codepen.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const os = require('os'); 3 | const path = require('path'); 4 | const open = require('open'); 5 | 6 | let tmpdir = process.env.TMPDIR || os.tmpdir(); 7 | // Cleanup only when the system tmpdir is unavailable 8 | let needCleanup = !tmpdir; 9 | 10 | if (!tmpdir) { 11 | tmpdir = __dirname; 12 | } 13 | 14 | function composeHTML(data) { 15 | let value = JSON.stringify(data) 16 | .replace(/"/g, """) 17 | .replace(/'/g, "'"); 18 | return ` 19 | 31 | 32 |
33 | 34 |
35 | 36 |

Redirecting to CodePen...

37 | 38 | 43 | `; 44 | } 45 | 46 | function openCodePen(data, fn = function() {}) { 47 | let filename = path.join(tmpdir, Date.now() + '.html'); 48 | try { 49 | fs.writeFileSync(filename, composeHTML(data)); 50 | } catch (error) { 51 | return fn(error); 52 | } 53 | 54 | open(filename) 55 | .then(() => { 56 | if (needCleanup) { 57 | setTimeout(() => { 58 | try { fs.unlinkSync(filename) } catch (e) {} 59 | fn(); 60 | }, 3000); 61 | } else { 62 | fn(); 63 | } 64 | }) 65 | .catch(fn); 66 | } 67 | 68 | module.exports = openCodePen; 69 | --------------------------------------------------------------------------------