Compare commits

...

9 Commits

Author SHA1 Message Date
HarelM 854e3eeb45 Rebuild lock file 2025-09-17 00:35:29 +03:00
HarelM 7c112d7c10 Added missing dependencies? 2025-09-17 00:32:24 +03:00
HarelM c870fefb14 Fix build 2025-09-17 00:22:32 +03:00
pre-commit-ci[bot] df64f5c1aa [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-09-16 21:17:25 +00:00
HarelM d4d161ccbe Updated changelog 2025-09-17 00:17:12 +03:00
pre-commit-ci[bot] 9d35c3d650 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-09-16 21:15:47 +00:00
HarelM 176e1e4ae8 Final fixes 2025-09-17 00:12:14 +03:00
HarelM b70b6f16b7 Merge branch 'main' into codemirror-upgrade 2025-09-16 21:07:10 +03:00
HarelM bc57cf69d2 Initial commit to upgrade code mirror 2025-09-16 11:24:41 +03:00
12 changed files with 2487 additions and 2048 deletions
+3
View File
@@ -8,6 +8,8 @@
- Add ability to control the projection of the map - either globe or mercator - Add ability to control the projection of the map - either globe or mercator
- Add markdown support for doc related to the style-spec fields - Add markdown support for doc related to the style-spec fields
- Added global state modal to allow editing the global state - Added global state modal to allow editing the global state
- Added color highlight for problematic properties
- Upgraded codemirror from version 5 to version 6
- _...Add new stuff here..._ - _...Add new stuff here..._
### 🐞 Bug fixes ### 🐞 Bug fixes
@@ -17,6 +19,7 @@
- Fixed an issue with the generation of tranlations - Fixed an issue with the generation of tranlations
- Fix missing spec info when clicking next to a property - Fix missing spec info when clicking next to a property
- Fix Firefox open file that stopped working due to react upgrade - Fix Firefox open file that stopped working due to react upgrade
- Fix issue with missing bottom error panel
- _...Add new stuff here..._ - _...Add new stuff here..._
## 3.0.0 ## 3.0.0
+2219 -1637
View File
File diff suppressed because it is too large Load Diff
+6 -2
View File
@@ -24,6 +24,11 @@
"license": "MIT", "license": "MIT",
"homepage": "https://github.com/maplibre/maputnik#readme", "homepage": "https://github.com/maplibre/maputnik#readme",
"dependencies": { "dependencies": {
"@codemirror/lang-json": "^6.0.2",
"@codemirror/lint": "^6.8.5",
"@codemirror/state": "^6.5.2",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.38.2",
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
@@ -31,11 +36,10 @@
"@maplibre/maplibre-gl-geocoder": "^1.9.0", "@maplibre/maplibre-gl-geocoder": "^1.9.0",
"@maplibre/maplibre-gl-inspect": "^1.7.1", "@maplibre/maplibre-gl-inspect": "^1.7.1",
"@maplibre/maplibre-gl-style-spec": "^23.3.0", "@maplibre/maplibre-gl-style-spec": "^23.3.0",
"@prantlf/jsonlint": "^16.0.0",
"array-move": "^4.0.0", "array-move": "^4.0.0",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"codemirror": "^5.65.20", "codemirror": "^6.0.2",
"color": "^5.0.2", "color": "^5.0.2",
"detect-browser": "^5.3.0", "detect-browser": "^5.3.0",
"downshift": "^9.0.10", "downshift": "^9.0.10",
+4 -3
View File
@@ -3,7 +3,8 @@ import { TbMathFunction } from "react-icons/tb";
import { PiListPlusBold } from "react-icons/pi"; import { PiListPlusBold } from "react-icons/pi";
import {isEqual} from "lodash"; import {isEqual} from "lodash";
import {type ExpressionSpecification, type LegacyFilterSpecification} from "maplibre-gl"; import {type ExpressionSpecification, type LegacyFilterSpecification} from "maplibre-gl";
import {latest, migrate, convertFilter} from "@maplibre/maplibre-gl-style-spec"; import {migrate, convertFilter} from "@maplibre/maplibre-gl-style-spec";
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
import {combiningFilterOps} from "../libs/filterops"; import {combiningFilterOps} from "../libs/filterops";
import InputSelect from "./InputSelect"; import InputSelect from "./InputSelect";
@@ -96,7 +97,7 @@ type FilterEditorInternalProps = {
properties?: {[key:string]: any} properties?: {[key:string]: any}
filter?: any[] filter?: any[]
errors?: MappedLayerErrors errors?: MappedLayerErrors
onChange(value: LegacyFilterSpecification | ExpressionSpecification): unknown onChange(value: LegacyFilterSpecification | ExpressionSpecification): void
} & WithTranslation; } & WithTranslation;
type FilterEditorState = { type FilterEditorState = {
@@ -293,7 +294,7 @@ class FilterEditorInternal extends React.Component<FilterEditorInternalProps, Fi
this.props.onChange(defaultFilter); this.props.onChange(defaultFilter);
}} }}
fieldName="filter" fieldName="filter"
fieldSpec={fieldSpec} fieldSpec={fieldSpec as any}
value={filter} value={filter}
errors={errors} errors={errors}
onChange={this.props.onChange} onChange={this.props.onChange}
+34 -78
View File
@@ -1,57 +1,37 @@
import React from "react"; import React from "react";
import classnames from "classnames"; import classnames from "classnames";
import CodeMirror, { type ModeSpec } from "codemirror"; import { type WithTranslation, withTranslation } from "react-i18next";
import { Trans, type WithTranslation, withTranslation } from "react-i18next";
import "codemirror/mode/javascript/javascript"; import { type EditorView } from "@codemirror/view";
import "codemirror/addon/lint/lint";
import "codemirror/addon/edit/matchbrackets";
import "codemirror/lib/codemirror.css";
import "codemirror/addon/lint/lint.css";
import stringifyPretty from "json-stringify-pretty-compact"; import stringifyPretty from "json-stringify-pretty-compact";
import "../libs/codemirror-mgl";
import type { LayerSpecification } from "maplibre-gl";
import {createEditor} from "../libs/codemirror-mgl";
import type { StylePropertySpecification } from "maplibre-gl";
export type InputJsonProps = { export type InputJsonProps = {
layer: LayerSpecification value: object
maxHeight?: number maxHeight?: number
onChange?(...args: unknown[]): unknown
lineNumbers?: boolean
lineWrapping?: boolean
getValue?(data: any): string
gutters?: string[]
className?: string className?: string
onChange(object: object): void
onFocus?(...args: unknown[]): unknown onFocus?(...args: unknown[]): unknown
onBlur?(...args: unknown[]): unknown onBlur?(...args: unknown[]): unknown
onJSONValid?(...args: unknown[]): unknown lintType: "layer" | "style" | "expression" | "json"
onJSONInvalid?(...args: unknown[]): unknown spec?: StylePropertySpecification | undefined
mode?: ModeSpec<any>
lint?: boolean | object
}; };
type InputJsonInternalProps = InputJsonProps & WithTranslation; type InputJsonInternalProps = InputJsonProps & WithTranslation;
type InputJsonState = { type InputJsonState = {
isEditing: boolean isEditing: boolean
showMessage: boolean
prevValue: string prevValue: string
}; };
class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJsonState> { class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJsonState> {
static defaultProps = { static defaultProps = {
lineNumbers: true,
lineWrapping: false,
gutters: ["CodeMirror-lint-markers"],
getValue: (data: any) => {
return stringifyPretty(data, {indent: 2, maxLength: 40});
},
onFocus: () => {}, onFocus: () => {},
onBlur: () => {}, onBlur: () => {},
onJSONInvalid: () => {},
onJSONValid: () => {},
}; };
_keyEvent: string; _keyEvent: string;
_doc: CodeMirror.Editor | undefined; _view: EditorView | undefined;
_el: HTMLDivElement | null = null; _el: HTMLDivElement | null = null;
_cancelNextChange: boolean = false; _cancelNextChange: boolean = false;
@@ -60,33 +40,24 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
this._keyEvent = "keyboard"; this._keyEvent = "keyboard";
this.state = { this.state = {
isEditing: false, isEditing: false,
showMessage: false, prevValue: this.getPrettyJson(this.props.value),
prevValue: this.props.getValue!(this.props.layer),
}; };
} }
componentDidMount () { getPrettyJson(data: any) {
this._doc = CodeMirror(this._el!, { return stringifyPretty(data, {indent: 2, maxLength: 40});
value: this.props.getValue!(this.props.layer), }
mode: this.props.mode || {
name: "mgl",
},
lineWrapping: this.props.lineWrapping,
tabSize: 2,
theme: "maputnik",
viewportMargin: Infinity,
lineNumbers: this.props.lineNumbers,
lint: this.props.lint || {
context: "layer"
},
matchBrackets: true,
gutters: this.props.gutters,
scrollbarStyle: "null",
});
this._doc.on("change", this.onChange); componentDidMount () {
this._doc.on("focus", this.onFocus); this._view = createEditor({
this._doc.on("blur", this.onBlur); parent: this._el!,
value: this.getPrettyJson(this.props.value),
lintType: this.props.lintType || "layer",
onChange: (value:string) => this.onChange(value),
onFocus: () => this.onFocus(),
onBlur: () => this.onBlur(),
spec: this.props.spec
});
} }
onPointerDown = () => { onPointerDown = () => {
@@ -97,7 +68,6 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
if (this.props.onFocus) this.props.onFocus(); if (this.props.onFocus) this.props.onFocus();
this.setState({ this.setState({
isEditing: true, isEditing: true,
showMessage: (this._keyEvent === "keyboard"),
}); });
}; };
@@ -106,22 +76,19 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
if (this.props.onBlur) this.props.onBlur(); if (this.props.onBlur) this.props.onBlur();
this.setState({ this.setState({
isEditing: false, isEditing: false,
showMessage: false,
}); });
}; };
componentWillUnMount () {
this._doc!.off("change", this.onChange);
this._doc!.off("focus", this.onFocus);
this._doc!.off("blur", this.onBlur);
}
componentDidUpdate(prevProps: InputJsonProps) { componentDidUpdate(prevProps: InputJsonProps) {
if (!this.state.isEditing && prevProps.layer !== this.props.layer) { if (!this.state.isEditing && prevProps.value !== this.props.value) {
this._cancelNextChange = true; this._cancelNextChange = true;
this._doc!.setValue( this._view!.dispatch({
this.props.getValue!(this.props.layer), changes: {
); from: 0,
to: this._view!.state.doc.length,
insert: this.getPrettyJson(this.props.value)
}
});
} }
} }
@@ -129,11 +96,11 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
if (this._cancelNextChange) { if (this._cancelNextChange) {
this._cancelNextChange = false; this._cancelNextChange = false;
this.setState({ this.setState({
prevValue: this._doc!.getValue(), prevValue: this._view!.state.doc.toString(),
}); });
return; return;
} }
const newCode = this._doc!.getValue(); const newCode = this._view!.state.doc.toString();
if (this.state.prevValue !== newCode) { if (this.state.prevValue !== newCode) {
let parsedLayer, err; let parsedLayer, err;
@@ -144,12 +111,8 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
console.warn(_err); console.warn(_err);
} }
if (err && this.props.onJSONInvalid) { if (!err) {
this.props.onJSONInvalid();
}
else {
if (this.props.onChange) this.props.onChange(parsedLayer); if (this.props.onChange) this.props.onChange(parsedLayer);
if (this.props.onJSONValid) this.props.onJSONValid();
} }
} }
@@ -159,19 +122,12 @@ class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJso
}; };
render() { render() {
const t = this.props.t;
const {showMessage} = this.state;
const style = {} as {maxHeight?: number}; const style = {} as {maxHeight?: number};
if (this.props.maxHeight) { if (this.props.maxHeight) {
style.maxHeight = this.props.maxHeight; style.maxHeight = this.props.maxHeight;
} }
return <div className="JSONEditor" onPointerDown={this.onPointerDown} aria-hidden="true"> return <div className="JSONEditor" onPointerDown={this.onPointerDown} aria-hidden="true">
<div className={classnames("JSONEditor__message", {"JSONEditor__message--on": showMessage})}>
<Trans t={t}>
Press <kbd>ESC</kbd> to lose focus
</Trans>
</div>
<div <div
className={classnames("codemirror-container", this.props.className)} className={classnames("codemirror-container", this.props.className)}
ref={(el) => {this._el = el;}} ref={(el) => {this._el = el;}}
+4 -3
View File
@@ -119,7 +119,7 @@ type LayerEditorInternalProps = {
sources: {[key: string]: SourceSpecification & {layers: string[]}} sources: {[key: string]: SourceSpecification & {layers: string[]}}
vectorLayers: {[key: string]: any} vectorLayers: {[key: string]: any}
spec: any spec: any
onLayerChanged(...args: unknown[]): unknown onLayerChanged(index: number, layer: LayerSpecification): void
onLayerIdChange(...args: unknown[]): unknown onLayerIdChange(...args: unknown[]): unknown
onMoveLayer: OnMoveLayerCallback onMoveLayer: OnMoveLayerCallback
onLayerDestroy(...args: unknown[]): unknown onLayerDestroy(...args: unknown[]): unknown
@@ -280,8 +280,9 @@ class LayerEditorInternal extends React.Component<LayerEditorInternalProps, Laye
/>; />;
case "jsoneditor": case "jsoneditor":
return <FieldJson return <FieldJson
layer={this.props.layer} lintType="layer"
onChange={(layer) => { value={this.props.layer}
onChange={(layer: LayerSpecification) => {
this.props.onLayerChanged( this.props.onLayerChanged(
this.props.layerIndex, this.props.layerIndex,
layer layer
+14 -63
View File
@@ -1,34 +1,30 @@
import React from "react"; import React from "react";
import {MdDelete, MdUndo} from "react-icons/md"; import {MdDelete, MdUndo} from "react-icons/md";
import stringifyPretty from "json-stringify-pretty-compact";
import { type WithTranslation, withTranslation } from "react-i18next"; import { type WithTranslation, withTranslation } from "react-i18next";
import Block from "./Block"; import Block from "./Block";
import InputButton from "./InputButton"; import InputButton from "./InputButton";
import labelFromFieldName from "../libs/label-from-field-name"; import labelFromFieldName from "../libs/label-from-field-name";
import FieldJson from "./FieldJson"; import FieldJson from "./FieldJson";
import type { StylePropertySpecification } from "maplibre-gl";
import { type MappedLayerErrors } from "../libs/definitions"; import { type MappedLayerErrors } from "../libs/definitions";
type ExpressionPropertyInternalProps = { type ExpressionPropertyInternalProps = {
onDelete?(...args: unknown[]): unknown
fieldName: string fieldName: string
fieldType?: string fieldType?: string
fieldSpec?: object fieldSpec?: StylePropertySpecification
value?: any value?: any
errors?: MappedLayerErrors errors?: MappedLayerErrors
onChange?(...args: unknown[]): unknown onDelete?(...args: unknown[]): unknown
onChange(value: object): void
onUndo?(...args: unknown[]): unknown onUndo?(...args: unknown[]): unknown
canUndo?(...args: unknown[]): unknown canUndo?(...args: unknown[]): unknown
onFocus?(...args: unknown[]): unknown onFocus?(...args: unknown[]): unknown
onBlur?(...args: unknown[]): unknown onBlur?(...args: unknown[]): unknown
} & WithTranslation; } & WithTranslation;
type ExpressionPropertyState = { class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInternalProps> {
jsonError: boolean
};
class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInternalProps, ExpressionPropertyState> {
static defaultProps = { static defaultProps = {
errors: {}, errors: {},
onFocus: () => {}, onFocus: () => {},
@@ -42,21 +38,8 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
}; };
} }
onJSONInvalid = (_err: Error) => {
this.setState({
jsonError: true,
});
};
onJSONValid = () => {
this.setState({
jsonError: false,
});
};
render() { render() {
const {t, errors, fieldName, fieldType, value, canUndo} = this.props; const {t, value, canUndo} = this.props;
const {jsonError} = this.state;
const undoDisabled = canUndo ? !canUndo() : true; const undoDisabled = canUndo ? !canUndo() : true;
const deleteStopBtn = ( const deleteStopBtn = (
@@ -82,58 +65,26 @@ class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInter
</InputButton> </InputButton>
</> </>
); );
let error = undefined;
const fieldKey = fieldType === undefined ? fieldName : `${fieldType}.${fieldName}`; if (this.props.errors) {
const fieldKey = this.props.fieldType ? this.props.fieldType + "." + this.props.fieldName : this.props.fieldName;
const fieldError = errors![fieldKey]; error = this.props.errors[fieldKey];
const errorKeyStart = `${fieldKey}[`;
const foundErrors = [];
function getValue(data: any) {
return stringifyPretty(data, {indent: 2, maxLength: 38});
} }
if (jsonError) {
foundErrors.push({message: "Invalid JSON"});
}
else {
Object.entries(errors!)
.filter(([key, _error]) => {
return key.startsWith(errorKeyStart);
})
.forEach(([_key, error]) => {
return foundErrors.push(error);
});
if (fieldError) {
foundErrors.push(fieldError);
}
}
return <Block return <Block
// this feels like an incorrect type...? `foundErrors` is an array of objects, not a single object
error={foundErrors as any}
fieldSpec={this.props.fieldSpec} fieldSpec={this.props.fieldSpec}
label={t(labelFromFieldName(this.props.fieldName))} label={t(labelFromFieldName(this.props.fieldName))}
action={deleteStopBtn} action={deleteStopBtn}
wideMode={true} wideMode={true}
error={error}
> >
<FieldJson <FieldJson
mode={{name: "mgl"}} lintType="expression"
lint={{ spec={this.props.fieldSpec}
context: "expression",
spec: this.props.fieldSpec,
}}
className="maputnik-expression-editor" className="maputnik-expression-editor"
onFocus={this.props.onFocus} onFocus={this.props.onFocus}
onBlur={this.props.onBlur} onBlur={this.props.onBlur}
onJSONInvalid={this.onJSONInvalid} value={value}
onJSONValid={this.onJSONValid}
layer={value}
lineNumbers={false}
maxHeight={200} maxHeight={200}
lineWrapping={true}
getValue={getValue}
onChange={this.props.onChange} onChange={this.props.onChange}
/> />
</Block>; </Block>;
@@ -259,13 +259,9 @@ class GeoJSONSourceFieldJsonEditor extends React.Component<GeoJSONSourceFieldJso
return <div> return <div>
<Block label={t("GeoJSON")} fieldSpec={latest.source_geojson.data}> <Block label={t("GeoJSON")} fieldSpec={latest.source_geojson.data}>
<FieldJson <FieldJson
layer={this.props.source.data} value={this.props.source.data}
maxHeight={200} maxHeight={200}
mode={{ lintType="json"
name: "javascript",
json: true
}}
lint={true}
onChange={data => { onChange={data => {
this.props.onChange({ this.props.onChange({
...this.props.source, ...this.props.source,
+197 -152
View File
@@ -1,174 +1,219 @@
import {parse} from "@prantlf/jsonlint"; import { basicSetup } from "codemirror";
import CodeMirror, { type MarkerRange } from "codemirror"; import { EditorView } from "@codemirror/view";
import jsonToAst from "json-to-ast"; import { EditorState, Compartment } from "@codemirror/state";
import {expression, validateStyleMin} from "@maplibre/maplibre-gl-style-spec"; import { json, jsonParseLinter } from "@codemirror/lang-json";
import { linter, lintGutter, type Diagnostic } from "@codemirror/lint";
import { oneDark } from "@codemirror/theme-one-dark";
import { expression, type StylePropertySpecification, validateStyleMin } from "@maplibre/maplibre-gl-style-spec";
import jsonToAst, { type ValueNode, type PropertyNode } from "json-to-ast";
type MarkerRangeWithMessage = MarkerRange & {message: string}; export type LintType = "layer" | "style" | "expression" | "json";
type LinterError = {
key: string | null;
message: string;
};
CodeMirror.defineMode("mgl", (config, parserConfig) => { function getDiagnosticsFromExpressionErrors(errors: LinterError[], ast: ValueNode | PropertyNode) {
// Just using the javascript mode with json enabled. Our logic is in the linter below. const diagnostics: Diagnostic[] = [];
return CodeMirror.modes.javascript( for (const error of errors) {
{...config, json: true} as any, const {key, message} = error;
parserConfig if (!key) {
); diagnostics.push({
}); from: 0,
to: ast.loc ? ast.loc.end.offset : 0,
severity: "error",
function tryToParse(text: string) { message: message,
const found: MarkerRangeWithMessage[] = [];
try {
parse(text);
}
catch(err: any) {
const errorMatch = err.toString().match(/line (\d+), column (\d+)/);
if (errorMatch) {
const loc = {
first_line: parseInt(errorMatch[1], 10),
first_column: parseInt(errorMatch[2], 10),
last_line: parseInt(errorMatch[1], 10),
last_column: parseInt(errorMatch[2], 10)
};
// 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: err
}); });
} else {
const path = key.replace(/^\[|\]$/g, "").split(/\.|[[\]]+/).filter(Boolean);
const node = getArrayPositionalFromAst(ast, path);
if (!node) {
console.warn("Something went wrong parsing error:", error);
continue;
}
if (node.loc) {
diagnostics.push({
from: node.loc.start.offset,
to: node.loc.end.offset,
severity: "error",
message: message,
});
}
} }
} }
return diagnostics;
return found;
} }
CodeMirror.registerHelper("lint", "json", (text: string) => { function getArrayPositionalFromAst(node: ValueNode | PropertyNode | undefined, path: string[]) {
return tryToParse(text); if (!node) {
}); return undefined;
CodeMirror.registerHelper("lint", "mgl", (text: string, opts: any, doc: any) => {
const found: MarkerRangeWithMessage[] = tryToParse(text);
const {context} = opts;
if (found.length > 0) {
// JSON invalid so don't go any further
return found;
} }
if (path.length < 1) {
return node;
}
if (!("children" in node)) {
return undefined;
}
const key = path[0];
if (key.match(/^[0-9]+$/)) {
return getArrayPositionalFromAst(node.children[+path[0]], path.slice(1));
}
const newNode = node.children.find((childNode) => {
return (
"key" in childNode &&
childNode.key.type === "Identifier" &&
childNode.key.value === key
);
}) as PropertyNode | undefined;
return getArrayPositionalFromAst(newNode?.value, path.slice(1));
}
const ast = jsonToAst(text); function createMaplibreLayerLinter() {
const input = JSON.parse(text); return (view: EditorView) => {
const text = view.state.doc.toString();
function getArrayPositionalFromAst(node: any, path: string[]) { try {
if (!node) { // Parse the JSON. The jsonParseLinter will handle pure JSON syntax errors.
return undefined; const parsedJson = JSON.parse(text);
} const ast = jsonToAst(text);
else if (path.length < 1) {
return node; // Run the maplibre-gl-style-spec validator.
} const validationErrors = validateStyleMin({
else if (!node.children) { "version": 8,
return undefined; "name": "Empty Style",
} "metadata": {},
else { "sources": {},
const key = path[0]; "sprite": "",
let newNode; "glyphs": "https://example.com/glyphs/{fontstack}/{range}.pbf",
if (key.match(/^[0-9]+$/)) { "layers": [
newNode = node.children[path[0]]; parsedJson
} ]
else { });
newNode = node.children.find((childNode: any) => {
return ( const linterErrors = validationErrors
childNode.key && .filter(err => {
childNode.key.type === "Identifier" && // Remove missing 'layer source' errors, because we don't include them
childNode.key.value === key return !err.message.match(/^layers\[0\]: source ".*" not found$/);
); })
.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],
};
}); });
if (newNode) { return getDiagnosticsFromExpressionErrors(linterErrors, ast);
newNode = newNode.value; } catch {
} // The built-in JSON linter handles JSON parsing errors, so we don't need to report them again.
}
return getArrayPositionalFromAst(newNode, path.slice(1));
} }
} return [];
};
}
let out: ReturnType<typeof expression.createExpression> | null = null; function createMaplibreStyleLinter() {
if (context === "layer") { return (view: EditorView) => {
// Just an empty style so we can validate a layer. const text = view.state.doc.toString();
const errors = validateStyleMin({
"version": 8,
"name": "Empty Style",
"metadata": {},
"sources": {},
"sprite": "",
"glyphs": "https://example.com/glyphs/{fontstack}/{range}.pbf",
"layers": [
input
]
});
if (errors) { try {
out = { // Parse the JSON. The jsonParseLinter will handle pure JSON syntax errors.
result: "error", const parsedJson = JSON.parse(text);
value: errors const ast = jsonToAst(text);
.filter(err => {
// Remove missing 'layer source' errors, because we don't include them
return !err.message.match(/^layers\[0\]: source ".*" not found$/);
})
.map(err => {
// Remove the 'layers[0].' as we're validating the layer only here
const errMessageParts = err.message.replace(/^layers\[0\]./, "").split(":");
return {
name: "",
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") { // Run the maplibre-gl-style-spec validator.
const errors = out.value; const validationErrors = validateStyleMin(parsedJson);
errors.forEach(error => { const linterErrors = validationErrors.map(err => {
const {key, message} = error; return {
key: err.message.split(":")[0],
if (!key) { message: err.message,
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); });
} return getDiagnosticsFromExpressionErrors(linterErrors, ast);
else if (key) { } catch {
const path = key.replace(/^\[|\]$/g, "").split(/\.|[[\]]+/).filter(Boolean); // The built-in JSON linter handles JSON parsing errors, so we don't need to report them again.
const parsedError = getArrayPositionalFromAst(ast, path); }
if (!parsedError) { return [];
console.warn("Something went wrong parsing error:", error); };
return; }
}
const {loc} = parsedError; function createMaplibreExpressionLinter(spec: StylePropertySpecification) {
const {start, end} = loc; return (view: EditorView) => {
const text = view.state.doc.toString();
const parsedJson = JSON.parse(text);
const ast = jsonToAst(text);
const out = expression.createExpression(parsedJson, spec);
if (out?.result !== "error") {
return [];
}
const errors = out.value;
return getDiagnosticsFromExpressionErrors(errors, ast);
};
}
found.push({ export function createEditor(props: {
from: CodeMirror.Pos(start.line - 1, start.column), parent: HTMLElement,
to: CodeMirror.Pos(end.line - 1, end.column), value: string,
message: message, lintType: LintType,
}); onChange: (value: string) => void,
onFocus: () => void,
onBlur: () => void,
spec?: StylePropertySpecification,
}): EditorView {
let specificLinter: (view: EditorView) => Diagnostic[] = () => [];
switch (props.lintType) {
case "style":
specificLinter = createMaplibreStyleLinter();
break;
case "layer":
specificLinter = createMaplibreLayerLinter();
break;
case "expression":
if (!props.spec) {
throw new Error("spec is required for expression mode");
} }
}); specificLinter = createMaplibreExpressionLinter(props.spec);
break;
case "json":
specificLinter = () => [];
break;
} }
return found; return new EditorView({
}); doc: props.value,
extensions: [
basicSetup,
json(),
oneDark,
new Compartment().of(EditorState.tabSize.of(2)),
EditorView.theme({
"&": {
fontSize: "9pt"
}
}),
EditorView.updateListener.of((update) => {
if (update.docChanged) {
const doc = update.state.doc;
const value = doc.toString();
props.onChange(value);
}
if (update.focusChanged) {
if (update.view.hasFocus) {
props.onFocus();
} else {
props.onBlur();
}
}
}),
lintGutter(),
linter((view: EditorView) => {
const jsonErrors = jsonParseLinter()(view);
if (jsonErrors.length > 0) {
return jsonErrors;
}
return specificLinter(view);
})
],
parent: props.parent,
});
}
+4 -3
View File
@@ -1,7 +1,8 @@
import {latest} from "@maplibre/maplibre-gl-style-spec"; import {latest} from "@maplibre/maplibre-gl-style-spec";
import { type LayerSpecification } from "maplibre-gl"; import { type LayerSpecification } from "maplibre-gl";
export function changeType(layer: LayerSpecification, newType: string) {
export function changeType(layer: LayerSpecification, newType: string): LayerSpecification {
const changedPaintProps: LayerSpecification["paint"] = { ...layer.paint }; const changedPaintProps: LayerSpecification["paint"] = { ...layer.paint };
Object.keys(changedPaintProps).forEach(propertyName => { Object.keys(changedPaintProps).forEach(propertyName => {
if(!(propertyName in latest["paint_" + newType])) { if(!(propertyName in latest["paint_" + newType])) {
@@ -20,8 +21,8 @@ export function changeType(layer: LayerSpecification, newType: string) {
...layer, ...layer,
paint: changedPaintProps, paint: changedPaintProps,
layout: changedLayoutProps, layout: changedLayoutProps,
type: newType, type: newType
}; } as LayerSpecification;
} }
/** A {@property} in either the paint our layout {@group} has changed /** A {@property} in either the paint our layout {@group} has changed
-100
View File
@@ -1,100 +0,0 @@
@use "vars";
.CodeMirror-lint-tooltip {
z-index: 2000 !important;
}
.codemirror-container {
max-width: 100%;
position: relative;
overflow: auto;
}
.cm-s-maputnik.CodeMirror {
height: 100%;
font-size: 12px;
background: transparent;
}
.cm-s-maputnik.CodeMirror, .cm-s-maputnik .CodeMirror-gutters {
color: #8e8e8e;
border: none;
}
.cm-s-maputnik .CodeMirror-gutters {
background: #212328;
}
.cm-s-maputnik .CodeMirror-cursor {
border-left: solid thin #f0f0f0 !important;
}
.cm-s-maputnik.CodeMirror-focused div.CodeMirror-selected {
background: rgba(255, 255, 255, 0.10);
}
.cm-s-maputnik .CodeMirror-line::selection,
.cm-s-maputnik .CodeMirror-line > span::selection,
.cm-s-maputnik .CodeMirror-line > span > span::selection {
background: rgba(255, 255, 255, 0.10);
}
.cm-s-maputnik .CodeMirror-line::-moz-selection,
.cm-s-maputnik .CodeMirror-line > span::-moz-selection,
.cm-s-maputnik .CodeMirror-line > span > span::-moz-selection {
background: rgba(255, 255, 255, 0.10);
}
.cm-s-maputnik span.cm-string, .cm-s-maputnik span.cm-string-2 {
color: #8f9d6a;
}
.cm-s-maputnik span.cm-number { color: #91675f; }
.cm-s-maputnik span.cm-property { color: #b8a077; }
.cm-s-maputnik .CodeMirror-activeline-background {
background: rgba(255,255,255,0.1);
}
.cm-s-maputnik .CodeMirror-matchingbracket {
background: hsla(223, 12%, 35%, 1);
color: vars.$color-white !important;
}
.cm-s-maputnik .CodeMirror-nonmatchingbracket {
background-color: #bb0000;
color: white !important;
}
@keyframes JSONEditor__animation-fade {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.JSONEditor__message {
position: absolute;
right: 0;
font-size: 0.85em;
z-index: 99999;
padding: 0.3em 0.5em;
background: hsla(0, 0%, 0%, 0.3);
color: vars.$color-lowgray;
border-bottom-left-radius: 2px;
transition: opacity 320ms ease;
opacity: 0;
pointer-events: none;
&--on {
opacity: 1;
animation: 320ms ease 0s JSONEditor__animation-fade;
animation-delay: 2000ms;
animation-fill-mode: forwards;
}
kbd {
font-family: monospace;
}
}
-1
View File
@@ -15,7 +15,6 @@
@use 'zoomproperty'; @use 'zoomproperty';
@use 'popup'; @use 'popup';
@use 'map'; @use 'map';
@use 'codemirror';
@use 'react-collapse'; @use 'react-collapse';
.maputnik-layout { .maputnik-layout {