Skip to content

Commit

Permalink
Add JSON support (prettier#2173)
Browse files Browse the repository at this point in the history
* Add JSON support

This fixes prettier#1973 by using
[json-to-ast] to parse JSON, then converting the AST into a
mostly-Babylon AST, so as to piggyback on the existing printer logic.

Identifiers and literals are currently printed verbatim from the input,
but this could be improved upon later (at least for literals, not sure
about identifiers).

[json-to-ast]: https://github.com/vtrushin/json-to-ast

* Rename `json-to-ast` parser to `json`

See prettier#2173 (comment)

* json: Use createError to have a pretty error

See prettier#2173 (comment)

* json: Transform AST without mutations

See prettier#2173 (comment)
  • Loading branch information
josephfrazier authored and vjeux committed Jun 17, 2017
1 parent 59a294f commit a42db30
Show file tree
Hide file tree
Showing 13 changed files with 281 additions and 2 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ Prettier ships with a handful of customizable format options, usable in both the
| **Cursor Offset** - Specify where the cursor is. This option only works with `prettier.formatWithCursor`, and cannot be used with `rangeStart` and `rangeEnd`. | `-1` | `--cursor-offset <int>` | `cursorOffset: <int>` |
| **Range Start** - Format code starting at a given character offset. The range will extend backwards to the start of the first line containing the selected statement. This option cannot be used with `cursorOffset`. | `0` | `--range-start <int>` | `rangeStart: <int>` |
| **Range End** - Format code ending at a given character offset (exclusive). The range will extend forwards to the end of the selected statement. This option cannot be used with `cursorOffset`. | `Infinity` | `--range-end <int>` | `rangeEnd: <int>` |
| **Parser** - Specify which parser to use. Both the `babylon` and `flow` parsers support the same set of JavaScript features (including Flow). Prettier automatically infers the parser from the input file path, so you shouldn't have to change this setting. [Custom parsers](#custom-parser-api) are supported. | `babylon` | <code>--parser <flow&#124;babylon&#124;typescript&#124;postcss></code><br /><code>--parser ./path/to/my-parser</code> | <code>parser: "<flow&#124;babylon&#124;typescript&#124;postcss>"</code><br /><code>parser: require("./my-parser")</code> |
| **Parser** - Specify which parser to use. Both the `babylon` and `flow` parsers support the same set of JavaScript features (including Flow). Prettier automatically infers the parser from the input file path, so you shouldn't have to change this setting. [Custom parsers](#custom-parser-api) are supported. | `babylon` | <code>--parser <flow&#124;babylon&#124;typescript&#124;postcss&#124;json></code><br /><code>--parser ./path/to/my-parser</code> | <code>parser: "<flow&#124;babylon&#124;typescript&#124;postcss&#124;json>"</code><br /><code>parser: require("./my-parser")</code> |
| **Filepath** - Specify the input filepath this will be used to do parser inference.<br /><br /> Example: <br />`cat foo \| prettier --stdin-filepath foo.css`<br /> will default to use `postcss` parser | | `--stdin-filepath` | `filepath: <string>` |


Expand Down
2 changes: 1 addition & 1 deletion bin/prettier.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ if (argv["help"] || (!filepatterns.length && !stdin)) {
" --jsx-bracket-same-line Put > on the last line instead of at a new line.\n" +
" --trailing-comma <none|es5|all>\n" +
" Print trailing commas wherever possible. Defaults to none.\n" +
" --parser <flow|babylon|typescript|postcss>\n" +
" --parser <flow|babylon|typescript|postcss|json>\n" +
" Specify which parse to use. Defaults to babylon.\n" +
" --cursor-offset <int> Print (to stderr) where a cursor at the given position would move to after formatting.\n" +
" This option cannot be used with --range-start and --range-end\n" +
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"glob": "7.1.2",
"graphql": "0.10.1",
"jest-validate": "20.0.3",
"json-to-ast": "2.0.0-alpha1.2",
"minimist": "1.2.0",
"parse5": "3.0.2",
"postcss": "^6.0.1",
Expand Down
2 changes: 2 additions & 0 deletions src/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ function normalize(options) {
normalized.parser = "parse5";
} else if (/\.(ts|tsx)$/.test(filepath)) {
normalized.parser = "typescript";
} else if (/\.json$/.test(filepath)) {
normalized.parser = "json";
}

if (typeof normalized.trailingComma === "boolean") {
Expand Down
68 changes: 68 additions & 0 deletions src/parser-json.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"use strict";

const createError = require("./parser-create-error");

function parse(text) {
const jsonToAst = require("json-to-ast");
try {
const ast = jsonToAst(text);
return toBabylon(ast);
} catch (err) {
const firstNewlineIndex = err.message.indexOf("\n");
const firstLine = err.message.slice(0, firstNewlineIndex);
const lastSpaceIndex = firstLine.lastIndexOf(" ");
const message = firstLine.slice(0, lastSpaceIndex);
const locString = firstLine.slice(lastSpaceIndex + 1);
const lineCol = locString.split(":").map(Number);

throw createError("(json-to-ast) " + message, {
start: {
line: lineCol[0],
column: lineCol[1]
}
});
}
}

function toBabylon(node) {
const typeMap = {
object: "ObjectExpression",
property: "ObjectProperty",
identifier: "json-identifier",
array: "ArrayExpression",
literal: "json-literal"
};

const result = {
type: typeMap[node.type],
start: node.loc.start.offset,
end: node.loc.end.offset,
loc: node.loc
};

switch (node.type) {
case "object":
return Object.assign(result, {
properties: node.children.map(toBabylon)
});
case "property":
return Object.assign(result, {
key: toBabylon(node.key),
value: toBabylon(node.value)
});
case "identifier":
return Object.assign(result, {
value: node.value
});
case "array":
return Object.assign(result, {
elements: node.children.map(toBabylon)
});
case "literal":
return Object.assign(result, {
rawValue: node.rawValue
});
}
}

module.exports = parse;
3 changes: 3 additions & 0 deletions src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ const parsers = {
},
get postcss() {
return eval("require")("./parser-postcss");
},
get json() {
return eval("require")("./parser-json");
}
};

Expand Down
4 changes: 4 additions & 0 deletions src/printer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2634,6 +2634,10 @@ function genericPrintNoParens(path, options, print, args) {
return path.call(bodyPath => {
return printStatementSequence(bodyPath, options, print);
}, "body");
case "json-identifier":
return '"' + n.value + '"';
case "json-literal":
return n.rawValue;

default:
throw new Error("unknown type: " + JSON.stringify(n.type));
Expand Down
135 changes: 135 additions & 0 deletions tests/json/__snapshots__/jsfmt.spec.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`multi-line.json 1`] = `
{"key1":[true,false,null],"key2":{"key3":[1,2,"3",
1e10,1e-3]}}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
{
"key1": [true, false, null],
"key2": {
"key3": [1, 2, "3", 1e10, 1e-3]
}
}
`;

exports[`pass1.json 1`] = `
[
"JSON Test Pattern pass1",
{"object with 1 member":["array with 1 element"]},
{},
[],
-42,
true,
false,
null,
{
"integer": 1234567890,
"real": -9876.543210,
"e": 0.123456789e-12,
"E": 1.234567890E+34,
"": 23456789012E66,
"zero": 0,
"one": 1,
"space": " ",
"quote": "\\"",
"backslash": "\\\\",
"controls": "\\b\\f\\n\\r\\t",
"slash": "/ & \\/",
"alpha": "abcdefghijklmnopqrstuvwyz",
"ALPHA": "ABCDEFGHIJKLMNOPQRSTUVWYZ",
"digit": "0123456789",
"0123456789": "digit",
"special": "\`1~!@#$%^&*()_+-={':[,]}|;.</>?",
"hex": "\\u0123\\u4567\\u89AB\\uCDEF\\uabcd\\uef4A",
"true": true,
"false": false,
"null": null,
"array":[ ],
"object":{ },
"address": "50 St. James Street",
"url": "http://www.JSON.org/",
"comment": "// /* <!-- --",
"# -- --> */": " ",
" s p a c e d " :[1,2 , 3
,
4 , 5 , 6 ,7 ],"compact":[1,2,3,4,5,6,7],
"jsontext": "{\\"object with 1 member\\":[\\"array with 1 element\\"]}",
"quotes": "&#34; \\u0022 %22 0x22 034 &#x22;",
"\\/\\\\\\"\\uCAFE\\uBABE\\uAB98\\uFCDE\\ubcda\\uef4A\\b\\f\\n\\r\\t\`1~!@#$%^&*()_+-=[]{}|;:',./<>?"
: "A key can be any string"
},
0.5 ,98.6
,
99.44
,
1066,
1e1,
0.1e1,
1e-1,
1e00,2e+00,2e-00
,"rosebud"]~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[
"JSON Test Pattern pass1",
{ "object with 1 member": ["array with 1 element"] },
{},
[],
-42,
true,
false,
null,
{
"integer": 1234567890,
"real": -9876.543210,
"e": 0.123456789e-12,
"E": 1.234567890E+34,
"": 23456789012E66,
"zero": 0,
"one": 1,
"space": " ",
"quote": "\\"",
"backslash": "\\\\",
"controls": "\\b\\f\\n\\r\\t",
"slash": "/ & \\/",
"alpha": "abcdefghijklmnopqrstuvwyz",
"ALPHA": "ABCDEFGHIJKLMNOPQRSTUVWYZ",
"digit": "0123456789",
"0123456789": "digit",
"special": "\`1~!@#$%^&*()_+-={':[,]}|;.</>?",
"hex": "\\u0123\\u4567\\u89AB\\uCDEF\\uabcd\\uef4A",
"true": true,
"false": false,
"null": null,
"array": [],
"object": {},
"address": "50 St. James Street",
"url": "http://www.JSON.org/",
"comment": "// /* <!-- --",
"# -- --> */": " ",
" s p a c e d ": [1, 2, 3, 4, 5, 6, 7],
"compact": [1, 2, 3, 4, 5, 6, 7],
"jsontext": "{\\"object with 1 member\\":[\\"array with 1 element\\"]}",
"quotes": "&#34; \\u0022 %22 0x22 034 &#x22;",
"\\/\\\\\\"\\uCAFE\\uBABE\\uAB98\\uFCDE\\ubcda\\uef4A\\b\\f\\n\\r\\t\`1~!@#$%^&*()_+-=[]{}|;:',./<>?": "A key can be any string"
},
0.5,
98.6,
99.44,
1066,
1e1,
0.1e1,
1e-1,
1e00,
2e+00,
2e-00,
"rosebud"
]
`;
exports[`single-line.json 1`] = `
{"key1":[true,false,null],"key2":{"key3":[1,2,"3",1e10,1e-3]}}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
{ "key1": [true, false, null], "key2": { "key3": [1, 2, "3", 1e10, 1e-3] } }
`;
1 change: 1 addition & 0 deletions tests/json/jsfmt.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
run_spec(__dirname, { parser: "json" });
2 changes: 2 additions & 0 deletions tests/json/multi-line.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"key1":[true,false,null],"key2":{"key3":[1,2,"3",
1e10,1e-3]}}
58 changes: 58 additions & 0 deletions tests/json/pass1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
[
"JSON Test Pattern pass1",
{"object with 1 member":["array with 1 element"]},
{},
[],
-42,
true,
false,
null,
{
"integer": 1234567890,
"real": -9876.543210,
"e": 0.123456789e-12,
"E": 1.234567890E+34,
"": 23456789012E66,
"zero": 0,
"one": 1,
"space": " ",
"quote": "\"",
"backslash": "\\",
"controls": "\b\f\n\r\t",
"slash": "/ & \/",
"alpha": "abcdefghijklmnopqrstuvwyz",
"ALPHA": "ABCDEFGHIJKLMNOPQRSTUVWYZ",
"digit": "0123456789",
"0123456789": "digit",
"special": "`1~!@#$%^&*()_+-={':[,]}|;.</>?",
"hex": "\u0123\u4567\u89AB\uCDEF\uabcd\uef4A",
"true": true,
"false": false,
"null": null,
"array":[ ],
"object":{ },
"address": "50 St. James Street",
"url": "http://www.JSON.org/",
"comment": "// /* <!-- --",
"# -- --> */": " ",
" s p a c e d " :[1,2 , 3

,

4 , 5 , 6 ,7 ],"compact":[1,2,3,4,5,6,7],
"jsontext": "{\"object with 1 member\":[\"array with 1 element\"]}",
"quotes": "&#34; \u0022 %22 0x22 034 &#x22;",
"\/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?"
: "A key can be any string"
},
0.5 ,98.6
,
99.44
,

1066,
1e1,
0.1e1,
1e-1,
1e00,2e+00,2e-00
,"rosebud"]
1 change: 1 addition & 0 deletions tests/json/single-line.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"key1":[true,false,null],"key2":{"key3":[1,2,"3",1e10,1e-3]}}
4 changes: 4 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2316,6 +2316,10 @@ json-stringify-safe@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"

[email protected]:
version "2.0.0-alpha1.2"
resolved "https://registry.yarnpkg.com/json-to-ast/-/json-to-ast-2.0.0-alpha1.2.tgz#fe27fd89eb639eca1e01f578e79d46ee36e238e8"

json5@^0.5.0, json5@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
Expand Down

0 comments on commit a42db30

Please sign in to comment.