text.ts 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. import { TextMatch } from 'app/types/explore';
  2. import xss from 'xss';
  3. /**
  4. * Adapt findMatchesInText for react-highlight-words findChunks handler.
  5. * See https://github.com/bvaughn/react-highlight-words#props
  6. */
  7. export function findHighlightChunksInText({
  8. searchWords,
  9. textToHighlight,
  10. }: {
  11. searchWords: string[];
  12. textToHighlight: string;
  13. }) {
  14. return searchWords.reduce((acc: any, term: string) => [...acc, ...findMatchesInText(textToHighlight, term)], []);
  15. }
  16. const cleanNeedle = (needle: string): string => {
  17. return needle.replace(/[[{(][\w,.-?:*+]+$/, '');
  18. };
  19. /**
  20. * Returns a list of substring regexp matches.
  21. */
  22. export function findMatchesInText(haystack: string, needle: string): TextMatch[] {
  23. // Empty search can send re.exec() into infinite loop, exit early
  24. if (!haystack || !needle) {
  25. return [];
  26. }
  27. const matches: TextMatch[] = [];
  28. const { cleaned, flags } = parseFlags(cleanNeedle(needle));
  29. let regexp: RegExp;
  30. try {
  31. regexp = new RegExp(`(?:${cleaned})`, flags);
  32. } catch (error) {
  33. return matches;
  34. }
  35. haystack.replace(regexp, (substring, ...rest) => {
  36. if (substring) {
  37. const offset = rest[rest.length - 2];
  38. matches.push({
  39. text: substring,
  40. start: offset,
  41. length: substring.length,
  42. end: offset + substring.length,
  43. });
  44. }
  45. return '';
  46. });
  47. return matches;
  48. }
  49. const CLEAR_FLAG = '-';
  50. const FLAGS_REGEXP = /\(\?([ims-]+)\)/g;
  51. /**
  52. * Converts any mode modifers in the text to the Javascript equivalent flag
  53. */
  54. export function parseFlags(text: string): { cleaned: string; flags: string } {
  55. const flags: Set<string> = new Set(['g']);
  56. const cleaned = text.replace(FLAGS_REGEXP, (str, group) => {
  57. const clearAll = group.startsWith(CLEAR_FLAG);
  58. for (let i = 0; i < group.length; ++i) {
  59. const flag = group.charAt(i);
  60. if (clearAll || group.charAt(i - 1) === CLEAR_FLAG) {
  61. flags.delete(flag);
  62. } else if (flag !== CLEAR_FLAG) {
  63. flags.add(flag);
  64. }
  65. }
  66. return ''; // Remove flag from text
  67. });
  68. return {
  69. cleaned: cleaned,
  70. flags: Array.from(flags).join(''),
  71. };
  72. }
  73. const XSSWL = Object.keys(xss.whiteList).reduce((acc, element) => {
  74. // @ts-ignore
  75. acc[element] = xss.whiteList[element].concat(['class', 'style']);
  76. return acc;
  77. }, {});
  78. const sanitizeXSS = new xss.FilterXSS({
  79. whiteList: XSSWL,
  80. });
  81. /**
  82. * Returns string safe from XSS attacks.
  83. *
  84. * Even though we allow the style-attribute, there's still default filtering applied to it
  85. * Info: https://github.com/leizongmin/js-xss#customize-css-filter
  86. * Whitelist: https://github.com/leizongmin/js-css-filter/blob/master/lib/default.js
  87. */
  88. export function sanitize(unsanitizedString: string): string {
  89. try {
  90. return sanitizeXSS.process(unsanitizedString);
  91. } catch (error) {
  92. console.log('String could not be sanitized', unsanitizedString);
  93. return unsanitizedString;
  94. }
  95. }
  96. export function hasAnsiCodes(input: string): boolean {
  97. return /\u001b\[\d{1,2}m/.test(input);
  98. }
  99. export function escapeHtml(str: string): string {
  100. return String(str)
  101. .replace(/&/g, '&amp;')
  102. .replace(/</g, '&lt;')
  103. .replace(/>/g, '&gt;')
  104. .replace(/"/g, '&quot;');
  105. }