Skip to content

Commit 046dcda

Browse files
authored
Use json instead of yaml in custom model editor (graphhopper#2251)
1 parent 2b47a7a commit 046dcda

File tree

13 files changed

+993
-1005
lines changed

13 files changed

+993
-1005
lines changed

web-bundle/src/main/js/custom-model-editor/demo/index.html

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
</head>
1111
<body>
1212
<div id="custom_model_editor_box"></div>
13-
<button id="toggle-button">show json</button>
13+
<button id="send-button">send</button>
1414
<script type="module">
15-
// importing this module adds a global variable GHCustomModelEditor to,
15+
// importing this module adds a global variable GHCustomModelEditor
1616
// it would be better to be able to use es6 imports, but since we are building
1717
// with webpack this is not possible at the moment: https://github.com/webpack/webpack/issues/2933
1818
import "../dist/index.js";
@@ -32,20 +32,16 @@
3232
document.querySelector("#custom_model_editor_box").appendChild(element);
3333
});
3434
editor.categories = categories;
35-
editor.setExtraKey('Ctrl-Enter', () => {editor.value = (editor.value + '\n ... hello')});
36-
editor.value = 'hello world';
35+
editor.setExtraKey('Ctrl-Enter', () => {editor.value = (editor.value + '\n You pressed Ctrl-Enter')});
36+
editor.value = '{\n "hello": "world"\n}';
3737
editor.cm.focus();
3838
editor.cm.setCursor(editor.cm.lineCount())
39-
const button = document.querySelector("#toggle-button")
39+
const button = document.querySelector("#send-button")
4040
editor.validListener = (valid) => {
4141
button.disabled = !valid;
4242
if (valid)
4343
console.log(editor.jsonObj);
4444
}
45-
button.addEventListener("click", (e) => {
46-
editor.toggleJsonYAML();
47-
button.innerText = editor.yaml ? 'show json' : 'edit yaml';
48-
}, false);
4945
</script>
5046
</body>
5147
</html>

web-bundle/src/main/js/custom-model-editor/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,6 @@
2525
},
2626
"dependencies": {
2727
"codemirror": "^5.59.2",
28-
"yaml": "^2.0.0-3"
28+
"jsonc-parser": "^3.0.0"
2929
}
3030
}
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import {findNodeAtOffset, parseTree} from "jsonc-parser";
2+
3+
/**
4+
* Returns auto-complete suggestions for a json string and a given character position
5+
*/
6+
export function completeJson(content, pos) {
7+
// pad the content in case the position is out of range
8+
while (pos >= content.length) content += ' ';
9+
const jsonPath = getJsonPath(content, pos);
10+
const signatureString = jsonPath.signature.join('-');
11+
if (
12+
/^root-object(-property|-property-key)?$/.test(signatureString)
13+
) {
14+
let suggestions = ['"speed"', '"priority"', '"distance_influence"', '"areas"']
15+
.filter(s => !keyAlreadyExistsInOtherPairs(jsonPath.path[0].children, jsonPath.path[1], s));
16+
return {
17+
suggestions,
18+
range: jsonPath.signature[jsonPath.signature.length - 1] === 'key' ? jsonPath.tokenRange : [pos, pos + 1]
19+
}
20+
} else if (
21+
/^root-object-property\[distance_influence]-value$/.test(signatureString)
22+
) {
23+
return {
24+
suggestions: ['__hint__type a number'],
25+
range: jsonPath.tokenRange
26+
}
27+
} else if (
28+
/^root-object-property\[(speed|priority)]-array\[[0-9]+]-object(-property|-property-key)?$/.test(signatureString)
29+
) {
30+
const clauses = ['"if"', '"else_if"', '"else"'];
31+
const operators = ['"limit_to"', '"multiply_by"'];
32+
const hasClause = jsonPath.signature.length > 4 && keysAlreadyExistInOtherPairs(jsonPath.path[3].children, jsonPath.path[4], clauses);
33+
const hasOperator = jsonPath.signature.length > 4 && keysAlreadyExistInOtherPairs(jsonPath.path[3].children, jsonPath.path[4], operators);
34+
let suggestions = [];
35+
if (!hasClause)
36+
suggestions.push(...clauses);
37+
if (!hasOperator)
38+
suggestions.push(...operators);
39+
return {
40+
suggestions,
41+
range: jsonPath.signature[jsonPath.signature.length - 1] === 'key' ? jsonPath.tokenRange : [pos, pos + 1]
42+
}
43+
} else if (
44+
/^root-object-property\[(speed|priority)]-array\[[0-9]+]-object-property\[(if|else_if|else)]-value$/.test(signatureString)
45+
) {
46+
return {
47+
suggestions: ['__hint__type a condition'],
48+
range: jsonPath.tokenRange
49+
}
50+
} else if (
51+
/^root-object-property\[(speed|priority)]-array\[[0-9]+]-object-property\[(limit_to|multiply_by)]-value$/.test(signatureString)
52+
) {
53+
return {
54+
suggestions: ['__hint__type a number'],
55+
range: jsonPath.tokenRange
56+
}
57+
} else if (
58+
/^root-object-property\[areas]-object(-property-key)?$/.test(signatureString)
59+
) {
60+
return {
61+
suggestions: ['__hint__type an area name'],
62+
range: jsonPath.tokenRange
63+
}
64+
} else if (
65+
/^root-object-property\[areas]-object-property\[[a-zA-Z0-9_]*]-object(-property-key)?$/.test(signatureString)
66+
) {
67+
const suggestions = ['"geometry"', '"type"']
68+
.filter(s => !keyAlreadyExistsInOtherPairs(jsonPath.path[4].children, jsonPath.path[5], s));
69+
return {
70+
suggestions,
71+
range: jsonPath.signature[jsonPath.signature.length - 1] === 'key' ? jsonPath.tokenRange : [pos, pos + 1]
72+
}
73+
} else if (
74+
/^root-object-property\[areas]-object-property\[[a-zA-Z0-9_]*]-object-property\[type]-value$/.test(signatureString)
75+
) {
76+
return {
77+
suggestions: ['"Feature"'],
78+
range: jsonPath.tokenRange
79+
}
80+
} else if (
81+
/^root-object-property\[areas]-object-property\[[a-zA-Z0-9_]*]-object-property\[geometry]-object(-property-key)?$/.test(signatureString)
82+
) {
83+
const suggestions = ['"type"', '"coordinates"']
84+
.filter(s => !keyAlreadyExistsInOtherPairs(jsonPath.path[6].children, jsonPath.path[7], s));
85+
return {
86+
suggestions,
87+
range: jsonPath.signature[jsonPath.signature.length - 1] === 'key' ? jsonPath.tokenRange : [pos, pos + 1]
88+
}
89+
} else if (
90+
/^root-object-property\[areas]-object-property\[[a-zA-Z0-9_]*]-object-property\[geometry]-object-property\[type]-value$/.test(signatureString)
91+
) {
92+
return {
93+
suggestions: ['"Polygon"'],
94+
range: jsonPath.tokenRange
95+
}
96+
} else {
97+
return {
98+
suggestions: [],
99+
range: []
100+
}
101+
}
102+
}
103+
104+
/**
105+
* Returns the JSON path and a special string representation (the 'signature') for the given json string and position.
106+
* The returned object contains the path as array, its 'signature' as string array and the token range of the token at
107+
* pos.
108+
*/
109+
export function getJsonPath(json, pos) {
110+
if (json.trim().length === 0) {
111+
return {
112+
path: [],
113+
signature: ['root'],
114+
tokenRange: []
115+
}
116+
}
117+
const errors = [];
118+
const root = parseTree(json, errors, {
119+
allowEmptyContent: false,
120+
allowTrailingComma: false,
121+
disallowComments: true
122+
});
123+
if (!root)
124+
return {
125+
path: [],
126+
signature: [],
127+
tokenRange: []
128+
}
129+
const includeRightBound = true;
130+
const node = findNodeAtOffset(root, pos, includeRightBound);
131+
if (!node)
132+
return {
133+
path: [],
134+
signature: [],
135+
tokenRange: []
136+
}
137+
const nodePath = getNodePath(node);
138+
const signature = nodePath.map((n, i) => nodeToPathElement(n, i + 1 < nodePath.length ? nodePath[i + 1] : null))
139+
signature.unshift('root');
140+
const range = [node.offset, node.offset + node.length];
141+
if (node.type === 'property') {
142+
if (node.colonOffset && pos > node.colonOffset) {
143+
signature[signature.length - 1] = `property[${node.children[0].value}]`;
144+
signature.push('value');
145+
range[0] = node.colonOffset + 1;
146+
}
147+
}
148+
return {
149+
path: nodePath,
150+
signature: signature,
151+
tokenRange: range
152+
}
153+
}
154+
155+
function getNodePath(node) {
156+
const path = [node];
157+
let curr = node;
158+
while (curr.parent) {
159+
path.push(curr.parent);
160+
curr = curr.parent;
161+
}
162+
path.reverse();
163+
return path;
164+
}
165+
166+
function nodeToPathElement(node, child) {
167+
if (node.type === 'object') {
168+
return 'object';
169+
} else if (node.type === 'property') {
170+
if (child !== null && node.children[1] === child) {
171+
return `property[${node.children[0].value}]`
172+
} else {
173+
return 'property'
174+
}
175+
} else if (node.type === 'array') {
176+
if (child !== null) {
177+
return `array[${node.children.indexOf(child)}]`
178+
} else {
179+
return 'array';
180+
}
181+
} else if (node.type === 'string' || node.type === 'number' || node.type === 'boolean') {
182+
if (node.parent && node.parent.type === 'property')
183+
if (node.parent.children[0] === node)
184+
return 'key';
185+
else if (node.parent.children[1] === node)
186+
return 'value';
187+
else
188+
throw 'a literal in a property should be the first or second child';
189+
else
190+
return 'literal';
191+
} else {
192+
return `unknown[${node.type}]`;
193+
}
194+
}
195+
196+
function keyAlreadyExistsInOtherPairs(allPairs, thisPair, key) {
197+
return allPairs.some(p => p !== thisPair && p.children && `"${p.children[0].value}"` === key);
198+
}
199+
200+
function keysAlreadyExistInOtherPairs(allPairs, thisPair, keys) {
201+
return allPairs.some(p => p !== thisPair && p.children && keys.indexOf(`"${p.children[0].value}"`) >= 0);
202+
}

0 commit comments

Comments
 (0)