youssefreda9 commited on
Commit
75e0099
·
1 Parent(s): 9c49fff

Update extension UI, app pipeline, and Spelling Rules

Browse files
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-result-header">
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 quranUthmani = document.getElementById('quran-uthmani');
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
- let _quranVerse = '';
588
- let _quranRef = '';
589
- let _quranTransText = '';
590
- let _quranTransRef = '';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- quranUthmani.textContent = data.error;
610
- quranReference.textContent = '';
611
  return;
612
  }
613
 
614
- const seg = data.matched_segment || '';
615
- const refMatch = seg.match(/【([^】]+)】/);
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
- quranUthmani.textContent = 'خطأ في الاتصال — تحقق من الإنترنت';
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({ text, language }),
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-result-header">
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 (matching popup)
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 quranUthmani = document.getElementById('quran-uthmani');
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
- let _quranVerse = '';
736
- let _quranRef = '';
737
- let _quranTransText = '';
738
- let _quranTransRef = '';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- quranUthmani.textContent = data.error;
758
- quranReference.textContent = '';
759
  return;
760
  }
761
 
762
- const seg = data.matched_segment || '';
763
- const refMatch = seg.match(/【([^】]+)】/);
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
- quranUthmani.textContent = 'خطأ في الاتصال — تحقق من الإنترنت';
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 = re.sub(
53
- r'[\u064B-\u065F\u0670\u0671\u0656'
54
- r'\u06D6-\u06DC\u06DF-\u06E4\u06E7\u06E8\u06EA-\u06ED]',
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) -> dict:
 
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 > best_score:
310
- best_score = score
311
- best_match_idx = j
 
 
 
 
 
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(n))
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
- ayah_nums = [a_num for (_, a_num) in involved]
364
- sura_name = next(iter(involved.values()))["sura_name"]
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
- 'بسم الله الرحمن الرحيم', # standard alef
378
- 'بسم لله لرحمن لرحيم', # alef wasla stripped
379
- 'بسم لله لرحمـن لرحيم', # with tatweel ـ
380
  ]
381
  if first4_norm in basmala_forms:
382
  return ' '.join(words[4:])
383
  return text
384
 
385
- verse_parts = []
386
- for (s_num, a_num), data in involved.items():
387
- txt = data['target_text']
388
- # شيل البسملة من الآية الأولى (إلا سورة الفاتحة)
389
- if a_num == 1 and s_num != 1:
390
- txt = _strip_basmala(txt)
391
- verse_parts.append(f"{txt} ({fmt_num(a_num)})")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
 
393
- combined_body = " ".join(verse_parts)
 
 
 
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
- result_text = f"({combined_body}) 【{ref}】"
 
 
 
 
 
 
404
 
405
- is_exact = best_score >= 0.999
406
- return {
407
- "matched_segment": result_text,
408
- "full_verse": result_text,
 
 
 
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" style="background: var(--color-surface); border: 1px solid rgba(6,182,212,0.2); border-radius: 1rem; padding: 1.5rem;">
944
- <div class="flex items-center justify-between mb-3">
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
- async function searchQuranStandalone() {
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;') + '</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
- let _quranInlineTransText = '';
1467
- let _quranInlineTransRef = '';
1468
-
1469
- async function translateQuranInline() {
1470
- var lang = document.getElementById('quran-inline-lang').value;
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;') + '</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 copyQuranInlineTranslation() {
1518
- var text = (_quranInlineTransText || '') + (_quranInlineTransRef ? ' [' + _quranInlineTransRef + ']' : '');
1519
- if (!text.trim()) return;
1520
- navigator.clipboard.writeText(text).then(function() {
1521
- if (typeof showToast === 'function') showToast('✓ تم نسخ الترجمة');
1522
- });
 
 
 
 
 
 
 
 
 
1523
  }
1524
 
1525
- function _replaceQueryInEditor(newText, ref) {
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); };
1534
  var before = plain.substring(0, idx);
1535
  var after = plain.substring(idx + query.length);
1536
- var refHTML = ref ? ' <span class="quran-ref-inline">[' + esc(ref) + ']</span>' : '';
1537
- editor.innerHTML = esc(before) +
1538
  '<span class="quran-applied" contenteditable="false" data-quran="true">' +
1539
- esc(newText) + refHTML + '</span>' + esc(after);
1540
  editor.focus();
1541
  editor.dispatchEvent(new Event('input', { bubbles: true }));
1542
  return true;
1543
  }
1544
 
1545
- function applyQuranInlineResult() {
1546
- if (!_quranInlineVerse) return;
1547
- var editor = document.getElementById('editor-container');
1548
- if (!editor) return;
1549
- // Try to replace original query text in editor
1550
- if (_quranInlineQuery && _replaceQueryInEditor(_quranInlineVerse, _quranInlineRef)) {
1551
- switchTab('write');
1552
- if (typeof showToast === 'function') showToast('✓ تم استبدال النص بالنص القرآني المدقق');
1553
- } else {
1554
- // Fallback: append if original text not found in editor
1555
- if (typeof pushUndoState === 'function') pushUndoState();
1556
- var esc = function(t) { return t.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); };
1557
- var refHTML = _quranInlineRef ? ' <span class="quran-ref-inline">[' + esc(_quranInlineRef) + ']</span>' : '';
1558
- var existing = editor.innerHTML;
1559
- editor.innerHTML = existing + (existing ? '<br>' : '') +
1560
- '<span class="quran-applied" contenteditable="false" data-quran="true">' +
1561
- esc(_quranInlineVerse) + refHTML + '</span>';
1562
- editor.dispatchEvent(new Event('input', { bubbles: true }));
1563
- switchTab('write');
1564
- if (typeof showToast === 'function') showToast('✓ تم إضافة النص القرآني في المحرر');
1565
- }
1566
- }
 
 
 
 
 
 
 
 
 
1567
 
1568
- function applyQuranInlineTranslation() {
1569
- if (!_quranInlineTransText) return;
1570
- var editor = document.getElementById('editor-container');
1571
- if (!editor) return;
1572
- if (_quranInlineQuery) {
1573
- var plain = editor.textContent || '';
1574
- var idx = plain.indexOf(_quranInlineQuery);
1575
- if (idx !== -1) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1576
  if (typeof pushUndoState === 'function') pushUndoState();
1577
- var esc = function(t) { return t.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); };
1578
- var before = plain.substring(0, idx);
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); };
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- clean = re.sub(r'[^\w]', '', word)
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()