Commit ·
75e0099
1
Parent(s): 9c49fff
Update extension UI, app pipeline, and Spelling Rules
Browse files- extension/popup.css +16 -0
- extension/popup.html +1 -41
- extension/popup.js +94 -82
- extension/shared/bayan-api.js +4 -2
- extension/sidepanel/sidepanel.css +16 -0
- extension/sidepanel/sidepanel.html +1 -49
- extension/sidepanel/sidepanel.js +115 -102
- quran.py +138 -93
- src/app.py +3 -2
- src/css/components.css +14 -0
- src/index.html +156 -191
- src/nlp/spelling/araspell_rules.py +1 -2
extension/popup.css
CHANGED
|
@@ -1093,6 +1093,22 @@ body::-webkit-scrollbar-thumb {
|
|
| 1093 |
color: var(--text-color);
|
| 1094 |
}
|
| 1095 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1096 |
/* ══════════════════════════════════════════════
|
| 1097 |
Utilities
|
| 1098 |
══════════════════════════════════════════════ */
|
|
|
|
| 1093 |
color: var(--text-color);
|
| 1094 |
}
|
| 1095 |
|
| 1096 |
+
.bayan-quran-match-list {
|
| 1097 |
+
display: flex;
|
| 1098 |
+
flex-direction: column;
|
| 1099 |
+
gap: 12px;
|
| 1100 |
+
max-height: 420px;
|
| 1101 |
+
overflow-y: auto;
|
| 1102 |
+
}
|
| 1103 |
+
|
| 1104 |
+
.bayan-quran-match-card {
|
| 1105 |
+
background: var(--color-surface);
|
| 1106 |
+
border: 1px solid rgba(6, 182, 212, 0.2);
|
| 1107 |
+
border-radius: var(--bayan-radius);
|
| 1108 |
+
padding: 14px;
|
| 1109 |
+
animation: fadeSlideIn 300ms ease-out;
|
| 1110 |
+
}
|
| 1111 |
+
|
| 1112 |
/* ══════════════════════════════════════════════
|
| 1113 |
Utilities
|
| 1114 |
══════════════════════════════════════════════ */
|
extension/popup.html
CHANGED
|
@@ -276,47 +276,7 @@
|
|
| 276 |
|
| 277 |
<!-- Quran result -->
|
| 278 |
<div class="bayan-quran-result is-hidden" id="quran-result-section">
|
| 279 |
-
<div class="bayan-quran-
|
| 280 |
-
<span style="color:#06b6d4; font-size:12px; font-weight:700;">✓ النص القرآني المدقق</span>
|
| 281 |
-
<button class="bayan-btn-icon" id="btn-copy-quran" type="button" title="نسخ">
|
| 282 |
-
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
|
| 283 |
-
</button>
|
| 284 |
-
</div>
|
| 285 |
-
<p id="quran-uthmani" class="bayan-quran-uthmani" dir="rtl"></p>
|
| 286 |
-
<p id="quran-reference" class="bayan-quran-reference"></p>
|
| 287 |
-
|
| 288 |
-
<!-- Translation -->
|
| 289 |
-
<div class="bayan-quran-translate">
|
| 290 |
-
<div class="bayan-quran-translate-row">
|
| 291 |
-
<svg width="14" height="14" fill="none" stroke="#06b6d4" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129"/></svg>
|
| 292 |
-
<span style="font-size:12px; font-weight:600; color:var(--text-secondary);">ترجمة الآية</span>
|
| 293 |
-
<select id="quran-lang-select" class="bayan-quran-lang-select">
|
| 294 |
-
<option value="">— اختر لغة —</option>
|
| 295 |
-
<option value="english">🇬🇧 English</option>
|
| 296 |
-
<option value="french">🇫🇷 Français</option>
|
| 297 |
-
<option value="turkish">🇹🇷 Türkçe</option>
|
| 298 |
-
<option value="persian">🇮🇷 فارسی</option>
|
| 299 |
-
<option value="russian">🇷🇺 Русский</option>
|
| 300 |
-
<option value="spanish">🇪🇸 Español</option>
|
| 301 |
-
<option value="german">🇩🇪 Deutsch</option>
|
| 302 |
-
<option value="indonesian">🇮🇩 Indonesia</option>
|
| 303 |
-
<option value="malay">🇲🇾 Melayu</option>
|
| 304 |
-
<option value="bengali">🇧🇩 বাংলা</option>
|
| 305 |
-
<option value="bosnian">🇧🇦 Bosanski</option>
|
| 306 |
-
<option value="portuguese">🇵🇹 Português</option>
|
| 307 |
-
<option value="uzbek">🇺🇿 O'zbek</option>
|
| 308 |
-
</select>
|
| 309 |
-
</div>
|
| 310 |
-
<div id="quran-translation-section" class="bayan-quran-translation is-hidden">
|
| 311 |
-
<p id="quran-trans-text" dir="auto"></p>
|
| 312 |
-
<p id="quran-trans-ref" class="bayan-quran-reference" style="display:none;"></p>
|
| 313 |
-
<div class="bayan-result-actions" id="quran-trans-actions" style="display:none; justify-content:flex-end; gap:4px; margin-top:8px;">
|
| 314 |
-
<button class="bayan-btn-icon" id="btn-copy-quran-trans" type="button" title="نسخ الترجمة">
|
| 315 |
-
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
|
| 316 |
-
</button>
|
| 317 |
-
</div>
|
| 318 |
-
</div>
|
| 319 |
-
</div>
|
| 320 |
</div>
|
| 321 |
</section>
|
| 322 |
|
|
|
|
| 276 |
|
| 277 |
<!-- Quran result -->
|
| 278 |
<div class="bayan-quran-result is-hidden" id="quran-result-section">
|
| 279 |
+
<div id="quran-match-list" class="bayan-quran-match-list"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
</div>
|
| 281 |
</section>
|
| 282 |
|
extension/popup.js
CHANGED
|
@@ -575,19 +575,97 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 575 |
const quranCharCount = document.getElementById('quran-char-count');
|
| 576 |
const btnQuran = document.getElementById('btn-quran');
|
| 577 |
const quranResultSection = document.getElementById('quran-result-section');
|
| 578 |
-
const
|
| 579 |
-
const quranReference = document.getElementById('quran-reference');
|
| 580 |
-
const btnCopyQuran = document.getElementById('btn-copy-quran');
|
| 581 |
-
const quranLangSelect = document.getElementById('quran-lang-select');
|
| 582 |
-
const quranTransSection = document.getElementById('quran-translation-section');
|
| 583 |
-
const quranTransText = document.getElementById('quran-trans-text');
|
| 584 |
-
const quranTransRef = document.getElementById('quran-trans-ref');
|
| 585 |
|
| 586 |
let _quranQuery = '';
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 591 |
|
| 592 |
if (quranInput) {
|
| 593 |
quranInput.addEventListener('input', () => { updateCounts(quranInput, quranCharCount, null); saveState(); });
|
|
@@ -598,93 +676,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 598 |
|
| 599 |
_quranQuery = text;
|
| 600 |
setLoading(true, 'جارٍ التدقيق...');
|
| 601 |
-
quranTransSection.classList.add('is-hidden');
|
| 602 |
-
quranLangSelect.value = '';
|
| 603 |
|
| 604 |
try {
|
| 605 |
-
const data = await bayanQuran(text);
|
| 606 |
quranResultSection.classList.remove('is-hidden');
|
| 607 |
|
| 608 |
if (data.error) {
|
| 609 |
-
|
| 610 |
-
quranReference.textContent = '';
|
| 611 |
return;
|
| 612 |
}
|
| 613 |
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
const verseText = seg.replace(/\s*【[^】]+】\s*$/, '').replace(/^\(/, '').replace(/\)$/, '');
|
| 617 |
-
const reference = refMatch ? refMatch[1] : '';
|
| 618 |
-
|
| 619 |
-
_quranVerse = verseText;
|
| 620 |
-
_quranRef = reference;
|
| 621 |
-
quranUthmani.textContent = verseText;
|
| 622 |
-
quranReference.textContent = reference ? `[${reference}]` : '';
|
| 623 |
showToast('✓ تم التدقيق');
|
| 624 |
} catch (error) {
|
| 625 |
console.error('[Bayan] Quran error:', error);
|
| 626 |
quranResultSection.classList.remove('is-hidden');
|
| 627 |
-
|
| 628 |
-
quranReference.textContent = '';
|
| 629 |
} finally {
|
| 630 |
setLoading(false);
|
| 631 |
}
|
| 632 |
});
|
| 633 |
-
|
| 634 |
-
// Translation dropdown
|
| 635 |
-
quranLangSelect.addEventListener('change', async () => {
|
| 636 |
-
const lang = quranLangSelect.value;
|
| 637 |
-
if (!lang || !_quranQuery) return;
|
| 638 |
-
|
| 639 |
-
quranTransSection.classList.remove('is-hidden');
|
| 640 |
-
quranTransText.textContent = '⏳ جاري الترجمة...';
|
| 641 |
-
if (quranTransRef) quranTransRef.style.display = 'none';
|
| 642 |
-
|
| 643 |
-
try {
|
| 644 |
-
const data = await bayanQuran(_quranQuery, lang);
|
| 645 |
-
|
| 646 |
-
if (data.error) {
|
| 647 |
-
quranTransText.textContent = data.error;
|
| 648 |
-
return;
|
| 649 |
-
}
|
| 650 |
-
|
| 651 |
-
const seg = data.matched_segment || '';
|
| 652 |
-
const refMatch = seg.match(/【([^】]+)】/);
|
| 653 |
-
const transText = seg.replace(/\s*【[^】]+】\s*$/, '').replace(/^\(/, '').replace(/\)$/, '');
|
| 654 |
-
const transRef = refMatch ? refMatch[1] : '';
|
| 655 |
-
|
| 656 |
-
_quranTransText = transText;
|
| 657 |
-
_quranTransRef = transRef;
|
| 658 |
-
|
| 659 |
-
quranTransText.textContent = transText;
|
| 660 |
-
if (quranTransRef && transRef) {
|
| 661 |
-
quranTransRef.textContent = `[${transRef}]`;
|
| 662 |
-
quranTransRef.style.display = '';
|
| 663 |
-
}
|
| 664 |
-
const transActions = document.getElementById('quran-trans-actions');
|
| 665 |
-
if (transActions) transActions.style.display = 'flex';
|
| 666 |
-
} catch (error) {
|
| 667 |
-
console.error('[Bayan] Quran translation error:', error);
|
| 668 |
-
quranTransText.textContent = 'حدث خطأ في الترجمة';
|
| 669 |
-
}
|
| 670 |
-
});
|
| 671 |
-
|
| 672 |
-
btnCopyQuran.addEventListener('click', () => {
|
| 673 |
-
const text = (_quranVerse || '') + (_quranRef ? ` [${_quranRef}]` : '');
|
| 674 |
-
navigator.clipboard.writeText(text)
|
| 675 |
-
.then(() => showToast('✓ تم النسخ'))
|
| 676 |
-
.catch(() => showToast('تعذّر النسخ'));
|
| 677 |
-
});
|
| 678 |
-
|
| 679 |
-
const btnCopyQuranTrans = document.getElementById('btn-copy-quran-trans');
|
| 680 |
-
if (btnCopyQuranTrans) {
|
| 681 |
-
btnCopyQuranTrans.addEventListener('click', () => {
|
| 682 |
-
const text = (_quranTransText || '') + (_quranTransRef ? ` [${_quranTransRef}]` : '');
|
| 683 |
-
navigator.clipboard.writeText(text)
|
| 684 |
-
.then(() => showToast('✓ تم نسخ الترجمة'))
|
| 685 |
-
.catch(() => showToast('تعذّر النسخ'));
|
| 686 |
-
});
|
| 687 |
-
}
|
| 688 |
}
|
| 689 |
|
| 690 |
// ══════════════════════════════════════════════════════════
|
|
|
|
| 575 |
const quranCharCount = document.getElementById('quran-char-count');
|
| 576 |
const btnQuran = document.getElementById('btn-quran');
|
| 577 |
const quranResultSection = document.getElementById('quran-result-section');
|
| 578 |
+
const quranMatchList = document.getElementById('quran-match-list');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 579 |
|
| 580 |
let _quranQuery = '';
|
| 581 |
+
|
| 582 |
+
function _parseSegment(seg) {
|
| 583 |
+
const refMatch = seg.match(/【([^】]+)】/);
|
| 584 |
+
const verseText = seg.replace(/\s*【[^】]+】\s*$/, '').replace(/^\(/, '').replace(/\)$/, '');
|
| 585 |
+
const reference = refMatch ? refMatch[1] : '';
|
| 586 |
+
return { verseText, reference };
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
function _langOptionsHTML() {
|
| 590 |
+
return '<option value="">— اختر لغة —</option>'
|
| 591 |
+
+ '<option value="english">English</option>'
|
| 592 |
+
+ '<option value="french">Français</option>'
|
| 593 |
+
+ '<option value="turkish">Türkçe</option>'
|
| 594 |
+
+ '<option value="persian">فارسی</option>'
|
| 595 |
+
+ '<option value="russian">Русский</option>'
|
| 596 |
+
+ '<option value="spanish">Español</option>'
|
| 597 |
+
+ '<option value="german">Deutsch</option>'
|
| 598 |
+
+ '<option value="indonesian">Indonesia</option>'
|
| 599 |
+
+ '<option value="malay">Melayu</option>'
|
| 600 |
+
+ '<option value="bengali">বাংলা</option>'
|
| 601 |
+
+ '<option value="bosnian">Bosanski</option>'
|
| 602 |
+
+ '<option value="portuguese">Português</option>'
|
| 603 |
+
+ '<option value="uzbek">O'zbek</option>';
|
| 604 |
+
}
|
| 605 |
+
|
| 606 |
+
function _renderMatchCards(matches) {
|
| 607 |
+
quranMatchList.innerHTML = '';
|
| 608 |
+
matches.forEach(function(m, i) {
|
| 609 |
+
var parsed = _parseSegment(m.matched_segment || '');
|
| 610 |
+
var card = document.createElement('div');
|
| 611 |
+
card.className = 'bayan-quran-match-card';
|
| 612 |
+
card.innerHTML = '<div class="bayan-quran-result-header">'
|
| 613 |
+
+ '<span style="color:#06b6d4;font-size:12px;font-weight:700;">✓ نتيجة ' + (i + 1) + '</span>'
|
| 614 |
+
+ '<button class="bayan-btn-icon qmc-copy" type="button" title="نسخ">'
|
| 615 |
+
+ '<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>'
|
| 616 |
+
+ '</button></div>'
|
| 617 |
+
+ '<p class="bayan-quran-uthmani" dir="rtl">' + _escHTML(parsed.verseText) + '</p>'
|
| 618 |
+
+ '<p class="bayan-quran-reference">' + (parsed.reference ? '[' + _escHTML(parsed.reference) + ']' : '') + '</p>'
|
| 619 |
+
+ '<div class="bayan-quran-translate">'
|
| 620 |
+
+ '<div class="bayan-quran-translate-row">'
|
| 621 |
+
+ '<span style="font-size:12px;font-weight:600;color:var(--text-secondary);">ترجمة الآية</span>'
|
| 622 |
+
+ '<select class="bayan-quran-lang-select qmc-lang">' + _langOptionsHTML() + '</select>'
|
| 623 |
+
+ '</div>'
|
| 624 |
+
+ '<div class="bayan-quran-translation is-hidden qmc-trans-section">'
|
| 625 |
+
+ '<p class="qmc-trans-text" dir="auto"></p>'
|
| 626 |
+
+ '<button class="bayan-btn-icon qmc-copy-trans" type="button" title="نسخ الترجمة" style="margin-top:4px;">'
|
| 627 |
+
+ '<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>'
|
| 628 |
+
+ '</button></div></div>';
|
| 629 |
+
|
| 630 |
+
card.querySelector('.qmc-copy').addEventListener('click', function() {
|
| 631 |
+
var txt = parsed.verseText + (parsed.reference ? ' [' + parsed.reference + ']' : '');
|
| 632 |
+
navigator.clipboard.writeText(txt)
|
| 633 |
+
.then(function() { showToast('✓ تم النسخ'); })
|
| 634 |
+
.catch(function() { showToast('تعذّر النسخ'); });
|
| 635 |
+
});
|
| 636 |
+
|
| 637 |
+
var langSel = card.querySelector('.qmc-lang');
|
| 638 |
+
var transSec = card.querySelector('.qmc-trans-section');
|
| 639 |
+
var transP = card.querySelector('.qmc-trans-text');
|
| 640 |
+
var copyTransBtn = card.querySelector('.qmc-copy-trans');
|
| 641 |
+
|
| 642 |
+
langSel.addEventListener('change', async function() {
|
| 643 |
+
var lang = langSel.value;
|
| 644 |
+
if (!lang || !_quranQuery) return;
|
| 645 |
+
transSec.classList.remove('is-hidden');
|
| 646 |
+
transP.textContent = '⏳ جاري الترجمة...';
|
| 647 |
+
try {
|
| 648 |
+
var data = await bayanQuran(_quranQuery, lang);
|
| 649 |
+
if (data.error) { transP.textContent = data.error; return; }
|
| 650 |
+
var seg = data.matched_segment || '';
|
| 651 |
+
var p = _parseSegment(seg);
|
| 652 |
+
transP.textContent = p.verseText;
|
| 653 |
+
} catch (err) {
|
| 654 |
+
transP.textContent = 'حدث خطأ في الترجمة';
|
| 655 |
+
}
|
| 656 |
+
});
|
| 657 |
+
|
| 658 |
+
copyTransBtn.addEventListener('click', function() {
|
| 659 |
+
navigator.clipboard.writeText(transP.textContent || '')
|
| 660 |
+
.then(function() { showToast('✓ تم نسخ الترجمة'); })
|
| 661 |
+
.catch(function() { showToast('تعذّر النسخ'); });
|
| 662 |
+
});
|
| 663 |
+
|
| 664 |
+
quranMatchList.appendChild(card);
|
| 665 |
+
});
|
| 666 |
+
}
|
| 667 |
+
|
| 668 |
+
function _escHTML(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
| 669 |
|
| 670 |
if (quranInput) {
|
| 671 |
quranInput.addEventListener('input', () => { updateCounts(quranInput, quranCharCount, null); saveState(); });
|
|
|
|
| 676 |
|
| 677 |
_quranQuery = text;
|
| 678 |
setLoading(true, 'جارٍ التدقيق...');
|
|
|
|
|
|
|
| 679 |
|
| 680 |
try {
|
| 681 |
+
const data = await bayanQuran(text, 'تدقيق الايات', null, 5);
|
| 682 |
quranResultSection.classList.remove('is-hidden');
|
| 683 |
|
| 684 |
if (data.error) {
|
| 685 |
+
quranMatchList.innerHTML = '<p style="color:var(--text-secondary);font-size:13px;padding:8px;">' + _escHTML(data.error) + '</p>';
|
|
|
|
| 686 |
return;
|
| 687 |
}
|
| 688 |
|
| 689 |
+
var matches = data.matches || [data];
|
| 690 |
+
_renderMatchCards(matches);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 691 |
showToast('✓ تم التدقيق');
|
| 692 |
} catch (error) {
|
| 693 |
console.error('[Bayan] Quran error:', error);
|
| 694 |
quranResultSection.classList.remove('is-hidden');
|
| 695 |
+
quranMatchList.innerHTML = '<p style="color:var(--text-secondary);font-size:13px;padding:8px;">خطأ في الاتصال — تحقق من الإنترنت</p>';
|
|
|
|
| 696 |
} finally {
|
| 697 |
setLoading(false);
|
| 698 |
}
|
| 699 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 700 |
}
|
| 701 |
|
| 702 |
// ══════════════════════════════════════════════════════════
|
extension/shared/bayan-api.js
CHANGED
|
@@ -79,11 +79,13 @@ async function bayanDialect(text, signal) {
|
|
| 79 |
* @param {AbortSignal} [signal] - Optional abort signal
|
| 80 |
* @returns {Promise<Object>} { matched_segment, full_verse, ... } or { error }
|
| 81 |
*/
|
| 82 |
-
async function bayanQuran(text, language = 'تدقيق الايات', signal) {
|
|
|
|
|
|
|
| 83 |
const response = await fetch(`${CONFIG.API_BASE}/api/quran`, {
|
| 84 |
method: 'POST',
|
| 85 |
headers: { 'Content-Type': 'application/json' },
|
| 86 |
-
body: JSON.stringify(
|
| 87 |
signal,
|
| 88 |
});
|
| 89 |
// Quran endpoint returns 404 with a JSON {error} body on "no match" —
|
|
|
|
| 79 |
* @param {AbortSignal} [signal] - Optional abort signal
|
| 80 |
* @returns {Promise<Object>} { matched_segment, full_verse, ... } or { error }
|
| 81 |
*/
|
| 82 |
+
async function bayanQuran(text, language = 'تدقيق الايات', signal, topN = 1) {
|
| 83 |
+
const body = { text, language };
|
| 84 |
+
if (topN > 1) body.top_n = topN;
|
| 85 |
const response = await fetch(`${CONFIG.API_BASE}/api/quran`, {
|
| 86 |
method: 'POST',
|
| 87 |
headers: { 'Content-Type': 'application/json' },
|
| 88 |
+
body: JSON.stringify(body),
|
| 89 |
signal,
|
| 90 |
});
|
| 91 |
// Quran endpoint returns 404 with a JSON {error} body on "no match" —
|
extension/sidepanel/sidepanel.css
CHANGED
|
@@ -909,6 +909,22 @@ body {
|
|
| 909 |
color: var(--text-color);
|
| 910 |
}
|
| 911 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 912 |
.sp-select {
|
| 913 |
flex: 1;
|
| 914 |
min-width: 140px;
|
|
|
|
| 909 |
color: var(--text-color);
|
| 910 |
}
|
| 911 |
|
| 912 |
+
.sp-quran-match-list {
|
| 913 |
+
display: flex;
|
| 914 |
+
flex-direction: column;
|
| 915 |
+
gap: 12px;
|
| 916 |
+
max-height: 500px;
|
| 917 |
+
overflow-y: auto;
|
| 918 |
+
}
|
| 919 |
+
|
| 920 |
+
.sp-quran-match-card {
|
| 921 |
+
background: var(--color-surface);
|
| 922 |
+
border: 1px solid rgba(6, 182, 212, 0.2);
|
| 923 |
+
border-radius: var(--sp-radius);
|
| 924 |
+
padding: 14px;
|
| 925 |
+
animation: spFadeIn 300ms ease;
|
| 926 |
+
}
|
| 927 |
+
|
| 928 |
.sp-select {
|
| 929 |
flex: 1;
|
| 930 |
min-width: 140px;
|
extension/sidepanel/sidepanel.html
CHANGED
|
@@ -289,55 +289,7 @@
|
|
| 289 |
|
| 290 |
<!-- Quran result (matching popup structure) -->
|
| 291 |
<div class="sp-quran-result is-hidden" id="quran-result-section">
|
| 292 |
-
<div class="sp-quran-
|
| 293 |
-
<span style="color:#06b6d4; font-size:12px; font-weight:700;">✓ النص القرآني المدقق</span>
|
| 294 |
-
<div class="sp-result-actions">
|
| 295 |
-
<button class="sp-btn-icon" id="btn-apply-quran" type="button" title="تطبيق في الصفحة">
|
| 296 |
-
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
| 297 |
-
</button>
|
| 298 |
-
<button class="sp-btn-icon" id="btn-copy-quran" type="button" title="نسخ">
|
| 299 |
-
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
|
| 300 |
-
</button>
|
| 301 |
-
</div>
|
| 302 |
-
</div>
|
| 303 |
-
<p id="quran-uthmani" class="sp-quran-uthmani" dir="rtl"></p>
|
| 304 |
-
<p id="quran-reference" class="sp-quran-reference"></p>
|
| 305 |
-
|
| 306 |
-
<!-- Translation -->
|
| 307 |
-
<div class="sp-quran-translate">
|
| 308 |
-
<div class="sp-quran-translate-row">
|
| 309 |
-
<svg width="14" height="14" fill="none" stroke="#06b6d4" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129"/></svg>
|
| 310 |
-
<span style="font-size:12px; font-weight:600; color:var(--text-secondary);">ترجمة الآية</span>
|
| 311 |
-
<select id="quran-lang-select" class="sp-select">
|
| 312 |
-
<option value="">— اختر لغة —</option>
|
| 313 |
-
<option value="english">🇬🇧 English</option>
|
| 314 |
-
<option value="french">🇫🇷 Français</option>
|
| 315 |
-
<option value="turkish">🇹🇷 Türkçe</option>
|
| 316 |
-
<option value="persian">🇮🇷 فارسی</option>
|
| 317 |
-
<option value="russian">🇷🇺 Русский</option>
|
| 318 |
-
<option value="spanish">🇪🇸 Español</option>
|
| 319 |
-
<option value="german">🇩🇪 Deutsch</option>
|
| 320 |
-
<option value="indonesian">🇮🇩 Indonesia</option>
|
| 321 |
-
<option value="malay">🇲🇾 Melayu</option>
|
| 322 |
-
<option value="bengali">🇧🇩 বাংলা</option>
|
| 323 |
-
<option value="bosnian">🇧🇦 Bosanski</option>
|
| 324 |
-
<option value="portuguese">🇵🇹 Português</option>
|
| 325 |
-
<option value="uzbek">🇺🇿 O'zbek</option>
|
| 326 |
-
</select>
|
| 327 |
-
</div>
|
| 328 |
-
<div id="quran-translation-section" class="sp-quran-translation is-hidden">
|
| 329 |
-
<p id="quran-trans-text" dir="auto"></p>
|
| 330 |
-
<p id="quran-trans-ref" class="sp-quran-reference" style="display:none;"></p>
|
| 331 |
-
<div class="sp-result-actions" id="quran-trans-actions" style="display:none; justify-content:flex-end; gap:4px; margin-top:8px;">
|
| 332 |
-
<button class="sp-btn-icon" id="btn-apply-quran-trans" type="button" title="تطبيق الترجمة في الصفحة">
|
| 333 |
-
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
| 334 |
-
</button>
|
| 335 |
-
<button class="sp-btn-icon" id="btn-copy-quran-trans" type="button" title="نسخ الترجمة">
|
| 336 |
-
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>
|
| 337 |
-
</button>
|
| 338 |
-
</div>
|
| 339 |
-
</div>
|
| 340 |
-
</div>
|
| 341 |
</div>
|
| 342 |
</section>
|
| 343 |
|
|
|
|
| 289 |
|
| 290 |
<!-- Quran result (matching popup structure) -->
|
| 291 |
<div class="sp-quran-result is-hidden" id="quran-result-section">
|
| 292 |
+
<div id="quran-match-list" class="sp-quran-match-list"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
</div>
|
| 294 |
</section>
|
| 295 |
|
extension/sidepanel/sidepanel.js
CHANGED
|
@@ -717,25 +717,123 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 717 |
}
|
| 718 |
|
| 719 |
// ══════════════════════════════════════════════════════════
|
| 720 |
-
// Quran verification + translation (
|
| 721 |
// ══════════════════════════════════════════════════════════
|
| 722 |
const quranInput = document.getElementById('quran-input-text');
|
| 723 |
const quranCharCount = document.getElementById('quran-char-count');
|
| 724 |
const btnQuran = document.getElementById('btn-quran');
|
| 725 |
const quranResultSection = document.getElementById('quran-result-section');
|
| 726 |
-
const
|
| 727 |
-
const quranReference = document.getElementById('quran-reference');
|
| 728 |
-
const btnCopyQuran = document.getElementById('btn-copy-quran');
|
| 729 |
-
const quranLangSelect = document.getElementById('quran-lang-select');
|
| 730 |
-
const quranTransSection = document.getElementById('quran-translation-section');
|
| 731 |
-
const quranTransText = document.getElementById('quran-trans-text');
|
| 732 |
-
const quranTransRef = document.getElementById('quran-trans-ref');
|
| 733 |
|
| 734 |
let _quranQuery = '';
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 739 |
|
| 740 |
if (quranInput) {
|
| 741 |
quranInput.addEventListener('input', () => { updateCounts(quranInput, quranCharCount, null); saveState(); });
|
|
@@ -746,112 +844,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 746 |
|
| 747 |
_quranQuery = text;
|
| 748 |
setLoading(true, 'جارٍ التدقيق...');
|
| 749 |
-
quranTransSection.classList.add('is-hidden');
|
| 750 |
-
quranLangSelect.value = '';
|
| 751 |
|
| 752 |
try {
|
| 753 |
-
const data = await bayanQuran(text);
|
| 754 |
quranResultSection.classList.remove('is-hidden');
|
| 755 |
|
| 756 |
if (data.error) {
|
| 757 |
-
|
| 758 |
-
quranReference.textContent = '';
|
| 759 |
return;
|
| 760 |
}
|
| 761 |
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
const verseText = seg.replace(/\s*【[^】]+】\s*$/, '').replace(/^\(/, '').replace(/\)$/, '');
|
| 765 |
-
const reference = refMatch ? refMatch[1] : '';
|
| 766 |
-
|
| 767 |
-
_quranVerse = verseText;
|
| 768 |
-
_quranRef = reference;
|
| 769 |
-
quranUthmani.textContent = verseText;
|
| 770 |
-
quranReference.textContent = reference ? `[${reference}]` : '';
|
| 771 |
showToast('✓ تم التدقيق');
|
| 772 |
} catch (error) {
|
| 773 |
console.error('[Bayan SP] Quran error:', error);
|
| 774 |
quranResultSection.classList.remove('is-hidden');
|
| 775 |
-
|
| 776 |
-
quranReference.textContent = '';
|
| 777 |
} finally {
|
| 778 |
setLoading(false);
|
| 779 |
}
|
| 780 |
});
|
| 781 |
-
|
| 782 |
-
quranLangSelect.addEventListener('change', async () => {
|
| 783 |
-
const lang = quranLangSelect.value;
|
| 784 |
-
if (!lang || !_quranQuery) return;
|
| 785 |
-
|
| 786 |
-
quranTransSection.classList.remove('is-hidden');
|
| 787 |
-
quranTransText.textContent = '⏳ جاري الترجمة...';
|
| 788 |
-
if (quranTransRef) quranTransRef.style.display = 'none';
|
| 789 |
-
|
| 790 |
-
try {
|
| 791 |
-
const data = await bayanQuran(_quranQuery, lang);
|
| 792 |
-
|
| 793 |
-
if (data.error) {
|
| 794 |
-
quranTransText.textContent = data.error;
|
| 795 |
-
return;
|
| 796 |
-
}
|
| 797 |
-
|
| 798 |
-
const seg = data.matched_segment || '';
|
| 799 |
-
const refMatch = seg.match(/【([^】]+)】/);
|
| 800 |
-
const transText = seg.replace(/\s*【[^】]+】\s*$/, '').replace(/^\(/, '').replace(/\)$/, '');
|
| 801 |
-
const transRef = refMatch ? refMatch[1] : '';
|
| 802 |
-
|
| 803 |
-
_quranTransText = transText;
|
| 804 |
-
_quranTransRef = transRef;
|
| 805 |
-
|
| 806 |
-
quranTransText.textContent = transText;
|
| 807 |
-
if (quranTransRef && transRef) {
|
| 808 |
-
quranTransRef.textContent = `[${transRef}]`;
|
| 809 |
-
quranTransRef.style.display = '';
|
| 810 |
-
}
|
| 811 |
-
const transActions = document.getElementById('quran-trans-actions');
|
| 812 |
-
if (transActions) transActions.style.display = 'flex';
|
| 813 |
-
} catch (error) {
|
| 814 |
-
console.error('[Bayan SP] Quran translation error:', error);
|
| 815 |
-
quranTransText.textContent = 'حدث خطأ في الترجمة';
|
| 816 |
-
}
|
| 817 |
-
});
|
| 818 |
-
|
| 819 |
-
btnCopyQuran.addEventListener('click', () => {
|
| 820 |
-
const text = (_quranVerse || '') + (_quranRef ? ` [${_quranRef}]` : '');
|
| 821 |
-
navigator.clipboard.writeText(text)
|
| 822 |
-
.then(() => showToast('✓ تم النسخ'))
|
| 823 |
-
.catch(() => showToast('تعذّر النسخ'));
|
| 824 |
-
});
|
| 825 |
-
|
| 826 |
-
const btnApplyQuran = document.getElementById('btn-apply-quran');
|
| 827 |
-
if (btnApplyQuran) {
|
| 828 |
-
btnApplyQuran.addEventListener('click', () => {
|
| 829 |
-
const verse = _quranVerse || '';
|
| 830 |
-
if (!verse) { showToast('لا يوجد نص قرآني للتطبيق'); return; }
|
| 831 |
-
const textWithRef = verse + (_quranRef ? ` [${_quranRef}]` : '');
|
| 832 |
-
writeBackToPage(textWithRef, 'auto', 'quran', sourceSelectionText);
|
| 833 |
-
});
|
| 834 |
-
}
|
| 835 |
-
|
| 836 |
-
const btnCopyQuranTrans = document.getElementById('btn-copy-quran-trans');
|
| 837 |
-
if (btnCopyQuranTrans) {
|
| 838 |
-
btnCopyQuranTrans.addEventListener('click', () => {
|
| 839 |
-
const text = (_quranTransText || '') + (_quranTransRef ? ` [${_quranTransRef}]` : '');
|
| 840 |
-
navigator.clipboard.writeText(text)
|
| 841 |
-
.then(() => showToast('✓ تم نسخ الترجمة'))
|
| 842 |
-
.catch(() => showToast('تعذّر النسخ'));
|
| 843 |
-
});
|
| 844 |
-
}
|
| 845 |
-
|
| 846 |
-
const btnApplyQuranTrans = document.getElementById('btn-apply-quran-trans');
|
| 847 |
-
if (btnApplyQuranTrans) {
|
| 848 |
-
btnApplyQuranTrans.addEventListener('click', () => {
|
| 849 |
-
const text = _quranTransText || '';
|
| 850 |
-
if (!text) { showToast('لا يوجد ترجمة للتطبيق'); return; }
|
| 851 |
-
const textWithRef = text + (_quranTransRef ? ` [${_quranTransRef}]` : '');
|
| 852 |
-
writeBackToPage(textWithRef, 'auto', 'quran', sourceSelectionText);
|
| 853 |
-
});
|
| 854 |
-
}
|
| 855 |
}
|
| 856 |
|
| 857 |
// ══════════════════════════════════════════════════════════
|
|
|
|
| 717 |
}
|
| 718 |
|
| 719 |
// ══════════════════════════════════════════════════════════
|
| 720 |
+
// Quran verification + translation (multi-match)
|
| 721 |
// ══════════════════════════════════════════════════════════
|
| 722 |
const quranInput = document.getElementById('quran-input-text');
|
| 723 |
const quranCharCount = document.getElementById('quran-char-count');
|
| 724 |
const btnQuran = document.getElementById('btn-quran');
|
| 725 |
const quranResultSection = document.getElementById('quran-result-section');
|
| 726 |
+
const quranMatchList = document.getElementById('quran-match-list');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 727 |
|
| 728 |
let _quranQuery = '';
|
| 729 |
+
|
| 730 |
+
function _parseSegment(seg) {
|
| 731 |
+
const refMatch = seg.match(/【([^】]+)】/);
|
| 732 |
+
const verseText = seg.replace(/\s*【[^】]+】\s*$/, '').replace(/^\(/, '').replace(/\)$/, '');
|
| 733 |
+
const reference = refMatch ? refMatch[1] : '';
|
| 734 |
+
return { verseText, reference };
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
+
function _langOptionsHTML() {
|
| 738 |
+
return '<option value="">— اختر لغة —</option>'
|
| 739 |
+
+ '<option value="english">English</option>'
|
| 740 |
+
+ '<option value="french">Français</option>'
|
| 741 |
+
+ '<option value="turkish">Türkçe</option>'
|
| 742 |
+
+ '<option value="persian">فارسی</option>'
|
| 743 |
+
+ '<option value="russian">Русский</option>'
|
| 744 |
+
+ '<option value="spanish">Español</option>'
|
| 745 |
+
+ '<option value="german">Deutsch</option>'
|
| 746 |
+
+ '<option value="indonesian">Indonesia</option>'
|
| 747 |
+
+ '<option value="malay">Melayu</option>'
|
| 748 |
+
+ '<option value="bengali">বাংলা</option>'
|
| 749 |
+
+ '<option value="bosnian">Bosanski</option>'
|
| 750 |
+
+ '<option value="portuguese">Português</option>'
|
| 751 |
+
+ '<option value="uzbek">O'zbek</option>';
|
| 752 |
+
}
|
| 753 |
+
|
| 754 |
+
function _renderMatchCards(matches) {
|
| 755 |
+
quranMatchList.innerHTML = '';
|
| 756 |
+
matches.forEach(function(m, i) {
|
| 757 |
+
var parsed = _parseSegment(m.matched_segment || '');
|
| 758 |
+
var card = document.createElement('div');
|
| 759 |
+
card.className = 'sp-quran-match-card';
|
| 760 |
+
card.innerHTML = '<div class="sp-quran-result-header">'
|
| 761 |
+
+ '<span style="color:#06b6d4;font-size:12px;font-weight:700;">✓ نتيجة ' + (i + 1) + '</span>'
|
| 762 |
+
+ '<div class="sp-result-actions">'
|
| 763 |
+
+ '<button class="sp-btn-icon qmc-apply" type="button" title="تطبيق في الصفحة">'
|
| 764 |
+
+ '<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>'
|
| 765 |
+
+ '</button>'
|
| 766 |
+
+ '<button class="sp-btn-icon qmc-copy" type="button" title="نسخ">'
|
| 767 |
+
+ '<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>'
|
| 768 |
+
+ '</button></div></div>'
|
| 769 |
+
+ '<p class="sp-quran-uthmani" dir="rtl">' + _escHTML(parsed.verseText) + '</p>'
|
| 770 |
+
+ '<p class="sp-quran-reference">' + (parsed.reference ? '[' + _escHTML(parsed.reference) + ']' : '') + '</p>'
|
| 771 |
+
+ '<div class="sp-quran-translate">'
|
| 772 |
+
+ '<div class="sp-quran-translate-row">'
|
| 773 |
+
+ '<span style="font-size:12px;font-weight:600;color:var(--text-secondary);">ترجمة الآية</span>'
|
| 774 |
+
+ '<select class="sp-select qmc-lang">' + _langOptionsHTML() + '</select>'
|
| 775 |
+
+ '</div>'
|
| 776 |
+
+ '<div class="sp-quran-translation is-hidden qmc-trans-section">'
|
| 777 |
+
+ '<p class="qmc-trans-text" dir="auto"></p>'
|
| 778 |
+
+ '<div class="sp-result-actions" style="justify-content:flex-end;gap:4px;margin-top:4px;">'
|
| 779 |
+
+ '<button class="sp-btn-icon qmc-apply-trans" type="button" title="تطبيق الترجمة">'
|
| 780 |
+
+ '<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>'
|
| 781 |
+
+ '</button>'
|
| 782 |
+
+ '<button class="sp-btn-icon qmc-copy-trans" type="button" title="نسخ الترجمة">'
|
| 783 |
+
+ '<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" stroke-width="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" stroke-width="2"/></svg>'
|
| 784 |
+
+ '</button></div></div></div>';
|
| 785 |
+
|
| 786 |
+
card.querySelector('.qmc-copy').addEventListener('click', function() {
|
| 787 |
+
var txt = parsed.verseText + (parsed.reference ? ' [' + parsed.reference + ']' : '');
|
| 788 |
+
navigator.clipboard.writeText(txt)
|
| 789 |
+
.then(function() { showToast('✓ تم النسخ'); })
|
| 790 |
+
.catch(function() { showToast('تعذّر النسخ'); });
|
| 791 |
+
});
|
| 792 |
+
|
| 793 |
+
card.querySelector('.qmc-apply').addEventListener('click', function() {
|
| 794 |
+
var txt = parsed.verseText + (parsed.reference ? ' [' + parsed.reference + ']' : '');
|
| 795 |
+
writeBackToPage(txt, 'auto', 'quran', sourceSelectionText);
|
| 796 |
+
});
|
| 797 |
+
|
| 798 |
+
var langSel = card.querySelector('.qmc-lang');
|
| 799 |
+
var transSec = card.querySelector('.qmc-trans-section');
|
| 800 |
+
var transP = card.querySelector('.qmc-trans-text');
|
| 801 |
+
var copyTransBtn = card.querySelector('.qmc-copy-trans');
|
| 802 |
+
var applyTransBtn = card.querySelector('.qmc-apply-trans');
|
| 803 |
+
|
| 804 |
+
langSel.addEventListener('change', async function() {
|
| 805 |
+
var lang = langSel.value;
|
| 806 |
+
if (!lang || !_quranQuery) return;
|
| 807 |
+
transSec.classList.remove('is-hidden');
|
| 808 |
+
transP.textContent = '⏳ جاري الترجمة...';
|
| 809 |
+
try {
|
| 810 |
+
var data = await bayanQuran(_quranQuery, lang);
|
| 811 |
+
if (data.error) { transP.textContent = data.error; return; }
|
| 812 |
+
var seg = data.matched_segment || '';
|
| 813 |
+
var p = _parseSegment(seg);
|
| 814 |
+
transP.textContent = p.verseText;
|
| 815 |
+
} catch (err) {
|
| 816 |
+
transP.textContent = 'حدث خطأ في الترجمة';
|
| 817 |
+
}
|
| 818 |
+
});
|
| 819 |
+
|
| 820 |
+
copyTransBtn.addEventListener('click', function() {
|
| 821 |
+
navigator.clipboard.writeText(transP.textContent || '')
|
| 822 |
+
.then(function() { showToast('✓ تم نسخ الترجمة'); })
|
| 823 |
+
.catch(function() { showToast('تعذّر النسخ'); });
|
| 824 |
+
});
|
| 825 |
+
|
| 826 |
+
applyTransBtn.addEventListener('click', function() {
|
| 827 |
+
var txt = transP.textContent || '';
|
| 828 |
+
if (!txt) { showToast('لا يوجد ترجمة للتطبيق'); return; }
|
| 829 |
+
writeBackToPage(txt, 'auto', 'quran', sourceSelectionText);
|
| 830 |
+
});
|
| 831 |
+
|
| 832 |
+
quranMatchList.appendChild(card);
|
| 833 |
+
});
|
| 834 |
+
}
|
| 835 |
+
|
| 836 |
+
function _escHTML(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
| 837 |
|
| 838 |
if (quranInput) {
|
| 839 |
quranInput.addEventListener('input', () => { updateCounts(quranInput, quranCharCount, null); saveState(); });
|
|
|
|
| 844 |
|
| 845 |
_quranQuery = text;
|
| 846 |
setLoading(true, 'جارٍ التدقيق...');
|
|
|
|
|
|
|
| 847 |
|
| 848 |
try {
|
| 849 |
+
const data = await bayanQuran(text, 'تدقيق الايات', null, 5);
|
| 850 |
quranResultSection.classList.remove('is-hidden');
|
| 851 |
|
| 852 |
if (data.error) {
|
| 853 |
+
quranMatchList.innerHTML = '<p style="color:var(--text-secondary);font-size:13px;padding:8px;">' + _escHTML(data.error) + '</p>';
|
|
|
|
| 854 |
return;
|
| 855 |
}
|
| 856 |
|
| 857 |
+
var matches = data.matches || [data];
|
| 858 |
+
_renderMatchCards(matches);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 859 |
showToast('✓ تم التدقيق');
|
| 860 |
} catch (error) {
|
| 861 |
console.error('[Bayan SP] Quran error:', error);
|
| 862 |
quranResultSection.classList.remove('is-hidden');
|
| 863 |
+
quranMatchList.innerHTML = '<p style="color:var(--text-secondary);font-size:13px;padding:8px;">خطأ في الاتصال — تحقق من الإنترنت</p>';
|
|
|
|
| 864 |
} finally {
|
| 865 |
setLoading(false);
|
| 866 |
}
|
| 867 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 868 |
}
|
| 869 |
|
| 870 |
// ══════════════════════════════════════════════════════════
|
quran.py
CHANGED
|
@@ -40,6 +40,33 @@ def _build_similarity_table():
|
|
| 40 |
|
| 41 |
_SIM_TABLE = _build_similarity_table()
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
def normalize_arabic(text: str, *, deep: bool = False) -> str:
|
| 44 |
"""
|
| 45 |
تطبيع النص العربي.
|
|
@@ -48,17 +75,12 @@ def normalize_arabic(text: str, *, deep: bool = False) -> str:
|
|
| 48 |
"""
|
| 49 |
if not text:
|
| 50 |
return ""
|
| 51 |
-
|
| 52 |
-
text =
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
'', text
|
| 56 |
-
)
|
| 57 |
-
# همزات
|
| 58 |
text = re.sub(r'[أإآٱ]', 'ا', text)
|
| 59 |
-
# تاء مربوطة
|
| 60 |
text = re.sub(r'ة', 'ه', text)
|
| 61 |
-
# ألف مقصورة
|
| 62 |
text = re.sub(r'ى', 'ي', text)
|
| 63 |
|
| 64 |
if deep:
|
|
@@ -89,7 +111,6 @@ def _score_window(query_words: list[str], window_words: list[str],
|
|
| 89 |
|
| 90 |
total = 0.0
|
| 91 |
for qw, ww, qd, wd in zip(query_words, window_words, query_deep, window_deep):
|
| 92 |
-
# نأخذ أعلى نتيجة بين المقارنة الخفيفة والعميقة
|
| 93 |
s_light = _word_similarity(qw, ww)
|
| 94 |
s_deep = _word_similarity(qd, wd)
|
| 95 |
total += max(s_light, s_deep)
|
|
@@ -104,7 +125,8 @@ def _score_window(query_words: list[str], window_words: list[str],
|
|
| 104 |
def search_bayan(query_text: str,
|
| 105 |
target_type: str = "تدقيق الايات",
|
| 106 |
fuzzy_threshold: float = 0.72,
|
| 107 |
-
db_path: str = None
|
|
|
|
| 108 |
"""
|
| 109 |
البحث عن آية قرآنية مع دعم الأخطاء الإملائية.
|
| 110 |
|
|
@@ -154,7 +176,6 @@ def search_bayan(query_text: str,
|
|
| 154 |
n = len(query_light)
|
| 155 |
|
| 156 |
# ── إذا بدأ الاستعلام بالبسملة → أعطِ الفاتحة أولوية ──
|
| 157 |
-
# بدون ده، "بسم الله الرحمن الرحيم الحمد لله" بيطابق ١١٢ سورة
|
| 158 |
_query_starts_basmala = False
|
| 159 |
if n >= 4:
|
| 160 |
first4_q = normalize_arabic(' '.join(query_light[:4]), deep=False)
|
|
@@ -168,7 +189,6 @@ def search_bayan(query_text: str,
|
|
| 168 |
|
| 169 |
for i in range(n, 0, -1):
|
| 170 |
anchor = ' '.join(query_light[:i])
|
| 171 |
-
# بحث LIKE عادي أولاً (سريع)
|
| 172 |
cursor.execute("""
|
| 173 |
SELECT v.sura_num, v.aya_num
|
| 174 |
FROM verses v
|
|
@@ -189,9 +209,7 @@ def search_bayan(query_text: str,
|
|
| 189 |
candidate_starts.insert(0, fatiha)
|
| 190 |
|
| 191 |
# إذا لم تجد شيئاً بالمرساة الخفيفة → جرّب المرساة العميقة
|
| 192 |
-
# (يستخدم text_deep إذا كان موجوداً، وإلا يعود لـ text_clean)
|
| 193 |
if not candidate_starts:
|
| 194 |
-
# اكتشف أعمدة الجدول
|
| 195 |
cursor.execute("PRAGMA table_info(verses)")
|
| 196 |
cols = {row[1] for row in cursor.fetchall()}
|
| 197 |
deep_col = "v.text_deep" if "text_deep" in cols else "v.text_clean"
|
|
@@ -238,6 +256,7 @@ def search_bayan(query_text: str,
|
|
| 238 |
best_score = -1.0
|
| 239 |
best_match_idx = -1
|
| 240 |
best_rows = None
|
|
|
|
| 241 |
|
| 242 |
for start_sura, start_aya in candidate_starts:
|
| 243 |
cursor.execute(f"""
|
|
@@ -252,18 +271,16 @@ def search_bayan(query_text: str,
|
|
| 252 |
fetched = cursor.fetchall()
|
| 253 |
|
| 254 |
QURAN_MARKS = {
|
| 255 |
-
'ۖ', 'ۗ', 'ۘ', 'ۙ', 'ۚ',
|
| 256 |
-
'۞', '۩'
|
| 257 |
}
|
| 258 |
|
| 259 |
-
|
| 260 |
# بناء خريطة الكلمات
|
| 261 |
combined_light, combined_deep, word_map = [], [], []
|
| 262 |
for row in fetched:
|
| 263 |
s_num, a_num, t_clean, t_uthmani, t_target, s_name = row
|
| 264 |
|
| 265 |
# ── شيل البسملة من text_clean في البحث (مش الفاتحة) ──
|
| 266 |
-
# بدون ده، anchor "بسم الله الرحمن الرحيم" بيطابق ١١٢ سورة
|
| 267 |
if a_num == 1 and s_num != 1:
|
| 268 |
tc_words = t_clean.split()
|
| 269 |
if len(tc_words) >= 4:
|
|
@@ -300,111 +317,139 @@ def search_bayan(query_text: str,
|
|
| 300 |
if total_words < n:
|
| 301 |
continue
|
| 302 |
|
| 303 |
-
# نافذة منزلقة
|
|
|
|
|
|
|
| 304 |
for j in range(total_words - n + 1):
|
| 305 |
score = _score_window(
|
| 306 |
query_light, combined_light[j:j+n],
|
| 307 |
query_deep, combined_deep[j:j+n]
|
| 308 |
)
|
| 309 |
-
if score >
|
| 310 |
-
|
| 311 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
best_rows = word_map
|
|
|
|
|
|
|
| 313 |
|
| 314 |
-
|
| 315 |
-
if best_score >= 0.999:
|
| 316 |
break
|
| 317 |
|
| 318 |
conn.close()
|
| 319 |
|
| 320 |
-
# if best_score < fuzzy_threshold or best_match_idx == -1:
|
| 321 |
-
# return {
|
| 322 |
-
# "error": (
|
| 323 |
-
# f"أقرب تطابق وجدناه بدرجة {best_score:.0%} وهي أقل من الحد المقبول "
|
| 324 |
-
# f"({fuzzy_threshold:.0%}). تحقق من النص المُدخل."
|
| 325 |
-
# )
|
| 326 |
-
# }
|
| 327 |
-
|
| 328 |
# ==========================================
|
| 329 |
# تشكيل المخرجات
|
| 330 |
# ==========================================
|
| 331 |
|
| 332 |
-
|
| 333 |
-
def to_arabic_nums(n):
|
| 334 |
-
"""Convert 31 → ٣١"""
|
| 335 |
ar_digits = '٠١٢٣٤٥٦٧٨٩'
|
| 336 |
-
return ''.join(ar_digits[int(d)] for d in str(
|
| 337 |
-
|
| 338 |
-
matched_words = best_rows[best_match_idx: best_match_idx + n]
|
| 339 |
-
|
| 340 |
-
# دالة تنسيق الرقم حسب اللغة
|
| 341 |
-
def fmt_num(n):
|
| 342 |
-
"""Arabic-Indic for uthmani, Latin for translations"""
|
| 343 |
-
return to_arabic_nums(n) if lang_code == "uthmani" else str(n)
|
| 344 |
-
|
| 345 |
-
# الآيات المشمولة — مرتبة بالترتيب (من الـ matched window)
|
| 346 |
-
involved: dict[tuple, dict] = {}
|
| 347 |
-
for w in matched_words:
|
| 348 |
-
key = (w["sura_num"], w["aya_num"])
|
| 349 |
-
if key not in involved:
|
| 350 |
-
involved[key] = {
|
| 351 |
-
"sura_name": w["sura_name"],
|
| 352 |
-
"target_text": w["target_text"],
|
| 353 |
-
"uthmani": w.get("uthmani", ""),
|
| 354 |
-
}
|
| 355 |
-
|
| 356 |
-
# منع تجاوز حدود السورة: اختر السورة الأكثر تمثيلاً فقط
|
| 357 |
-
sura_counts: dict[int, int] = {}
|
| 358 |
-
for w in matched_words:
|
| 359 |
-
sura_counts[w["sura_num"]] = sura_counts.get(w["sura_num"], 0) + 1
|
| 360 |
-
primary_sura = max(sura_counts, key=sura_counts.get)
|
| 361 |
-
involved = {k: v for k, v in involved.items() if k[0] == primary_sura}
|
| 362 |
|
| 363 |
-
|
| 364 |
-
|
| 365 |
|
| 366 |
-
# ── بناء النص الكامل من الآيات الكاملة (مش من الـ window بس) ──
|
| 367 |
-
# إزالة البسملة من أول آية (مدمجة في الـ DB) — ماعدا الفاتحة
|
| 368 |
def _strip_basmala(text):
|
| 369 |
-
"""Remove Basmala from beginning of verse text"""
|
| 370 |
words = text.split()
|
| 371 |
if len(words) < 4:
|
| 372 |
return text
|
| 373 |
-
# Normalize first 4 words and check against known Basmala forms
|
| 374 |
first4_norm = normalize_arabic(' '.join(words[:4]), deep=False)
|
| 375 |
-
# ٱ (Alef Wasla) gets stripped during normalization → 'بسم لله لرحمـن لرحيم'
|
| 376 |
basmala_forms = [
|
| 377 |
-
'بسم الله الرحمن الرحيم',
|
| 378 |
-
'بسم لله لرحمن لرحيم',
|
| 379 |
-
'بسم لله لرحمـن لرحيم',
|
| 380 |
]
|
| 381 |
if first4_norm in basmala_forms:
|
| 382 |
return ' '.join(words[4:])
|
| 383 |
return text
|
| 384 |
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
|
| 393 |
-
|
|
|
|
|
|
|
|
|
|
| 394 |
|
| 395 |
-
|
| 396 |
-
if len(ayah_nums) == 1:
|
| 397 |
-
ref = f"{sura_name}: {fmt_num(ayah_nums[0])}"
|
| 398 |
-
else:
|
| 399 |
-
first = fmt_num(ayah_nums[0])
|
| 400 |
-
last = fmt_num(ayah_nums[-1])
|
| 401 |
-
ref = f"{sura_name}: {first}-{last}"
|
| 402 |
|
| 403 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 404 |
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
|
|
|
|
|
|
|
|
|
| 409 |
|
| 410 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
_SIM_TABLE = _build_similarity_table()
|
| 42 |
|
| 43 |
+
# Compiled regex patterns for input sanitization
|
| 44 |
+
_RE_INVISIBLE = re.compile(
|
| 45 |
+
'[-
]'
|
| 46 |
+
)
|
| 47 |
+
_RE_SPACES = re.compile(
|
| 48 |
+
'[ - ]'
|
| 49 |
+
)
|
| 50 |
+
_PUNCT_CHARS = (
|
| 51 |
+
'،؛؟'
|
| 52 |
+
'!.,:;?\\-—–'
|
| 53 |
+
'()\\[\\]{}«»'
|
| 54 |
+
'﴾﴿⌈⌉❝❞'
|
| 55 |
+
+ chr(0x201C) + chr(0x201D) + chr(0x2018) + chr(0x2019)
|
| 56 |
+
+ chr(0x22) + chr(0x27) +
|
| 57 |
+
'ـ'
|
| 58 |
+
'-'
|
| 59 |
+
'0-9٠-٩'
|
| 60 |
+
'⁰¹²³⁴-⁹'
|
| 61 |
+
'ﷺﷻﷲ'
|
| 62 |
+
'#@&%\\^*+=/\\\\|~`_'
|
| 63 |
+
)
|
| 64 |
+
_RE_PUNCT = re.compile('[' + _PUNCT_CHARS + ']')
|
| 65 |
+
_RE_DIACRITICS = re.compile(
|
| 66 |
+
'[ً-ٰٟٱٖ'
|
| 67 |
+
'ۖ-ۜ۟-۪ۤۧۨ-ۭ]'
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
def normalize_arabic(text: str, *, deep: bool = False) -> str:
|
| 71 |
"""
|
| 72 |
تطبيع النص العربي.
|
|
|
|
| 75 |
"""
|
| 76 |
if not text:
|
| 77 |
return ""
|
| 78 |
+
text = _RE_INVISIBLE.sub('', text)
|
| 79 |
+
text = _RE_SPACES.sub(' ', text)
|
| 80 |
+
text = _RE_PUNCT.sub('', text)
|
| 81 |
+
text = _RE_DIACRITICS.sub('', text)
|
|
|
|
|
|
|
|
|
|
| 82 |
text = re.sub(r'[أإآٱ]', 'ا', text)
|
|
|
|
| 83 |
text = re.sub(r'ة', 'ه', text)
|
|
|
|
| 84 |
text = re.sub(r'ى', 'ي', text)
|
| 85 |
|
| 86 |
if deep:
|
|
|
|
| 111 |
|
| 112 |
total = 0.0
|
| 113 |
for qw, ww, qd, wd in zip(query_words, window_words, query_deep, window_deep):
|
|
|
|
| 114 |
s_light = _word_similarity(qw, ww)
|
| 115 |
s_deep = _word_similarity(qd, wd)
|
| 116 |
total += max(s_light, s_deep)
|
|
|
|
| 125 |
def search_bayan(query_text: str,
|
| 126 |
target_type: str = "تدقيق الايات",
|
| 127 |
fuzzy_threshold: float = 0.72,
|
| 128 |
+
db_path: str = None,
|
| 129 |
+
top_n: int = 1) -> dict:
|
| 130 |
"""
|
| 131 |
البحث عن آية قرآنية مع دعم الأخطاء الإملائية.
|
| 132 |
|
|
|
|
| 176 |
n = len(query_light)
|
| 177 |
|
| 178 |
# ── إذا بدأ الاستعلام بالبسملة → أعطِ الفاتحة أولوية ──
|
|
|
|
| 179 |
_query_starts_basmala = False
|
| 180 |
if n >= 4:
|
| 181 |
first4_q = normalize_arabic(' '.join(query_light[:4]), deep=False)
|
|
|
|
| 189 |
|
| 190 |
for i in range(n, 0, -1):
|
| 191 |
anchor = ' '.join(query_light[:i])
|
|
|
|
| 192 |
cursor.execute("""
|
| 193 |
SELECT v.sura_num, v.aya_num
|
| 194 |
FROM verses v
|
|
|
|
| 209 |
candidate_starts.insert(0, fatiha)
|
| 210 |
|
| 211 |
# إذا لم تجد شيئاً بالمرساة الخفيفة → جرّب المرساة العميقة
|
|
|
|
| 212 |
if not candidate_starts:
|
|
|
|
| 213 |
cursor.execute("PRAGMA table_info(verses)")
|
| 214 |
cols = {row[1] for row in cursor.fetchall()}
|
| 215 |
deep_col = "v.text_deep" if "text_deep" in cols else "v.text_clean"
|
|
|
|
| 256 |
best_score = -1.0
|
| 257 |
best_match_idx = -1
|
| 258 |
best_rows = None
|
| 259 |
+
all_matches = []
|
| 260 |
|
| 261 |
for start_sura, start_aya in candidate_starts:
|
| 262 |
cursor.execute(f"""
|
|
|
|
| 271 |
fetched = cursor.fetchall()
|
| 272 |
|
| 273 |
QURAN_MARKS = {
|
| 274 |
+
'ۖ', 'ۗ', 'ۘ', 'ۙ', 'ۚ',
|
| 275 |
+
'ۛ', 'ۜ', '', '۞', '۩'
|
| 276 |
}
|
| 277 |
|
|
|
|
| 278 |
# بناء خريطة الكلمات
|
| 279 |
combined_light, combined_deep, word_map = [], [], []
|
| 280 |
for row in fetched:
|
| 281 |
s_num, a_num, t_clean, t_uthmani, t_target, s_name = row
|
| 282 |
|
| 283 |
# ── شيل البسملة من text_clean في البحث (مش الفاتحة) ──
|
|
|
|
| 284 |
if a_num == 1 and s_num != 1:
|
| 285 |
tc_words = t_clean.split()
|
| 286 |
if len(tc_words) >= 4:
|
|
|
|
| 317 |
if total_words < n:
|
| 318 |
continue
|
| 319 |
|
| 320 |
+
# نافذة منزلقة
|
| 321 |
+
best_cand_score = -1.0
|
| 322 |
+
best_cand_idx = -1
|
| 323 |
for j in range(total_words - n + 1):
|
| 324 |
score = _score_window(
|
| 325 |
query_light, combined_light[j:j+n],
|
| 326 |
query_deep, combined_deep[j:j+n]
|
| 327 |
)
|
| 328 |
+
if score > best_cand_score:
|
| 329 |
+
best_cand_score = score
|
| 330 |
+
best_cand_idx = j
|
| 331 |
+
|
| 332 |
+
if best_cand_score >= fuzzy_threshold and best_cand_idx != -1:
|
| 333 |
+
if best_cand_score > best_score:
|
| 334 |
+
best_score = best_cand_score
|
| 335 |
+
best_match_idx = best_cand_idx
|
| 336 |
best_rows = word_map
|
| 337 |
+
if top_n > 1:
|
| 338 |
+
all_matches.append((best_cand_score, best_cand_idx, word_map))
|
| 339 |
|
| 340 |
+
if best_score >= 0.999 and top_n == 1:
|
|
|
|
| 341 |
break
|
| 342 |
|
| 343 |
conn.close()
|
| 344 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
# ==========================================
|
| 346 |
# تشكيل المخرجات
|
| 347 |
# ==========================================
|
| 348 |
|
| 349 |
+
def to_arabic_nums(num):
|
|
|
|
|
|
|
| 350 |
ar_digits = '٠١٢٣٤٥٦٧٨٩'
|
| 351 |
+
return ''.join(ar_digits[int(d)] for d in str(num))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
|
| 353 |
+
def fmt_num(num):
|
| 354 |
+
return to_arabic_nums(num) if lang_code == "uthmani" else str(num)
|
| 355 |
|
|
|
|
|
|
|
| 356 |
def _strip_basmala(text):
|
|
|
|
| 357 |
words = text.split()
|
| 358 |
if len(words) < 4:
|
| 359 |
return text
|
|
|
|
| 360 |
first4_norm = normalize_arabic(' '.join(words[:4]), deep=False)
|
|
|
|
| 361 |
basmala_forms = [
|
| 362 |
+
'بسم الله الرحمن الرحيم',
|
| 363 |
+
'بسم لله لرحمن لرحيم',
|
| 364 |
+
'بسم لله لرحمـن لرحيم',
|
| 365 |
]
|
| 366 |
if first4_norm in basmala_forms:
|
| 367 |
return ' '.join(words[4:])
|
| 368 |
return text
|
| 369 |
|
| 370 |
+
def _format_one(rows, idx, score_val):
|
| 371 |
+
matched_words = rows[idx: idx + n]
|
| 372 |
+
if not matched_words:
|
| 373 |
+
return None
|
| 374 |
+
|
| 375 |
+
involved = {}
|
| 376 |
+
for w in matched_words:
|
| 377 |
+
key = (w["sura_num"], w["aya_num"])
|
| 378 |
+
if key not in involved:
|
| 379 |
+
involved[key] = {
|
| 380 |
+
"sura_name": w["sura_name"],
|
| 381 |
+
"target_text": w["target_text"],
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
sura_counts = {}
|
| 385 |
+
for w in matched_words:
|
| 386 |
+
sura_counts[w["sura_num"]] = sura_counts.get(w["sura_num"], 0) + 1
|
| 387 |
+
p_sura = max(sura_counts, key=sura_counts.get)
|
| 388 |
+
involved = {k: v for k, v in involved.items() if k[0] == p_sura}
|
| 389 |
+
|
| 390 |
+
ayah_nums = [a for (_, a) in involved]
|
| 391 |
+
s_name = next(iter(involved.values()))["sura_name"]
|
| 392 |
+
|
| 393 |
+
verse_parts = []
|
| 394 |
+
for (s_num, a_num), data in involved.items():
|
| 395 |
+
txt = data['target_text']
|
| 396 |
+
if a_num == 1 and s_num != 1:
|
| 397 |
+
txt = _strip_basmala(txt)
|
| 398 |
+
verse_parts.append(f"{txt} ({fmt_num(a_num)})")
|
| 399 |
+
|
| 400 |
+
combined_body = " ".join(verse_parts)
|
| 401 |
|
| 402 |
+
if len(ayah_nums) == 1:
|
| 403 |
+
ref = f"{s_name}: {fmt_num(ayah_nums[0])}"
|
| 404 |
+
else:
|
| 405 |
+
ref = f"{s_name}: {fmt_num(ayah_nums[0])}-{fmt_num(ayah_nums[-1])}"
|
| 406 |
|
| 407 |
+
result_text = f"({combined_body}) 【{ref}】"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 408 |
|
| 409 |
+
return {
|
| 410 |
+
"matched_segment": result_text,
|
| 411 |
+
"full_verse": result_text,
|
| 412 |
+
"similarity_score": score_val,
|
| 413 |
+
"sura_num": p_sura,
|
| 414 |
+
"aya_start": ayah_nums[0],
|
| 415 |
+
}
|
| 416 |
|
| 417 |
+
# ── مسار النتيجة الواحدة (backward compat) ──
|
| 418 |
+
if top_n <= 1:
|
| 419 |
+
if best_score < fuzzy_threshold or best_match_idx == -1:
|
| 420 |
+
return {
|
| 421 |
+
"error": "لم يُعثر على تطابق كافٍ — تحقق من النص المُدخل."
|
| 422 |
+
}
|
| 423 |
+
return _format_one(best_rows, best_match_idx, best_score)
|
| 424 |
|
| 425 |
+
# ── مسار النتائج المتعددة ──
|
| 426 |
+
if not all_matches:
|
| 427 |
+
return {
|
| 428 |
+
"error": "لم يُعثر على تطابق كافٍ — تحقق من النص المُدخل."
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
# ترتيب حسب الدرجة تنازلياً + إزالة التكرار بنفس (سورة، آية)
|
| 432 |
+
all_matches.sort(key=lambda x: x[0], reverse=True)
|
| 433 |
+
seen_keys = set()
|
| 434 |
+
unique = []
|
| 435 |
+
for score_val, idx, rows in all_matches:
|
| 436 |
+
result = _format_one(rows, idx, score_val)
|
| 437 |
+
if result is None:
|
| 438 |
+
continue
|
| 439 |
+
dedup_key = (result["sura_num"], result["aya_start"])
|
| 440 |
+
if dedup_key in seen_keys:
|
| 441 |
+
continue
|
| 442 |
+
seen_keys.add(dedup_key)
|
| 443 |
+
unique.append(result)
|
| 444 |
+
if len(unique) >= top_n:
|
| 445 |
+
break
|
| 446 |
+
|
| 447 |
+
if not unique:
|
| 448 |
+
return {
|
| 449 |
+
"error": "لم يُعثر على تطابق كافٍ — تحقق من النص المُدخل."
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
return {
|
| 453 |
+
"matches": unique,
|
| 454 |
+
"total_found": len(all_matches),
|
| 455 |
+
}
|
src/app.py
CHANGED
|
@@ -1532,6 +1532,7 @@ def quran_verify():
|
|
| 1532 |
data = request.get_json()
|
| 1533 |
text = data.get('text', '').strip()
|
| 1534 |
language = data.get('language', 'تدقيق الايات').strip()
|
|
|
|
| 1535 |
|
| 1536 |
if not text:
|
| 1537 |
return jsonify({'error': 'النص المُدخل فارغ'}), 400
|
|
@@ -1539,10 +1540,10 @@ def quran_verify():
|
|
| 1539 |
if len(text) > 2000:
|
| 1540 |
return jsonify({'error': 'النص طويل جداً (الحد الأقصى 2000 حرف)'}), 400
|
| 1541 |
|
| 1542 |
-
app.logger.info(f'[QURAN] Query: "{text[:60]}..." lang={language}')
|
| 1543 |
start_time = time.time()
|
| 1544 |
|
| 1545 |
-
result = search_bayan(text, target_type=language)
|
| 1546 |
|
| 1547 |
elapsed = int((time.time() - start_time) * 1000)
|
| 1548 |
app.logger.info(f'[QURAN] Done in {elapsed}ms')
|
|
|
|
| 1532 |
data = request.get_json()
|
| 1533 |
text = data.get('text', '').strip()
|
| 1534 |
language = data.get('language', 'تدقيق الايات').strip()
|
| 1535 |
+
top_n = min(int(data.get('top_n', 1)), 10)
|
| 1536 |
|
| 1537 |
if not text:
|
| 1538 |
return jsonify({'error': 'النص المُدخل فارغ'}), 400
|
|
|
|
| 1540 |
if len(text) > 2000:
|
| 1541 |
return jsonify({'error': 'النص طويل جداً (الحد الأقصى 2000 حرف)'}), 400
|
| 1542 |
|
| 1543 |
+
app.logger.info(f'[QURAN] Query: "{text[:60]}..." lang={language} top_n={top_n}')
|
| 1544 |
start_time = time.time()
|
| 1545 |
|
| 1546 |
+
result = search_bayan(text, target_type=language, top_n=top_n)
|
| 1547 |
|
| 1548 |
elapsed = int((time.time() - start_time) * 1000)
|
| 1549 |
app.logger.info(f'[QURAN] Done in {elapsed}ms')
|
src/css/components.css
CHANGED
|
@@ -3347,6 +3347,20 @@ input[type="range"]::-moz-range-thumb {
|
|
| 3347 |
box-shadow: 0 0 0 3px rgba(6, 182, 212, 0.15);
|
| 3348 |
}
|
| 3349 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3350 |
@media (max-width: 640px) {
|
| 3351 |
.quran-modal-panel {
|
| 3352 |
max-width: 100%;
|
|
|
|
| 3347 |
box-shadow: 0 0 0 3px rgba(6, 182, 212, 0.15);
|
| 3348 |
}
|
| 3349 |
|
| 3350 |
+
.quran-inline-match-list {
|
| 3351 |
+
display: flex;
|
| 3352 |
+
flex-direction: column;
|
| 3353 |
+
gap: 16px;
|
| 3354 |
+
}
|
| 3355 |
+
|
| 3356 |
+
.quran-inline-match-card {
|
| 3357 |
+
background: var(--color-surface);
|
| 3358 |
+
border: 1px solid rgba(6, 182, 212, 0.2);
|
| 3359 |
+
border-radius: 1rem;
|
| 3360 |
+
padding: 1.5rem;
|
| 3361 |
+
animation: fadeSlideIn 300ms ease-out;
|
| 3362 |
+
}
|
| 3363 |
+
|
| 3364 |
@media (max-width: 640px) {
|
| 3365 |
.quran-modal-panel {
|
| 3366 |
max-width: 100%;
|
src/index.html
CHANGED
|
@@ -940,46 +940,8 @@
|
|
| 940 |
</div>
|
| 941 |
<textarea id="quran-input" class="w-full p-4 rounded-xl text-right text-lg" dir="rtl" rows="4" placeholder="اكتب الآية أو جزءًا منها هنا..." style="background: var(--color-surface); border: 1px solid var(--color-border); color: var(--color-text-primary); resize: vertical; font-family: 'Amiri Quran', 'Cairo', serif; font-size: 20px; line-height: 2;"></textarea>
|
| 942 |
<button id="quran-search-btn" onclick="searchQuranStandalone()" class="btn-primary w-full py-4 text-lg mt-4 mb-4" type="button">بحث وتدقيق</button>
|
| 943 |
-
<div id="quran-inline-result" class="is-hidden"
|
| 944 |
-
|
| 945 |
-
<div class="text-sm font-bold" style="color:#06b6d4;">✓ النص القرآني المدقق</div>
|
| 946 |
-
<div class="flex items-center gap-2">
|
| 947 |
-
<button onclick="copyQuranInlineResult()" class="quran-copy-btn" type="button" title="نسخ">📋</button>
|
| 948 |
-
<button id="quran-inline-apply-btn" onclick="applyQuranInlineResult()" class="quran-apply-btn" type="button">تطبيق في المحرر ✓</button>
|
| 949 |
-
</div>
|
| 950 |
-
</div>
|
| 951 |
-
<p id="quran-inline-uthmani" class="quran-uthmani" style="font-size: 24px; line-height: 2.2; text-align: center;"></p>
|
| 952 |
-
<p id="quran-inline-reference" class="quran-reference text-center mt-2"></p>
|
| 953 |
-
<div class="mt-4 pt-4" style="border-top:1px solid var(--color-border);">
|
| 954 |
-
<div class="flex items-center gap-3 mb-3 flex-wrap">
|
| 955 |
-
<svg width="16" height="16" fill="none" stroke="#06b6d4" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129"/></svg>
|
| 956 |
-
<span class="text-sm font-bold">ترجمة الآية</span>
|
| 957 |
-
<select id="quran-inline-lang" onchange="translateQuranInline()" class="quran-lang-select">
|
| 958 |
-
<option value="">— اختر لغة —</option>
|
| 959 |
-
<option value="english">🇬🇧 English</option>
|
| 960 |
-
<option value="french">🇫🇷 Français</option>
|
| 961 |
-
<option value="turkish">🇹🇷 Türkçe</option>
|
| 962 |
-
<option value="persian">🇮🇷 فارسی</option>
|
| 963 |
-
<option value="russian">🇷🇺 Русский</option>
|
| 964 |
-
<option value="spanish">🇪🇸 Español</option>
|
| 965 |
-
<option value="german">🇩🇪 Deutsch</option>
|
| 966 |
-
<option value="indonesian">🇮🇩 Indonesia</option>
|
| 967 |
-
<option value="malay">🇲🇾 Melayu</option>
|
| 968 |
-
<option value="bengali">🇧🇩 বাংলা</option>
|
| 969 |
-
<option value="bosnian">🇧🇦 Bosanski</option>
|
| 970 |
-
<option value="portuguese">🇵🇹 Português</option>
|
| 971 |
-
<option value="uzbek">🇺🇿 O'zbek</option>
|
| 972 |
-
</select>
|
| 973 |
-
</div>
|
| 974 |
-
<div id="quran-inline-translation" class="is-hidden p-4 rounded-xl" style="background:rgba(6,182,212,0.06); border:1px solid rgba(6,182,212,0.15);">
|
| 975 |
-
<p id="quran-inline-trans-text" style="font-size:18px; line-height:2; color:var(--color-text-primary); text-align:center;"></p>
|
| 976 |
-
<p id="quran-inline-trans-ref" class="text-sm text-center mt-2" style="color:#06b6d4; font-weight:600; display:none;"></p>
|
| 977 |
-
<div class="flex items-center justify-center gap-2 mt-3" id="quran-inline-trans-actions" style="display:none;">
|
| 978 |
-
<button onclick="copyQuranInlineTranslation()" class="quran-copy-btn" type="button" title="نسخ الترجمة">📋 نسخ</button>
|
| 979 |
-
<button onclick="applyQuranInlineTranslation()" class="quran-apply-btn" type="button">تطبيق الترجمة ✓</button>
|
| 980 |
-
</div>
|
| 981 |
-
</div>
|
| 982 |
-
</div>
|
| 983 |
</div>
|
| 984 |
</div>
|
| 985 |
</div>
|
|
@@ -1412,117 +1374,37 @@
|
|
| 1412 |
}
|
| 1413 |
|
| 1414 |
/* ═══════════════════════════════════════════
|
| 1415 |
-
Quran Standalone Panel Functions
|
| 1416 |
═══════════════════════════════════════════ */
|
| 1417 |
-
let _quranInlineVerse = '';
|
| 1418 |
-
let _quranInlineRef = '';
|
| 1419 |
let _quranInlineQuery = '';
|
| 1420 |
|
| 1421 |
-
|
| 1422 |
-
var input = document.getElementById('quran-input').value.trim();
|
| 1423 |
-
if (!input) { if (typeof showToast === 'function') showToast('الرجاء كتابة نص قرآني أولاً', 'warning'); return; }
|
| 1424 |
-
_quranInlineQuery = input;
|
| 1425 |
-
var resultDiv = document.getElementById('quran-inline-result');
|
| 1426 |
-
var uthmaniEl = document.getElementById('quran-inline-uthmani');
|
| 1427 |
-
var refEl = document.getElementById('quran-inline-reference');
|
| 1428 |
-
var searchBtn = document.getElementById('quran-search-btn');
|
| 1429 |
-
uthmaniEl.innerHTML = '<span class="text-secondary">⏳ جاري البحث...</span>';
|
| 1430 |
-
refEl.textContent = '';
|
| 1431 |
-
resultDiv.classList.remove('is-hidden');
|
| 1432 |
-
document.getElementById('quran-inline-translation').classList.add('is-hidden');
|
| 1433 |
-
document.getElementById('quran-inline-lang').value = '';
|
| 1434 |
-
if (searchBtn) { searchBtn.disabled = true; searchBtn.textContent = '⏳ جاري البحث...'; }
|
| 1435 |
-
var _abortCtrl = new AbortController();
|
| 1436 |
-
var _timeout = setTimeout(function(){ _abortCtrl.abort(); }, 30000);
|
| 1437 |
-
try {
|
| 1438 |
-
var res = await fetch('/api/quran', {
|
| 1439 |
-
method: 'POST',
|
| 1440 |
-
headers: { 'Content-Type': 'application/json' },
|
| 1441 |
-
body: JSON.stringify({ text: input, language: 'تدقيق الايات' }),
|
| 1442 |
-
signal: _abortCtrl.signal
|
| 1443 |
-
});
|
| 1444 |
-
var data = await res.json();
|
| 1445 |
-
if (data.error) {
|
| 1446 |
-
uthmaniEl.innerHTML = '<span class="text-secondary">' + data.error.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') + '</span>';
|
| 1447 |
-
return;
|
| 1448 |
-
}
|
| 1449 |
-
var seg = data.matched_segment || '';
|
| 1450 |
-
var refMatch = seg.match(/【([^】]+)】/);
|
| 1451 |
-
var verseText = seg.replace(/\s*【[^】]+】\s*$/, '').replace(/^\(/, '').replace(/\)$/, '');
|
| 1452 |
-
var reference = refMatch ? refMatch[1] : '';
|
| 1453 |
-
_quranInlineVerse = verseText;
|
| 1454 |
-
_quranInlineRef = reference;
|
| 1455 |
-
uthmaniEl.textContent = verseText;
|
| 1456 |
-
refEl.textContent = reference ? '[' + reference + ']' : '';
|
| 1457 |
-
} catch (err) {
|
| 1458 |
-
var msg = err.name === 'AbortError' ? 'انتهى وقت الانتظار — حاول مرة أخرى' : 'حدث خطأ أثناء البحث — تأكد من الاتصال';
|
| 1459 |
-
uthmaniEl.innerHTML = '<span class="text-secondary">' + msg + '</span>';
|
| 1460 |
-
} finally {
|
| 1461 |
-
clearTimeout(_timeout);
|
| 1462 |
-
if (searchBtn) { searchBtn.disabled = false; searchBtn.textContent = 'بحث وتدقيق'; }
|
| 1463 |
-
}
|
| 1464 |
-
}
|
| 1465 |
|
| 1466 |
-
|
| 1467 |
-
|
| 1468 |
-
|
| 1469 |
-
|
| 1470 |
-
|
| 1471 |
-
if (!lang || !_quranInlineQuery) return;
|
| 1472 |
-
var resultDiv = document.getElementById('quran-inline-translation');
|
| 1473 |
-
var textEl = document.getElementById('quran-inline-trans-text');
|
| 1474 |
-
var refEl = document.getElementById('quran-inline-trans-ref');
|
| 1475 |
-
var actionsEl = document.getElementById('quran-inline-trans-actions');
|
| 1476 |
-
textEl.innerHTML = '<span class="text-secondary">⏳ جاري الترجمة...</span>';
|
| 1477 |
-
if (refEl) refEl.style.display = 'none';
|
| 1478 |
-
if (actionsEl) actionsEl.style.display = 'none';
|
| 1479 |
-
resultDiv.classList.remove('is-hidden');
|
| 1480 |
-
var _abortCtrl = new AbortController();
|
| 1481 |
-
var _timeout = setTimeout(function(){ _abortCtrl.abort(); }, 30000);
|
| 1482 |
-
try {
|
| 1483 |
-
var res = await fetch('/api/quran', {
|
| 1484 |
-
method: 'POST',
|
| 1485 |
-
headers: { 'Content-Type': 'application/json' },
|
| 1486 |
-
body: JSON.stringify({ text: _quranInlineQuery, language: lang }),
|
| 1487 |
-
signal: _abortCtrl.signal
|
| 1488 |
-
});
|
| 1489 |
-
var data = await res.json();
|
| 1490 |
-
if (data.error) {
|
| 1491 |
-
textEl.innerHTML = '<span class="text-secondary">' + data.error.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') + '</span>';
|
| 1492 |
-
return;
|
| 1493 |
-
}
|
| 1494 |
-
var seg = data.matched_segment || '';
|
| 1495 |
-
var refMatch = seg.match(/【([^】]+)】/);
|
| 1496 |
-
var transText = seg.replace(/\s*【[^】]+】\s*$/, '').replace(/^\(/, '').replace(/\)$/, '');
|
| 1497 |
-
var transRef = refMatch ? refMatch[1] : '';
|
| 1498 |
-
_quranInlineTransText = transText;
|
| 1499 |
-
_quranInlineTransRef = transRef;
|
| 1500 |
-
textEl.textContent = transText;
|
| 1501 |
-
if (refEl && transRef) { refEl.textContent = '[' + transRef + ']'; refEl.style.display = ''; }
|
| 1502 |
-
if (actionsEl) actionsEl.style.display = '';
|
| 1503 |
-
} catch (err) {
|
| 1504 |
-
var msg = err.name === 'AbortError' ? 'انتهى وقت الانتظار' : 'حدث خطأ في الترجمة';
|
| 1505 |
-
textEl.innerHTML = '<span class="text-secondary">' + msg + '</span>';
|
| 1506 |
-
} finally { clearTimeout(_timeout); }
|
| 1507 |
-
}
|
| 1508 |
-
|
| 1509 |
-
function copyQuranInlineResult() {
|
| 1510 |
-
var text = (_quranInlineVerse || '') + (_quranInlineRef ? ' [' + _quranInlineRef + ']' : '');
|
| 1511 |
-
if (!text.trim()) return;
|
| 1512 |
-
navigator.clipboard.writeText(text).then(function() {
|
| 1513 |
-
if (typeof showToast === 'function') showToast('✓ تم نسخ النص المدقق');
|
| 1514 |
-
});
|
| 1515 |
}
|
| 1516 |
|
| 1517 |
-
function
|
| 1518 |
-
|
| 1519 |
-
|
| 1520 |
-
|
| 1521 |
-
|
| 1522 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1523 |
}
|
| 1524 |
|
| 1525 |
-
function
|
| 1526 |
var editor = document.getElementById('editor-container');
|
| 1527 |
if (!editor) return false;
|
| 1528 |
var plain = editor.textContent || '';
|
|
@@ -1530,70 +1412,153 @@
|
|
| 1530 |
var idx = plain.indexOf(query);
|
| 1531 |
if (idx === -1) return false;
|
| 1532 |
if (typeof pushUndoState === 'function') pushUndoState();
|
| 1533 |
-
var esc = function(t) { return t.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); };
|
| 1534 |
var before = plain.substring(0, idx);
|
| 1535 |
var after = plain.substring(idx + query.length);
|
| 1536 |
-
var refHTML = ref ? ' <span class="quran-ref-inline">[' +
|
| 1537 |
-
editor.innerHTML =
|
| 1538 |
'<span class="quran-applied" contenteditable="false" data-quran="true">' +
|
| 1539 |
-
|
| 1540 |
editor.focus();
|
| 1541 |
editor.dispatchEvent(new Event('input', { bubbles: true }));
|
| 1542 |
return true;
|
| 1543 |
}
|
| 1544 |
|
| 1545 |
-
function
|
| 1546 |
-
|
| 1547 |
-
|
| 1548 |
-
|
| 1549 |
-
|
| 1550 |
-
|
| 1551 |
-
|
| 1552 |
-
|
| 1553 |
-
|
| 1554 |
-
|
| 1555 |
-
|
| 1556 |
-
|
| 1557 |
-
|
| 1558 |
-
|
| 1559 |
-
|
| 1560 |
-
'<
|
| 1561 |
-
|
| 1562 |
-
|
| 1563 |
-
|
| 1564 |
-
|
| 1565 |
-
|
| 1566 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1567 |
|
| 1568 |
-
|
| 1569 |
-
|
| 1570 |
-
|
| 1571 |
-
|
| 1572 |
-
|
| 1573 |
-
|
| 1574 |
-
|
| 1575 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1576 |
if (typeof pushUndoState === 'function') pushUndoState();
|
| 1577 |
-
var
|
| 1578 |
-
|
| 1579 |
-
var after = plain.substring(idx + _quranInlineQuery.length);
|
| 1580 |
-
var refHTML = _quranInlineTransRef ? ' <span class="quran-ref-inline">[' + esc(_quranInlineTransRef) + ']</span>' : '';
|
| 1581 |
-
editor.innerHTML = esc(before) + esc(_quranInlineTransText) + refHTML + esc(after);
|
| 1582 |
-
editor.focus();
|
| 1583 |
editor.dispatchEvent(new Event('input', { bubbles: true }));
|
| 1584 |
switchTab('write');
|
| 1585 |
if (typeof showToast === 'function') showToast('✓ تم تطبيق الترجمة في المحرر');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1586 |
return;
|
| 1587 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1588 |
}
|
| 1589 |
-
// Fallback: append
|
| 1590 |
-
if (typeof pushUndoState === 'function') pushUndoState();
|
| 1591 |
-
var esc2 = function(t) { return t.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); };
|
| 1592 |
-
var existing = editor.innerHTML;
|
| 1593 |
-
editor.innerHTML = existing + (existing ? '<br>' : '') + esc2(_quranInlineTransText);
|
| 1594 |
-
editor.dispatchEvent(new Event('input', { bubbles: true }));
|
| 1595 |
-
switchTab('write');
|
| 1596 |
-
if (typeof showToast === 'function') showToast('✓ تم إضافة الترجمة في المحرر');
|
| 1597 |
}
|
| 1598 |
let _dialectResult = '';
|
| 1599 |
let _dialectInput = '';
|
|
|
|
| 940 |
</div>
|
| 941 |
<textarea id="quran-input" class="w-full p-4 rounded-xl text-right text-lg" dir="rtl" rows="4" placeholder="اكتب الآية أو جزءًا منها هنا..." style="background: var(--color-surface); border: 1px solid var(--color-border); color: var(--color-text-primary); resize: vertical; font-family: 'Amiri Quran', 'Cairo', serif; font-size: 20px; line-height: 2;"></textarea>
|
| 942 |
<button id="quran-search-btn" onclick="searchQuranStandalone()" class="btn-primary w-full py-4 text-lg mt-4 mb-4" type="button">بحث وتدقيق</button>
|
| 943 |
+
<div id="quran-inline-result" class="is-hidden">
|
| 944 |
+
<div id="quran-inline-match-list" class="quran-inline-match-list"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 945 |
</div>
|
| 946 |
</div>
|
| 947 |
</div>
|
|
|
|
| 1374 |
}
|
| 1375 |
|
| 1376 |
/* ═══════════════════════════════════════════
|
| 1377 |
+
Quran Standalone Panel Functions (multi-match)
|
| 1378 |
═══════════════════════════════════════════ */
|
|
|
|
|
|
|
| 1379 |
let _quranInlineQuery = '';
|
| 1380 |
|
| 1381 |
+
function _qEsc(t) { return t.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1382 |
|
| 1383 |
+
function _qParseSegment(seg) {
|
| 1384 |
+
var refMatch = seg.match(/【([^】]+)】/);
|
| 1385 |
+
var verseText = seg.replace(/\s*���[^】]+】\s*$/, '').replace(/^\(/, '').replace(/\)$/, '');
|
| 1386 |
+
var reference = refMatch ? refMatch[1] : '';
|
| 1387 |
+
return { verseText: verseText, reference: reference };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1388 |
}
|
| 1389 |
|
| 1390 |
+
function _qLangOptions() {
|
| 1391 |
+
return '<option value="">— اختر لغة —</option>'
|
| 1392 |
+
+ '<option value="english">English</option>'
|
| 1393 |
+
+ '<option value="french">Français</option>'
|
| 1394 |
+
+ '<option value="turkish">Türkçe</option>'
|
| 1395 |
+
+ '<option value="persian">فارسی</option>'
|
| 1396 |
+
+ '<option value="russian">Русский</option>'
|
| 1397 |
+
+ '<option value="spanish">Español</option>'
|
| 1398 |
+
+ '<option value="german">Deutsch</option>'
|
| 1399 |
+
+ '<option value="indonesian">Indonesia</option>'
|
| 1400 |
+
+ '<option value="malay">Melayu</option>'
|
| 1401 |
+
+ '<option value="bengali">বাংলা</option>'
|
| 1402 |
+
+ '<option value="bosnian">Bosanski</option>'
|
| 1403 |
+
+ '<option value="portuguese">Português</option>'
|
| 1404 |
+
+ '<option value="uzbek">O\'zbek</option>';
|
| 1405 |
}
|
| 1406 |
|
| 1407 |
+
function _qReplaceInEditor(newText, ref) {
|
| 1408 |
var editor = document.getElementById('editor-container');
|
| 1409 |
if (!editor) return false;
|
| 1410 |
var plain = editor.textContent || '';
|
|
|
|
| 1412 |
var idx = plain.indexOf(query);
|
| 1413 |
if (idx === -1) return false;
|
| 1414 |
if (typeof pushUndoState === 'function') pushUndoState();
|
|
|
|
| 1415 |
var before = plain.substring(0, idx);
|
| 1416 |
var after = plain.substring(idx + query.length);
|
| 1417 |
+
var refHTML = ref ? ' <span class="quran-ref-inline">[' + _qEsc(ref) + ']</span>' : '';
|
| 1418 |
+
editor.innerHTML = _qEsc(before) +
|
| 1419 |
'<span class="quran-applied" contenteditable="false" data-quran="true">' +
|
| 1420 |
+
_qEsc(newText) + refHTML + '</span>' + _qEsc(after);
|
| 1421 |
editor.focus();
|
| 1422 |
editor.dispatchEvent(new Event('input', { bubbles: true }));
|
| 1423 |
return true;
|
| 1424 |
}
|
| 1425 |
|
| 1426 |
+
function _qRenderInlineCards(matches) {
|
| 1427 |
+
var list = document.getElementById('quran-inline-match-list');
|
| 1428 |
+
list.innerHTML = '';
|
| 1429 |
+
matches.forEach(function(m, i) {
|
| 1430 |
+
var p = _qParseSegment(m.matched_segment || '');
|
| 1431 |
+
var card = document.createElement('div');
|
| 1432 |
+
card.className = 'quran-inline-match-card';
|
| 1433 |
+
card.innerHTML = '<div class="flex items-center justify-between mb-3">'
|
| 1434 |
+
+ '<div class="text-sm font-bold" style="color:#06b6d4;">✓ نتيجة ' + (i + 1) + '</div>'
|
| 1435 |
+
+ '<div class="flex items-center gap-2">'
|
| 1436 |
+
+ '<button class="quran-copy-btn qic-copy" type="button" title="نسخ">📋</button>'
|
| 1437 |
+
+ '<button class="quran-apply-btn qic-apply" type="button">تطبيق في المحرر ✓</button>'
|
| 1438 |
+
+ '</div></div>'
|
| 1439 |
+
+ '<p class="quran-uthmani" style="font-size:24px;line-height:2.2;text-align:center;" dir="rtl">' + _qEsc(p.verseText) + '</p>'
|
| 1440 |
+
+ '<p class="quran-reference text-center mt-2">' + (p.reference ? '[' + _qEsc(p.reference) + ']' : '') + '</p>'
|
| 1441 |
+
+ '<div class="mt-4 pt-4" style="border-top:1px solid var(--color-border);">'
|
| 1442 |
+
+ '<div class="flex items-center gap-3 mb-3 flex-wrap">'
|
| 1443 |
+
+ '<span class="text-sm font-bold">ترجمة الآية</span>'
|
| 1444 |
+
+ '<select class="quran-lang-select qic-lang">' + _qLangOptions() + '</select>'
|
| 1445 |
+
+ '</div>'
|
| 1446 |
+
+ '<div class="qic-trans-section is-hidden p-4 rounded-xl" style="background:rgba(6,182,212,0.06);border:1px solid rgba(6,182,212,0.15);">'
|
| 1447 |
+
+ '<p class="qic-trans-text" style="font-size:18px;line-height:2;color:var(--color-text-primary);text-align:center;"></p>'
|
| 1448 |
+
+ '<div class="flex items-center justify-center gap-2 mt-3">'
|
| 1449 |
+
+ '<button class="quran-copy-btn qic-copy-trans" type="button" title="نسخ الترجمة">📋 نسخ</button>'
|
| 1450 |
+
+ '<button class="quran-apply-btn qic-apply-trans" type="button">تطبيق الترجمة ✓</button>'
|
| 1451 |
+
+ '</div></div></div>';
|
| 1452 |
+
|
| 1453 |
+
card.querySelector('.qic-copy').addEventListener('click', function() {
|
| 1454 |
+
var txt = p.verseText + (p.reference ? ' [' + p.reference + ']' : '');
|
| 1455 |
+
navigator.clipboard.writeText(txt).then(function() { if (typeof showToast === 'function') showToast('✓ تم النسخ'); });
|
| 1456 |
+
});
|
| 1457 |
|
| 1458 |
+
card.querySelector('.qic-apply').addEventListener('click', function() {
|
| 1459 |
+
if (_qReplaceInEditor(p.verseText, p.reference)) {
|
| 1460 |
+
switchTab('write');
|
| 1461 |
+
if (typeof showToast === 'function') showToast('✓ تم استبدال النص بالنص القرآني المدقق');
|
| 1462 |
+
} else {
|
| 1463 |
+
var editor = document.getElementById('editor-container');
|
| 1464 |
+
if (!editor) return;
|
| 1465 |
+
if (typeof pushUndoState === 'function') pushUndoState();
|
| 1466 |
+
var refHTML = p.reference ? ' <span class="quran-ref-inline">[' + _qEsc(p.reference) + ']</span>' : '';
|
| 1467 |
+
var existing = editor.innerHTML;
|
| 1468 |
+
editor.innerHTML = existing + (existing ? '<br>' : '') +
|
| 1469 |
+
'<span class="quran-applied" contenteditable="false" data-quran="true">' +
|
| 1470 |
+
_qEsc(p.verseText) + refHTML + '</span>';
|
| 1471 |
+
editor.dispatchEvent(new Event('input', { bubbles: true }));
|
| 1472 |
+
switchTab('write');
|
| 1473 |
+
if (typeof showToast === 'function') showToast('✓ تم إضافة النص القرآني في المحرر');
|
| 1474 |
+
}
|
| 1475 |
+
});
|
| 1476 |
+
|
| 1477 |
+
var langSel = card.querySelector('.qic-lang');
|
| 1478 |
+
var transSec = card.querySelector('.qic-trans-section');
|
| 1479 |
+
var transP = card.querySelector('.qic-trans-text');
|
| 1480 |
+
var copyTransBtn = card.querySelector('.qic-copy-trans');
|
| 1481 |
+
var applyTransBtn = card.querySelector('.qic-apply-trans');
|
| 1482 |
+
|
| 1483 |
+
langSel.addEventListener('change', async function() {
|
| 1484 |
+
var lang = langSel.value;
|
| 1485 |
+
if (!lang || !_quranInlineQuery) return;
|
| 1486 |
+
transSec.classList.remove('is-hidden');
|
| 1487 |
+
transP.innerHTML = '<span class="text-secondary">⏳ جاري الترجمة...</span>';
|
| 1488 |
+
var ctrl = new AbortController();
|
| 1489 |
+
var tmo = setTimeout(function(){ ctrl.abort(); }, 30000);
|
| 1490 |
+
try {
|
| 1491 |
+
var res = await fetch('/api/quran', {
|
| 1492 |
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
| 1493 |
+
body: JSON.stringify({ text: _quranInlineQuery, language: lang }),
|
| 1494 |
+
signal: ctrl.signal
|
| 1495 |
+
});
|
| 1496 |
+
var data = await res.json();
|
| 1497 |
+
if (data.error) { transP.innerHTML = '<span class="text-secondary">' + _qEsc(data.error) + '</span>'; return; }
|
| 1498 |
+
var seg = data.matched_segment || '';
|
| 1499 |
+
var tp = _qParseSegment(seg);
|
| 1500 |
+
transP.textContent = tp.verseText;
|
| 1501 |
+
} catch (err) {
|
| 1502 |
+
transP.innerHTML = '<span class="text-secondary">' + (err.name === 'AbortError' ? 'انتهى وقت الانتظار' : 'حدث خطأ في الترجمة') + '</span>';
|
| 1503 |
+
} finally { clearTimeout(tmo); }
|
| 1504 |
+
});
|
| 1505 |
+
|
| 1506 |
+
copyTransBtn.addEventListener('click', function() {
|
| 1507 |
+
navigator.clipboard.writeText(transP.textContent || '').then(function() {
|
| 1508 |
+
if (typeof showToast === 'function') showToast('✓ تم نسخ الترجمة');
|
| 1509 |
+
});
|
| 1510 |
+
});
|
| 1511 |
+
|
| 1512 |
+
applyTransBtn.addEventListener('click', function() {
|
| 1513 |
+
var txt = transP.textContent || '';
|
| 1514 |
+
if (!txt) return;
|
| 1515 |
+
var editor = document.getElementById('editor-container');
|
| 1516 |
+
if (!editor) return;
|
| 1517 |
if (typeof pushUndoState === 'function') pushUndoState();
|
| 1518 |
+
var existing = editor.innerHTML;
|
| 1519 |
+
editor.innerHTML = existing + (existing ? '<br>' : '') + _qEsc(txt);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1520 |
editor.dispatchEvent(new Event('input', { bubbles: true }));
|
| 1521 |
switchTab('write');
|
| 1522 |
if (typeof showToast === 'function') showToast('✓ تم تطبيق الترجمة في المحرر');
|
| 1523 |
+
});
|
| 1524 |
+
|
| 1525 |
+
list.appendChild(card);
|
| 1526 |
+
});
|
| 1527 |
+
}
|
| 1528 |
+
|
| 1529 |
+
async function searchQuranStandalone() {
|
| 1530 |
+
var input = document.getElementById('quran-input').value.trim();
|
| 1531 |
+
if (!input) { if (typeof showToast === 'function') showToast('الرجاء كتابة نص قرآني أولاً', 'warning'); return; }
|
| 1532 |
+
_quranInlineQuery = input;
|
| 1533 |
+
var resultDiv = document.getElementById('quran-inline-result');
|
| 1534 |
+
var matchList = document.getElementById('quran-inline-match-list');
|
| 1535 |
+
var searchBtn = document.getElementById('quran-search-btn');
|
| 1536 |
+
matchList.innerHTML = '<p class="text-secondary text-center" style="padding:1rem;">⏳ جاري البحث...</p>';
|
| 1537 |
+
resultDiv.classList.remove('is-hidden');
|
| 1538 |
+
if (searchBtn) { searchBtn.disabled = true; searchBtn.textContent = '⏳ جاري البحث...'; }
|
| 1539 |
+
var _abortCtrl = new AbortController();
|
| 1540 |
+
var _timeout = setTimeout(function(){ _abortCtrl.abort(); }, 30000);
|
| 1541 |
+
try {
|
| 1542 |
+
var res = await fetch('/api/quran', {
|
| 1543 |
+
method: 'POST',
|
| 1544 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1545 |
+
body: JSON.stringify({ text: input, language: 'تدقيق الايات', top_n: 5 }),
|
| 1546 |
+
signal: _abortCtrl.signal
|
| 1547 |
+
});
|
| 1548 |
+
var data = await res.json();
|
| 1549 |
+
if (data.error) {
|
| 1550 |
+
matchList.innerHTML = '<p class="text-secondary text-center" style="padding:1rem;">' + _qEsc(data.error) + '</p>';
|
| 1551 |
return;
|
| 1552 |
}
|
| 1553 |
+
var matches = data.matches || [data];
|
| 1554 |
+
_qRenderInlineCards(matches);
|
| 1555 |
+
} catch (err) {
|
| 1556 |
+
var msg = err.name === 'AbortError' ? 'انتهى وقت الانتظار — حاول مرة أخرى' : 'حدث خطأ أثناء البحث — تأكد من الاتصال';
|
| 1557 |
+
matchList.innerHTML = '<p class="text-secondary text-center" style="padding:1rem;">' + msg + '</p>';
|
| 1558 |
+
} finally {
|
| 1559 |
+
clearTimeout(_timeout);
|
| 1560 |
+
if (searchBtn) { searchBtn.disabled = false; searchBtn.textContent = 'بحث وتدقيق'; }
|
| 1561 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1562 |
}
|
| 1563 |
let _dialectResult = '';
|
| 1564 |
let _dialectInput = '';
|
src/nlp/spelling/araspell_rules.py
CHANGED
|
@@ -684,8 +684,7 @@ class VocabularyManager:
|
|
| 684 |
return not self.is_iv(word)
|
| 685 |
|
| 686 |
def get_frequency_rank(self, word: str) -> int:
|
| 687 |
-
|
| 688 |
-
return self.vocab_rank.get(clean, 999999)
|
| 689 |
|
| 690 |
def all_words_iv(self, text: str) -> bool:
|
| 691 |
words = text.split()
|
|
|
|
| 684 |
return not self.is_iv(word)
|
| 685 |
|
| 686 |
def get_frequency_rank(self, word: str) -> int:
|
| 687 |
+
return 999999
|
|
|
|
| 688 |
|
| 689 |
def all_words_iv(self, text: str) -> bool:
|
| 690 |
words = text.split()
|