export function Highlight()

in translate/src/modules/placeable/components/Highlight.tsx [52:226]


export function Highlight({
  children,
  search,
  terms,
}: {
  children: string;
  search?: string | null;
  terms?: TermState;
}) {
  const source = String(children);
  const marks: Array<{
    index: number;
    length: number;
    mark: ReactElement;
  }> = [];
  const location = useContext(Location);

  for (const match of source.matchAll(placeholder)) {
    let l10nId: string;
    let hidden = '';
    const text = match[0];
    switch (text[0]) {
      case '<':
        l10nId = 'highlight-placeholder-html';
        break;
      case '{':
      case '$':
        l10nId = 'highlight-placeholder';
        break;
      case '%':
        l10nId = 'highlight-placeholder-printf';
        break;
      case '&':
        l10nId = 'highlight-placeholder-entity';
        break;
      case '\\':
        l10nId = 'highlight-escape';
        break;
      case '-':
        l10nId = 'highlight-cli-option';
        break;
      case 'f':
      case 'h':
        l10nId = 'highlight-url';
        break;
      case '\n':
        l10nId = 'highlight-newline';
        hidden = '¶';
        break;
      case '\t':
        l10nId = 'highlight-tab';
        hidden = ' →';
        break;
      default:
        l10nId = /^\s/.test(text)
          ? 'highlight-spaces'
          : 'highlight-punctuation';
    }
    marks.push({
      index: match.index ?? -1,
      length: text.length,
      mark: (
        <Localized id={l10nId} attrs={{ title: true }} key={++keyCounter}>
          <mark className='placeable' data-match={text}>
            {hidden ? <span aria-hidden>{hidden}</span> : null}
            {text}
          </mark>
        </Localized>
      ),
    });
  }

  for (const { l10nId, re } of [
    { l10nId: 'highlight-email', re: /(?:mailto:)?\w[\w.-]*@\w[\w.]*\w/g },
    { l10nId: 'highlight-number', re: /[-+]?\d+(?:[\u00A0.,]\d+)*\b/gu },
  ]) {
    for (const match of source.matchAll(re)) {
      const text = match[0];
      marks.push({
        index: match.index ?? -1,
        length: text.length,
        mark: (
          <Localized id={l10nId} attrs={{ title: true }} key={++keyCounter}>
            <mark className='placeable' data-match={text}>
              {text}
            </mark>
          </Localized>
        ),
      });
    }
  }

  const lcSource = source.toLowerCase();

  if (terms?.terms && !terms.fetching) {
    const sourceTerms = terms.terms
      .filter((t) => lcSource.includes(t.text.toLowerCase()))
      .map((t) => t.text)
      .sort((a, b) => (a.length < b.length ? 1 : -1));
    for (const term of sourceTerms) {
      const re = new RegExp(`\\b${escapeRegExp(term)}[a-zA-z]*\\b`, 'gi');
      for (const match of source.matchAll(re)) {
        marks.push({
          index: match.index ?? -1,
          length: match[0].length,
          mark: (
            <mark className='term' data-match={term} key={++keyCounter}>
              {match[0]}
            </mark>
          ),
        });
      }
    }
  }

  // Sort by position, prefer longer marks
  marks.sort((a, b) => a.index - b.index || b.length - a.length);

  if (search) {
    let regexp: RegExp;
    try {
      regexp = new RegExp(String.raw`(?<!\\)"(?:\\"|[^"])+(?<!\\)"|\S+`, 'g');
    } catch {
      // Fallback for older browsers (e.g. iOS 15) not supporting lookbehind.
      regexp = /"(?:\\"|[^"])+"|\S+/g;
    }
    const searchTerms = search.match(regexp);

    for (let term of searchTerms ?? []) {
      if (term.startsWith('"') && term.length >= 3 && term.endsWith('"')) {
        term = term.slice(1, -1);
      }
      const highlightSource = location.search_match_case ? source : lcSource;
      let next: number;
      const regexFlags = location.search_match_case ? 'g' : 'gi';
      const re = location.search_match_whole_word
        ? new RegExp(`\\b${escapeRegExp(term)}\\b`, regexFlags)
        : new RegExp(`${escapeRegExp(term)}`, regexFlags);
      let match;

      while ((match = re.exec(highlightSource)) !== null) {
        next = match.index;
        let i = marks.findIndex((m) => m.index + m.length > next);
        if (i === -1) {
          i = marks.length;
        }
        marks.splice(i, 0, {
          index: next,
          length: term.length,
          mark: (
            <mark className='search' key={++keyCounter}>
              {source.substring(next, next + term.length)}
            </mark>
          ),
        });
      }
    }
  }

  const res: Array<string | ReactElement> = [];
  let pos = 0;
  for (const { index, length, mark } of marks) {
    if (index > pos) {
      res.push(source.slice(pos, index));
    }
    if (index >= pos) {
      res.push(mark);
      pos = index + length;
    }
  }
  if (pos < source.length) {
    res.push(source.slice(pos));
  }
  return <>{res}</>;
}