Skip to content

Prettier enhancements: handle more file types & improve loading states #184

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 46 additions & 22 deletions components/Article.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
/* global prettier, prettierPlugins, marked */
import { html, css } from '../utils/rplus.js';
/* global marked */
import { html, css, react } from '../utils/rplus.js';
import {
prettierButtonMessages,
duringPrettierState,
loadPrettierParserScriptForExtension,
pickInitialPrettierState,
} from '../utils/prettier.js';

import Editor from './Editor.js';
import FileIcon from './FileIcon.js';
Expand All @@ -10,7 +16,32 @@ import { useStateValue } from '../utils/globalState.js';

export default () => {
const [{ request, cache }, dispatch] = useStateValue();

const initialPrettierState = pickInitialPrettierState(request.file);

const [prettierButtonState, setPrettierButtonState] = react.useState(
initialPrettierState
);

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) {
loadPrettierParserScriptForExtension({
fileData,
setPrettierButtonState,
dispatch,
request,
});
}
}, [prettierButtonState.message]);

return html`
<article className=${styles.container}>
${request.path
Expand All @@ -22,26 +53,19 @@ export default () => {
${request.path}
</span>
</h1>
<button
onClick=${() => {
const code = prettier.format(fileData.code, {
parser: 'babylon',
plugins: prettierPlugins,
});
dispatch({
type: 'setCache',
payload: {
['https://unpkg.com/' + request.path]: {
...fileData,
code,
},
},
});
}}
>
${PrettierIcon}
<span>Format Code</span>
</button>
${prettierButtonState.hidden
? null
: html`
<button
disabled=${prettierButtonState.disabled}
onClick=${() => {
setPrettierButtonState(duringPrettierState);
}}
>
${prettierButtonState.icon || PrettierIcon}
<span>${prettierButtonState.message}</span>
</button>
`}
</header>
`
: html`
Expand Down
14 changes: 14 additions & 0 deletions components/PrettierCheckMarkIcon.js
Original file line number Diff line number Diff line change
@@ -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`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0zm0 0h24v24H0V0z" fill="none" />
<path
fill="white"
d="M16.59 7.58L10 14.17l-3.59-3.58L5 12l5 5 8-8zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
/>
</svg>
`;
14 changes: 14 additions & 0 deletions components/PrettierErrorIcon.js
Original file line number Diff line number Diff line change
@@ -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`
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />
<path
fill="white"
d="M11 15h2v2h-2zm0-8h2v6h-2zm.99-5C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
/>
</svg>
`;
42 changes: 42 additions & 0 deletions components/PrettierLoadingIcon.js
Original file line number Diff line number Diff line change
@@ -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`
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="feather feather-loader"
>
<line x1="12" y1="2" x2="12" y2="6"></line>
<line x1="12" y1="18" x2="12" y2="22"></line>
<line x1="4.93" y1="4.93" x2="7.76" y2="7.76"></line>
<line x1="16.24" y1="16.24" x2="19.07" y2="19.07"></line>
<line x1="2" y1="12" x2="6" y2="12"></line>
<line x1="18" y1="12" x2="22" y2="12"></line>
<line x1="4.93" y1="19.07" x2="7.76" y2="16.24"></line>
<line x1="16.24" y1="7.76" x2="19.07" y2="4.93"></line>
<animateTransform
attributeName="transform"
attributeType="XML"
type="rotate"
from="0"
to="360"
dur="2.5s"
repeatCount="indefinite"
/>
</svg>
`;
184 changes: 184 additions & 0 deletions utils/prettier.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/* 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 = {
before: 'Format Code',
during: 'Formatting...',
after: 'Formatted',
error: 'Formatting Failed',
};

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,
};
Comment on lines +25 to +52
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🆒 way to handle the UI states

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤗 Yeah I prefer the neatness of having these separated out clearly! Probably influenced by TS enums and Redux


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);
Comment on lines +156 to +159
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 put the button state setting in the UI component? better separation of concerns?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 right after the call to loadPrettierParserScriptForExtension?
https://github.com/FormidableLabs/runpkg/pull/184/files#diff-bb8820440d0617732459bfe856f864abR42

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm 50/50 about this! On one hand it could be nice to keep the setStates in the component where the useState is used instead of passing the setter to this file, but on the other hand I feel this way keeps the Article component less cluttered (and is a bit easier).

Open to going the route you suggest though 🤔. Might see what others think

}
};

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,
duringPrettierState,
errorPrettierState,
loadPrettierParserScriptForExtension,
pickInitialPrettierState,
};