├── .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 | [](https://circleci.com/gh/egoist/codepan/tree/master) [](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 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
86 |
--------------------------------------------------------------------------------
/src/components/CSSPan.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 | {{ humanlizedTransformerName }}
11 |
12 |
13 | CSS
14 | cssnext
15 | LESS
16 | SASS
17 | SCSS
18 | Stylus
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
41 |
--------------------------------------------------------------------------------
/src/components/CompiledCodeDialog.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 | Compiled with {{ transformerName }}
9 | {{ transforming ? 'Compiling..' : transformedCode }}
10 |
11 |
12 |
13 |
63 |
64 |
87 |
--------------------------------------------------------------------------------
/src/components/CompiledCodeSwitcher.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
30 |
--------------------------------------------------------------------------------
/src/components/ConsolePan.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
11 | Console
12 |
13 |
18 | Clear
19 |
20 |
21 |
30 |
31 |
32 |
33 |
34 |
90 |
91 |
105 |
--------------------------------------------------------------------------------
/src/components/HTMLPan.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 | {{ humanlizedTransformerName }}
11 |
12 |
13 | HTML
14 | Pug
15 | Markdown
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
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 |
2 |
190 |
191 |
192 |
362 |
363 |
462 |
--------------------------------------------------------------------------------
/src/components/JSPan.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 | {{ humanlizedTransformerName }}
11 |
12 |
13 | JavaScript
14 | Babel
15 | TypeScript
16 | Vue JSX
17 | Reason
18 | CoffeeScript 2
19 | Rust
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
43 |
--------------------------------------------------------------------------------
/src/components/OutputPan.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 | Output
10 |
11 |
14 |
15 |
16 |
17 |
318 |
319 |
331 |
--------------------------------------------------------------------------------
/src/components/PanResizer.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
91 |
92 |
106 |
--------------------------------------------------------------------------------
/src/components/Spinner.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
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 |
5 |
--------------------------------------------------------------------------------
/src/svg/check.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/svg/code.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/svg/export.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/svg/loading.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 |
2 |
3 |
4 |
5 |
11 |
12 |
13 |
19 |
20 |
21 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
40 |
41 |
42 |
43 |
194 |
195 |
198 |
201 |
202 |
217 |
218 |
270 |
--------------------------------------------------------------------------------
/src/views/GitHubSuccess.vue:
--------------------------------------------------------------------------------
1 |
2 | success!
3 |
4 |
5 |
6 |
17 |
--------------------------------------------------------------------------------
/src/views/NotFound.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
404
4 |
5 | Return to Home Page
6 |
7 |
8 |
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 | }));
--------------------------------------------------------------------------------