diff --git a/src/CodeMirror.js b/src/CodeMirror.js index 0dfe9f7..8726047 100644 --- a/src/CodeMirror.js +++ b/src/CodeMirror.js @@ -8,6 +8,7 @@ require('./paraiso-light.css'); require('codemirror/mode/xml/xml'); require('codemirror/mode/javascript/javascript'); require('codemirror/keymap/sublime'); +require('codemirror/addon/display/placeholder'); const DEFAULT_CODE_MIRROR_OPTIONS = { autoCloseBrackets: true, diff --git a/src/Repl.js b/src/Repl.js index 143525b..98a3775 100644 --- a/src/Repl.js +++ b/src/Repl.js @@ -2,24 +2,27 @@ import React, { Component } from 'react'; import { debounce, cloneDeep } from 'lodash-es'; import CodeMirrorPanel from './CodeMirrorPanel'; -import { getCodeSizeInBytes } from './lib/helpers'; +import { getCodeSizeInBytes, loadState, saveState } from './lib/helpers'; import terserOptions, { evalOptions } from './lib/terser-options'; import styles from './Repl.module.css'; const DEBOUNCE_DELAY = 500; +const defaultState = { + optionsCode: terserOptions, + code: '', + minified: '', + terserOptions: evalOptions(), + rawSize: 0, + minifiedSize: 0, +}; + class Repl extends Component { - state = { - optionsCode: terserOptions, - code: '// write or paste code here\n\n', - minified: "// terser's output will be shown here", - terserOptions: evalOptions(), - rawSize: 0, - minifiedSize: 0 - }; + state = loadState() || defaultState; - options = { + _minifyId = 0; + _options = { lineWrapping: true, fileSize: true }; @@ -37,26 +40,26 @@ class Repl extends Component { options={{ lineWrapping: true }} theme="paraiso-light" errorMessage={this.state.optionsErrorMessage} - placeholder="Edit terser config here" + placeholder="// Edit terser config here" /> @@ -84,21 +87,20 @@ class Repl extends Component { this.setState({ optionsErrorMessage: e.message }); } - this._minify(this.state.code); + this._minifyToState(this.state.code); }; - _minifyToState = debounce( - code => this._minify(code, this._persistState), - DEBOUNCE_DELAY - ); + _minifyToState = debounce(code => this._minify(code), DEBOUNCE_DELAY); _minify = async (code, setStateCallback) => { // we need to clone this because terser mutates the options object :( const terserOpts = cloneDeep(this.state.terserOptions); + const minifyId = ++this._minifyId; // TODO: put this in a worker to avoid blocking the UI on heavy content try { const result = await this.props.terser.minify(code, terserOpts); + if (this._minifyId !== minifyId) return; if (result.error) { this.setState({ errorMessage: result.error.message }); @@ -108,6 +110,7 @@ class Repl extends Component { minifiedSize: getCodeSizeInBytes(result.code), errorMessage: null }); + saveState(this.state); } } catch (e) { this.setState({ errorMessage: e.message }); diff --git a/src/lib/helpers.js b/src/lib/helpers.js index 863aae2..584ebfd 100644 --- a/src/lib/helpers.js +++ b/src/lib/helpers.js @@ -1,3 +1,34 @@ export const getCodeSizeInBytes = code => { return new Blob([code], { type: 'text/plain' }).size; }; + +const base64UrlDecodeChars = {'-': '+', '_': '/', '.': '='}; +const base64UrlDecode = input => { + try { + return atob(input.replace(/[-_.]/g, c => base64UrlDecodeChars[c])); + } catch { + return null; + } +} + +const base64UrlEncodeChars = {'+': '-', '/': '_', '=': '.'}; +const base64UrlEncode = input => { + try { + return btoa(input).replace(/[+/=]/g, c => base64UrlEncodeChars[c]); + } catch { + return null; + } +} + +export const saveState = state => { + const base64 = base64UrlEncode(JSON.stringify(state)); + window.history.replaceState(state, '', `#${base64}`); +}; + +export const loadState = () => { + try { + const base64 = window.location.hash.slice(1); + return JSON.parse(base64UrlDecode(base64)); + } catch {} + return null; +} diff --git a/src/lib/terser-options.js b/src/lib/terser-options.js index 5bc4464..29750de 100644 --- a/src/lib/terser-options.js +++ b/src/lib/terser-options.js @@ -141,7 +141,15 @@ const options = `// edit terser options rename: {}, }`; -/* eslint-disable-next-line no-eval */ -export const evalOptions = (opts) => eval(`(${opts||options})`) +export const evalOptions = (opts) => { + opts = opts || options; + // Strip line comments + opts = opts.replace(/\/\/.*/g, ''); + // Trim trailing commas + opts = opts.replace(/,\s*([\]}])/g, '$1'); + // Quote property names + opts = opts.replace(/^\s*(\w+):/gm, '"$1":'); + return JSON.parse(opts); +} export default options;