Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/adelpro/quran-search-engine/llms.txt

Use this file to discover all available pages before exploring further.

The highlighting system provides character-level ranges for matched tokens without generating HTML. This keeps the library UI-agnostic while giving you complete control over rendering.

Overview

The getHighlightRanges() function computes non-overlapping highlight ranges from search results. The consumer controls rendering - no dangerouslySetInnerHTML required.

Basic usage

getHighlightRanges(text, matchedTokens, tokenTypes?)

Parameters:
  • text: string - The text to highlight (typically verse.uthmani)
  • matchedTokens: readonly string[] | undefined - Tokens matched by the search
  • tokenTypes?: Record<string, MatchType> - Optional map of token to match type
Returns: HighlightRange[]
import { getHighlightRanges } from 'quran-search-engine';

const ranges = getHighlightRanges(verse.uthmani, verse.matchedTokens, verse.tokenTypes);

// Example output:
// [
//   { start: 12, end: 23, token: 'الله', matchType: 'exact' },
//   { start: 30, end: 45, token: 'الرحمن', matchType: 'lemma' },
// ]

HighlightRange type

From src/utils/highlight.ts:4-9:
export type HighlightRange = {
  start: number;
  end: number;
  token: string;
  matchType: MatchType;
};
Fields:
  • start - Starting character index (inclusive)
  • end - Ending character index (exclusive)
  • token - The original query token that matched
  • matchType - Type of match: 'exact' | 'lemma' | 'root' | 'fuzzy' | 'none'
Ranges are non-overlapping and sorted by position. If tokens overlap, the longest match takes priority.

React rendering example

From the README, here’s how to render highlights in React:
import { getHighlightRanges, type ScoredQuranText } from 'quran-search-engine';
import type { ReactNode } from 'react';

export function Verse({ verse }: { verse: ScoredQuranText }) {
  const ranges = getHighlightRanges(verse.uthmani, verse.matchedTokens, verse.tokenTypes);
  if (ranges.length === 0) return <span>{verse.uthmani}</span>;

  const parts: ReactNode[] = [];
  let cursor = 0;

  ranges.forEach((r, i) => {
    if (cursor < r.start) parts.push(verse.uthmani.slice(cursor, r.start));
    parts.push(
      <span key={`${r.start}-${r.end}-${i}`} className={`highlight highlight-${r.matchType}`}>
        {verse.uthmani.slice(r.start, r.end)}
      </span>,
    );
    cursor = r.end;
  });

  if (cursor < verse.uthmani.length) parts.push(verse.uthmani.slice(cursor));

  return <span>{parts}</span>;
}

Styling by match type

.highlight {
  padding: 2px 4px;
  border-radius: 3px;
}

.highlight-exact {
  background-color: #ffeb3b;
  font-weight: bold;
}

.highlight-lemma {
  background-color: #b3e5fc;
}

.highlight-root {
  background-color: #c8e6c9;
}

.highlight-fuzzy {
  background-color: #ffe0b2;
  font-style: italic;
}

How it works

1

Diacritic-aware regex creation

For each matched token, create a regex pattern that matches the token with optional diacritics and handles character variants.From src/utils/highlight.ts:17-36:
const createDiacriticRegex = (token: string) => {
  const normalizedToken = normalizeArabic(token);
  const escaped = escapeRegExp(normalizedToken);
  const tashkeel = '[\\u064B-\\u065F\\u0670\\u06D6-\\u06ED\\u0640]*?';

  const letters = escaped.split('').map((char) => {
    if (char === 'ا') return '[اأإآٱ\\u0670و]';
    if (char === 'ي') return '[يى\\u06CC\\u0626]';
    if (char === 'ى') return '[ىي\\u06CC\\u0ئ]';
    if (char === 'ة') return '[ةهت]';
    if (char === 'ه') return '[هة]';
    if (char === 'ك') return '[ك\\u06AC\\u06AD\\u06AE\\u06AF\\u06B0]';
    if (char === 'ء') return '[ءؤئ]';
    if (char === 'و') return '[وؤ]';
    return char;
  });

  const tokenPattern = letters.join(tashkeel);
  return new RegExp(`([^\\s]*${tokenPattern}[^\\s]*)`, 'gu');
};
2

Find all matches

For each token, find all matching positions in the text and record their ranges with priority based on match length.
for (const token of matchedTokens) {
  const regex = createDiacriticRegex(token);
  let match: RegExpExecArray | null;

  while ((match = regex.exec(text)) !== null) {
    const matchType = tokenTypes?.[token] ?? 'fuzzy';
    matches.push({
      start: match.index,
      end: match.index + match[0].length,
      token,
      matchType: matchType === 'none' ? 'fuzzy' : matchType,
      priority: match[0].length,
    });
  }
}
3

Sort by priority

Sort matches by length (longer matches first), then by position.
matches.sort((a, b) => {
  if (a.priority !== b.priority) return b.priority - a.priority;
  return a.start - b.start;
});
4

Remove overlaps

Keep only non-overlapping ranges using a character occupation tracker.
const finalRanges: InternalMatchRange[] = [];
const occupied = new Array(text.length).fill(false);

for (const m of matches) {
  let isFree = true;
  for (let i = m.start; i < m.end; i++) {
    if (occupied[i]) {
      isFree = false;
      break;
    }
  }

  if (!isFree) continue;

  finalRanges.push(m);
  for (let i = m.start; i < m.end; i++) {
    occupied[i] = true;
  }
}
5

Sort by position and return

Sort final ranges by starting position and remove internal priority field.
finalRanges.sort((a, b) => a.start - b.start);

return finalRanges.map(({ priority: _priority, ...range }) => range);

Character variant handling

The regex generator handles Arabic character variants to ensure robust matching:
Input CharacterMatches
ا (alef)ا أ إ آ ٱ و (with dagger alef)
ي (ya)ي ى and variants
ة (ta marbuta)ة ه ت
ه (ha)ه ة
ك (kaf)ك and Persian/Urdu variants
ء (hamza)ء ؤ ئ
و (waw)و ؤ
The regex patterns allow optional diacritics between every character, so “الله” will match “ٱللَّهِ” even with full tashkeel.

Empty results

If no tokens are provided or no matches are found, the function returns an empty array:
import { getHighlightRanges } from 'quran-search-engine';

const ranges = getHighlightRanges(text, undefined);
// ranges => []

const ranges2 = getHighlightRanges(text, []);
// ranges2 => []
Always check if ranges are empty before attempting to render highlights. Rendering logic should gracefully handle empty range arrays.

Other frameworks

While the React example is shown above, the same principle applies to any framework:

Vue example

<template>
  <span>
    <template v-for="(part, i) in renderParts" :key="i">
      <span v-if="part.isHighlight" :class="`highlight highlight-${part.matchType}`">
        {{ part.text }}
      </span>
      <template v-else>{{ part.text }}</template>
    </template>
  </span>
</template>

<script setup>
import { getHighlightRanges } from 'quran-search-engine';
import { computed } from 'vue';

const props = defineProps(['verse']);

const renderParts = computed(() => {
  const ranges = getHighlightRanges(
    props.verse.uthmani,
    props.verse.matchedTokens,
    props.verse.tokenTypes
  );
  
  if (ranges.length === 0) {
    return [{ text: props.verse.uthmani, isHighlight: false }];
  }
  
  const parts = [];
  let cursor = 0;
  
  ranges.forEach((r) => {
    if (cursor < r.start) {
      parts.push({ text: props.verse.uthmani.slice(cursor, r.start), isHighlight: false });
    }
    parts.push({
      text: props.verse.uthmani.slice(r.start, r.end),
      isHighlight: true,
      matchType: r.matchType,
    });
    cursor = r.end;
  });
  
  if (cursor < props.verse.uthmani.length) {
    parts.push({ text: props.verse.uthmani.slice(cursor), isHighlight: false });
  }
  
  return parts;
});
</script>

Vanilla JavaScript example

import { getHighlightRanges } from 'quran-search-engine';

function renderHighlightedVerse(verse) {
  const ranges = getHighlightRanges(verse.uthmani, verse.matchedTokens, verse.tokenTypes);
  
  if (ranges.length === 0) {
    return document.createTextNode(verse.uthmani);
  }
  
  const container = document.createElement('span');
  let cursor = 0;
  
  ranges.forEach((r) => {
    if (cursor < r.start) {
      container.appendChild(document.createTextNode(verse.uthmani.slice(cursor, r.start)));
    }
    
    const highlight = document.createElement('span');
    highlight.className = `highlight highlight-${r.matchType}`;
    highlight.textContent = verse.uthmani.slice(r.start, r.end);
    container.appendChild(highlight);
    
    cursor = r.end;
  });
  
  if (cursor < verse.uthmani.length) {
    container.appendChild(document.createTextNode(verse.uthmani.slice(cursor)));
  }
  
  return container;
}

Build docs developers (and LLMs) love