Added inline errors to the code-mirror editors based on field spec.

This commit is contained in:
orangemug 2020-03-08 18:38:32 +00:00
parent be7642976b
commit ce976991d4
6 changed files with 185 additions and 8 deletions

View file

@ -30,7 +30,8 @@
"color": "^3.1.2",
"detect-browser": "^4.8.0",
"file-saver": "^2.0.2",
"jsonlint": "github:josdejong/jsonlint#85a19d7",
"json-to-ast": "^2.1.0",
"jsonlint": "^1.6.3",
"lodash": "^4.17.15",
"lodash.capitalize": "^4.2.1",
"lodash.clamp": "^4.0.3",

View file

@ -358,7 +358,9 @@ export default class App extends React.Component {
if (message) {
try {
const objPath = message.split(":")[0];
unset(dirtyMapStyle, objPath);
// Errors can be deply nested for example 'layers[0].filter[1][1][0]' we only care upto the property 'layers[0].filter'
const unsetPath = objPath.match(/^\S+?\[\d+\]\.[^\[]+/)[0];
unset(dirtyMapStyle, unsetPath);
}
catch (err) {
console.warn(err);

View file

@ -83,6 +83,10 @@ export default class ExpressionProperty extends React.Component {
const fieldError = errors[fieldKey];
const errorKeyStart = `${fieldKey}[`;
const foundErrors = [];
function getValue (data) {
return stringifyPretty(data, {indent: 2, maxLength: 38})
}
if (jsonError) {
foundErrors.push({message: "Invalid JSON"});
@ -109,6 +113,11 @@ export default class ExpressionProperty extends React.Component {
wideMode={true}
>
<JSONEditor
mode={{name: "mgl"}}
lint={{
context: "expression",
spec: this.props.fieldSpec,
}}
className="maputnik-expression-editor"
onFocus={this.props.onFocus}
onBlur={this.props.onBlur}
@ -118,7 +127,7 @@ export default class ExpressionProperty extends React.Component {
lineNumbers={false}
maxHeight={200}
lineWrapping={true}
getValue={(data) => stringifyPretty(data, {indent: 2, maxLength: 50})}
getValue={getValue}
onChange={this.props.onChange}
/>
</InputBlock>

View file

@ -16,6 +16,10 @@ import ExpressionProperty from '../fields/_ExpressionProperty';
function combiningFilter (props) {
let filter = props.filter || ['all'];
if (!Array.isArray(filter)) {
return filter;
}
let combiningOp = filter[0];
let filters = filter.slice(1);

View file

@ -13,6 +13,7 @@ import 'codemirror/lib/codemirror.css'
import 'codemirror/addon/lint/lint.css'
import jsonlint from 'jsonlint'
import stringifyPretty from 'json-stringify-pretty-compact'
import '../util/codemirror-mgl';
// This is mainly because of this issue <https://github.com/zaach/jsonlint/issues/57> also the API has changed, see comment in file
import '../../vendor/codemirror/addon/lint/json-lint'
@ -32,6 +33,11 @@ class JSONEditor extends React.Component {
onBlur: PropTypes.func,
onJSONValid: PropTypes.func,
onJSONInvalid: PropTypes.func,
mode: PropTypes.object,
lint: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.object,
]),
}
static defaultProps = {
@ -39,7 +45,7 @@ class JSONEditor extends React.Component {
lineWrapping: false,
gutters: ["CodeMirror-lint-markers"],
getValue: (data) => {
return stringifyPretty(data, {indent: 2, maxLength: 50});
return stringifyPretty(data, {indent: 2, maxLength: 40});
},
onFocus: () => {},
onBlur: () => {},
@ -58,16 +64,17 @@ class JSONEditor extends React.Component {
componentDidMount () {
this._doc = CodeMirror(this._el, {
value: this.props.getValue(this.props.layer),
mode: {
name: "javascript",
json: true
mode: this.props.mode || {
name: "mgl",
},
lineWrapping: this.props.lineWrapping,
tabSize: 2,
theme: 'maputnik',
viewportMargin: Infinity,
lineNumbers: this.props.lineNumbers,
lint: true,
lint: this.props.lint || {
context: "layer"
},
matchBrackets: true,
gutters: this.props.gutters,
scrollbarStyle: "null",

View file

@ -0,0 +1,154 @@
import jsonlint from 'jsonlint';
import CodeMirror from 'codemirror';
import jsonToAst from 'json-to-ast';
import {expression, validate, latest} from '@mapbox/mapbox-gl-style-spec';
CodeMirror.defineMode("mgl", function(config, parserConfig) {
// Just using the javascript mode with json enabled. Our logic is in the linter below.
return CodeMirror.modes.javascript(
{...config, json: true},
parserConfig
);
});
CodeMirror.registerHelper("lint", "mgl", function(text, opts, doc) {
const found = [];
const {parser} = jsonlint;
const {context} = opts;
parser.parseError = function(str, hash) {
const loc = hash.loc;
found.push({
from: CodeMirror.Pos(loc.first_line - 1, loc.first_column),
to: CodeMirror.Pos(loc.last_line - 1, loc.last_column),
message: str
});
};
try {
parser.parse(text);
}
catch (e) {}
if (found.length > 0) {
// JSON invalid so don't go any further
return found;
}
const ast = jsonToAst(text);
const input = JSON.parse(text);
function getArrayPositionalFromAst (node, path) {
if (!node) {
return undefined;
}
else if (path.length < 1) {
return node;
}
else if (!node.children) {
return undefined;
}
else {
const key = path[0];
let newNode;
if (key.match(/^[0-9]+$/)) {
newNode = node.children[path[0]];
}
else {
newNode = node.children.find(childNode => {
return (
childNode.key &&
childNode.key.type === "Identifier" &&
childNode.key.value === key
);
});
if (newNode) {
newNode = newNode.value;
}
}
return getArrayPositionalFromAst(newNode, path.slice(1))
}
}
let out;
if (context === "layer") {
// Just an empty style so we can validate a layer.
const errors = validate({
"version": 8,
"name": "Empty Style",
"metadata": {},
"sources": {},
"sprite": "",
"glyphs": "https://example.com/glyphs/{fontstack}/{range}.pbf",
"layers": [
input
]
});
if (errors) {
out = {
result: "error",
value: errors
.filter(err => {
// Remove missing 'layer source' errors, because we don't include them
if (err.message.match(/^layers\[0\]: source ".*" not found$/)) {
return false;
}
else {
return true;
}
})
.map(err => {
// Remove the 'layers[0].' as we're validating the layer only here
const errMessageParts = err.message.replace(/^layers\[0\]./, "").split(":");
return {
key: errMessageParts[0],
message: errMessageParts[1],
};
})
}
}
}
else if (context === "expression") {
out = expression.createExpression(input, opts.spec);
}
else {
throw new Error(`Invalid context ${context}`);
}
if (out.result === "error") {
const errors = out.value;
errors.forEach(error => {
const {key, message} = error;
if (!key) {
const lastLineHandle = doc.getLineHandle(doc.lastLine());
const err = {
from: CodeMirror.Pos(doc.firstLine(), 0),
to: CodeMirror.Pos(doc.lastLine(), lastLineHandle.text.length),
message: message,
}
found.push(err);
}
else if (key) {
const path = key.replace(/^\[|\]$/g, "").split(/\.|[\[\]]+/).filter(Boolean)
const parsedError = getArrayPositionalFromAst(ast, path);
if (!parsedError) {
console.warn("Something went wrong parsing error:", error);
return;
}
const {loc} = parsedError;
const {start, end} = loc;
found.push({
from: CodeMirror.Pos(start.line - 1, start.column),
to: CodeMirror.Pos(end.line - 1, end.column),
message: message,
});
}
})
}
return found;
});