├── .circleci └── config.yml ├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── add-transformer.md └── embed.md ├── index.ejs ├── package.json ├── poi.config.js ├── src ├── boilerplates │ ├── es-import │ │ ├── codepan.js │ │ └── index.js │ ├── hyperapp │ │ ├── codepan.html │ │ ├── codepan.js │ │ └── index.js │ ├── pixi │ │ ├── codepan.html │ │ ├── codepan.js │ │ └── index.js │ ├── preact │ │ ├── codepan.html │ │ ├── codepan.js │ │ └── index.js │ ├── react │ │ ├── codepan.html │ │ ├── codepan.js │ │ └── index.js │ ├── rust │ │ ├── codepan.rs │ │ └── index.js │ ├── rxjs │ │ ├── codepan.html │ │ ├── codepan.js │ │ └── index.js │ ├── vue-jsx │ │ ├── codepan.css │ │ ├── codepan.html │ │ ├── codepan.js │ │ └── index.js │ └── vue │ │ ├── codepan.html │ │ ├── codepan.js │ │ └── index.js ├── components │ ├── App.vue │ ├── CSSPan.vue │ ├── CompiledCodeDialog.vue │ ├── CompiledCodeSwitcher.vue │ ├── ConsolePan.vue │ ├── HTMLPan.vue │ ├── Highlight.js │ ├── HomeHeader.vue │ ├── JSPan.vue │ ├── OutputPan.vue │ ├── PanResizer.vue │ ├── Spinner.vue │ └── SvgIcon.vue ├── index.js ├── polyfill.js ├── pwa.js ├── router │ └── index.js ├── store │ └── index.js ├── svg │ ├── alert.svg │ ├── check.svg │ ├── code.svg │ ├── export.svg │ └── loading.svg ├── utils │ ├── create-editor.js │ ├── create-pan.js │ ├── event.js │ ├── get-imports.js │ ├── get-scripts.js │ ├── github-api.js │ ├── highlight.js │ ├── iframe.js │ ├── index.js │ ├── pan-position.js │ ├── popup.js │ ├── proxy-console.js │ ├── transform.js │ ├── transformer.js │ └── vue-jsx-merge-props.js └── views │ ├── EditorPage.vue │ ├── GitHubSuccess.vue │ └── NotFound.vue ├── static ├── CNAME ├── _redirects ├── favicon-114.png ├── favicon-120.png ├── favicon-144.png ├── favicon-152.png ├── favicon-180.png ├── favicon-192.png ├── favicon-32.png ├── favicon-36.png ├── favicon-48.png ├── favicon-57.png ├── favicon-60.png ├── favicon-72.png ├── favicon-76.png ├── favicon-96.png ├── favicon.ico ├── manifest.json └── vendor │ ├── coffeescript-2.js │ ├── reason │ ├── bs.js │ └── refmt.js │ ├── sass │ ├── sass.js │ └── sass.worker.js │ └── stylus.js └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/github/codepan 5 | docker: 6 | - image: circleci/node:8.0.0 7 | branches: 8 | ignore: 9 | - gh-pages # list of branches to ignore 10 | - /release\/.*/ # or ignore regexes 11 | steps: 12 | - checkout 13 | - restore_cache: 14 | key: dependency-cache-{{ checksum "yarn.lock" }} 15 | - run: 16 | name: install dependences 17 | command: yarn 18 | - save_cache: 19 | key: dependency-cache-{{ checksum "yarn.lock" }} 20 | paths: 21 | - ./node_modules 22 | - run: 23 | name: test 24 | command: yarn test 25 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .DS_Store 4 | 5 | # produced by vbuild 6 | dist 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) EGOIST <0x142857@gmail.com> (github.com/egoist) 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CodePan 2 | 3 | [![CircleCI](https://circleci.com/gh/egoist/codepan/tree/master.svg?style=shield&circle-token=e811a08d6464123dd65d2dcd52f62806bf9e37fc)](https://circleci.com/gh/egoist/codepan/tree/master) [![chat](https://img.shields.io/badge/chat-on%20discord-7289DA.svg?style=flat)](https://chat.egoist.moe) 4 | 5 | Play with JS/CSS/HTML so simple it hurts, the web playground that works offline. 6 | 7 | ## Why 8 | 9 | > Aren't there already JSBin/CodePen/JSFiddle? 10 | 11 | Yep! So why not one more? And this one could work **offline** for you! 12 | 13 | How? `codepan` is just a single page app with **no-backend**! Built with Webpack and Vue.js, and the offline feature is provided by [offline-plugin](https://github.com/NekR/offline-plugin). 14 | 15 | ## Browser Support 16 | 17 | We aim to support latest version of Chrome, Safari, Firefox and Microsoft Edge. 18 | 19 | ## Development 20 | 21 | Clone this repository and install dependencies by running `yarn`, then: 22 | 23 | - `yarn dev`: Run in development mode 24 | - `yarn build`: Build in production mode 25 | - `yarn lint`: Run eslint 26 | 27 | ## License 28 | 29 | MIT © [EGOIST](https://github.com/egoist) 30 | -------------------------------------------------------------------------------- /docs/add-transformer.md: -------------------------------------------------------------------------------- 1 | 1. Add it to the dropdown menu of relevant editor. 2 | 2. Define the lazy-load logic in `src/utils/transformer.js` 3 | 3. Update the `updateTransformer` action in `src/store/index.js` 4 | 4. Update `getHumanlizedTransformerName` `getEditorModeByTransfomer` in `src/utils/index.js` 5 | 5. Update transform logic in `src/utils/transform.js` 6 | -------------------------------------------------------------------------------- /docs/embed.md: -------------------------------------------------------------------------------- 1 | You can embed the URL using an iframe in your website. 2 | 3 | Optionally append `?readonly` to make the editor read-only. 4 | -------------------------------------------------------------------------------- /index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= htmlWebpackPlugin.options.title %> 8 | <% if (htmlWebpackPlugin.options.description) { %> 9 | 13 | <% } %> 14 | 15 | 19 | 23 | 27 | 31 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 47 | 48 | 49 | 54 | 55 | 56 | 61 | 62 | 63 | 68 | 69 | 70 | 75 | 76 | 77 | 82 | 83 | 84 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 |
107 | 127 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "codepan", 4 | "productName": "CodePan", 5 | "description": "Play with JS/CSS/HTML so simple it hurts", 6 | "details": "CodePan is where people prototype front-end apps, you are free to use it offline anytime anywhere.", 7 | "main": "src/index.js", 8 | "homepage": "https://codepan.net/", 9 | "version": "0.1.0", 10 | "repository": {}, 11 | "scripts": { 12 | "test": "npm run lint", 13 | "lint": "xo", 14 | "dev": "poi", 15 | "build": "poi build", 16 | "deploy": "surge -p dist -d codepan.net", 17 | "predeploy": "npm run build && cp dist/index.html dist/200.html", 18 | "report": "poi build --bundle-report" 19 | }, 20 | "xo": { 21 | "parser": "babel-eslint", 22 | "extends": [ 23 | "rem" 24 | ], 25 | "envs": [ 26 | "browser" 27 | ], 28 | "extensions": [ 29 | "vue" 30 | ], 31 | "plugins": [ 32 | "html" 33 | ], 34 | "rules": { 35 | "no-new": 0, 36 | "import/no-unresolved": 0, 37 | "import/no-extraneous-dependencies": 0, 38 | "import/no-unassigned-import": 0, 39 | "no-warning-comments": 0, 40 | "import/prefer-default-export": 0, 41 | "no-multi-assign": 0, 42 | "complexity": 0, 43 | "guard-for-in": 0, 44 | "unicorn/filename-case": 0, 45 | "import/no-webpack-loader-syntax": 0, 46 | "unicorn/no-abusive-eslint-disable": 0, 47 | "no-case-declarations": 0 48 | }, 49 | "ignores": [ 50 | "src/boilerplates/**", 51 | "src/utils/vue-jsx-merge-props.js", 52 | "static/**" 53 | ] 54 | }, 55 | "devDependencies": { 56 | "babel-eslint": "^7.2.3", 57 | "babel-plugin-component": "^0.10.0", 58 | "babel-preset-babili": "^0.1.4", 59 | "buble-loader": "^0.4.1", 60 | "eslint-config-rem": "^3.2.0", 61 | "eslint-plugin-html": "^3.2.0", 62 | "gh-pages": "^1.0.0", 63 | "less": "^2.7.3", 64 | "offline-plugin": "^4.8.3", 65 | "poi": "^9.6.7", 66 | "poi-preset-babel-minify": "^1.0.3", 67 | "poi-preset-bundle-report": "^2.0.1", 68 | "poi-preset-offline": "^9.0.3", 69 | "raw-loader": "^0.5.1", 70 | "repo-latest-commit": "^1.0.0", 71 | "stylus": "^0.54.5", 72 | "stylus-loader": "^3.0.1", 73 | "surge": "^0.19.0", 74 | "webpack-node-modules": "^0.1.1", 75 | "xo": "^0.18.2" 76 | }, 77 | "license": "MIT", 78 | "dependencies": { 79 | "@babel/core": "^7.0.0-beta.32", 80 | "axios": "^0.16.2", 81 | "babel-preset-vue": "^2.0.0", 82 | "cm-highlight": "^0.1.1", 83 | "codemirror": "^5.28.0", 84 | "codemirror-emmet": "^1.0.0", 85 | "debounce": "^1.0.2", 86 | "element-ui": "^2.0.11", 87 | "is-electron": "^2.1.0", 88 | "loadjs": "^3.5.1", 89 | "marked3": "^0.5.1", 90 | "notie": "^4.3.1", 91 | "nprogress": "^0.2.0", 92 | "object-assign": "^4.1.1", 93 | "parse-package-name": "^0.1.0", 94 | "pify": "^3.0.0", 95 | "promise-polyfill": "^6.0.2", 96 | "reqjs": "^1.0.3", 97 | "v-tippy": "^1.0.0", 98 | "vue-feather-icons": "^4.5.0", 99 | "vue-ga": "^1.0.0", 100 | "vue-inline": "^1.0.1", 101 | "vue-router": "^2.7.0", 102 | "vue-slim-modal": "^1.0.4", 103 | "vuex": "^2.3.1" 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /poi.config.js: -------------------------------------------------------------------------------- 1 | const nodeModules = require('webpack-node-modules') 2 | const repoLatestCommit = require('repo-latest-commit') 3 | const pkg = require('./package') 4 | 5 | const cdns = { 6 | BABEL_CDN: 'https://cdn.jsdelivr.net/npm/@babel/standalone@7.0.0-beta.32/babel.min.js', 7 | PUG_CDN: 'https://cdn.jsdelivr.net/npm/browserified-pug@0.3.0/index.js', 8 | CSSNEXT_CDN: 'https://cdn.jsdelivr.net/npm/browserified-postcss-cssnext@0.3.0/index.js', 9 | POSTCSS_CDN: 'https://cdn.jsdelivr.net/npm/browserified-postcss@0.3.0/index.js', 10 | TYPESCRIPT_CDN: 'https://cdn.jsdelivr.net/npm/browserified-typescript@0.3.0/index.js' 11 | } 12 | 13 | module.exports = { 14 | extendWebpack(config) { 15 | config.module.set('noParse', /babel-preset-vue/) 16 | 17 | config.module.rule('js') 18 | .include 19 | .add(nodeModules()) 20 | 21 | config.node.set('fs', 'empty') 22 | 23 | config.externals({ 24 | electron: 'commonjs electron' 25 | }) 26 | }, 27 | production: { 28 | sourceMap: false 29 | }, 30 | hash: false, 31 | homepage: '/', 32 | env: Object.assign({ 33 | VERSION: `v${pkg.version}-${repoLatestCommit().commit.slice(0, 7)}`, 34 | LATEST_COMMIT: repoLatestCommit().commit.slice(0, 7) 35 | }, cdns), 36 | presets: [ 37 | require('poi-preset-bundle-report')(), 38 | require('poi-preset-babel-minify')(), 39 | require('poi-preset-offline')({ 40 | pluginOptions: { 41 | version: '[hash]', 42 | autoUpdate: true, 43 | safeToUseOptionalCaches: true, 44 | caches: { 45 | main: ['index.html', 'client.*', 'vendor.*', 'editor-page.chunk.js'], 46 | additional: ['*.chunk.js', ':externals:'], 47 | optional: [':rest:'] 48 | }, 49 | ServiceWorker: { 50 | events: true, 51 | navigateFallbackURL: '/' 52 | }, 53 | AppCache: { 54 | events: true, 55 | FALLBACK: { '/': '/' } 56 | }, 57 | externals: [].concat(Object.keys(cdns).reduce((res, name) => { 58 | return res.concat(cdns[name]) 59 | }, [])) 60 | } 61 | }) 62 | ], 63 | babel: { 64 | babelrc: false, 65 | presets: [ 66 | require.resolve('babel-preset-poi') 67 | ], 68 | plugins: [[require.resolve('babel-plugin-component'), [ 69 | { 70 | libraryName: 'element-ui', 71 | styleLibraryName: 'theme-chalk' 72 | } 73 | ]]] 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/boilerplates/es-import/codepan.js: -------------------------------------------------------------------------------- 1 | import babel from '@babel/core' 2 | import env from '@babel/preset-env' 3 | 4 | const { code } = babel.transform(` 5 | class Foo { 6 | bar() {} 7 | } 8 | `, { 9 | presets: [env] 10 | }) 11 | 12 | console.log(code) 13 | -------------------------------------------------------------------------------- /src/boilerplates/es-import/index.js: -------------------------------------------------------------------------------- 1 | export default async () => { 2 | const jsCode = await import('!raw-loader!./codepan.js') 3 | 4 | return { 5 | js: { 6 | code: jsCode, 7 | transformer: 'babel' 8 | }, 9 | showPans: ['js', 'console'] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/boilerplates/hyperapp/codepan.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/boilerplates/hyperapp/codepan.js: -------------------------------------------------------------------------------- 1 | /* @jsx h */ 2 | const { h, app } = hyperapp 3 | 4 | const state = { 5 | count: 0 6 | } 7 | 8 | const actions = { 9 | down: () => state => ({ count: state.count - 1 }), 10 | up: () => state => ({ count: state.count + 1 }) 11 | } 12 | 13 | const view = (state, actions) => ( 14 |
15 |

{state.count}

16 | 17 | 18 |
19 | ) 20 | 21 | const main = app(state, actions, view, document.body) 22 | -------------------------------------------------------------------------------- /src/boilerplates/hyperapp/index.js: -------------------------------------------------------------------------------- 1 | export default async () => { 2 | const [htmlCode, jsCode] = await Promise.all([ 3 | import('!raw-loader!./codepan.html'), 4 | import('!raw-loader!./codepan.js') 5 | ]) 6 | 7 | return { 8 | html: { 9 | code: htmlCode, 10 | transformer: 'html' 11 | }, 12 | js: { 13 | code: jsCode, 14 | transformer: 'babel' 15 | }, 16 | showPans: ['js', 'output'] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/boilerplates/pixi/codepan.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/boilerplates/pixi/codepan.js: -------------------------------------------------------------------------------- 1 | const sprite = createSprite() 2 | const app = createApp() 3 | 4 | function createSprite() { 5 | const sprite = PIXI.Sprite.from('https://pixijs.io/examples/examples/assets/bunny.png') 6 | sprite.anchor.set(0.5) 7 | sprite.scale.set(3) 8 | return sprite 9 | } 10 | 11 | function createApp() { 12 | return new PIXI.Tiled.FullscreenApplication(tick, { 13 | backgroundColor: 0xffffff 14 | }) 15 | } 16 | 17 | function tick(time) { 18 | sprite.position.set(innerWidth / 2, innerHeight / 2) 19 | sprite.rotation = time / 100 20 | } 21 | 22 | app.stage.addChild(sprite) 23 | -------------------------------------------------------------------------------- /src/boilerplates/pixi/index.js: -------------------------------------------------------------------------------- 1 | export default async () => { 2 | const [htmlCode, jsCode] = await Promise.all([ 3 | import('!raw-loader!./codepan.html'), 4 | import('!raw-loader!./codepan.js') 5 | ]) 6 | 7 | return { 8 | js: { 9 | code: jsCode, 10 | transformer: 'js' 11 | }, 12 | html: { 13 | code: htmlCode, 14 | transformer: 'html' 15 | }, 16 | showPans: ['js', 'output'] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/boilerplates/preact/codepan.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/boilerplates/preact/codepan.js: -------------------------------------------------------------------------------- 1 | /* @jsx h */ 2 | const { h, render } = preact 3 | const { useState } = preactHooks 4 | 5 | const App = () => { 6 | const [count, setCount] = useState(0) 7 | 8 | const inc = () => setCount(count + 1) 9 | 10 | const dec = () => setCount(count - 1) 11 | 12 | return ( 13 |
14 |

{count}

15 | 16 | 17 |
18 | ) 19 | } 20 | 21 | render(, document.body) 22 | -------------------------------------------------------------------------------- /src/boilerplates/preact/index.js: -------------------------------------------------------------------------------- 1 | export default async () => { 2 | const [htmlCode, jsCode] = await Promise.all([ 3 | import('!raw-loader!./codepan.html'), 4 | import('!raw-loader!./codepan.js') 5 | ]) 6 | 7 | return { 8 | html: { 9 | code: htmlCode, 10 | transformer: 'html' 11 | }, 12 | js: { 13 | code: jsCode, 14 | transformer: 'babel' 15 | }, 16 | showPans: ['js', 'output'] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/boilerplates/react/codepan.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | -------------------------------------------------------------------------------- /src/boilerplates/react/codepan.js: -------------------------------------------------------------------------------- 1 | class App extends React.Component { 2 | state = { 3 | count: 0 4 | } 5 | 6 | inc = () => this.setState({ 7 | count: this.state.count + 1 8 | }) 9 | 10 | dec = () => this.setState({ 11 | count: this.state.count - 1 12 | }) 13 | 14 | render() { 15 | return ( 16 |
17 |

{ this.state.count }

18 | 19 | 20 |
21 | ) 22 | } 23 | } 24 | 25 | ReactDOM.render(, document.getElementById('app')) 26 | -------------------------------------------------------------------------------- /src/boilerplates/react/index.js: -------------------------------------------------------------------------------- 1 | export default async () => { 2 | const [htmlCode, jsCode] = await Promise.all([ 3 | import('!raw-loader!./codepan.html'), 4 | import('!raw-loader!./codepan.js') 5 | ]) 6 | 7 | return { 8 | html: { 9 | code: htmlCode, 10 | transformer: 'html' 11 | }, 12 | js: { 13 | code: jsCode, 14 | transformer: 'babel' 15 | }, 16 | showPans: ['js', 'output'] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/boilerplates/rust/codepan.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let greetings = ["Hello", "Hola", "Bonjour", 3 | "Ciao", "こんにちは", "안녕하세요", 4 | "Cześć", "Olá", "Здравствуйте", 5 | "Chào bạn", "您好", "Hallo", 6 | "Hej", "Ahoj", "سلام"]; 7 | 8 | for (num, greeting) in greetings.iter().enumerate() { 9 | print!("{} : ", greeting); 10 | match num { 11 | 0 => println!("This code is editable and runnable!"), 12 | 1 => println!("¡Este código es editable y ejecutable!"), 13 | 2 => println!("Ce code est modifiable et exécutable !"), 14 | 3 => println!("Questo codice è modificabile ed eseguibile!"), 15 | 4 => println!("このコードは編集して実行出来ます!"), 16 | 5 => println!("여기에서 코드를 수정하고 실행할 수 있습니다!"), 17 | 6 => println!("Ten kod można edytować oraz uruchomić!"), 18 | 7 => println!("Este código é editável e executável!"), 19 | 8 => println!("Этот код можно отредактировать и запустить!"), 20 | 9 => println!("Bạn có thể edit và run code trực tiếp!"), 21 | 10 => println!("这段代码是可以编辑并且能够运行的!"), 22 | 11 => println!("Dieser Code kann bearbeitet und ausgeführt werden!"), 23 | 12 => println!("Den här koden kan redigeras och köras!"), 24 | 13 => println!("Tento kód můžete upravit a spustit"), 25 | 14 => println!("این کد قابلیت ویرایش و اجرا دارد!"), 26 | _ => {}, 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/boilerplates/rust/index.js: -------------------------------------------------------------------------------- 1 | export default async () => { 2 | return { 3 | js: { 4 | code: await import('!raw-loader!./codepan.rs'), 5 | transformer: 'rust' 6 | }, 7 | showPans: ['js', 'console'] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/boilerplates/rxjs/codepan.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/boilerplates/rxjs/codepan.js: -------------------------------------------------------------------------------- 1 | /* 2 | timer takes a second argument, how often to emit subsequent values 3 | in this case we will emit first value after 1 second and subsequent 4 | values every 2 seconds after 5 | */ 6 | const source = Rx.Observable.timer(1000, 2000); 7 | //output: 0,1,2,3,4,5...... 8 | const subscribe = source.subscribe(val => console.log(val)); 9 | -------------------------------------------------------------------------------- /src/boilerplates/rxjs/index.js: -------------------------------------------------------------------------------- 1 | export default async () => { 2 | const [htmlCode, jsCode] = await Promise.all([ 3 | import('!raw-loader!./codepan.html'), 4 | import('!raw-loader!./codepan.js') 5 | ]) 6 | 7 | return { 8 | js: { 9 | code: jsCode, 10 | transformer: 'js' 11 | }, 12 | html: { 13 | code: htmlCode, 14 | transformer: 'html' 15 | }, 16 | showPans: ['js', 'console'] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/boilerplates/vue-jsx/codepan.css: -------------------------------------------------------------------------------- 1 | .button { 2 | color: white; 3 | border: 1px solid #e2e2e2; 4 | background: magenta; 5 | padding: 20px 0; 6 | font-size: 2rem; 7 | width: 200px; 8 | } 9 | 10 | .counter { 11 | margin-top: 20px; 12 | } 13 | -------------------------------------------------------------------------------- /src/boilerplates/vue-jsx/codepan.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | -------------------------------------------------------------------------------- /src/boilerplates/vue-jsx/codepan.js: -------------------------------------------------------------------------------- 1 | new Vue({ 2 | el: '#app', 3 | data: { count: 0 }, 4 | methods: { 5 | inc() { 6 | this.count++ 7 | }, 8 | dec() { 9 | this.count-- 10 | } 11 | }, 12 | render() { 13 | return ( 14 |
15 |

{this.count}

16 | 17 | 18 |
19 | ) 20 | } 21 | }) 22 | -------------------------------------------------------------------------------- /src/boilerplates/vue-jsx/index.js: -------------------------------------------------------------------------------- 1 | export default async () => { 2 | const [htmlCode, jsCode, cssCode] = await Promise.all([ 3 | import(/* webpackChunkName: "boilerplate-vue-jsx" */ '!raw-loader!./codepan.html'), 4 | import(/* webpackChunkName: "boilerplate-vue-jsx" */'!raw-loader!./codepan.js'), 5 | import(/* webpackChunkName: "boilerplate-vue-jsx" */'!raw-loader!./codepan.css') 6 | ]) 7 | 8 | return { 9 | js: { 10 | code: jsCode, 11 | transformer: 'vue-jsx' 12 | }, 13 | html: { 14 | code: htmlCode, 15 | transformer: 'html' 16 | }, 17 | css: { 18 | code: cssCode, 19 | transformer: 'css' 20 | }, 21 | showPans: ['js', 'output'] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/boilerplates/vue/codepan.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |

{{ count }}

5 | 6 | 7 |
8 | -------------------------------------------------------------------------------- /src/boilerplates/vue/codepan.js: -------------------------------------------------------------------------------- 1 | const { createApp, h, ref } = Vue 2 | 3 | const app = createApp({ 4 | setup() { 5 | const count = ref(0) 6 | const inc = () => count.value++ 7 | const dec = () => count.value-- 8 | return { 9 | count, 10 | inc, 11 | dec 12 | } 13 | } 14 | }) 15 | 16 | app.mount('#app') 17 | -------------------------------------------------------------------------------- /src/boilerplates/vue/index.js: -------------------------------------------------------------------------------- 1 | export default async () => { 2 | const [htmlCode, jsCode] = await Promise.all([ 3 | import('!raw-loader!./codepan.html'), 4 | import('!raw-loader!./codepan.js') 5 | ]) 6 | 7 | return { 8 | js: { 9 | code: jsCode, 10 | transformer: 'vue-jsx' 11 | }, 12 | html: { 13 | code: htmlCode, 14 | transformer: 'html' 15 | }, 16 | showPans: ['html', 'js', 'output'] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 86 | -------------------------------------------------------------------------------- /src/components/CSSPan.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 41 | -------------------------------------------------------------------------------- /src/components/CompiledCodeDialog.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 63 | 64 | 87 | -------------------------------------------------------------------------------- /src/components/CompiledCodeSwitcher.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 30 | -------------------------------------------------------------------------------- /src/components/ConsolePan.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 90 | 91 | 105 | -------------------------------------------------------------------------------- /src/components/HTMLPan.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 37 | -------------------------------------------------------------------------------- /src/components/Highlight.js: -------------------------------------------------------------------------------- 1 | import highlight from 'cm-highlight' 2 | 3 | export default { 4 | name: 'highlight', 5 | functional: true, 6 | render(h, ctx) { 7 | const { theme = 'default', mode = 'javascript' } = ctx.props 8 | const code = highlight(ctx.props.code || ctx.children[0].text, { mode }) 9 | return
10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/HomeHeader.vue: -------------------------------------------------------------------------------- 1 | 191 | 192 | 362 | 363 | 462 | -------------------------------------------------------------------------------- /src/components/JSPan.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 43 | -------------------------------------------------------------------------------- /src/components/OutputPan.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 318 | 319 | 331 | -------------------------------------------------------------------------------- /src/components/PanResizer.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 91 | 92 | 106 | -------------------------------------------------------------------------------- /src/components/Spinner.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 26 | 27 | 28 | 62 | -------------------------------------------------------------------------------- /src/components/SvgIcon.vue: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import './polyfill' 2 | import Vue from 'vue' 3 | import Tippy from 'v-tippy' 4 | // @ is the path to `./src` folder 5 | import App from '@/components/App' 6 | import router from '@/router' 7 | import store from '@/store' 8 | 9 | Vue.config.productionTip = false 10 | 11 | Vue.use(Tippy, { 12 | position: 'bottom' 13 | }) 14 | 15 | new Vue({ 16 | el: '#app', 17 | router, 18 | store, 19 | render: h => h(App) 20 | }) 21 | 22 | if (process.env.NODE_ENV === 'production') { 23 | require('./pwa') 24 | } 25 | -------------------------------------------------------------------------------- /src/polyfill.js: -------------------------------------------------------------------------------- 1 | import Promise from 'promise-polyfill' 2 | 3 | if (!window.Promise) { 4 | window.Promise = Promise 5 | } 6 | 7 | Object.assign = require('object-assign') 8 | -------------------------------------------------------------------------------- /src/pwa.js: -------------------------------------------------------------------------------- 1 | import runtime from 'offline-plugin/runtime' 2 | import { Notification } from 'element-ui' 3 | 4 | runtime.install({ 5 | onUpdateReady() { 6 | runtime.applyUpdate() 7 | }, 8 | onUpdated() { 9 | console.info('Reload this page to apply updates!') 10 | // eslint-disable-next-line new-cap 11 | Notification({ 12 | title: 'CodePan has been updated!', 13 | message: 'Tap this or refresh page to apply updates.', 14 | duration: 10000, 15 | type: 'success', 16 | customClass: 'update-notifier', 17 | onClick() { 18 | window.location.reload() 19 | } 20 | }) 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import progress from 'nprogress' 4 | import ga from 'vue-ga' 5 | 6 | Vue.use(Router) 7 | 8 | const EditorPage = () => import(/* webpackChunkName: "editor-page" */ '@/views/EditorPage.vue') 9 | const NotFound = () => import(/* webpackChunkName: "not-found-page" */ '@/views/NotFound.vue') 10 | const GitHubSuccess = () => import(/* webpackChunkName: "ghlogin-result" */ '@/views/GitHubSuccess.vue') 11 | 12 | const router = new Router({ 13 | mode: 'history', 14 | routes: [ 15 | { 16 | name: 'home', 17 | path: '/', 18 | component: EditorPage 19 | }, 20 | { 21 | name: 'gist', 22 | path: '/gist/:gist', 23 | component: EditorPage 24 | }, 25 | { 26 | name: 'boilerplate', 27 | path: '/boilerplate/:boilerplate', 28 | component: EditorPage 29 | }, 30 | { 31 | name: 'github-success', 32 | path: '/github_success', 33 | component: GitHubSuccess 34 | }, 35 | { 36 | path: '*', 37 | component: NotFound 38 | } 39 | ] 40 | }) 41 | 42 | ga(router, 'UA-54857209-13') 43 | 44 | router.beforeEach((to, from, next) => { 45 | progress.start() 46 | next() 47 | }) 48 | 49 | export default router 50 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import { 4 | loadBabel, 5 | loadPug, 6 | loadMarkdown, 7 | loadReason, 8 | loadCoffeeScript2, 9 | loadCssnext, 10 | loadLess, 11 | loadSass, 12 | loadRust, 13 | loadTypescript, 14 | loadStylus 15 | } from '@/utils/transformer' 16 | import progress from 'nprogress' 17 | import api from '@/utils/github-api' 18 | import req from 'reqjs' 19 | import Event from '@/utils/event' 20 | 21 | Vue.use(Vuex) 22 | 23 | const pans = ['html', 'css', 'js', 'console', 'output'] 24 | const sortPans = ps => { 25 | return ps.sort((a, b) => { 26 | return pans.indexOf(a) > pans.indexOf(b) 27 | }) 28 | } 29 | 30 | const emptyPans = () => ({ 31 | js: { 32 | code: '', 33 | transformer: 'js' 34 | }, 35 | css: { 36 | code: '', 37 | transformer: 'css' 38 | }, 39 | html: { 40 | code: '', 41 | transformer: 'html' 42 | } 43 | }) 44 | 45 | const getFileNameByLang = { 46 | html: 'index.html', 47 | js: 'script.js', 48 | css: 'style.css' 49 | } 50 | 51 | // Load entries of all boilerplates 52 | const boilerplates = { 53 | empty: async () => ({ 54 | ...emptyPans(), 55 | showPans: ['html', 'js', 'output'] 56 | }) 57 | } 58 | function importAll(r) { 59 | r.keys().forEach(key => { 60 | const name = /^\.\/(.+)\//.exec(key)[1] 61 | boilerplates[name] = r(key).default 62 | }) 63 | } 64 | importAll(require.context('@/boilerplates', true, /index.js$/)) 65 | 66 | const store = new Vuex.Store({ 67 | state: { 68 | ...emptyPans(), 69 | logs: [], 70 | visiblePans: ['html', 'js', 'output'], 71 | activePan: 'js', 72 | autoRun: false, 73 | githubToken: localStorage.getItem('codepan:gh-token') || '', 74 | gistMeta: {}, 75 | userMeta: JSON.parse(localStorage.getItem('codepan:user-meta')) || {}, 76 | editorStatus: 'saved', 77 | iframeStatus: null, 78 | transforming: false 79 | }, 80 | mutations: { 81 | UPDATE_CODE(state, { type, code }) { 82 | state[type].code = code 83 | }, 84 | UPDATE_TRANSFORMER(state, { type, transformer }) { 85 | state[type].transformer = transformer 86 | }, 87 | ADD_LOG(state, log) { 88 | state.logs.push(log) 89 | }, 90 | CLEAR_LOGS(state) { 91 | state.logs = [] 92 | }, 93 | TOGGLE_PAN(state, pan) { 94 | const pans = state.visiblePans 95 | const idx = pans.indexOf(pan) 96 | if (idx === -1) { 97 | pans.push(pan) 98 | } else { 99 | pans.splice(idx, 1) 100 | } 101 | state.visiblePans = sortPans(pans) 102 | }, 103 | SHOW_PANS(state, pans) { 104 | state.visiblePans = sortPans(pans) 105 | }, 106 | ACTIVE_PAN(state, pan) { 107 | state.activePan = pan 108 | }, 109 | SET_GIST_META(state, meta) { 110 | state.gistMeta = meta 111 | }, 112 | SET_USER_META(state, meta) { 113 | state.userMeta = meta 114 | }, 115 | SET_GITHUB_TOKEN(state, token) { 116 | state.githubToken = token 117 | }, 118 | SET_EDITOR_STATUS(state, status) { 119 | state.editorStatus = status 120 | }, 121 | SET_AUTO_RUN(state, status) { 122 | state.autoRun = status 123 | }, 124 | SET_IFRAME_STATUS(state, status) { 125 | state.iframeStatus = status 126 | }, 127 | SET_TRANSFORM(state, status) { 128 | state.transforming = status 129 | } 130 | }, 131 | actions: { 132 | updateCode({ commit }, payload) { 133 | commit('UPDATE_CODE', payload) 134 | }, 135 | updateError({ commit }, payload) { 136 | commit('UPDATE_ERROR', payload) 137 | }, 138 | addLog({ commit }, payload) { 139 | commit('ADD_LOG', payload) 140 | }, 141 | clearLogs({ commit }) { 142 | commit('CLEAR_LOGS') 143 | }, 144 | setActivePan({ commit }, pan) { 145 | commit('ACTIVE_PAN', pan) 146 | }, 147 | togglePan({ commit }, payload) { 148 | commit('TOGGLE_PAN', payload) 149 | }, 150 | showPans({ commit }, pans) { 151 | commit('SHOW_PANS', pans) 152 | }, 153 | async updateTransformer({ commit }, { type, transformer }) { 154 | if ( 155 | transformer === 'babel' || 156 | transformer === 'jsx' || // @deprecated, use "babel" 157 | transformer === 'vue-jsx' 158 | ) { 159 | await loadBabel() 160 | } else if (transformer === 'pug') { 161 | await loadPug() 162 | } else if (transformer === 'markdown') { 163 | await loadMarkdown() 164 | } else if (transformer === 'reason') { 165 | await loadReason() 166 | } else if (transformer === 'coffeescript-2') { 167 | await loadCoffeeScript2() 168 | } else if (transformer === 'cssnext') { 169 | await loadCssnext() 170 | } else if (transformer === 'less') { 171 | await loadLess() 172 | } else if (transformer === 'sass' || transformer === 'scss') { 173 | await loadSass() 174 | } else if (transformer === 'rust') { 175 | await loadRust() 176 | } else if (transformer === 'typescript') { 177 | await loadTypescript() 178 | } else if (transformer === 'stylus') { 179 | await loadStylus() 180 | } 181 | commit('UPDATE_TRANSFORMER', { type, transformer }) 182 | }, 183 | transform({ commit }, status) { 184 | commit('SET_TRANSFORM', status) 185 | }, 186 | // todo: simplify this action 187 | async setBoilerplate({ dispatch }, boilerplate) { 188 | progress.start() 189 | 190 | if (typeof boilerplate === 'string') { 191 | boilerplate = await boilerplates[boilerplate]() 192 | } 193 | 194 | const ps = [] 195 | 196 | const defaultPans = emptyPans() 197 | 198 | for (const type of ['html', 'js', 'css']) { 199 | const { code, transformer } = { 200 | code: defaultPans[type].code, 201 | transformer: defaultPans[type].transformer, 202 | ...boilerplate[type] 203 | } 204 | ps.push( 205 | dispatch('updateCode', { type, code }), 206 | dispatch('updateTransformer', { 207 | type, 208 | transformer 209 | }) 210 | ) 211 | } 212 | 213 | if (boilerplate.showPans) { 214 | ps.push(dispatch('showPans', boilerplate.showPans)) 215 | } 216 | 217 | const { activePan = 'js' } = boilerplate 218 | ps.push(dispatch('setActivePan', activePan)) 219 | ps.push(dispatch('clearLogs')) 220 | 221 | await Promise.all(ps) 222 | 223 | setTimeout(() => { 224 | dispatch('editorSaved') 225 | Event.$emit('focus-editor', activePan) 226 | }) 227 | 228 | progress.done() 229 | }, 230 | async setGist({ commit, dispatch, state }, id) { 231 | const data = await api(`gists/${id}`, state.githubToken, progress.done) 232 | const files = data.files 233 | 234 | if (!files) return 235 | 236 | const main = { 237 | html: {}, 238 | css: {}, 239 | js: {}, 240 | ...(files['index.js'] ? req(files['index.js'].content) : {}), 241 | ...(files['codepan.js'] ? req(files['codepan.js'].content) : {}), 242 | ...(files['codepan.json'] ? JSON.parse(files['codepan.json'].content) : {}) 243 | } 244 | for (const type of ['html', 'js', 'css']) { 245 | if (!main[type].code) { 246 | const filename = main[type].filename || getFileNameByLang[type] 247 | if (files[filename]) { 248 | main[type].code = files[filename].content 249 | } 250 | } 251 | } 252 | await dispatch('setBoilerplate', main) 253 | 254 | delete data.files 255 | commit('SET_GIST_META', data) 256 | }, 257 | async setGitHubToken({ commit, dispatch }, token) { 258 | commit('SET_GITHUB_TOKEN', token) 259 | let userMeta = {} 260 | if (token) { 261 | localStorage.setItem('codepan:gh-token', token) 262 | userMeta = await api('user', token) 263 | } else { 264 | localStorage.removeItem('codepan:gh-token') 265 | } 266 | commit('SET_USER_META', userMeta) 267 | if (Object.keys(userMeta).length > 0) { 268 | localStorage.setItem('codepan:user-meta', JSON.stringify(userMeta)) 269 | } else { 270 | localStorage.removeItem('codepan:user-meta') 271 | } 272 | }, 273 | editorSaved({ commit }) { 274 | commit('SET_EDITOR_STATUS', 'saved') 275 | }, 276 | editorChanged({ commit }) { 277 | commit('SET_EDITOR_STATUS', 'changed') 278 | }, 279 | editorSaving({ commit }) { 280 | commit('SET_EDITOR_STATUS', 'saving') 281 | }, 282 | editorSavingError({ commit }) { 283 | commit('SET_EDITOR_STATUS', 'error') 284 | }, 285 | setAutoRun({ commit }, status) { 286 | commit('SET_AUTO_RUN', status) 287 | }, 288 | setIframeStatus({ commit }, status) { 289 | commit('SET_IFRAME_STATUS', status) 290 | } 291 | }, 292 | getters: { 293 | isLoggedIn({ githubToken }) { 294 | return Boolean(githubToken) 295 | }, 296 | canUpdateGist({ gistMeta, userMeta }) { 297 | return gistMeta && userMeta && 298 | gistMeta.owner && 299 | gistMeta.owner.id === userMeta.id 300 | } 301 | } 302 | }) 303 | 304 | export default store 305 | -------------------------------------------------------------------------------- /src/svg/alert.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/svg/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/svg/code.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/svg/export.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/svg/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/utils/create-editor.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unassigned-import */ 2 | import CodeMirror from 'codemirror' 3 | import 'codemirror/mode/htmlmixed/htmlmixed' 4 | import 'codemirror/mode/jsx/jsx' 5 | import 'codemirror/mode/css/css' 6 | import 'codemirror/mode/mllike/mllike' 7 | import 'codemirror/addon/selection/active-line' 8 | import 'codemirror/addon/edit/matchtags' 9 | import 'codemirror/addon/edit/matchbrackets' 10 | import 'codemirror/addon/edit/closebrackets' 11 | import 'codemirror/addon/edit/closetag' 12 | import 'codemirror/addon/comment/comment' 13 | import 'codemirror/addon/fold/foldcode' 14 | import 'codemirror/addon/fold/foldgutter' 15 | import 'codemirror/addon/fold/brace-fold' 16 | import 'codemirror/addon/fold/xml-fold' 17 | import 'codemirror/addon/fold/markdown-fold' 18 | import 'codemirror/addon/fold/comment-fold' 19 | 20 | const isMac = CodeMirror.keyMap.default === CodeMirror.keyMap.macDefault 21 | 22 | export default function (el, opts = {}) { 23 | const editor = CodeMirror.fromTextArea(el, { 24 | lineNumbers: true, 25 | lineWrapping: true, 26 | styleActiveLine: true, 27 | matchTags: { bothTags: true }, 28 | matchBrackets: true, 29 | foldGutter: true, 30 | gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], 31 | ...opts 32 | }) 33 | 34 | editor.setOption('extraKeys', { 35 | ...editor.getOption('extraKeys'), 36 | Tab(cm) { 37 | // Indent, or place 2 spaces 38 | if (cm.somethingSelected()) { 39 | cm.indentSelection('add') 40 | } else if (cm.getOption('mode').indexOf('html') > -1) { 41 | try { 42 | cm.execCommand('emmetExpandAbbreviation') 43 | } catch (err) { 44 | console.error(err) 45 | } 46 | } else { 47 | const spaces = Array(cm.getOption('indentUnit') + 1).join(' ') 48 | cm.replaceSelection(spaces, 'end', '+input') 49 | } 50 | }, 51 | [isMac ? 'Cmd-/' : 'Ctrl-/'](cm) { 52 | cm.toggleComment() 53 | } 54 | }) 55 | 56 | editor.on('gutterClick', (cm, line, gutter) => { 57 | if (gutter === 'CodeMirror-linenumbers') { 58 | // eslint-disable-next-line new-cap 59 | return cm.setSelection(CodeMirror.Pos(line, 0), CodeMirror.Pos(line + 1, 0)) 60 | } 61 | }) 62 | 63 | import(/* webpackChunkName: "codemirror-emmet" */ 'codemirror-emmet').then(emmet => { 64 | emmet(CodeMirror) 65 | editor.setOption('extraKeys', { 66 | ...editor.getOption('extraKeys'), 67 | Enter: 'emmetInsertLineBreak' 68 | }) 69 | editor.setOption('emmet', { 70 | markupSnippets: { 71 | 'script:unpkg': 'script[src="https://unpkg.com/"]', 72 | 'script:jsd': 'script[src="https://cdn.jsdelivr.net/npm/"]' 73 | } 74 | }) 75 | }) 76 | 77 | return editor 78 | } 79 | -------------------------------------------------------------------------------- /src/utils/create-pan.js: -------------------------------------------------------------------------------- 1 | import { mapActions, mapState } from 'vuex' 2 | import debounce from 'debounce' 3 | import { Dropdown, DropdownMenu, DropdownItem } from 'element-ui' 4 | import PanResizer from '@/components/PanResizer.vue' 5 | import CompiledCodeSwitcher from '@/components/CompiledCodeSwitcher.vue' 6 | import createEditor from '@/utils/create-editor' 7 | import Event from '@/utils/event' 8 | import panPosition from '@/utils/pan-position' 9 | import { hasNextPan, getHumanlizedTransformerName, getEditorModeByTransfomer } from '@/utils' 10 | 11 | export default ({ name, editor, components } = {}) => { 12 | return { 13 | name: `${name}-pan`, 14 | data() { 15 | return { 16 | style: {} 17 | } 18 | }, 19 | computed: { 20 | ...mapState([name, 'visiblePans', 'activePan', 'autoRun']), 21 | ...mapState({ 22 | isVisible: state => state.visiblePans.indexOf(name) !== -1 23 | }), 24 | enableResizer() { 25 | return hasNextPan(this.visiblePans, name) 26 | }, 27 | isActivePan() { 28 | return this.activePan === name 29 | }, 30 | humanlizedTransformerName() { 31 | return getHumanlizedTransformerName(this[name].transformer) 32 | } 33 | }, 34 | watch: { 35 | isVisible() { 36 | this.editor.refresh() 37 | }, 38 | visiblePans: { 39 | immediate: true, 40 | handler(val) { 41 | this.style = panPosition(val, name) 42 | } 43 | }, 44 | [`${name}.transformer`](val) { 45 | const mode = getEditorModeByTransfomer(val) 46 | this.editor.setOption('mode', mode) 47 | }, 48 | [`${name}.code`]() { 49 | if (this.autoRun) { 50 | this.debounceRunCode() 51 | } 52 | } 53 | }, 54 | mounted() { 55 | this.editor = createEditor(this.$refs.editor, { 56 | ...editor, 57 | readOnly: 'readonly' in this.$route.query 58 | }) 59 | this.editor.on('change', e => { 60 | this.updateCode({ code: e.getValue(), type: name }) 61 | this.editorChanged() 62 | }) 63 | this.editor.on('focus', () => { 64 | if (this.activePan !== name && this.visiblePans.indexOf(name) > -1) { 65 | this.setActivePan(name) 66 | } 67 | }) 68 | Event.$on('refresh-editor', () => { 69 | this.editor.setValue(this[name].code) 70 | this.editor.refresh() 71 | }) 72 | // Focus the editor 73 | // This is usually emitted after setting boilerplate or gist 74 | Event.$on('focus-editor', active => { 75 | if (active === name) { 76 | this.editor.focus() 77 | } 78 | }) 79 | Event.$on(`set-${name}-pan-style`, style => { 80 | this.style = { 81 | ...this.style, 82 | ...style 83 | } 84 | }) 85 | }, 86 | methods: { 87 | ...mapActions(['updateCode', 'updateTransformer', 'setActivePan', 'editorChanged']), 88 | async setTransformer(transformer) { 89 | await this.updateTransformer({ type: name, transformer }) 90 | }, 91 | debounceRunCode: debounce(() => { 92 | Event.$emit('run') 93 | }, 500) 94 | }, 95 | components: { 96 | 'el-dropdown': Dropdown, 97 | 'el-dropdown-menu': DropdownMenu, 98 | 'el-dropdown-item': DropdownItem, 99 | PanResizer, 100 | CompiledCodeSwitcher, 101 | ...components 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/utils/event.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | // global event bus used for refreshing codemirror instance 4 | export default new Vue() 5 | -------------------------------------------------------------------------------- /src/utils/get-imports.js: -------------------------------------------------------------------------------- 1 | const getImports = (code, { imports }) => { 2 | return { 3 | name: 'get-imports', 4 | 5 | visitor: { 6 | ImportDeclaration(path) { 7 | imports.push({ 8 | variables: path.node.specifiers.map(spec => ({ 9 | local: spec.local.name, 10 | imported: spec.imported ? spec.imported.name : 'default' 11 | })), 12 | module: path.node.source.value 13 | }) 14 | path.remove() 15 | } 16 | } 17 | } 18 | } 19 | 20 | export default input => { 21 | const imports = [] 22 | const { code } = window.Babel.transform(input, { 23 | plugins: [ 24 | [getImports, { imports }] 25 | ] 26 | }) 27 | return { 28 | code, 29 | imports 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/get-scripts.js: -------------------------------------------------------------------------------- 1 | import parsePackageName from 'parse-package-name' 2 | import getImports from './get-imports' 3 | import { loadBabel } from './transformer' 4 | 5 | export default async (code, scripts) => { 6 | if (!/\bimport\b/.test(code)) return code 7 | 8 | await loadBabel() 9 | 10 | const replacements = [] 11 | const res = getImports(code) 12 | code = res.code 13 | for (const [index, item] of res.imports.entries()) { 14 | const moduleName = `__npm_module_${index}` 15 | const pkg = parsePackageName(item.module) 16 | const version = pkg.version || 'latest' 17 | scripts.push({ 18 | path: pkg.path ? `/${pkg.path}` : '', 19 | name: moduleName, 20 | module: (pkg.name === 'vue' && !pkg.path) ? 21 | `vue@${version}/dist/vue.esm.js` : 22 | `${pkg.name}@${version}` 23 | }) 24 | let replacement = '\n' 25 | for (const variable of item.variables) { 26 | if (variable.imported === 'default') { 27 | replacement += `var ${variable.local} = ${moduleName}.default || ${moduleName};\n` 28 | } else { 29 | replacement += `var ${variable.local} = ${moduleName}.${variable.imported};\n` 30 | } 31 | } 32 | if (replacement) { 33 | replacements.push(replacement) 34 | } 35 | } 36 | 37 | if (replacements.length > 0) { 38 | code = replacements.join('\n') + code 39 | } 40 | 41 | return code 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/github-api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import notie from 'notie' 3 | 4 | export default async function (endpoint, token, errCb = () => {}) { 5 | const params = { 6 | // eslint-disable-next-line camelcase 7 | access_token: token 8 | } 9 | 10 | try { 11 | const data = await axios.get(`https://api.github.com/${endpoint}`, { 12 | params 13 | }).then(res => res.data) 14 | 15 | return data 16 | } catch (err) { 17 | errCb() 18 | if (err.response) { 19 | const { headers, status } = err.response 20 | if (!token && status === 403 && headers['x-ratelimit-remaining'] === '0') { 21 | notie.confirm({ 22 | text: 'API rate limit exceeded, do you want to login?', 23 | submitCallback() { 24 | Event.$emit('showLogin') 25 | } 26 | }) 27 | } else { 28 | notie.alert({ 29 | type: 'error', 30 | text: err.response.data.message, 31 | time: 5 32 | }) 33 | } 34 | } else { 35 | notie.alert({ 36 | type: 'error', 37 | text: err.message || 'GitHub API Error' 38 | }) 39 | } 40 | } 41 | 42 | return {} 43 | } 44 | -------------------------------------------------------------------------------- /src/utils/highlight.js: -------------------------------------------------------------------------------- 1 | import CodeMirror from 'codemirror' 2 | import 'codemirror/addon/runmode/runmode' 3 | 4 | CodeMirror.highlight = function (string, options = {}) { 5 | let html = '' 6 | let col = 0 7 | const tabSize = options.tabSize || 2 8 | 9 | CodeMirror.runMode(string, options.mode, (text, style) => { 10 | if (text === '\n') { 11 | html += '\n' 12 | col = 0 13 | return 14 | } 15 | 16 | let content = '' 17 | 18 | // replace tabs 19 | for (let pos = 0; ;) { 20 | const idx = text.indexOf('\t', pos) 21 | if (idx === -1) { 22 | content += text.slice(pos) 23 | col += text.length - pos 24 | break 25 | } else { 26 | col += idx - pos 27 | content += text.slice(pos, idx) 28 | const size = tabSize - (col % tabSize) 29 | col += size 30 | for (let i = 0; i < size; ++i) content += ' ' 31 | pos = idx + 1 32 | } 33 | } 34 | 35 | if (style) { 36 | const className = 'cm-' + style.replace(/ +/g, 'cm-') 37 | content = `${content}` 38 | } 39 | html += content 40 | }) 41 | 42 | return html 43 | } 44 | -------------------------------------------------------------------------------- /src/utils/iframe.js: -------------------------------------------------------------------------------- 1 | class Iframe { 2 | constructor({ el, sandboxAttributes = [] }) { 3 | if (!el) { 4 | throw new Error('Expect "el" to mount iframe to!') 5 | } 6 | this.$el = el 7 | this.sandboxAttributes = sandboxAttributes 8 | } 9 | 10 | setHTML(obj) { 11 | let html 12 | 13 | if (typeof obj === 'string') { 14 | html = obj 15 | } else { 16 | const { head = '', body = '' } = obj 17 | html = `${head}${body}` 18 | } 19 | 20 | const iframe = this.createIframe() 21 | 22 | this.$el.parentNode.replaceChild(iframe, this.$el) 23 | iframe.contentWindow.document.open() 24 | iframe.contentWindow.document.write(html) 25 | iframe.contentWindow.document.close() 26 | 27 | this.$el = iframe 28 | } 29 | 30 | createIframe() { 31 | const iframe = document.createElement('iframe') 32 | iframe.setAttribute('sandbox', this.sandboxAttributes.join(' ')) 33 | iframe.setAttribute('scrolling', 'yes') 34 | iframe.style.width = '100%' 35 | iframe.style.height = '100%' 36 | iframe.style.border = '0' 37 | return iframe 38 | } 39 | } 40 | 41 | export default (...args) => new Iframe(...args) 42 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export function hasNextPan(pans, pan) { 2 | return pans.length - 1 > pans.indexOf(pan) 3 | } 4 | 5 | export const getHumanlizedTransformerName = transformer => { 6 | const names = { 7 | html: 'HTML', 8 | pug: 'Pug', 9 | markdown: 'Markdown', 10 | js: 'JavaScript', 11 | 'vue-jsx': 'Vue JSX', 12 | babel: 'Babel', 13 | jsx: 'JSX', // @deprecated, use "babel" 14 | css: 'CSS', 15 | reason: 'Reason', 16 | 'coffeescript-2': 'CoffeeScript 2', 17 | cssnext: 'cssnext', 18 | less: 'LESS', 19 | typescript: 'TypeScript', 20 | sass: 'SASS', 21 | scss: 'SCSS', 22 | rust: 'Rust', 23 | stylus: 'Stylus' 24 | } 25 | 26 | return names[transformer] || transformer 27 | } 28 | 29 | export const getEditorModeByTransfomer = transformer => { 30 | const modes = { 31 | html: 'htmlmixed', 32 | pug: 'pug', 33 | markdown: 'markdown', 34 | js: 'jsx', 35 | 'vue-jsx': 'jsx', 36 | babel: 'jsx', 37 | jsx: 'jsx', // @deprecated, use "babel" 38 | css: 'css', 39 | reason: 'mllike', 40 | 'coffeescript-2': 'coffeescript', 41 | cssnext: 'css', 42 | less: 'text/x-less', 43 | typescript: 'text/typescript', 44 | sass: 'text/x-sass', 45 | scss: 'text/x-scss', 46 | rust: 'rust', 47 | stylus: 'text/x-styl' 48 | } 49 | return modes[transformer] 50 | } 51 | 52 | export const inIframe = window.self !== window.top 53 | -------------------------------------------------------------------------------- /src/utils/pan-position.js: -------------------------------------------------------------------------------- 1 | export default (pans, pan) => { 2 | const panWidth = 100 / pans.length 3 | const pansCount = matchedPans => { 4 | return pans.filter(p => { 5 | return matchedPans.indexOf(p) !== -1 6 | }).length 7 | } 8 | const rightOffset = leftCount => pans.length - 1 - leftCount 9 | const suffix = count => `${count * panWidth}%` 10 | 11 | if (pan === 'html') { 12 | return { 13 | left: 0, 14 | right: suffix(rightOffset(0)) 15 | } 16 | } 17 | 18 | if (pan === 'css') { 19 | const leftCount = pansCount(['html']) 20 | return { 21 | left: suffix(leftCount), 22 | right: suffix(rightOffset(leftCount)) 23 | } 24 | } 25 | 26 | if (pan === 'js') { 27 | const leftCount = pansCount(['html', 'css']) 28 | return { 29 | left: suffix(leftCount), 30 | right: suffix(rightOffset(leftCount)) 31 | } 32 | } 33 | 34 | if (pan === 'console') { 35 | const leftCount = pansCount(['html', 'css', 'js']) 36 | return { 37 | left: suffix(leftCount), 38 | right: suffix(rightOffset(leftCount)) 39 | } 40 | } 41 | 42 | if (pan === 'output') { 43 | const leftCount = pansCount(['html', 'css', 'js', 'console']) 44 | return { 45 | left: suffix(leftCount), 46 | right: 0 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/utils/popup.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default (url, title, w, h) => { 3 | const dualScreenLeft = window.screenLeft != undefined ? window.screenLeft : screen.left 4 | const dualScreenTop = window.screenTop != undefined ? window.screenTop : screen.top 5 | 6 | const width = window.innerWidth ? window.innerWidth : document.documentElement.clientWidth ? document.documentElement.clientWidth : screen.width 7 | const height = window.innerHeight ? window.innerHeight : document.documentElement.clientHeight ? document.documentElement.clientHeight : screen.height 8 | 9 | const left = ((width / 2) - (w / 2)) + dualScreenLeft 10 | const top = ((height / 2) - (h / 2)) + dualScreenTop 11 | const newWindow = window.open(url, title, 'scrollbars=yes, width=' + w + ', height=' + h + ', top=' + top + ', left=' + left) 12 | 13 | // Puts focus on the newWindow 14 | if (window.focus) { 15 | newWindow.focus() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/proxy-console.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | window.onerror = function (message) { 3 | window.parent.postMessage({ type: 'iframe-error', message }, '*') 4 | } 5 | window.addEventListener('unhandledrejection', err => { 6 | window.parent.postMessage( 7 | { type: 'iframe-error', message: err.reason.stack }, 8 | '*' 9 | ) 10 | }) 11 | window.addEventListener('click', () => { 12 | window.parent.postMessage({ type: 'codepan-make-output-active' }, '*') 13 | }) 14 | 15 | /** 16 | * Stringify. 17 | * Inspect native browser objects and functions. 18 | */ 19 | const stringify = (function () { 20 | const sortci = function (a, b) { 21 | return a.toLowerCase() < b.toLowerCase() ? -1 : 1 22 | } 23 | 24 | const htmlEntities = function (str) { 25 | return String(str) 26 | // .replace(/&/g, '&') 27 | // .replace(//g, '>') 29 | // .replace(/"/g, '"') 30 | } 31 | 32 | /** 33 | * Recursively stringify an object. Keeps track of which objects it has 34 | * visited to avoid hitting circular references, and a buffer for indentation. 35 | * Goes 2 levels deep. 36 | */ 37 | return function stringify(o, visited, buffer) { 38 | // eslint-disable-line complexity 39 | let i 40 | let vi 41 | let type = '' 42 | const parts = [] 43 | // const circular = false 44 | buffer = buffer || '' 45 | visited = visited || [] 46 | 47 | // Get out fast with primitives that don't like toString 48 | if (o === null) { 49 | return 'null' 50 | } 51 | if (typeof o === 'undefined') { 52 | return 'undefined' 53 | } 54 | 55 | // Determine the type 56 | try { 57 | type = {}.toString.call(o) 58 | } catch (err) { 59 | // only happens when typeof is protected (...randomly) 60 | type = '[object Object]' 61 | } 62 | 63 | // Handle the primitive types 64 | if (type === '[object Number]') { 65 | return String(o) 66 | } 67 | if (type === '[object Boolean]') { 68 | return o ? 'true' : 'false' 69 | } 70 | if (type === '[object Function]') { 71 | return o.toString().split('\n ').join('\n' + buffer) 72 | } 73 | if (type === '[object String]') { 74 | return '"' + htmlEntities(o.replace(/"/g, '\\"')) + '"' 75 | } 76 | 77 | // Check for circular references 78 | for (vi = 0; vi < visited.length; vi++) { 79 | if (o === visited[vi]) { 80 | // Notify the user that a circular object was found and, if available, 81 | // show the object's outerHTML (for body and elements) 82 | return ( 83 | '[circular ' + 84 | type.slice(1) + 85 | ('outerHTML' in o ? 86 | ' :\n' + 87 | htmlEntities(o.outerHTML).split('\n').join('\n' + buffer) : 88 | '') 89 | ) 90 | } 91 | } 92 | 93 | // Remember that we visited this object 94 | visited.push(o) 95 | 96 | // Stringify each member of the array 97 | if (type === '[object Array]') { 98 | for (i = 0; i < o.length; i++) { 99 | parts.push(stringify(o[i], visited)) 100 | } 101 | return '[' + parts.join(', ') + ']' 102 | } 103 | 104 | // Fake array – very tricksy, get out quickly 105 | if (type.match(/Array/)) { 106 | return type 107 | } 108 | 109 | const typeStr = type + ' ' 110 | const newBuffer = buffer + ' ' 111 | 112 | // Dive down if we're less than 2 levels deep 113 | if (buffer.length / 2 < 2) { 114 | const names = [] 115 | // Some objects don't like 'in', so just skip them 116 | try { 117 | for (i in o) { 118 | // eslint-disable-line guard-for-in 119 | names.push(i) 120 | } 121 | } catch (err) {} 122 | 123 | names.sort(sortci) 124 | for (i = 0; i < names.length; i++) { 125 | try { 126 | parts.push( 127 | newBuffer + 128 | names[i] + 129 | ': ' + 130 | stringify(o[names[i]], visited, newBuffer) 131 | ) 132 | } catch (err) {} 133 | } 134 | } 135 | 136 | // If nothing was gathered, return empty object 137 | if (parts.length === 0) return typeStr + '{ ... }' 138 | 139 | // Return the indented object with new lines 140 | return typeStr + '{\n' + parts.join(',\n') + '\n' + buffer + '}' 141 | } 142 | })() 143 | /** ========================================================================= 144 | * Console 145 | * Proxy console.logs out to the parent window 146 | * ========================================================================== */ 147 | 148 | const proxyConsole = (function () { 149 | const ProxyConsole = function () {} 150 | 151 | /** 152 | * Stringify all of the console objects from an array for proxying 153 | */ 154 | const stringifyArgs = function (args) { 155 | const newArgs = [] 156 | // TODO this was forEach but when the array is [undefined] it wouldn't 157 | // iterate over them 158 | let i = 0 159 | const length = args.length 160 | let arg 161 | for (; i < length; i++) { 162 | arg = args[i] 163 | if (typeof arg === 'undefined') { 164 | newArgs.push('undefined') 165 | } else { 166 | newArgs.push(stringify(arg)) 167 | } 168 | } 169 | return newArgs 170 | } 171 | 172 | /** 173 | * Add colors for console string 174 | */ 175 | const styleText = function (textArray, styles) { 176 | return textArray.map((text, index) => { 177 | return index ? `${text}` : text 178 | }) 179 | } 180 | 181 | /** 182 | * Add string replace for console string 183 | */ 184 | const replaceText = function (text, texts) { 185 | let output = text 186 | while (output.indexOf('%s') !== -1) { 187 | output = output.replace('%s', texts.shift()) 188 | } 189 | return output 190 | } 191 | 192 | /** 193 | * Add colors/string replace for console string or fallback on stringifyArgs for non-string types 194 | */ 195 | const handleArgs = function (args) { 196 | if (!args || args.length === 0) return [] 197 | 198 | if (typeof args[0] !== 'string') { 199 | return stringifyArgs(args) 200 | } 201 | 202 | const replacements = args[0].match(/(%[sc])([^%]*)/gm) 203 | const texts = [] 204 | const styles = [] 205 | for (let i = 1; i < args.length; i++) { 206 | switch (replacements.shift().substr(0, 2)) { 207 | case '%s': texts.push(args[i]) 208 | break 209 | case '%c': styles.push(args[i]) 210 | break 211 | default: 212 | } 213 | } 214 | 215 | const replaced = replaceText(args[0], texts) 216 | return styleText(replaced.split('%c'), styles) 217 | } 218 | 219 | // Create each of these methods on the proxy, and postMessage up to JS Bin 220 | // when one is called. 221 | const methods = (ProxyConsole.prototype.methods = [ 222 | 'debug', 223 | 'clear', 224 | 'error', 225 | 'info', 226 | 'log', 227 | 'warn', 228 | 'dir', 229 | 'props', 230 | '_raw', 231 | 'group', 232 | 'groupEnd', 233 | 'dirxml', 234 | 'table', 235 | 'trace', 236 | 'assert', 237 | 'count', 238 | 'markTimeline', 239 | 'profile', 240 | 'profileEnd', 241 | 'time', 242 | 'timeEnd', 243 | 'timeStamp', 244 | 'groupCollapsed' 245 | ]) 246 | 247 | methods.forEach(method => { 248 | // Create console method 249 | const originalMethod = console[method] 250 | const originalClear = console.clear 251 | ProxyConsole.prototype[method] = function () { 252 | // Replace args that can't be sent through postMessage 253 | const originalArgs = [].slice.call(arguments) 254 | const args = handleArgs(originalArgs) 255 | 256 | // Post up with method and the arguments 257 | window.parent.postMessage( 258 | { 259 | type: 'codepan-console', 260 | method: method === '_raw' ? originalArgs.shift() : method, 261 | args: method === '_raw' ? args.slice(1) : args 262 | }, 263 | '*' 264 | ) 265 | 266 | // If the browner supports it, use the browser console but ignore _raw, 267 | // as _raw should only go to the proxy console. 268 | // Ignore clear if it doesn't exist as it's beahviour is different than 269 | // log and we let it fallback to jsconsole for the panel and to nothing 270 | // for the browser console 271 | if (!originalMethod) { 272 | method = 'log' 273 | } 274 | 275 | if (method !== '_raw') { 276 | if (method !== 'clear' || (method === 'clear' && originalClear)) { 277 | originalMethod.apply(ProxyConsole, originalArgs) 278 | } 279 | } 280 | } 281 | }) 282 | 283 | return new ProxyConsole() 284 | })() 285 | 286 | window.console = proxyConsole 287 | })() // eslint-disable-line semi 288 | -------------------------------------------------------------------------------- /src/utils/transform.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { transformers } from '@/utils/transformer' 3 | 4 | const defaultPresets = [ 5 | [ 6 | 'es2015', 7 | { 8 | modules: false 9 | } 10 | ], 11 | 'es2016', 12 | 'es2017', 13 | 'stage-0' 14 | ] 15 | 16 | export async function js({ code, transformer }) { 17 | if (transformer === 'js') { 18 | return code 19 | } else if ( 20 | transformer === 'babel' || 21 | transformer === 'jsx' /* @deprecated, use "babel" */ 22 | ) { 23 | return window.Babel.transform(code, { 24 | presets: [...defaultPresets, 'flow', 'react'] 25 | }).code 26 | } else if (transformer === 'typescript') { 27 | const res = window.typescript.transpileModule(code, { 28 | fileName: '/foo.ts', 29 | reportDiagnostics: true, 30 | compilerOptions: { 31 | module: 'es2015' 32 | } 33 | }) 34 | console.log(res) 35 | return res.outputText 36 | } else if (transformer === 'vue-jsx') { 37 | return window.Babel.transform(code, { 38 | presets: [...defaultPresets, 'flow', transformers.get('VuePreset')] 39 | }).code.replace( 40 | /import [^\s]+ from ['"]babel-helper-vue-jsx-merge-props['"];?/, 41 | transformers.get('VueJSXMergeProps') 42 | ) 43 | } else if (transformer === 'reason') { 44 | const wrapInExports = code => 45 | `;(function(exports) {\n${code}\n})(window.exports = {})` 46 | 47 | try { 48 | const ocamlCode = window.printML(window.parseRE(code)) 49 | const res = JSON.parse(window.ocaml.compile(ocamlCode)) 50 | if (res.js_error_msg) return res.js_error_msg 51 | return wrapInExports(res.js_code) 52 | } catch (err) { 53 | console.log(err) 54 | return `${err.message}${ 55 | err.location ? `\n${JSON.stringify(err.location, null, 2)}` : '' 56 | }` 57 | } 58 | } else if (transformer === 'coffeescript-2') { 59 | const esCode = window.CoffeeScript.compile(code) 60 | return window.Babel.transform(esCode, { 61 | presets: [...defaultPresets, 'react'] 62 | }).code 63 | } else if (transformer === 'rust') { 64 | const { data } = await axios.post('https://play.rust-lang.org/evaluate.json', { 65 | code, 66 | optimize: '0', 67 | version: 'beta' 68 | }) 69 | if (data.error) { 70 | return data.error.trim() 71 | } 72 | return `console.log(${JSON.stringify(data.result.trim())})` 73 | } 74 | throw new Error(`Unknow transformer: ${transformer}`) 75 | } 76 | 77 | export async function html({ code, transformer }) { 78 | if (transformer === 'html') { 79 | return code 80 | } else if (transformer === 'pug') { 81 | return window.pug.render(code) 82 | } else if (transformer === 'markdown') { 83 | return transformers.get('markdown')(code) 84 | } 85 | throw new Error(`Unknow transformer: ${transformer}`) 86 | } 87 | 88 | export async function css({ code, transformer }) { 89 | switch (transformer) { 90 | case 'css': 91 | return code 92 | case 'cssnext': 93 | return window 94 | .postcss([window.cssnext]) 95 | .process(code) 96 | .then(res => res.css) 97 | case 'less': 98 | return transformers 99 | .get('less') 100 | .render(code) 101 | .then(res => res.css) 102 | case 'scss': 103 | case 'sass': 104 | return new Promise((resolve, reject) => { 105 | transformers.get('sass').compile(code, { 106 | indentedSyntax: transformer === 'sass' 107 | }, result => { 108 | if (result.status === 0) return resolve(result.text) 109 | reject(new Error(result.formatted)) 110 | }) 111 | }) 112 | case 'stylus': 113 | return new Promise((resolve, reject) => { 114 | window.stylus.render(code, { filename: 'codepan.styl' }, (err, css) => { 115 | if (err) return reject(err) 116 | resolve(css) 117 | }) 118 | }) 119 | default: 120 | throw new Error(`Unknow transformer: ${transformer}`) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/utils/transformer.js: -------------------------------------------------------------------------------- 1 | // eslint-disable import/no-mutable-exports 2 | import progress from 'nprogress' 3 | import loadjs from 'loadjs' 4 | import pify from 'pify' 5 | 6 | function asyncLoad(resources, name) { 7 | return new Promise((resolve, reject) => { 8 | if (loadjs.isDefined(name)) { 9 | resolve() 10 | } else { 11 | loadjs(resources, name, { 12 | success() { 13 | resolve() 14 | }, 15 | error() { 16 | progress.done() 17 | reject(new Error('network error')) 18 | } 19 | }) 20 | } 21 | }) 22 | } 23 | 24 | class Transformers { 25 | constructor() { 26 | this.map = {} 27 | } 28 | 29 | set(k, v) { 30 | this.map[k] = v 31 | } 32 | 33 | get(k) { 34 | return this.map[k] 35 | } 36 | } 37 | 38 | const transformers = new Transformers() 39 | 40 | async function loadBabel() { 41 | if (loadjs.isDefined('babel')) return 42 | 43 | progress.start() 44 | const [, VuePreset, VueJSXMergeProps] = await Promise.all([ 45 | asyncLoad(process.env.BABEL_CDN, 'babel'), 46 | import(/* webpackChunkName: "babel-stuffs" */ 'babel-preset-vue/dist/babel-preset-vue'), // use umd bundle since we don't want to parse `require` 47 | import(/* webpackChunkName: "babel-stuffs" */ '!raw-loader!./vue-jsx-merge-props') 48 | ]) 49 | transformers.set('VuePreset', VuePreset) 50 | transformers.set('VueJSXMergeProps', VueJSXMergeProps) 51 | progress.done() 52 | } 53 | 54 | async function loadPug() { 55 | if (loadjs.isDefined('pug')) return 56 | 57 | progress.start() 58 | await Promise.all([ 59 | asyncLoad(process.env.PUG_CDN, 'pug'), 60 | import(/* webpackChunkName: "codemirror-mode-pug" */ 'codemirror/mode/pug/pug') 61 | ]) 62 | progress.done() 63 | } 64 | 65 | async function loadMarkdown() { 66 | if (!transformers.get('markdown')) { 67 | progress.start() 68 | const [marked] = await Promise.all([ 69 | import('marked3').then(m => m.default), 70 | import('codemirror/mode/markdown/markdown') 71 | ]) 72 | transformers.set('markdown', marked) 73 | progress.done() 74 | } 75 | } 76 | 77 | async function loadRust() { 78 | if (!transformers.get('rust')) { 79 | progress.start() 80 | await import('codemirror/mode/rust/rust') 81 | transformers.set('rust', true) 82 | progress.done() 83 | } 84 | } 85 | 86 | async function loadReason() { 87 | if (loadjs.isDefined('reason')) return 88 | 89 | progress.start() 90 | await asyncLoad(['/vendor/reason/bs.js', '/vendor/reason/refmt.js'], 'reason') 91 | progress.done() 92 | } 93 | 94 | async function loadCoffeeScript2() { 95 | if (loadjs.isDefined('coffeescript-2')) return 96 | 97 | progress.start() 98 | await Promise.all([ 99 | asyncLoad( 100 | [ 101 | '/vendor/coffeescript-2.js', 102 | // Need babel to transform JSX 103 | process.env.BABEL_CDN 104 | ], 105 | 'coffeescript-2' 106 | ), 107 | import('codemirror/mode/coffeescript/coffeescript') 108 | ]) 109 | progress.done() 110 | } 111 | 112 | async function loadCssnext() { 113 | if (loadjs.isDefined('cssnext')) return 114 | 115 | progress.start() 116 | await asyncLoad([process.env.CSSNEXT_CDN, process.env.POSTCSS_CDN], 'cssnext') 117 | progress.done() 118 | } 119 | 120 | async function loadLess() { 121 | if (!transformers.get('less')) { 122 | progress.start() 123 | const less = await import('less') 124 | transformers.set('less', pify(less)) 125 | progress.done() 126 | } 127 | } 128 | 129 | async function loadSass() { 130 | if (!transformers.get('sass')) { 131 | progress.start() 132 | const [Sass] = await Promise.all([ 133 | import('../../static/vendor/sass/sass'), 134 | import(/* webpackChunkName: "codemirror-mode" */ 'codemirror/mode/sass/sass.js') 135 | ]) 136 | Sass.setWorkerUrl('/vendor/sass/sass.worker.js') 137 | transformers.set('sass', new Sass()) 138 | progress.done() 139 | } 140 | } 141 | 142 | async function loadTypescript() { 143 | if (loadjs.isDefined('typescript')) return 144 | 145 | progress.start() 146 | await asyncLoad([process.env.TYPESCRIPT_CDN], 'typescript') 147 | progress.done() 148 | } 149 | 150 | async function loadStylus() { 151 | if (loadjs.isDefined('stylus')) return 152 | 153 | progress.start() 154 | await Promise.all([ 155 | import(/* webpackChunkName: "codemirror-mode" */ 'codemirror/mode/stylus/stylus'), 156 | asyncLoad('/vendor/stylus.js', 'stylus') 157 | ]) 158 | progress.done() 159 | } 160 | 161 | export { 162 | loadBabel, 163 | loadPug, 164 | loadMarkdown, 165 | transformers, 166 | loadReason, 167 | loadCoffeeScript2, 168 | loadCssnext, 169 | loadLess, 170 | loadSass, 171 | loadRust, 172 | loadTypescript, 173 | loadStylus 174 | } 175 | -------------------------------------------------------------------------------- /src/utils/vue-jsx-merge-props.js: -------------------------------------------------------------------------------- 1 | window._mergeJSXProps = function (objs) { 2 | var nestRE = /^(attrs|props|on|nativeOn|class|style|hook)$/ 3 | 4 | function mergeFn (a, b) { 5 | return function () { 6 | a.apply(this, arguments) 7 | b.apply(this, arguments) 8 | } 9 | } 10 | 11 | return objs.reduce(function (a, b) { 12 | var aa, bb, key, nestedKey, temp 13 | for (key in b) { 14 | aa = a[key] 15 | bb = b[key] 16 | if (aa && nestRE.test(key)) { 17 | // normalize class 18 | if (key === 'class') { 19 | if (typeof aa === 'string') { 20 | temp = aa 21 | a[key] = aa = {} 22 | aa[temp] = true 23 | } 24 | if (typeof bb === 'string') { 25 | temp = bb 26 | b[key] = bb = {} 27 | bb[temp] = true 28 | } 29 | } 30 | if (key === 'on' || key === 'nativeOn' || key === 'hook') { 31 | // merge functions 32 | for (nestedKey in bb) { 33 | aa[nestedKey] = mergeFn(aa[nestedKey], bb[nestedKey]) 34 | } 35 | } else if (Array.isArray(aa)) { 36 | a[key] = aa.concat(bb) 37 | } else if (Array.isArray(bb)) { 38 | a[key] = [aa].concat(bb) 39 | } else { 40 | for (nestedKey in bb) { 41 | aa[nestedKey] = bb[nestedKey] 42 | } 43 | } 44 | } else { 45 | a[key] = b[key] 46 | } 47 | } 48 | return a 49 | }, {}) 50 | } 51 | -------------------------------------------------------------------------------- /src/views/EditorPage.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 194 | 195 | 198 | 201 | 202 | 217 | 218 | 270 | -------------------------------------------------------------------------------- /src/views/GitHubSuccess.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 17 | -------------------------------------------------------------------------------- /src/views/NotFound.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | 22 | 44 | 45 | -------------------------------------------------------------------------------- /static/CNAME: -------------------------------------------------------------------------------- 1 | codepan.net 2 | -------------------------------------------------------------------------------- /static/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | -------------------------------------------------------------------------------- /static/favicon-114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egoist/codepan/26bd29ebe5430b8850d30a3eb0801994a7611f6d/static/favicon-114.png -------------------------------------------------------------------------------- /static/favicon-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egoist/codepan/26bd29ebe5430b8850d30a3eb0801994a7611f6d/static/favicon-120.png -------------------------------------------------------------------------------- /static/favicon-144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egoist/codepan/26bd29ebe5430b8850d30a3eb0801994a7611f6d/static/favicon-144.png -------------------------------------------------------------------------------- /static/favicon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egoist/codepan/26bd29ebe5430b8850d30a3eb0801994a7611f6d/static/favicon-152.png -------------------------------------------------------------------------------- /static/favicon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egoist/codepan/26bd29ebe5430b8850d30a3eb0801994a7611f6d/static/favicon-180.png -------------------------------------------------------------------------------- /static/favicon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egoist/codepan/26bd29ebe5430b8850d30a3eb0801994a7611f6d/static/favicon-192.png -------------------------------------------------------------------------------- /static/favicon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egoist/codepan/26bd29ebe5430b8850d30a3eb0801994a7611f6d/static/favicon-32.png -------------------------------------------------------------------------------- /static/favicon-36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egoist/codepan/26bd29ebe5430b8850d30a3eb0801994a7611f6d/static/favicon-36.png -------------------------------------------------------------------------------- /static/favicon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egoist/codepan/26bd29ebe5430b8850d30a3eb0801994a7611f6d/static/favicon-48.png -------------------------------------------------------------------------------- /static/favicon-57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egoist/codepan/26bd29ebe5430b8850d30a3eb0801994a7611f6d/static/favicon-57.png -------------------------------------------------------------------------------- /static/favicon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egoist/codepan/26bd29ebe5430b8850d30a3eb0801994a7611f6d/static/favicon-60.png -------------------------------------------------------------------------------- /static/favicon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egoist/codepan/26bd29ebe5430b8850d30a3eb0801994a7611f6d/static/favicon-72.png -------------------------------------------------------------------------------- /static/favicon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egoist/codepan/26bd29ebe5430b8850d30a3eb0801994a7611f6d/static/favicon-76.png -------------------------------------------------------------------------------- /static/favicon-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egoist/codepan/26bd29ebe5430b8850d30a3eb0801994a7611f6d/static/favicon-96.png -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egoist/codepan/26bd29ebe5430b8850d30a3eb0801994a7611f6d/static/favicon.ico -------------------------------------------------------------------------------- /static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CodePan", 3 | "short_name": "CodePan", 4 | "start_url": "./", 5 | "display": "standalone", 6 | "orientation": "portrait", 7 | "background_color": "#fff", 8 | "theme_color": "#673ab8", 9 | "icons": [ 10 | { 11 | "src": "\/favicon-36.png", 12 | "sizes": "36x36", 13 | "type": "image\/png", 14 | "density": 0.75 15 | }, 16 | { 17 | "src": "\/favicon-48.png", 18 | "sizes": "48x48", 19 | "type": "image\/png", 20 | "density": 1 21 | }, 22 | { 23 | "src": "\/favicon-72.png", 24 | "sizes": "72x72", 25 | "type": "image\/png", 26 | "density": 1.5 27 | }, 28 | { 29 | "src": "\/favicon-96.png", 30 | "sizes": "96x96", 31 | "type": "image\/png", 32 | "density": 2 33 | }, 34 | { 35 | "src": "\/favicon-144.png", 36 | "sizes": "144x144", 37 | "type": "image\/png", 38 | "density": 3 39 | }, 40 | { 41 | "src": "\/favicon-192.png", 42 | "sizes": "192x192", 43 | "type": "image\/png", 44 | "density": 4 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /static/vendor/sass/sass.js: -------------------------------------------------------------------------------- 1 | /*! sass.js - v0.10.8 (eb28f5f) - built 2018-01-21 2 | providing libsass 3.4.8 (a1f13edf) 3 | via emscripten 1.37.0 () 4 | */ 5 | 6 | (function (root, factory) { 7 | 'use strict'; 8 | if (typeof define === 'function' && define.amd) { 9 | define([], factory); 10 | } else if (typeof exports === 'object') { 11 | module.exports = factory(); 12 | } else { 13 | root.Sass = factory(); 14 | } 15 | }(this, function () {/*global document*/ 16 | // identify the path sass.js is located at in case we're loaded by a simple 17 | // 18 | // this path can be used to identify the location of 19 | // * sass.worker.js from sass.js 20 | // * libsass.js.mem from sass.sync.js 21 | // see https://github.com/medialize/sass.js/pull/32#issuecomment-103142214 22 | // see https://github.com/medialize/sass.js/issues/33 23 | var SASSJS_RELATIVE_PATH = (function() { 24 | 'use strict'; 25 | 26 | // in Node things are rather simple 27 | if (typeof __dirname !== 'undefined') { 28 | return __dirname; 29 | } 30 | 31 | // we can only run this test in the browser, 32 | // so make sure we actually have a DOM to work with. 33 | if (typeof document === 'undefined' || !document.getElementsByTagName) { 34 | return null; 35 | } 36 | 37 | // http://www.2ality.com/2014/05/current-script.html 38 | var currentScript = document.currentScript || (function() { 39 | var scripts = document.getElementsByTagName('script'); 40 | return scripts[scripts.length - 1]; 41 | })(); 42 | 43 | var path = currentScript && currentScript.src; 44 | if (!path) { 45 | return null; 46 | } 47 | 48 | // [worker] make sure we're not running in some concatenated thing 49 | if (path.slice(-8) === '/sass.js') { 50 | return path.slice(0, -8); 51 | } 52 | 53 | // [sync] make sure we're not running in some concatenated thing 54 | if (path.slice(-13) === '/sass.sync.js') { 55 | return path.slice(0, -13); 56 | } 57 | 58 | return null; 59 | })() || '.'; 60 | 61 | /*global Worker, SASSJS_RELATIVE_PATH*/ 62 | 'use strict'; 63 | 64 | var noop = function(){}; 65 | var slice = [].slice; 66 | // defined upon first Sass.initialize() call 67 | var globalWorkerUrl; 68 | 69 | function Sass(workerUrl) { 70 | if (!workerUrl && !globalWorkerUrl) { 71 | /*jshint laxbreak:true */ 72 | throw new Error( 73 | 'Sass needs to be initialized with the URL of sass.worker.js - ' 74 | + 'either via Sass.setWorkerUrl(url) or by new Sass(url)' 75 | ); 76 | /*jshint laxbreak:false */ 77 | } 78 | 79 | if (!globalWorkerUrl) { 80 | globalWorkerUrl = workerUrl; 81 | } 82 | 83 | // bind all functions 84 | // we're doing this because we used to have a single hard-wired instance that allowed 85 | // [].map(Sass.removeFile) and we need to maintain that for now (at least until 1.0.0) 86 | for (var key in this) { 87 | if (typeof this[key] === 'function') { 88 | this[key] = this[key].bind(this); 89 | } 90 | } 91 | 92 | this._callbacks = {}; 93 | this._worker = new Worker(workerUrl || globalWorkerUrl); 94 | this._worker.addEventListener('message', this._handleWorkerMessage, false); 95 | } 96 | 97 | // allow setting the workerUrl before the first Sass instance is initialized, 98 | // where registering the global workerUrl would've happened automatically 99 | Sass.setWorkerUrl = function(workerUrl) { 100 | globalWorkerUrl = workerUrl; 101 | }; 102 | 103 | Sass.style = { 104 | nested: 0, 105 | expanded: 1, 106 | compact: 2, 107 | compressed: 3 108 | }; 109 | 110 | Sass.comments = { 111 | 'none': 0, 112 | 'default': 1 113 | }; 114 | 115 | Sass.prototype = { 116 | style: Sass.style, 117 | comments: Sass.comments, 118 | 119 | destroy: function() { 120 | this._worker && this._worker.terminate(); 121 | this._worker = null; 122 | this._callbacks = {}; 123 | this._importer = null; 124 | }, 125 | 126 | _handleWorkerMessage: function(event) { 127 | if (event.data.command) { 128 | this[event.data.command](event.data.args); 129 | } 130 | 131 | this._callbacks[event.data.id] && this._callbacks[event.data.id](event.data.result); 132 | delete this._callbacks[event.data.id]; 133 | }, 134 | 135 | _dispatch: function(options, callback) { 136 | if (!this._worker) { 137 | throw new Error('Sass worker has been terminated'); 138 | } 139 | 140 | options.id = 'cb' + Date.now() + Math.random(); 141 | this._callbacks[options.id] = callback; 142 | this._worker.postMessage(options); 143 | }, 144 | 145 | _importerInit: function(args) { 146 | // importer API done callback pushing results 147 | // back to the worker 148 | var done = function done(result) { 149 | this._worker.postMessage({ 150 | command: '_importerFinish', 151 | args: [result] 152 | }); 153 | }.bind(this); 154 | 155 | try { 156 | this._importer(args[0], done); 157 | } catch(e) { 158 | done({ error: e.message }); 159 | throw e; 160 | } 161 | }, 162 | 163 | importer: function(importerCallback, callback) { 164 | if (typeof importerCallback !== 'function' && importerCallback !== null) { 165 | throw new Error('importer callback must either be a function or null'); 166 | } 167 | 168 | // callback is executed in the main EventLoop 169 | this._importer = importerCallback; 170 | // tell worker to activate importer callback 171 | this._worker.postMessage({ 172 | command: 'importer', 173 | args: [Boolean(importerCallback)] 174 | }); 175 | 176 | callback && callback(); 177 | }, 178 | }; 179 | 180 | var commands = 'writeFile readFile listFiles removeFile clearFiles lazyFiles preloadFiles options compile compileFile'; 181 | commands.split(' ').forEach(function(command) { 182 | Sass.prototype[command] = function() { 183 | var callback = slice.call(arguments, -1)[0]; 184 | var args = slice.call(arguments, 0, -1); 185 | if (typeof callback !== 'function') { 186 | args.push(callback); 187 | callback = noop; 188 | } 189 | 190 | this._dispatch({ 191 | command: command, 192 | args: args 193 | }, callback); 194 | }; 195 | }); 196 | 197 | // automatically set the workerUrl in case we're loaded by a simple 198 | // 199 | // see https://github.com/medialize/sass.js/pull/32#issuecomment-103142214 200 | Sass.setWorkerUrl(SASSJS_RELATIVE_PATH + '/sass.worker.js'); 201 | return Sass; 202 | })); --------------------------------------------------------------------------------