Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable react/no-danger */

import React, { MouseEvent, useMemo } from 'react';
import React, { MouseEvent, useContext, useMemo } from 'react';

import classNames from 'classnames';
import useTranslation from 'next-translate/useTranslation';
Expand All @@ -13,7 +13,8 @@ import Spinner from '@/dls/Spinner/Spinner';
import CloseIcon from '@/icons/close.svg';
import Language from '@/types/Language';
import { getLanguageDataById, findLanguageIdByLocale, toLocalizedNumber } from '@/utils/locale';
import { formatVerseReferencesToLinks, isNumericString } from '@/utils/string';
import { findChapterContext, formatVerseReferencesToLinks, isNumericString } from '@/utils/string';
import DataContext from '@/contexts/DataContext';
import Footnote from 'types/Footnote';

interface FootnoteTextProps {
Expand All @@ -31,6 +32,7 @@ const FootnoteText: React.FC<FootnoteTextProps> = ({
onTextClicked,
isLoading,
}) => {
const chaptersData = useContext(DataContext);
const { t, lang } = useTranslation('quran-reader');

// App locale language data (for container/header direction)
Expand All @@ -54,8 +56,9 @@ const FootnoteText: React.FC<FootnoteTextProps> = ({

const updatedText = useMemo(() => {
if (!footnote?.text) return '';
return formatVerseReferencesToLinks(footnote.text);
}, [footnote?.text]);
const chapterContext = findChapterContext(footnote.text, chaptersData);
return formatVerseReferencesToLinks(footnote.text, chapterContext);
}, [footnote?.text, chaptersData]);

return (
<div
Expand Down
80 changes: 78 additions & 2 deletions src/utils/string.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type Chapter from '@/types/Chapter';

/**
* Shorten a text by setting the maximum number of characters
* by the value of the parameter and adding "..." at the end.
Expand Down Expand Up @@ -187,17 +189,91 @@ export const cleanTranscript = (text: string): string => {
* Converts verse references in text to clickable links.
* Example: "1:1" or "1:1-2" or "1:1-2:3" will be converted to HTML anchor tags.
*
* When a reference includes a chapter number (e.g. "37:7-10"), the URL
* uses the format /chapter/verses (e.g. "/37/7-10").
* Bare verse ranges without a chapter prefix (e.g. "7-10") are not linked
* because the chapter cannot be determined without context.
*
* @param {string} text - The text containing verse references
* @param {string} [chapterContext] - Optional chapter ID to use for bare verse ranges
* @returns {string} The text with verse references converted to links
*/
export const formatVerseReferencesToLinks = (text: string): string => {
export const formatVerseReferencesToLinks = (text: string, chapterContext?: string): string => {
if (!text) return '';
return text.replace(
/(\d{1,3}[:-]\d{1,3}(?:-\d{1,3}(?::\d{1,3})?)?)(?![^<]*<\/a>)/g,
(match) => `<a href="${`/${match}`}" target="_blank">${match}</a>`,
(match) => {
const colonIndex = match.indexOf(':');
if (colonIndex !== -1) {
const chapter = match.slice(0, colonIndex);
const verses = match.slice(colonIndex + 1);
return `<a href="/${chapter}/${verses}" target="_blank">${match}</a>`;
Comment thread
Mubashir78 marked this conversation as resolved.
}
if (chapterContext) {
return `<a href="/${chapterContext}/${match}" target="_blank">${match}</a>`;
}
return match;
},
);
};

/**
* Scans footnote text for a mention of a Surah name and returns
* the corresponding chapter ID. This allows bare verse ranges
* (e.g. "7-10") to be linked with the correct chapter prefix
* when the Surah is named earlier in the text.
*
* Uses transliterated names and slugs from chapter data. Works best
* when the Surah name appears in Latin script.
*
* @param {string} text - The footnote text to scan
* @param {Record<string, Chapter>} chaptersData - Chapter data keyed by ID
* @returns {string | null} The detected chapter ID, or null if no match
*/
export const findChapterContext = (
text: string,
chaptersData: Record<string, Chapter>,
): string | null => {
if (!text || !chaptersData) return null;

const textLower = text.toLowerCase();
let lastMatch: string | null = null;
let lastIndex = -1;

for (const [chapterId, chapter] of Object.entries(chaptersData)) {
const names = new Set<string>();

if (typeof chapter.defaultSlug === 'string') {
names.add(chapter.defaultSlug);
} else if (chapter.defaultSlug?.slug) {
names.add(chapter.defaultSlug.slug);
Comment thread
Mubashir78 marked this conversation as resolved.
}

if (chapter.transliteratedName) {
names.add(chapter.transliteratedName);
}

for (const name of names) {
const withoutAl = name.replace(/^al[- ]/i, '');
if (withoutAl !== name) {
names.add(withoutAl);
}
}

for (const name of names) {
if (!name) continue;
const nameLower = name.toLowerCase();
const index = textLower.indexOf(nameLower);
if (index !== -1 && index > lastIndex) {
lastMatch = chapterId;
lastIndex = index;
}
}
}

return lastMatch;
};

/**
* Count the number of words in a text string.
*
Expand Down