From 69dbc25276fd39ef12ac1bd8d776167d767c8b3d Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Wed, 1 Sep 2021 01:31:05 -0400 Subject: [PATCH 1/2] Allow sharing of the REPL --- src/Repl.js | 37 ++++++++++++++++++++----------------- src/lib/helpers.js | 31 +++++++++++++++++++++++++++++++ src/lib/terser-options.js | 12 ++++++++++-- 3 files changed, 61 insertions(+), 19 deletions(-) diff --git a/src/Repl.js b/src/Repl.js index 5f84baf..bcebf0a 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: '// write or paste code here\n\n', + minified: "// terser's ouput will be shown here", + terserOptions: evalOptions(), + rawSize: 0, + minifiedSize: 0, +}; + class Repl extends Component { - state = { - optionsCode: terserOptions, - code: '// write or paste code here\n\n', - minified: "// terser's ouput will be shown here", - terserOptions: evalOptions(), - rawSize: 0, - minifiedSize: 0 - }; + state = loadState() || defaultState; - options = { + _minifyId = 0; + _options = { lineWrapping: true, fileSize: true }; @@ -43,7 +46,7 @@ class Repl extends Component { className={styles.codeMirrorPanelInput} code={this.state.code} onChange={this._updateCode} - options={this.options} + options={this._options} fileSize={this.state.rawSize} theme="paraiso-light" errorMessage={this.state.errorMessage} @@ -53,7 +56,7 @@ class Repl extends Component { 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; From 6b7e2fe0cf248349b2721aaf25ba27409cdb1161 Mon Sep 17 00:00:00 2001 From: Justin Ridgewell Date: Wed, 1 Sep 2021 01:41:47 -0400 Subject: [PATCH 2/2] Use codemirror's placeholder plugin --- src/CodeMirror.js | 1 + src/Repl.js | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) 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 bcebf0a..98a3775 100644 --- a/src/Repl.js +++ b/src/Repl.js @@ -11,8 +11,8 @@ const DEBOUNCE_DELAY = 500; const defaultState = { optionsCode: terserOptions, - code: '// write or paste code here\n\n', - minified: "// terser's ouput will be shown here", + code: '', + minified: '', terserOptions: evalOptions(), rawSize: 0, minifiedSize: 0, @@ -40,7 +40,7 @@ class Repl extends Component { options={{ lineWrapping: true }} theme="paraiso-light" errorMessage={this.state.optionsErrorMessage} - placeholder="Edit terser config here" + placeholder="// Edit terser config here" />