mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[CodeEditor] add support of triple quotes (#112656)
* [CodeEditor] add support of triple quotes * add tests for grammar * an escaped quote can be appended to the end of triple quotes' Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
31e7428f49
commit
079fbce79d
7 changed files with 274 additions and 46 deletions
189
packages/kbn-monaco/src/xjson/grammar.test.ts
Normal file
189
packages/kbn-monaco/src/xjson/grammar.test.ts
Normal file
|
@ -0,0 +1,189 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { createParser } from './grammar';
|
||||
|
||||
describe('createParser', () => {
|
||||
let parser: ReturnType<typeof createParser>;
|
||||
|
||||
beforeEach(() => {
|
||||
parser = createParser();
|
||||
});
|
||||
|
||||
test('should create a xjson grammar parser', () => {
|
||||
expect(createParser()).toBeInstanceOf(Function);
|
||||
});
|
||||
|
||||
test('should return no annotations in case of valid json', () => {
|
||||
expect(
|
||||
parser(`
|
||||
{"menu": {
|
||||
"id": "file",
|
||||
"value": "File",
|
||||
"quotes": "'\\"",
|
||||
"popup": {
|
||||
"actions": [
|
||||
"new",
|
||||
"open",
|
||||
"close"
|
||||
],
|
||||
"menuitem": [
|
||||
{"value": "New"},
|
||||
{"value": "Open"},
|
||||
{"value": "Close"}
|
||||
]
|
||||
}
|
||||
}}
|
||||
`)
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"annotations": Array [],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('should support triple quotes', () => {
|
||||
expect(
|
||||
parser(`
|
||||
{"menu": {
|
||||
"id": """
|
||||
file
|
||||
""",
|
||||
"value": "File"
|
||||
}}
|
||||
`)
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"annotations": Array [],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('triple quotes should be correctly closed', () => {
|
||||
expect(
|
||||
parser(`
|
||||
{"menu": {
|
||||
"id": """"
|
||||
file
|
||||
"",
|
||||
"value": "File"
|
||||
}}
|
||||
`)
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"annotations": Array [
|
||||
Object {
|
||||
"at": 36,
|
||||
"text": "Expected ',' instead of '\\"'",
|
||||
"type": "error",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('an escaped quote can be appended to the end of triple quotes', () => {
|
||||
expect(
|
||||
parser(`
|
||||
{"menu": {
|
||||
"id": """
|
||||
file
|
||||
\\"""",
|
||||
"value": "File"
|
||||
}}
|
||||
`)
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"annotations": Array [],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('text values should be wrapper into quotes', () => {
|
||||
expect(
|
||||
parser(`
|
||||
{"menu": {
|
||||
"id": id,
|
||||
"value": "File"
|
||||
}}
|
||||
`)
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"annotations": Array [
|
||||
Object {
|
||||
"at": 36,
|
||||
"text": "Unexpected 'i'",
|
||||
"type": "error",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('check for close quotes', () => {
|
||||
expect(
|
||||
parser(`
|
||||
{"menu": {
|
||||
"id": "id,
|
||||
"value": "File"
|
||||
}}
|
||||
`)
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"annotations": Array [
|
||||
Object {
|
||||
"at": 52,
|
||||
"text": "Expected ',' instead of 'v'",
|
||||
"type": "error",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
test('no duplicate keys', () => {
|
||||
expect(
|
||||
parser(`
|
||||
{"menu": {
|
||||
"id": "id",
|
||||
"id": "File"
|
||||
}}
|
||||
`)
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"annotations": Array [
|
||||
Object {
|
||||
"at": 53,
|
||||
"text": "Duplicate key \\"id\\"",
|
||||
"type": "warning",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('all curly quotes should be closed', () => {
|
||||
expect(
|
||||
parser(`
|
||||
{"menu": {
|
||||
"id": "id",
|
||||
"name": "File"
|
||||
}
|
||||
`)
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"annotations": Array [
|
||||
Object {
|
||||
"at": 82,
|
||||
"text": "Expected ',' instead of ''",
|
||||
"type": "error",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -57,10 +57,6 @@ export const createParser = () => {
|
|||
text: m,
|
||||
});
|
||||
},
|
||||
reset = function (newAt: number) {
|
||||
ch = text.charAt(newAt);
|
||||
at = newAt + 1;
|
||||
},
|
||||
next = function (c?: string) {
|
||||
return (
|
||||
c && c !== ch && error("Expected '" + c + "' instead of '" + ch + "'"),
|
||||
|
@ -69,15 +65,6 @@ export const createParser = () => {
|
|||
ch
|
||||
);
|
||||
},
|
||||
nextUpTo = function (upTo: any, errorMessage: string) {
|
||||
let currentAt = at,
|
||||
i = text.indexOf(upTo, currentAt);
|
||||
if (i < 0) {
|
||||
error(errorMessage || "Expected '" + upTo + "'");
|
||||
}
|
||||
reset(i + upTo.length);
|
||||
return text.substring(currentAt, i);
|
||||
},
|
||||
peek = function (c: string) {
|
||||
return text.substr(at, c.length) === c; // nocommit - double check
|
||||
},
|
||||
|
@ -96,37 +83,50 @@ export const createParser = () => {
|
|||
(string += ch), next();
|
||||
return (number = +string), isNaN(number) ? (error('Bad number'), void 0) : number;
|
||||
},
|
||||
stringLiteral = function () {
|
||||
let quotes = '"""';
|
||||
let end = text.indexOf('\\"' + quotes, at + quotes.length);
|
||||
|
||||
if (end >= 0) {
|
||||
quotes = '\\"' + quotes;
|
||||
} else {
|
||||
end = text.indexOf(quotes, at + quotes.length);
|
||||
}
|
||||
|
||||
if (end >= 0) {
|
||||
for (let l = end - at + quotes.length; l > 0; l--) {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
return next();
|
||||
},
|
||||
string = function () {
|
||||
let hex: any,
|
||||
i: any,
|
||||
uffff: any,
|
||||
string = '';
|
||||
|
||||
if ('"' === ch) {
|
||||
if (peek('""')) {
|
||||
// literal
|
||||
next('"');
|
||||
next('"');
|
||||
return nextUpTo('"""', 'failed to find closing \'"""\'');
|
||||
} else {
|
||||
for (; next(); ) {
|
||||
if ('"' === ch) return next(), string;
|
||||
if ('\\' === ch)
|
||||
if ((next(), 'u' === ch)) {
|
||||
for (
|
||||
uffff = 0, i = 0;
|
||||
4 > i && ((hex = parseInt(next(), 16)), isFinite(hex));
|
||||
i += 1
|
||||
)
|
||||
uffff = 16 * uffff + hex;
|
||||
string += String.fromCharCode(uffff);
|
||||
} else {
|
||||
if ('string' != typeof escapee[ch]) break;
|
||||
string += escapee[ch];
|
||||
}
|
||||
else string += ch;
|
||||
}
|
||||
for (; next(); ) {
|
||||
if ('"' === ch) return next(), string;
|
||||
if ('\\' === ch)
|
||||
if ((next(), 'u' === ch)) {
|
||||
for (
|
||||
uffff = 0, i = 0;
|
||||
4 > i && ((hex = parseInt(next(), 16)), isFinite(hex));
|
||||
i += 1
|
||||
)
|
||||
uffff = 16 * uffff + hex;
|
||||
string += String.fromCharCode(uffff);
|
||||
} else {
|
||||
if ('string' != typeof escapee[ch]) break;
|
||||
string += escapee[ch];
|
||||
}
|
||||
else string += ch;
|
||||
}
|
||||
}
|
||||
|
||||
error('Bad string');
|
||||
},
|
||||
white = function () {
|
||||
|
@ -165,9 +165,9 @@ export const createParser = () => {
|
|||
((key = string()),
|
||||
white(),
|
||||
next(':'),
|
||||
Object.hasOwnProperty.call(object, key) &&
|
||||
Object.hasOwnProperty.call(object, key!) &&
|
||||
warning('Duplicate key "' + key + '"', latchKeyStart),
|
||||
(object[key] = value()),
|
||||
(object[key!] = value()),
|
||||
white(),
|
||||
'}' === ch)
|
||||
)
|
||||
|
@ -179,6 +179,9 @@ export const createParser = () => {
|
|||
};
|
||||
return (
|
||||
(value = function () {
|
||||
if (peek('"""')) {
|
||||
return stringLiteral();
|
||||
}
|
||||
switch ((white(), ch)) {
|
||||
case '{':
|
||||
return object();
|
||||
|
|
|
@ -103,6 +103,7 @@ export const lexerRules: monaco.languages.IMonarchLanguage = {
|
|||
|
||||
string_literal: [
|
||||
[/"""/, { token: 'punctuation.end_triple_quote', next: '@pop' }],
|
||||
[/\\""""/, { token: 'punctuation.end_triple_quote', next: '@pop' }],
|
||||
[/./, { token: 'multi_string' }],
|
||||
],
|
||||
},
|
||||
|
|
|
@ -67,10 +67,34 @@ describe('JSON', function () {
|
|||
aggParam.write(aggConfig, output);
|
||||
expect(aggConfig.params).toHaveProperty(paramName);
|
||||
|
||||
expect(output.params).toEqual({
|
||||
existing: 'true',
|
||||
new_param: 'should exist in output',
|
||||
});
|
||||
expect(output.params).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"existing": "true",
|
||||
"new_param": "should exist in output",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should append param when valid JSON with triple quotes', () => {
|
||||
const aggParam = initAggParam();
|
||||
const jsonData = `{
|
||||
"a": """
|
||||
multiline string - line 1
|
||||
"""
|
||||
}`;
|
||||
|
||||
aggConfig.params[paramName] = jsonData;
|
||||
|
||||
aggParam.write(aggConfig, output);
|
||||
expect(aggConfig.params).toHaveProperty(paramName);
|
||||
|
||||
expect(output.params).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"a": "
|
||||
multiline string - line 1
|
||||
",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not overwrite existing params', () => {
|
||||
|
|
|
@ -11,6 +11,17 @@ import _ from 'lodash';
|
|||
import { IAggConfig } from '../agg_config';
|
||||
import { BaseParamType } from './base';
|
||||
|
||||
function collapseLiteralStrings(xjson: string) {
|
||||
const tripleQuotes = '"""';
|
||||
const splitData = xjson.split(tripleQuotes);
|
||||
|
||||
for (let idx = 1; idx < splitData.length - 1; idx += 2) {
|
||||
splitData[idx] = JSON.stringify(splitData[idx]);
|
||||
}
|
||||
|
||||
return splitData.join('');
|
||||
}
|
||||
|
||||
export class JsonParamType extends BaseParamType {
|
||||
constructor(config: Record<string, any>) {
|
||||
super(config);
|
||||
|
@ -26,9 +37,8 @@ export class JsonParamType extends BaseParamType {
|
|||
return;
|
||||
}
|
||||
|
||||
// handle invalid Json input
|
||||
try {
|
||||
paramJson = JSON.parse(param);
|
||||
paramJson = JSON.parse(collapseLiteralStrings(param));
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"version": "kibana",
|
||||
"ui": true,
|
||||
"optionalPlugins": ["visualize"],
|
||||
"requiredBundles": ["kibanaUtils", "kibanaReact", "data", "fieldFormats", "discover"],
|
||||
"requiredBundles": ["kibanaUtils", "kibanaReact", "data", "fieldFormats", "discover", "esUiShared"],
|
||||
"owner": {
|
||||
"name": "Vis Editors",
|
||||
"githubTeam": "kibana-vis-editors"
|
||||
|
|
|
@ -12,6 +12,7 @@ import { EuiFormRow, EuiIconTip, EuiScreenReaderOnly } from '@elastic/eui';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { XJsonLang } from '@kbn/monaco';
|
||||
import { CodeEditor } from '../../../../kibana_react/public';
|
||||
import { XJson } from '../../../../es_ui_shared/public';
|
||||
|
||||
import { AggParamEditorProps } from '../agg_param_props';
|
||||
|
||||
|
@ -58,7 +59,7 @@ function RawJsonParamEditor({
|
|||
let isJsonValid = true;
|
||||
try {
|
||||
if (newValue) {
|
||||
JSON.parse(newValue);
|
||||
JSON.parse(XJson.collapseLiteralStrings(newValue));
|
||||
}
|
||||
} catch (e) {
|
||||
isJsonValid = false;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue