-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[CodeEditor] add support of triple quotes #112656
Changes from 4 commits
8ed18cc
02b748e
781b2f3
9dbbd78
9ae8be5
c2d3300
060f17b
2a188f2
614e277
1c23d07
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
/* | ||
* 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", | ||
"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('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", | ||
}, | ||
], | ||
} | ||
`); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -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,42 @@ export const createParser = () => { | |||
(string += ch), next(); | ||||
return (number = +string), isNaN(number) ? (error('Bad number'), void 0) : number; | ||||
}, | ||||
stringLiteral = function(quotes: string) { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The original functionality was airlifted out of:
This is, unfortunately, not exactly the most reader-friendly code and so without tests it's all the more tough to say if a regression has snuck in or not. I know with it being "the same-ish" as Console we had more guarantees that the code worked as expected (according to the tests on Console). It has also been fairly "manually" tested since then. I apologise for not adding tests here at the time 🤦🏻. I think it would be useful to add tests for the difference you're expecting to see with these changes, but AFAICT it looks like this was just a refactor and we are expecting the same behaviour? If that's the case I'd recommend breaking this change out into a follow up PR that adds tests for the "before" and "after" behaviour against this parser. Let me know what you think, or whether I've missed something! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
++ Great suggestion. I'd also feel more comfortable with this change if I better understood the difference in behavior we're expecting (if any) to help ensure we're not introducing any regressions. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've pushed some tests to be sure that everything works as before. Please if you see some specific case which you want to cover please let me know. |
||||
const 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(); ) { | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. in this part, only the
|
||||
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 +157,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 +171,10 @@ export const createParser = () => { | |||
}; | ||||
return ( | ||||
(value = function () { | ||||
const tripleQuotes = '"""'; | ||||
if (peek(tripleQuotes)) { | ||||
return stringLiteral(tripleQuotes); | ||||
} | ||||
switch ((white(), ch)) { | ||||
case '{': | ||||
return object(); | ||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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(''); | ||
} | ||
Comment on lines
+14
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm guessing this was copy-pasted from I wonder whether it is worth moving that function to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jloleysens not sure that it's a working idea due to two thoughts:
If you want to reuse that code probably it should be moved into I thing now you see why that code was duplicated There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Ah I see, that does make sense. @alisonelizabeth Looking closer at the dependence on |
||
|
||
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; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder what would happen if we added
\"
at the end of a triple quote value, so\""""
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thank you, that case was broken 🥇