Skip to content

Commit fb83547

Browse files
Escape plain text description hover correctly (#283)
* Escape plain text description hover correctly * Trim and escape `<` * Update test * Add PR description example as a test case Removed `MarkedString.fromPlainText` as that escapes the input and is not what we want here (we use `toMarkdown` which isn't the same) --------- Co-authored-by: Martin Aeschlimann <martinae@microsoft.com>
1 parent de5e9b5 commit fb83547

File tree

2 files changed

+64
-54
lines changed

2 files changed

+64
-54
lines changed

src/services/jsonHover.ts

Lines changed: 45 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -60,61 +60,64 @@ export class JSONHover {
6060
}
6161

6262
return this.schemaService.getSchemaForResource(document.uri, doc).then((schema) => {
63-
if (schema && node) {
64-
const matchingSchemas = doc.getMatchingSchemas(schema.schema, node.offset);
65-
66-
let title: string | undefined = undefined;
67-
let markdownDescription: string | undefined = undefined;
68-
let markdownEnumValueDescription: string | undefined = undefined, enumValue: string | undefined = undefined;
69-
matchingSchemas.every((s) => {
70-
if (s.node === node && !s.inverted && s.schema) {
71-
title = title || s.schema.title;
72-
markdownDescription = markdownDescription || s.schema.markdownDescription || toMarkdown(s.schema.description);
73-
if (s.schema.enum) {
74-
const idx = s.schema.enum.indexOf(Parser.getNodeValue(node));
75-
if (s.schema.markdownEnumDescriptions) {
76-
markdownEnumValueDescription = s.schema.markdownEnumDescriptions[idx];
77-
} else if (s.schema.enumDescriptions) {
78-
markdownEnumValueDescription = toMarkdown(s.schema.enumDescriptions[idx]);
79-
}
80-
if (markdownEnumValueDescription) {
81-
enumValue = s.schema.enum[idx];
82-
if (typeof enumValue !== 'string') {
83-
enumValue = JSON.stringify(enumValue);
84-
}
85-
}
63+
if (!schema) {
64+
return null
65+
}
66+
67+
let title: string | undefined = undefined;
68+
let markdownDescription: string | undefined = undefined;
69+
let markdownEnumValueDescription: string | undefined = undefined, enumValue: string | undefined = undefined;
70+
71+
const matchingSchemas = doc.getMatchingSchemas(schema.schema, node.offset).filter((s) => s.node === node && !s.inverted).map((s) => s.schema);
72+
for (const schema of matchingSchemas) {
73+
title = title || schema.title;
74+
markdownDescription = markdownDescription || schema.markdownDescription || toMarkdown(schema.description);
75+
if (schema.enum) {
76+
const idx = schema.enum.indexOf(Parser.getNodeValue(node));
77+
if (schema.markdownEnumDescriptions) {
78+
markdownEnumValueDescription = schema.markdownEnumDescriptions[idx];
79+
} else if (schema.enumDescriptions) {
80+
markdownEnumValueDescription = toMarkdown(schema.enumDescriptions[idx]);
81+
}
82+
if (markdownEnumValueDescription) {
83+
enumValue = schema.enum[idx];
84+
if (typeof enumValue !== 'string') {
85+
enumValue = JSON.stringify(enumValue);
8686
}
8787
}
88-
return true;
89-
});
90-
let result = '';
91-
if (title) {
92-
result = toMarkdown(title);
9388
}
94-
if (markdownDescription) {
95-
if (result.length > 0) {
96-
result += "\n\n";
97-
}
98-
result += markdownDescription;
89+
}
90+
91+
let result = '';
92+
if (title) {
93+
result = toMarkdown(title);
94+
}
95+
if (markdownDescription) {
96+
if (result.length > 0) {
97+
result += "\n\n";
9998
}
100-
if (markdownEnumValueDescription) {
101-
if (result.length > 0) {
102-
result += "\n\n";
103-
}
104-
result += `\`${toMarkdownCodeBlock(enumValue!)}\`: ${markdownEnumValueDescription}`;
99+
result += markdownDescription;
100+
}
101+
if (markdownEnumValueDescription) {
102+
if (result.length > 0) {
103+
result += "\n\n";
105104
}
106-
return createHover([result]);
105+
result += `\`${toMarkdownCodeBlock(enumValue!)}\`: ${markdownEnumValueDescription}`;
107106
}
108-
return null;
107+
return createHover([result]);
109108
});
110109
}
111110
}
111+
112112
function toMarkdown(plain: string): string;
113113
function toMarkdown(plain: string | undefined): string | undefined;
114114
function toMarkdown(plain: string | undefined): string | undefined {
115115
if (plain) {
116-
const res = plain.replace(/([^\n\r])(\r?\n)([^\n\r])/gm, '$1\n\n$3'); // single new lines to \n\n (Markdown paragraph)
117-
return res.replace(/[\\`*_{}[\]()#+\-.!]/g, "\\$&"); // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash
116+
return plain
117+
.trim()
118+
.replace(/[\\`*_{}[\]()<>#+\-.!]/g, '\\$&') // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash
119+
.replace(/([ \t]+)/g, (_match, g1) => '&nbsp;'.repeat(g1.length)) // escape spaces tabs
120+
.replace(/\n/g, '\\\n'); // escape new lines
118121
}
119122
return undefined;
120123
}

src/test/hover.test.ts

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import * as assert from 'assert';
77

8-
import { Hover, Position, MarkedString, TextDocument, getLanguageService, JSONSchema, LanguageServiceParams } from '../jsonLanguageService';
8+
import { Hover, Position, TextDocument, getLanguageService, JSONSchema, LanguageServiceParams } from '../jsonLanguageService';
99

1010
suite('JSON Hover', () => {
1111

@@ -33,7 +33,7 @@ suite('JSON Hover', () => {
3333

3434
test('Simple schema', async function () {
3535

36-
const content = '{"a": 42, "b": "hello", "c": false}';
36+
const content = '{"a": 42, "b": "hello", "c": false, "complex-description": false}';
3737
const schema: JSONSchema = {
3838
type: 'object',
3939
description: 'a very special object',
@@ -49,20 +49,27 @@ suite('JSON Hover', () => {
4949
'c': {
5050
type: 'boolean',
5151
description: 'C'
52-
}
52+
},
53+
'complex-description': {
54+
type: 'boolean',
55+
description: 'For example:\n\n<script>\n alert(1)\n</script>\n\n Test [1]'
56+
},
5357
}
5458
};
5559
await testComputeInfo(content, schema, { line: 0, character: 0 }).then((result) => {
56-
assert.deepEqual(result.contents, [MarkedString.fromPlainText('a very special object')]);
60+
assert.deepEqual(result.contents, ['a&nbsp;very&nbsp;special&nbsp;object']);
5761
});
5862
await testComputeInfo(content, schema, { line: 0, character: 1 }).then((result) => {
59-
assert.deepEqual(result.contents, [MarkedString.fromPlainText('A')]);
63+
assert.deepEqual(result.contents, ['A']);
6064
});
6165
await testComputeInfo(content, schema, { line: 0, character: 32 }).then((result) => {
62-
assert.deepEqual(result.contents, [MarkedString.fromPlainText('C')]);
66+
assert.deepEqual(result.contents, ['C']);
67+
});
68+
await testComputeInfo(content, schema, { line: 0, character: 37 }).then((result) => {
69+
assert.deepEqual(result.contents, ['For&nbsp;example:\\\n\\\n\\<script\\>\\\n&nbsp;&nbsp;alert\\(1\\)\\\n\\</script\\>\\\n\\\n&nbsp;&nbsp;&nbsp;&nbsp;Test&nbsp;\\[1\\]']);
6370
});
6471
await testComputeInfo(content, schema, { line: 0, character: 7 }).then((result) => {
65-
assert.deepEqual(result.contents, [MarkedString.fromPlainText('A')]);
72+
assert.deepEqual(result.contents, ['A']);
6673
});
6774
});
6875

@@ -89,13 +96,13 @@ suite('JSON Hover', () => {
8996
}]
9097
};
9198
await testComputeInfo(content, schema, { line: 0, character: 0 }).then((result) => {
92-
assert.deepEqual(result.contents, [MarkedString.fromPlainText('a very special object')]);
99+
assert.deepEqual(result.contents, ['a&nbsp;very&nbsp;special&nbsp;object']);
93100
});
94101
await testComputeInfo(content, schema, { line: 0, character: 1 }).then((result) => {
95-
assert.deepEqual(result.contents, [MarkedString.fromPlainText('A')]);
102+
assert.deepEqual(result.contents, ['A']);
96103
});
97104
await testComputeInfo(content, schema, { line: 0, character: 10 }).then((result) => {
98-
assert.deepEqual(result.contents, [MarkedString.fromPlainText('B\n\nIt\'s B')]);
105+
assert.deepEqual(result.contents, ['B\n\nIt\'s&nbsp;B']);
99106
});
100107
});
101108

@@ -154,10 +161,10 @@ suite('JSON Hover', () => {
154161
};
155162

156163
await testComputeInfo('{ "prop1": "e1', schema, { line: 0, character: 12 }).then(result => {
157-
assert.deepEqual(result.contents, ['line1\n\nline2\n\nline3\n\n\nline4\n']);
164+
assert.deepEqual(result.contents, ['line1\\\nline2\\\n\\\nline3\\\n\\\n\\\nline4']);
158165
});
159166
await testComputeInfo('{ "prop2": "e1', schema, { line: 0, character: 12 }).then(result => {
160-
assert.deepEqual(result.contents, ['line1\n\nline2\r\n\r\nline3']);
167+
assert.deepEqual(result.contents, ['line1\r\\\nline2\r\\\n\r\\\nline3']);
161168
});
162169
});
163170

0 commit comments

Comments
 (0)