From 50a9a73df440565c5a9cbff84e12f3ce5c455e17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Chalifour?= Date: Wed, 9 Sep 2020 14:53:52 +0200 Subject: [PATCH] feat(algolia): escape values in highlighting utils --- .../src/__tests__/formatting.test.ts | 372 ++++++++++++++++++ .../src/formatting.ts | 39 +- 2 files changed, 408 insertions(+), 3 deletions(-) diff --git a/packages/autocomplete-preset-algolia/src/__tests__/formatting.test.ts b/packages/autocomplete-preset-algolia/src/__tests__/formatting.test.ts index d975f2767..014d21ec5 100644 --- a/packages/autocomplete-preset-algolia/src/__tests__/formatting.test.ts +++ b/packages/autocomplete-preset-algolia/src/__tests__/formatting.test.ts @@ -42,6 +42,99 @@ describe('highlight', () => { ] `); }); + + test('allows custom highlightPreTag and highlightPostTag', () => { + expect( + parseHighlightedAttribute({ + attribute: 'title', + hit: { + _highlightResult: { + title: { + value: 'Hello there', + }, + }, + }, + highlightPreTag: '', + highlightPostTag: '', + }) + ).toMatchInlineSnapshot(` + Array [ + Object { + "isHighlighted": true, + "value": "He", + }, + Object { + "isHighlighted": false, + "value": "llo t", + }, + Object { + "isHighlighted": true, + "value": "he", + }, + Object { + "isHighlighted": false, + "value": "re", + }, + ] + `); + }); + + test('escapes characters', () => { + expect( + parseHighlightedAttribute({ + attribute: 'title', + hit: { + _highlightResult: { + title: { + value: `Food & 'n' "Music"`, + }, + }, + }, + highlightPreTag: '', + highlightPostTag: '', + }) + ).toMatchInlineSnapshot(` + Array [ + Object { + "isHighlighted": true, + "value": "Food", + }, + Object { + "isHighlighted": false, + "value": " & <Drinks> 'n' "Music"", + }, + ] + `); + }); + + test('do not escape ignored characters', () => { + expect( + parseHighlightedAttribute({ + attribute: 'title', + hit: { + _highlightResult: { + title: { + value: `Food & 'n' "Music"`, + }, + }, + }, + highlightPreTag: '', + highlightPostTag: '', + ignoreEscape: ["'"], + }) + ).toMatchInlineSnapshot(` + Array [ + Object { + "isHighlighted": true, + "value": "Food", + }, + Object { + "isHighlighted": false, + "value": " & <Drinks> 'n' "Music"", + }, + ] + `); + }); }); describe('parseReverseHighlightedAttribute', () => { @@ -98,6 +191,99 @@ describe('highlight', () => { ] `); }); + + test('allows custom highlightPreTag and highlightPostTag', () => { + expect( + parseReverseHighlightedAttribute({ + attribute: 'title', + hit: { + _highlightResult: { + title: { + value: 'Hello there', + }, + }, + }, + highlightPreTag: '', + highlightPostTag: '', + }) + ).toMatchInlineSnapshot(` + Array [ + Object { + "isHighlighted": false, + "value": "He", + }, + Object { + "isHighlighted": true, + "value": "llo t", + }, + Object { + "isHighlighted": false, + "value": "he", + }, + Object { + "isHighlighted": true, + "value": "re", + }, + ] + `); + }); + + test('escapes characters', () => { + expect( + parseReverseHighlightedAttribute({ + attribute: 'title', + hit: { + _highlightResult: { + title: { + value: `Food & 'n' "Music"`, + }, + }, + }, + highlightPreTag: '', + highlightPostTag: '', + }) + ).toMatchInlineSnapshot(` + Array [ + Object { + "isHighlighted": false, + "value": "Food", + }, + Object { + "isHighlighted": true, + "value": " & <Drinks> 'n' "Music"", + }, + ] + `); + }); + + test('do not escape ignored characters', () => { + expect( + parseReverseHighlightedAttribute({ + attribute: 'title', + hit: { + _highlightResult: { + title: { + value: `Food & 'n' "Music"`, + }, + }, + }, + highlightPreTag: '', + highlightPostTag: '', + ignoreEscape: ["'"], + }) + ).toMatchInlineSnapshot(` + Array [ + Object { + "isHighlighted": false, + "value": "Food", + }, + Object { + "isHighlighted": true, + "value": " & <Drinks> 'n' "Music"", + }, + ] + `); + }); }); describe('parseSnippetedAttribute', () => { @@ -136,6 +322,99 @@ describe('highlight', () => { ] `); }); + + test('allows custom highlightPreTag and highlightPostTag', () => { + expect( + parseSnippetedAttribute({ + attribute: 'title', + hit: { + _snippetResult: { + title: { + value: 'Hello there', + }, + }, + }, + highlightPreTag: '', + highlightPostTag: '', + }) + ).toMatchInlineSnapshot(` + Array [ + Object { + "isHighlighted": true, + "value": "He", + }, + Object { + "isHighlighted": false, + "value": "llo t", + }, + Object { + "isHighlighted": true, + "value": "he", + }, + Object { + "isHighlighted": false, + "value": "re", + }, + ] + `); + }); + + test('escapes characters', () => { + expect( + parseSnippetedAttribute({ + attribute: 'title', + hit: { + _snippetResult: { + title: { + value: `Food & 'n' "Music"`, + }, + }, + }, + highlightPreTag: '', + highlightPostTag: '', + }) + ).toMatchInlineSnapshot(` + Array [ + Object { + "isHighlighted": true, + "value": "Food", + }, + Object { + "isHighlighted": false, + "value": " & <Drinks> 'n' "Music"", + }, + ] + `); + }); + + test('do not escape ignored characters', () => { + expect( + parseSnippetedAttribute({ + attribute: 'title', + hit: { + _snippetResult: { + title: { + value: `Food & 'n' "Music"`, + }, + }, + }, + highlightPreTag: '', + highlightPostTag: '', + ignoreEscape: ["'"], + }) + ).toMatchInlineSnapshot(` + Array [ + Object { + "isHighlighted": true, + "value": "Food", + }, + Object { + "isHighlighted": false, + "value": " & <Drinks> 'n' "Music"", + }, + ] + `); + }); }); describe('parseReverseSnippetedAttribute', () => { @@ -175,4 +454,97 @@ describe('highlight', () => { `); }); }); + + test('allows custom highlightPreTag and highlightPostTag', () => { + expect( + parseReverseSnippetedAttribute({ + attribute: 'title', + hit: { + _snippetResult: { + title: { + value: 'Hello there', + }, + }, + }, + highlightPreTag: '', + highlightPostTag: '', + }) + ).toMatchInlineSnapshot(` + Array [ + Object { + "isHighlighted": false, + "value": "He", + }, + Object { + "isHighlighted": true, + "value": "llo t", + }, + Object { + "isHighlighted": false, + "value": "he", + }, + Object { + "isHighlighted": true, + "value": "re", + }, + ] + `); + }); + + test('escapes characters', () => { + expect( + parseReverseSnippetedAttribute({ + attribute: 'title', + hit: { + _snippetResult: { + title: { + value: `Food & 'n' "Music"`, + }, + }, + }, + highlightPreTag: '', + highlightPostTag: '', + }) + ).toMatchInlineSnapshot(` + Array [ + Object { + "isHighlighted": false, + "value": "Food", + }, + Object { + "isHighlighted": true, + "value": " & <Drinks> 'n' "Music"", + }, + ] + `); + }); + + test('do not escape ignored characters', () => { + expect( + parseReverseSnippetedAttribute({ + attribute: 'title', + hit: { + _snippetResult: { + title: { + value: `Food & 'n' "Music"`, + }, + }, + }, + highlightPreTag: '', + highlightPostTag: '', + ignoreEscape: ["'"], + }) + ).toMatchInlineSnapshot(` + Array [ + Object { + "isHighlighted": false, + "value": "Food", + }, + Object { + "isHighlighted": true, + "value": " & <Drinks> 'n' "Music"", + }, + ] + `); + }); }); diff --git a/packages/autocomplete-preset-algolia/src/formatting.ts b/packages/autocomplete-preset-algolia/src/formatting.ts index b42f24501..5ef18f6cb 100644 --- a/packages/autocomplete-preset-algolia/src/formatting.ts +++ b/packages/autocomplete-preset-algolia/src/formatting.ts @@ -1,7 +1,16 @@ +const htmlEscapes = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', +}; + type ParseAttributeParams = { highlightPreTag?: string; highlightPostTag?: string; highlightedValue: string; + ignoreEscape?: string[]; }; type ParsedAttribute = { value: string; isHighlighted: boolean }; @@ -10,7 +19,22 @@ export function parseAttribute({ highlightPreTag = '', highlightPostTag = '', highlightedValue, + ignoreEscape = [], }: ParseAttributeParams): ParsedAttribute[] { + const unescapedHtmlRegex = new RegExp( + `[${Object.keys(htmlEscapes) + .filter((character) => ignoreEscape.indexOf(character) === -1) + .join('')}]`, + 'g' + ); + const hasUnescapedHtmlRegex = RegExp(unescapedHtmlRegex.source); + + function escape(value: string) { + return hasUnescapedHtmlRegex.test(value) + ? value.replace(unescapedHtmlRegex, (key) => htmlEscapes[key]) + : value; + } + const splitByPreTag = highlightedValue.split(highlightPreTag); const firstValue = splitByPreTag.shift(); const elements = !firstValue @@ -21,7 +45,7 @@ export function parseAttribute({ let isHighlighted = true; splitByPreTag.forEach((split) => { - elements.push({ value: split, isHighlighted }); + elements.push({ value: escape(split), isHighlighted }); isHighlighted = !isHighlighted; }); } else { @@ -29,13 +53,13 @@ export function parseAttribute({ const splitByPostTag = split.split(highlightPostTag); elements.push({ - value: splitByPostTag[0], + value: escape(splitByPostTag[0]), isHighlighted: true, }); if (splitByPostTag[1] !== '') { elements.push({ - value: splitByPostTag[1], + value: escape(splitByPostTag[1]), isHighlighted: false, }); } @@ -72,6 +96,7 @@ type SharedParseAttributeParams = { attribute: string; highlightPreTag?: string; highlightPostTag?: string; + ignoreEscape?: string[]; }; export function parseHighlightedAttribute({ @@ -79,6 +104,7 @@ export function parseHighlightedAttribute({ attribute, highlightPreTag, highlightPostTag, + ignoreEscape, }: SharedParseAttributeParams): ParsedAttribute[] { const highlightedValue = getAttributeValueByPath( hit, @@ -89,6 +115,7 @@ export function parseHighlightedAttribute({ highlightPreTag, highlightPostTag, highlightedValue, + ignoreEscape, }); } @@ -97,6 +124,7 @@ export function parseReverseHighlightedAttribute({ attribute, highlightPreTag, highlightPostTag, + ignoreEscape, }: SharedParseAttributeParams): ParsedAttribute[] { return reverseHighlightedParts( parseHighlightedAttribute({ @@ -104,6 +132,7 @@ export function parseReverseHighlightedAttribute({ attribute, highlightPreTag, highlightPostTag, + ignoreEscape, }) ); } @@ -113,6 +142,7 @@ export function parseSnippetedAttribute({ attribute, highlightPreTag, highlightPostTag, + ignoreEscape, }: SharedParseAttributeParams): ParsedAttribute[] { const highlightedValue = getAttributeValueByPath( hit, @@ -123,6 +153,7 @@ export function parseSnippetedAttribute({ highlightPreTag, highlightPostTag, highlightedValue, + ignoreEscape, }); } @@ -131,6 +162,7 @@ export function parseReverseSnippetedAttribute({ attribute, highlightPreTag, highlightPostTag, + ignoreEscape, }: SharedParseAttributeParams): ParsedAttribute[] { return reverseHighlightedParts( parseSnippetedAttribute({ @@ -138,6 +170,7 @@ export function parseReverseSnippetedAttribute({ attribute, highlightPreTag, highlightPostTag, + ignoreEscape, }) ); }