Playground: doc explorer (#10183)
This commit is contained in:
1 parent
ab72a2c11c
commit
b65cb6c1f6
13 files changed
+217
-24
No files matched your search
@@ -0,0 +1,3 @@
|
||||
#### "Doc Explorer" mode for the Playground (#10183 by @thorn0)
|
||||
|
||||
Switch the `parser` option to the special `doc-explorer` value to play with Prettier's intermediate representation and see how it's printed with different options.
|
||||
@@ -9,4 +9,6 @@ The basic idea is that the printer takes an AST and returns an intermediate repr
|
||||
|
||||
This means that most of the logic of printing an AST involves generating an abstract representation of the output involving certain commands. For example, `["(", line, arg, line, ")"]` would represent a concatenation of opening parens, an argument, and closing parens. But if that doesn’t fit on one line, the printer can break where `line` is specified.
|
||||
|
||||
The [Playground](https://prettier.io/playground) has a special mode for exploring how Prettier’s intermediate representation is printed. To get there, open the sidebar (the "Show options" button) and set the `parser` option to the special value `doc-explorer`.
|
||||
|
||||
More (rough) details can be found in [commands.md](https://github.com/prettier/prettier/blob/main/commands.md).
|
||||
@@ -4,3 +4,7 @@
|
||||
[build.environment]
|
||||
NODE_VERSION = "lts/*"
|
||||
YARN_VERSION = "1.22.15"
|
||||
[[headers]]
|
||||
for = "/worker.js"
|
||||
[headers.values]
|
||||
Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-eval'"
|
||||
+1
-1
@@ -96,7 +96,7 @@ function parse(text, opts) {
|
||||
}
|
||||
|
||||
/* istanbul ignore next */
|
||||
throw error.stack;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,10 +67,13 @@ class Playground extends React.Component {
|
||||
options.parser = "babel";
|
||||
}
|
||||
|
||||
const content = original.content || getCodeSample(options.parser);
|
||||
const codeSample = getCodeSample(options.parser);
|
||||
const content = original.content || codeSample;
|
||||
const needsClickForFirstRun =
|
||||
options.parser === "doc-explorer" && content !== codeSample;
|
||||
const selection = {};
|
||||
|
||||
this.state = { content, options, selection };
|
||||
this.state = { content, options, selection, needsClickForFirstRun };
|
||||
|
||||
this.handleOptionValueChange = this.handleOptionValueChange.bind(this);
|
||||
|
||||
@@ -80,14 +83,10 @@ class Playground extends React.Component {
|
||||
this.setSelection = (selection) => this.setState({ selection });
|
||||
this.setSelectionAsRange = () => {
|
||||
const { selection, content, options } = this.state;
|
||||
const { head, anchor } = selection;
|
||||
const range = [head, anchor].map(
|
||||
({ ch, line }) =>
|
||||
content.split("\n").slice(0, line).join("\n").length +
|
||||
ch +
|
||||
(line ? 1 : 0)
|
||||
const [rangeStart, rangeEnd] = util.convertSelectionToRange(
|
||||
selection,
|
||||
content
|
||||
);
|
||||
const [rangeStart, rangeEnd] = range.sort((a, b) => a - b);
|
||||
const updatedOptions = { ...options, rangeStart, rangeEnd };
|
||||
if (rangeStart === rangeEnd) {
|
||||
delete updatedOptions.rangeStart;
|
||||
@@ -103,6 +102,8 @@ class Playground extends React.Component {
|
||||
this.rangeEndOption = props.availableOptions.find(
|
||||
(opt) => opt.name === "rangeEnd"
|
||||
);
|
||||
|
||||
this.handleInputPanelFormat = this.handleInputPanelFormat.bind(this);
|
||||
}
|
||||
|
||||
componentDidUpdate(_, prevState) {
|
||||
@@ -131,6 +132,10 @@ class Playground extends React.Component {
|
||||
? getCodeSample(options.parser)
|
||||
: state.content;
|
||||
|
||||
if (option.name === "parser") {
|
||||
state.needsClickForFirstRun = false;
|
||||
}
|
||||
|
||||
return { options, content };
|
||||
});
|
||||
}
|
||||
@@ -158,6 +163,30 @@ class Playground extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
handleInputPanelFormat() {
|
||||
if (this.state.options.parser !== "doc-explorer") {
|
||||
return;
|
||||
}
|
||||
|
||||
const { content, selection } = this.state;
|
||||
|
||||
return this.props.worker
|
||||
.format(content, {
|
||||
parser: "__js_expression",
|
||||
cursorOffset: util.convertSelectionToRange(selection, content)[0],
|
||||
})
|
||||
.then(({ error, formatted, cursorOffset }) => {
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
value: formatted,
|
||||
cursor: util.convertOffsetToPosition(cursorOffset, formatted),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { worker, version } = this.props;
|
||||
const { content, options } = this.state;
|
||||
@@ -171,6 +200,7 @@ class Playground extends React.Component {
|
||||
<EditorState>
|
||||
{(editorState) => (
|
||||
<PrettierFormat
|
||||
enabled={!this.state.needsClickForFirstRun}
|
||||
worker={worker}
|
||||
code={content}
|
||||
options={options}
|
||||
@@ -259,9 +289,10 @@ class Playground extends React.Component {
|
||||
checked={editorState.showSecondFormat}
|
||||
onChange={editorState.toggleSecondFormat}
|
||||
/>
|
||||
{editorState.showDoc && debug.doc && (
|
||||
{editorState.showDoc && (
|
||||
<ClipboardButton
|
||||
copy={() => this.getMarkdown({ doc: debug.doc })}
|
||||
disabled={!debug.doc}
|
||||
>
|
||||
Copy doc
|
||||
</ClipboardButton>
|
||||
@@ -284,6 +315,7 @@ class Playground extends React.Component {
|
||||
overlayEnd={options.rangeEnd}
|
||||
onChange={this.setContent}
|
||||
onSelectionChange={this.setSelection}
|
||||
onFormat={this.handleInputPanelFormat}
|
||||
/>
|
||||
) : null}
|
||||
{editorState.showAst ? (
|
||||
@@ -302,11 +334,36 @@ class Playground extends React.Component {
|
||||
/>
|
||||
) : null}
|
||||
{editorState.showOutput ? (
|
||||
<OutputPanel
|
||||
mode={util.getCodemirrorMode(options.parser)}
|
||||
value={formatted}
|
||||
ruler={options.printWidth}
|
||||
/>
|
||||
this.state.needsClickForFirstRun ? (
|
||||
<div className="editor disabled-output-panel">
|
||||
<div className="explanation">
|
||||
<code>doc-explorer</code> involves running code
|
||||
provided by users.
|
||||
</div>
|
||||
<div className="explanation">
|
||||
To stay on the safe side and prevent abuse, an
|
||||
explicit user action is required when a direct
|
||||
link to a <code>doc-explorer</code> playground is
|
||||
opened.
|
||||
</div>
|
||||
<div className="explanation">
|
||||
Click the button below to start the playground.
|
||||
</div>
|
||||
<Button
|
||||
onClick={() =>
|
||||
this.setState({ needsClickForFirstRun: false })
|
||||
}
|
||||
>
|
||||
Start
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<OutputPanel
|
||||
mode={util.getCodemirrorMode(options.parser)}
|
||||
value={formatted}
|
||||
ruler={options.printWidth}
|
||||
/>
|
||||
)
|
||||
) : null}
|
||||
{editorState.showSecondFormat ? (
|
||||
<OutputPanel
|
||||
|
||||
@@ -12,6 +12,7 @@ export default class PrettierFormat extends React.Component {
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
for (const key of [
|
||||
"enabled",
|
||||
"code",
|
||||
"options",
|
||||
"debugAst",
|
||||
@@ -28,6 +29,7 @@ export default class PrettierFormat extends React.Component {
|
||||
|
||||
format() {
|
||||
const {
|
||||
enabled,
|
||||
worker,
|
||||
code,
|
||||
options,
|
||||
@@ -37,6 +39,10 @@ export default class PrettierFormat extends React.Component {
|
||||
reformat,
|
||||
} = this.props;
|
||||
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
worker
|
||||
.format(code, options, { ast, doc, comments, reformat })
|
||||
.then((result) => this.setState(result));
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function WorkerApi(source) {
|
||||
getMetadata() {
|
||||
return postMessage({ type: "meta" });
|
||||
},
|
||||
format(code, options, debug) {
|
||||
format(code, options, debug = {}) {
|
||||
return postMessage({ type: "format", code, options, debug });
|
||||
},
|
||||
postMessage,
|
||||
|
||||
@@ -308,6 +308,24 @@ export default function getCodeSamples(parser) {
|
||||
" </body>",
|
||||
"</HTML>",
|
||||
].join("\n");
|
||||
case "doc-explorer":
|
||||
return [
|
||||
"group([",
|
||||
' "(*",',
|
||||
" indent([",
|
||||
" line,",
|
||||
' "Prettier:",',
|
||||
" line,",
|
||||
' "Opinionated,",',
|
||||
" line,",
|
||||
' "Code,",',
|
||||
" line,",
|
||||
' "Formatter",',
|
||||
" ]),",
|
||||
" line,",
|
||||
' "*)",',
|
||||
"])",
|
||||
].join("\n");
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ function getMarkdownSyntax(options) {
|
||||
case "acorn":
|
||||
case "espree":
|
||||
case "meriyah":
|
||||
case "doc-explorer":
|
||||
return "jsx";
|
||||
case "babel-ts":
|
||||
case "typescript":
|
||||
|
||||
@@ -19,6 +19,7 @@ class CodeMirrorPanel extends React.Component {
|
||||
delete options.rulerColor;
|
||||
delete options.value;
|
||||
delete options.onChange;
|
||||
delete options.onFormat;
|
||||
|
||||
options.rulers = [makeRuler(this.props)];
|
||||
|
||||
@@ -26,6 +27,12 @@ class CodeMirrorPanel extends React.Component {
|
||||
options.gutters = ["CodeMirror-linenumbers", "CodeMirror-foldgutter"];
|
||||
}
|
||||
|
||||
if (this.props.onFormat) {
|
||||
options.extraKeys = {
|
||||
"Shift-Alt-F": this.handleFormat.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
this._codeMirror = CodeMirror.fromTextArea(
|
||||
this._textareaRef.current,
|
||||
options
|
||||
@@ -112,6 +119,17 @@ class CodeMirrorPanel extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleFormat() {
|
||||
const result = this.props.onFormat();
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
Promise.resolve(result).then(({ value, cursor }) => {
|
||||
this.props.onChange(value);
|
||||
this._codeMirror.setCursor(cursor);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="editor input">
|
||||
|
||||
@@ -95,6 +95,30 @@ export function getAstAutoFold(parser) {
|
||||
}
|
||||
}
|
||||
|
||||
export function convertSelectionToRange({ head, anchor }, content) {
|
||||
const lines = content.split("\n");
|
||||
return [head, anchor]
|
||||
.map(
|
||||
({ ch, line }) =>
|
||||
lines.slice(0, line).join("\n").length + ch + (line ? 1 : 0)
|
||||
)
|
||||
.sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
export function convertOffsetToPosition(offset, content) {
|
||||
let line = 0;
|
||||
let ch = 0;
|
||||
for (let i = 0; i < offset && i <= content.length; i++) {
|
||||
if (content[i] === "\n") {
|
||||
line++;
|
||||
ch = 0;
|
||||
} else {
|
||||
ch++;
|
||||
}
|
||||
}
|
||||
return { line, ch };
|
||||
}
|
||||
|
||||
/**
|
||||
* Copied from https://github.com/prettier/prettier/blob/6fe21780115cf5f74f83876d64b03a727fbab220/src/cli/utils.js#L6-L27
|
||||
* @template Obj
|
||||
|
||||
@@ -112,6 +112,25 @@ header h1 {
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.disabled-output-panel {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.disabled-output-panel .explanation {
|
||||
font-size: 16px;
|
||||
max-width: 400px;
|
||||
color: darkred;
|
||||
}
|
||||
|
||||
.disabled-output-panel .btn {
|
||||
font-size: 22px;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
/* display as 2x2 grid */
|
||||
@media (min-width: 800px) {
|
||||
.options-container {
|
||||
@@ -258,7 +277,7 @@ input[type="number"] {
|
||||
border-color: #c8e1ff;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
.btn:hover:enabled {
|
||||
background-color: #e6ebf1;
|
||||
background-image: linear-gradient(to bottom, #f0f3f6, #dce3ec);
|
||||
border-color: #afb1b2;
|
||||
@@ -271,6 +290,10 @@ input[type="number"] {
|
||||
box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
color: #afb1b2;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
z-index: 6;
|
||||
|
||||
@@ -30,6 +30,25 @@ for (const file in parsersLocation) {
|
||||
}
|
||||
}
|
||||
|
||||
const docExplorerPlugin = {
|
||||
parsers: {
|
||||
"doc-explorer": {
|
||||
parse: (text) =>
|
||||
new Function(
|
||||
`{ ${Object.keys(prettier.doc.builders)} }`,
|
||||
`const result = (${text || "''"}\n); return result;`
|
||||
)(prettier.doc.builders),
|
||||
astFormat: "doc-explorer",
|
||||
},
|
||||
},
|
||||
printers: {
|
||||
"doc-explorer": {
|
||||
print: (path) => path.getValue(),
|
||||
},
|
||||
},
|
||||
languages: [{ name: "doc-explorer", parsers: ["doc-explorer"] }],
|
||||
};
|
||||
|
||||
self.onmessage = function (event) {
|
||||
self.postMessage({
|
||||
uid: event.data.uid,
|
||||
@@ -45,6 +64,8 @@ function serializeAst(ast) {
|
||||
? { name: value.name, message: value.message, ...value }
|
||||
: typeof value === "bigint"
|
||||
? `BigInt('${String(value)}')`
|
||||
: typeof value === "symbol"
|
||||
? String(value)
|
||||
: value,
|
||||
2
|
||||
);
|
||||
@@ -58,6 +79,7 @@ function handleMessage(message) {
|
||||
JSON.stringify(
|
||||
prettier.getSupportInfo({
|
||||
showUnreleased: true,
|
||||
plugins: [docExplorerPlugin],
|
||||
})
|
||||
)
|
||||
),
|
||||
@@ -72,13 +94,15 @@ function handleMessage(message) {
|
||||
delete options.doc;
|
||||
delete options.output2;
|
||||
|
||||
const plugins = [{ parsers }];
|
||||
const plugins = [{ parsers }, docExplorerPlugin];
|
||||
options.plugins = plugins;
|
||||
|
||||
const formatResult = formatCode(message.code, options);
|
||||
|
||||
const response = {
|
||||
formatted: formatResult.formatted,
|
||||
cursorOffset: formatResult.cursorOffset,
|
||||
error: formatResult.error,
|
||||
debug: {
|
||||
ast: null,
|
||||
doc: null,
|
||||
@@ -111,10 +135,10 @@ function handleMessage(message) {
|
||||
try {
|
||||
response.debug.doc = prettier.__debug.formatDoc(
|
||||
prettier.__debug.printToDoc(message.code, options),
|
||||
{ parser: "babel", plugins }
|
||||
{ plugins }
|
||||
);
|
||||
} catch (e) {
|
||||
response.debug.doc = String(e);
|
||||
} catch {
|
||||
response.debug.doc = "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,10 +166,23 @@ function formatCode(text, options) {
|
||||
} catch (e) {
|
||||
if (e.constructor && e.constructor.name === "SyntaxError") {
|
||||
// Likely something wrong with the user's code
|
||||
return { formatted: String(e) };
|
||||
return { formatted: String(e), error: true };
|
||||
}
|
||||
// Likely a bug in Prettier
|
||||
// Provide the whole stack for debugging
|
||||
return { formatted: e.stack || String(e) };
|
||||
return { formatted: stringifyError(e), error: true };
|
||||
}
|
||||
}
|
||||
|
||||
function stringifyError(e) {
|
||||
const stringified = String(e);
|
||||
if (typeof e.stack !== "string") {
|
||||
return stringified;
|
||||
}
|
||||
if (e.stack.includes(stringified)) {
|
||||
// Chrome
|
||||
return e.stack;
|
||||
}
|
||||
// Firefox
|
||||
return stringified + "\n" + e.stack;
|
||||
}
|
||||
Reference in new issue
Block a user