[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:
Alexey Antonov 2021-10-21 18:26:28 +03:00 committed by GitHub
parent 31e7428f49
commit 079fbce79d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 274 additions and 46 deletions

View 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",
},
],
}
`);
});
});

View file

@ -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();

View file

@ -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' }],
],
},

View file

@ -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', () => {

View file

@ -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;
}

View file

@ -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"

View file

@ -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;