Skip to content

Commit

Permalink
[CodeEditor] add support of triple quotes (#112656)
Browse files Browse the repository at this point in the history
* [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>
  • Loading branch information
alexwizp and kibanamachine authored Oct 21, 2021
1 parent 31e7428 commit 079fbce
Show file tree
Hide file tree
Showing 7 changed files with 274 additions and 46 deletions.
189 changes: 189 additions & 0 deletions packages/kbn-monaco/src/xjson/grammar.test.ts
Original file line number Diff line number Diff line change
@@ -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",
},
],
}
`);
});
});
79 changes: 41 additions & 38 deletions packages/kbn-monaco/src/xjson/grammar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 + "'"),
Expand All @@ -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
},
Expand All @@ -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 () {
Expand Down Expand Up @@ -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)
)
Expand All @@ -179,6 +179,9 @@ export const createParser = () => {
};
return (
(value = function () {
if (peek('"""')) {
return stringLiteral();
}
switch ((white(), ch)) {
case '{':
return object();
Expand Down
1 change: 1 addition & 0 deletions packages/kbn-monaco/src/xjson/lexer_rules/xjson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' }],
],
},
Expand Down
32 changes: 28 additions & 4 deletions src/plugins/data/common/search/aggs/param_types/json.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
14 changes: 12 additions & 2 deletions src/plugins/data/common/search/aggs/param_types/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/vis_default_editor/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading

0 comments on commit 079fbce

Please sign in to comment.