From 4b67a9edef2174bbc5577ba1c8a5865b55cf394e Mon Sep 17 00:00:00 2001 From: Dominic Coelho Date: Thu, 5 Mar 2020 09:49:07 -0700 Subject: [PATCH 1/2] add prettier button state for before, during, after, error > #176 --- components/Article.js | 60 ++++++++++++++++++++++++++++++++----------- utils/prettier.js | 36 ++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 15 deletions(-) create mode 100644 utils/prettier.js diff --git a/components/Article.js b/components/Article.js index d555d8d..035005d 100644 --- a/components/Article.js +++ b/components/Article.js @@ -1,5 +1,12 @@ /* global prettier, prettierPlugins, marked */ -import { html, css } from '../utils/rplus.js'; +import { html, css, react } from '../utils/rplus.js'; +import { + prettierButtonMessages, + initialPrettierState, + duringPrettierState, + afterPrettierState, + errorPrettierState, +} from '../utils/prettier.js'; import Editor from './Editor.js'; import FileIcon from './FileIcon.js'; @@ -10,7 +17,41 @@ import { useStateValue } from '../utils/globalState.js'; export default () => { const [{ request, cache }, dispatch] = useStateValue(); + + const [prettierButtonState, setPrettierButtonState] = react.useState( + initialPrettierState + ); + const fileData = cache['https://unpkg.com/' + request.path] || {}; + + react.useEffect(() => { + setPrettierButtonState(initialPrettierState); + }, [request]); + + react.useEffect(() => { + if (prettierButtonState.message === prettierButtonMessages.during) { + try { + const code = prettier.format(fileData.code, { + parser: 'babylon', + plugins: prettierPlugins, + }); + dispatch({ + type: 'setCache', + payload: { + ['https://unpkg.com/' + request.path]: { + ...fileData, + code, + }, + }, + }); + setPrettierButtonState(afterPrettierState); + } catch (e) { + console.error(e); + setPrettierButtonState(errorPrettierState); + } + } + }, [prettierButtonState.message]); + return html`
${request.path @@ -23,24 +64,13 @@ export default () => { ` diff --git a/utils/prettier.js b/utils/prettier.js new file mode 100644 index 0000000..cb6b37d --- /dev/null +++ b/utils/prettier.js @@ -0,0 +1,36 @@ +// [...document.querySelectorAll("head script")].some(script => script.src === "https://unpkg.com/prettier@1.13.0/parser-babylon.js") + +const prettierButtonMessages = { + initial: 'Format Code', + during: 'Formatting...', + after: 'Formatted', + error: 'Formatting Failed', +}; + +const initialPrettierState = { + message: prettierButtonMessages.initial, + disabled: false, +}; + +const duringPrettierState = { + message: prettierButtonMessages.during, + disabled: true, +}; + +const afterPrettierState = { + message: prettierButtonMessages.after, + disabled: true, +}; + +const errorPrettierState = { + message: prettierButtonMessages.error, + disabled: true, +}; + +export { + prettierButtonMessages, + initialPrettierState, + duringPrettierState, + afterPrettierState, + errorPrettierState, +}; From 7fdf5fe8f388685a1baf2fea617b3f605a537b2f Mon Sep 17 00:00:00 2001 From: Dominic Coelho Date: Thu, 5 Mar 2020 15:27:07 -0700 Subject: [PATCH 2/2] prettier enhancements > relates #183 #176 : - Add to prettier state (text and icons) - Extract prettier logic to util file - Set correct parder for a few common filetypes beyond js - Hide the prettier button if not able to process the file type --- components/Article.js | 58 +++++----- components/PrettierCheckMarkIcon.js | 14 +++ components/PrettierErrorIcon.js | 14 +++ components/PrettierLoadingIcon.js | 42 ++++++++ utils/prettier.js | 162 ++++++++++++++++++++++++++-- 5 files changed, 251 insertions(+), 39 deletions(-) create mode 100644 components/PrettierCheckMarkIcon.js create mode 100644 components/PrettierErrorIcon.js create mode 100644 components/PrettierLoadingIcon.js diff --git a/components/Article.js b/components/Article.js index 035005d..334bafe 100644 --- a/components/Article.js +++ b/components/Article.js @@ -1,11 +1,10 @@ -/* global prettier, prettierPlugins, marked */ +/* global marked */ import { html, css, react } from '../utils/rplus.js'; import { prettierButtonMessages, - initialPrettierState, duringPrettierState, - afterPrettierState, - errorPrettierState, + loadPrettierParserScriptForExtension, + pickInitialPrettierState, } from '../utils/prettier.js'; import Editor from './Editor.js'; @@ -18,6 +17,8 @@ import { useStateValue } from '../utils/globalState.js'; export default () => { const [{ request, cache }, dispatch] = useStateValue(); + const initialPrettierState = pickInitialPrettierState(request.file); + const [prettierButtonState, setPrettierButtonState] = react.useState( initialPrettierState ); @@ -25,30 +26,19 @@ export default () => { const fileData = cache['https://unpkg.com/' + request.path] || {}; react.useEffect(() => { + // Reset button when viewing a new file setPrettierButtonState(initialPrettierState); }, [request]); react.useEffect(() => { + // Format code with prettier after button press and loading state displayed if (prettierButtonState.message === prettierButtonMessages.during) { - try { - const code = prettier.format(fileData.code, { - parser: 'babylon', - plugins: prettierPlugins, - }); - dispatch({ - type: 'setCache', - payload: { - ['https://unpkg.com/' + request.path]: { - ...fileData, - code, - }, - }, - }); - setPrettierButtonState(afterPrettierState); - } catch (e) { - console.error(e); - setPrettierButtonState(errorPrettierState); - } + loadPrettierParserScriptForExtension({ + fileData, + setPrettierButtonState, + dispatch, + request, + }); } }, [prettierButtonState.message]); @@ -63,15 +53,19 @@ export default () => { ${request.path} - + ${prettierButtonState.hidden + ? null + : html` + + `} ` : html` diff --git a/components/PrettierCheckMarkIcon.js b/components/PrettierCheckMarkIcon.js new file mode 100644 index 0000000..01ec497 --- /dev/null +++ b/components/PrettierCheckMarkIcon.js @@ -0,0 +1,14 @@ +import { html } from '../utils/rplus.js'; + +// Taken from https://material.io/resources/icons/?search=check&icon=check_circle_outline&style=baseline +// Modified with a `fill="white"` + +export default html` + + + + +`; diff --git a/components/PrettierErrorIcon.js b/components/PrettierErrorIcon.js new file mode 100644 index 0000000..1a18385 --- /dev/null +++ b/components/PrettierErrorIcon.js @@ -0,0 +1,14 @@ +import { html } from '../utils/rplus.js'; + +// Taken from https://material.io/resources/icons/?search=error&icon=error_outline&style=baseline +// Modified with a `fill="white"` + +export default html` + + + + +`; diff --git a/components/PrettierLoadingIcon.js b/components/PrettierLoadingIcon.js new file mode 100644 index 0000000..c2949a0 --- /dev/null +++ b/components/PrettierLoadingIcon.js @@ -0,0 +1,42 @@ +import { html } from '../utils/rplus.js'; + +/** + * `loader` from https://feathericons.com/ + * animateTransform adapted from https://developer.mozilla.org/en-US/docs/Web/SVG/Element/animateTransform + * + * NOTE: This will not actually animate in its current usage as it gets blocked by the synchronous prettier format function + * May be able to use Web Worker... + */ + +export default html` + + + + + + + + + + + +`; diff --git a/utils/prettier.js b/utils/prettier.js index cb6b37d..dc2af54 100644 --- a/utils/prettier.js +++ b/utils/prettier.js @@ -1,36 +1,184 @@ -// [...document.querySelectorAll("head script")].some(script => script.src === "https://unpkg.com/prettier@1.13.0/parser-babylon.js") +/* global prettier, prettierPlugins */ +import PrettierIcon from '../components/PrettierIcon.js'; +import CheckMarkIcon from '../components/PrettierCheckMarkIcon.js'; +import LoadingIcon from '../components/PrettierLoadingIcon.js'; +import ErrorIcon from '../components/PrettierErrorIcon.js'; + +/** + * UTILS + */ + +const getFinalExtension = ext => + ext ? ext.slice(ext.lastIndexOf('.') + 1) : ''; // Also works when no `.` present in string + +/** + * STATE SETUP + */ const prettierButtonMessages = { - initial: 'Format Code', + before: 'Format Code', during: 'Formatting...', after: 'Formatted', error: 'Formatting Failed', }; -const initialPrettierState = { - message: prettierButtonMessages.initial, - disabled: false, +const beforePrettierState = { + message: prettierButtonMessages.before, + disabled: false, // redundant? + icon: PrettierIcon, }; const duringPrettierState = { message: prettierButtonMessages.during, disabled: true, + icon: LoadingIcon, }; const afterPrettierState = { message: prettierButtonMessages.after, disabled: true, + icon: CheckMarkIcon, }; const errorPrettierState = { message: prettierButtonMessages.error, disabled: true, + icon: ErrorIcon, +}; + +const cannotPrettierState = { + hidden: true, + icon: null, +}; + +const pickInitialPrettierState = requestFile => { + if (!prettierParserMap[getFinalExtension(requestFile)]) { + return cannotPrettierState; + } + return beforePrettierState; +}; + +/** + * HANDLING OF VARIOUS FILE TYPES + * `parserScriptUrl`s from https://unpkg.com/browse/prettier@1.13.0/ + * `parserName`s from https://prettier.io/docs/en/options.html#parser + */ + +const prettierParserMap = { + css: { + parserScriptUrl: 'https://unpkg.com/prettier@1.13.0/parser-postcss.js', + parserName: 'css', + }, + js: { + parserScriptUrl: 'https://unpkg.com/prettier@1.13.0/parser-babylon.js', + parserName: 'babylon', + }, + json: { + parserScriptUrl: 'https://unpkg.com/prettier@1.13.0/parser-babylon.js', + parserName: 'json', + }, + // TODO: uncomment markdown entry after addressing issue no. 181 + // https://github.com/FormidableLabs/runpkg/issues/181 + // md: { + // parserScriptUrl: 'https://unpkg.com/prettier@1.13.0/parser-markdown.js', + // parserName: 'markdown', + // }, + less: { + parserScriptUrl: 'https://unpkg.com/prettier@1.13.0/parser-postcss.js', + parserName: 'css', + }, + map: { + // e.g. `vuetify@2.2.15/lib/components/VDatePicker/VDatePickerDateTable.js.map` + parserScriptUrl: 'https://unpkg.com/prettier@1.13.0/parser-babylon.js', + parserName: 'json', + }, + mjs: { + parserScriptUrl: 'https://unpkg.com/prettier@1.13.0/parser-babylon.js', + parserName: 'babylon', + }, + sass: { + parserScriptUrl: 'https://unpkg.com/prettier@1.13.0/parser-postcss.js', + parserName: 'css', + }, + scss: { + parserScriptUrl: 'https://unpkg.com/prettier@1.13.0/parser-postcss.js', + parserName: 'css', + }, + ts: { + parserScriptUrl: 'https://unpkg.com/prettier@1.13.0/parser-typescript.js', + parserName: 'typescript', + }, +}; + +/** + * HANDLING PRETTIER FORMAT BUTTON PRESS + */ + +const loadPrettierParserScriptForExtension = ({ + fileData, + setPrettierButtonState, + dispatch, + request, +}) => { + const trueExt = getFinalExtension(fileData.extension); + const parserLangConfig = prettierParserMap[trueExt]; + + if (!parserLangConfig) { + // This *should* never happen thanks to `pickInitialPrettierState` + console.error( + 'File extension not supported. Prettier button should be hidden/disabled' + ); + setPrettierButtonState(cannotPrettierState); + return; + } + + const scriptUrl = parserLangConfig.parserScriptUrl; + + const isScriptAlreadyPresent = [ + ...document.querySelectorAll('head script'), + ].some(script => script.src === scriptUrl); + + const tryToPrettify = () => { + try { + const code = prettier.format(fileData.code, { + parser: parserLangConfig.parserName, + plugins: prettierPlugins, + }); + dispatch({ + type: 'setCache', + payload: { + ['https://unpkg.com/' + request.path]: { + ...fileData, + code, + }, + }, + }); + setPrettierButtonState(afterPrettierState); + } catch (e) { + console.error(e); + setPrettierButtonState(errorPrettierState); + } + }; + + if (isScriptAlreadyPresent) { + tryToPrettify(); + return; + } + + // If we have have a parser for this extension and the script isn't already imported... + const parserScript = document.createElement('script'); + parserScript.src = scriptUrl; + parserScript.onload = () => { + tryToPrettify(); + }; + + document.head.appendChild(parserScript); }; export { prettierButtonMessages, - initialPrettierState, duringPrettierState, - afterPrettierState, errorPrettierState, + loadPrettierParserScriptForExtension, + pickInitialPrettierState, };