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.

This example demonstrates a framework-free implementation of the Quran Search Engine library using vanilla TypeScript and Vite.

Features

  • No framework dependencies (pure TypeScript)
  • Real-time search with debouncing
  • Multiple search modes (exact, lemma, root, fuzzy)
  • Highlighted search results
  • Lightweight and fast
  • Simple class-based architecture

Setup

1

Install dependencies

pnpm install
2

Run the development server

From the workspace root:
pnpm -C examples/vanilla-ts dev
Or from the example directory:
cd examples/vanilla-ts
pnpm dev
3

Open your browser

Navigate to http://localhost:5173

Project structure

examples/vanilla-ts/
├── src/
│   └── main.ts          # Application logic
├── index.html           # HTML template
├── package.json
└── vite.config.ts
This example demonstrates that the Quran Search Engine library works in any JavaScript environment, not just with frameworks.

Application class

The example uses a simple class to manage state and UI:
main.ts
import {
  loadQuranData,
  loadMorphology,
  loadWordMap,
  search,
  type QuranText,
  type MorphologyAya,
  type WordMap,
  type SearchResponse,
  getHighlightRanges,
} from 'quran-search-engine';

class QuranSearchApp {
  private quranData: QuranText[] = [];
  private morphologyMap: Map<number, MorphologyAya> | null = null;
  private wordMap: WordMap | null = null;
  private loading = true;

  private searchInput: HTMLInputElement;
  private lemmaCheckbox: HTMLInputElement;
  private rootCheckbox: HTMLInputElement;
  private fuzzyCheckbox: HTMLInputElement;
  private resultsDiv: HTMLDivElement;

  constructor() {
    this.searchInput = document.getElementById('search-input') as HTMLInputElement;
    this.lemmaCheckbox = document.getElementById('lemma') as HTMLInputElement;
    this.rootCheckbox = document.getElementById('root') as HTMLInputElement;
    this.fuzzyCheckbox = document.getElementById('fuzzy') as HTMLInputElement;
    this.resultsDiv = document.getElementById('results') as HTMLDivElement;

    this.init();
    this.setupEventListeners();
  }

  // Implementation...
}

Loading data

Load all datasets asynchronously on initialization:
main.ts
private async init() {
  try {
    this.showLoading();
    const [data, morphology, dictionary] = await Promise.all([
      loadQuranData(),
      loadMorphology(),
      loadWordMap(),
    ]);
    this.quranData = data;
    this.morphologyMap = morphology;
    this.wordMap = dictionary;
  } catch (error) {
    console.error('Failed to load Quran data:', error);
    this.showError('Failed to load Quran data');
  } finally {
    this.loading = false;
    this.hideLoading();
  }
}

Event listeners with debouncing

Set up event listeners for search input and options:
main.ts
private setupEventListeners() {
  this.searchInput.addEventListener('input', this.debounce(this.handleSearch.bind(this), 300));
  this.lemmaCheckbox.addEventListener('change', this.handleSearch.bind(this));
  this.rootCheckbox.addEventListener('change', this.handleSearch.bind(this));
  this.fuzzyCheckbox.addEventListener('change', this.handleSearch.bind(this));
}

private debounce<T extends (...args: any[]) => any>(func: T, wait: number): T {
  let timeout: NodeJS.Timeout;
  return ((...args: any[]) => {
    clearTimeout(timeout);
    timeout = setTimeout(() => func(...args), wait);
  }) as T;
}
The debounce function prevents search execution on every keystroke, improving performance.

Search implementation

Perform search with user-selected options:
main.ts
private handleSearch() {
  const query = this.searchInput.value.trim();
  if (!query || this.loading) {
    this.resultsDiv.innerHTML = '';
    return;
  }

  const options = {
    lemma: this.lemmaCheckbox.checked,
    root: this.rootCheckbox.checked,
    fuzzy: this.fuzzyCheckbox.checked,
  };

  try {
    const response = search(query, this.quranData, this.morphologyMap!, this.wordMap!, options, {
      page: 1,
      limit: 20,
    });

    this.renderResults(response);
  } catch (error) {
    console.error('Search error:', error);
    this.showError('Search failed');
  }
}

Rendering results

Render search results with match statistics:
main.ts
private renderResults(response: SearchResponse) {
  if (!response.results.length) {
    this.resultsDiv.innerHTML = '<p>No results found.</p>';
    return;
  }

  const html = `
    <div class="results-info">
      <div>Found <strong>${response.pagination.totalResults}</strong> matches</div>
      <div class="stats">
        <span class="stat-item">
          <span class="indicator indicator-exact"></span>
          <span>Exact: ${response.counts.simple}</span>
        </span>
        <span class="stat-item">
          <span class="indicator indicator-lemma"></span>
          <span>Lemma: ${response.counts.lemma}</span>
        </span>
        <span class="stat-item">
          <span class="indicator indicator-root"></span>
          <span>Root: ${response.counts.root}</span>
        </span>
        <span class="stat-item">
          <span class="indicator indicator-fuzzy"></span>
          <span>Fuzzy: ${response.counts.fuzzy}</span>
        </span>
      </div>
    </div>
    ${response.results.map((verse) => this.renderVerse(verse)).join('')}
  `;

  this.resultsDiv.innerHTML = html;
}

Verse rendering with highlighting

Render individual verses with highlighted matches:
main.ts
private renderVerse(verse: any) {
  const ranges = getHighlightRanges(verse.uthmani, verse.matchedTokens, verse.tokenTypes);
  let highlightedText = verse.uthmani;

  if (ranges.length > 0) {
    const parts: string[] = [];
    let cursor = 0;

    for (const range of ranges) {
      if (cursor < range.start) {
        parts.push(verse.uthmani.slice(cursor, range.start));
      }
      const segment = verse.uthmani.slice(range.start, range.end);
      parts.push(`<span class="highlight-${range.matchType}">${segment}</span>`);
      cursor = range.end;
    }

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

    highlightedText = parts.join('');
  }

  return `
    <div class="verse-card">
      <div class="verse-header">
        <span>${verse.sura_name} (${verse.sura_id}:${verse.aya_id})</span>
        <span class="match-tag">${verse.matchType === 'none' ? 'fuzzy' : verse.matchType} (Score: ${verse.matchScore})</span>
      </div>
      <div class="verse-arabic">${highlightedText}</div>
    </div>
  `;
}

Application initialization

Initialize the app when the DOM is loaded:
main.ts
// Initialize the app when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
  new QuranSearchApp();
});

Dependencies

Minimal dependencies for a lightweight setup:
package.json
{
  "dependencies": {
    "quran-search-engine": "workspace:*"
  },
  "devDependencies": {
    "@types/node": "^24.10.1",
    "typescript": "~5.9.3",
    "vite": "^7.2.4"
  }
}
This example requires only the library itself as a runtime dependency. Vite and TypeScript are development dependencies for the build process.

Key features demonstrated

  • No framework overhead: Pure TypeScript without React, Vue, or Angular
  • Simple architecture: Class-based design for easy understanding
  • Direct DOM manipulation: Using native browser APIs
  • Type safety: Full TypeScript support
  • Modern tooling: Vite for fast development and building
  • Minimal dependencies: Only the essentials

Build docs developers (and LLMs) love