Skip to content

Commit

Permalink
support previewing ayat before the quiz; fixes
Browse files Browse the repository at this point in the history
now you can preview a recitation before being quizzed on it, or even
during the quizzing (which would erase your progress as you'll have to
restart all over again).

data-related fixes:

- remove tajweed marks -- no tajweed color support yet.
- fix three missing vowel marks in the Uthmani text.
- remove the space after alef+fatha+waw+fatha.
  see: aliftype/quran-data#17

improvements:

- put basmala centered on a line by its own, in the preview or the quiz.
- force a line break between the end of sura 8 and the start of sura 9,
  in place of the non-existent basmala.

styling:

- improve qari-selector & teacher-option styling
- use KacstOne for the UI, and update the relevant styles.
- make line spacing a bit larger.
- make basmala font-size adapt to very narrow screens (< 300px).
- make legend adapt to very narrow screens.

other fixes:

- fix goatcounter script-adding script (messed up in `ef8f3e7` commit).
- separate .logic.js into general .logic.js and specific .quran.js.
- remove unused code in .z.js
- fix an error after a "WAIT" after the game-end.
  • Loading branch information
noureddin committed Jul 25, 2024
1 parent ef8f3e7 commit 6cbedc3
Show file tree
Hide file tree
Showing 26 changed files with 262 additions and 161 deletions.
2 changes: 1 addition & 1 deletion .gc.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
window.goatcounter = { path: location.href, allow_frame: true }
// privacy-friendly statistics, no tracking of personal data, no need for GDPR consent; see goatcounter.com
document.body.append(make_elem('script', { data: { goatcounter: 'https://ihkam.goatcounter.com/count' }, async: true, src: '//gc.zgo.at/count.js' }))
document.body.append(make_elem('script', { Dataset: { goatcounter: 'https://ihkam.goatcounter.com/count' }, async: true, src: '//gc.zgo.at/count.js' }))
20 changes: 12 additions & 8 deletions .index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
<center>
<div id="body">
<div id="allselectors">
<div class=option><label>تلاوة بصوت:&ensp;<select id="qaris"><option value="" selected>بغير تلاوة صوتية</option><<qaris>></select></label></div>
<div id="teacher_option" class=option hidden><label><input id="teacher_input" type="checkbox">&ensp;معلم؟ (التلاوة الصوتية قبل الآية)</label></div>
<div class="option"><label>تلاوة بصوت:&ensp;<select id="qaris"><option value="" selected>بغير تلاوة صوتية</option><<qaris>></select></label></div>
<div id="teacher_option" class="option" hidden><label><input id="teacher_input" type="checkbox">&ensp;معلم؟ (التلاوة الصوتية قبل الآية)</label></div>
<div class="as">
<label for="sura_bgn">من<span class="sura">&nbsp;سورة</span>:&thinsp;</label>
<select id="sura_bgn"></select>&thinsp;
Expand All @@ -26,19 +26,23 @@
<select id="aaya_end"></select>&thinsp;
<button class="search">🔍<span class="srcttl"> ابحث</span></button>
</div>
<div id="levels_option" class=option>
<div id="levels_option" class="option">
<label><input name=lvl id="l0" type=radio>&thinsp;مبتدئ</label>&ensp;
<label><input name=lvl id="l1" type=radio>&thinsp;سهل</label>&ensp;
<label><input name=lvl id="l2" type=radio checked>&thinsp;وسط</label>&ensp;
<label><input name=lvl id="l3" type=radio>&thinsp;صعب</label>&ensp;
<label><input name=lvl id="l4" type=radio>&thinsp;مستحيل</label>&ensp;
</div>
<button title="حدد ما تريد مراجعته أو تسميعه، ثم اضغط على هذا الزر." id="ok">ابدأ</button>
<div class=bh>
<button title="اختر الآيات التي تريد مراجعتها، ثم اضغط على هذا الزر لبدء الاختبار بغير رؤيتها أولا." id="ok">ابدأ الاختبار</button>
<button title="اختر الآيات التي تريد مراجعتها، ثم اضغط على هذا الزر لرؤيتها قبل بدء الاختبار." id="show">اعرض الآيات</button>
</div>
</div>
<div hidden id="header">
<p id="title"></p>
<button title="اضغط لبدء مراجعة جديدة" id="new">جديد</button>&emsp;
<button title="اضغط لإعادة هذا التسميع من البداية" id="repeat">إعادة</button>
<div hidden id="header" class="bh">
<p id="title"></p>
<button title="اضغط لبدء اختبار جديد." id="new">تغيير الآيات</button>
<button title="اضغط لإعادة هذا الاختبار من البداية." id="repeat">إعادة</button>
<button title="اضغط لقراءة الآيات بالكامل." id="reshow">اعرض الآيات</button>
</div>
<p id="p"></p>
<p id="x"></p>
Expand Down
115 changes: 49 additions & 66 deletions .logic.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,15 @@

const unmark = (phrase) => phrase
// remove mosħaf formatting signs
.r(/\xa0\u06dd[٠-٩]+(?:\xa0\u06e9)?/, '') // ayah number & sajda if any
.r(/\u06de\xa0/, '') // start of rub el hizb if found
.r(/[\u06D6-\u06DC] /, '') // waqf signs
.r(/\u0305/g, '') // combining overline
.r(/^(.)\u0651/g, '$1') // initial shadda-of-idgham
// remove final tashkeel signs (except shadda)
.r(/[\u06e4-\u06e6]+$/g, '') // madd-monfasel & madd sela
.r(/\u06e1$/, '') // jazm (quranic sukun)
.r(/[\u064e-\u0650]$/, '') // fatha, damma, kasra
.r(/[\u064c\u064d]$/, '') // tanween {damm, kasr}
.r(/[\u08f1\u08f2]$/, '') // open tanween {damm, kasr}
.r(/\u064f\u06e2$/, '') // iqlab tanween damm
.r(/\u0650\u06ed$/, '') // iqlab tanween kasr
.r(/\u064b([اى]?)$/, '$1') // tanween fath
.r(/\u08f0([اى]?)$/, '$1') // open tanween fath
.r(/\u064e\u06e2([اى]?)$/, '$1') // iqlab tanween fath
.r(/\u064e([اى]?)$/, '$1') // just fath, before final alef (either kind), because of tanween (eg, إذا)
.r(/(ى)\u0670$/, '$1') // dagger alef from final alef maqsura (its existence depends on the first letter of the next word)

function phrasify_ayat (ayat) {
// equiv. to: return ayat.flatMap(a => a.split(/(?<=[\u06D6-\u06DC] |\n)/))
// but supports older-ish browsers that don't have flatMap nor lookbehind
// (that "\n" is for the added basmala before the beginning of almost all suar)
const ret = []
const arr = ayat.map(a => a.replace(/([\u06D6-\u06DC] |\n)/g, '$1X').split('X'))
for (let i = 0; i < arr.length; ++i) { ret.push(...arr[i]) }
return ret
}
const {
unmark,
phrasify,
repeat_title,
preview_format,
preview_put,
current_idx,
recite_init,
recite_done,
do_drop,
} = aayaat_logic

let int

Expand All @@ -39,10 +20,36 @@ function clear_board () {
el_endmsg.hidden = true
}

function init_board () {
if (int != null) { clearInterval(int); int = null }
el_repeat.innerText = 'إعادة'
el_repeat.title = 'اضغط لإعادة هذا الاختبار من البداية.'
el_repeat.dataset.goatcounterClick = 'repeat'
el_reshow.style.display = ''
el_header.classList.add('btn3')
//
el_endmsg.hidden = true
el_p.innerHTML = ''
el_x.innerHTML = ''
el_p.hidden = false
el_x.hidden = false
}

function preview (content) {
init_board()
el_repeat.innerText = 'ابدأ الاختبار'
el_repeat.title = 'ابدأ في ترتيب ' + repeat_title + '.'
el_repeat.dataset.goatcounterClick = 'start'
el_reshow.style.display = 'none'
el_header.classList.remove('btn3')
preview_put(content)
}

// WAIT is the time before showing a hint, in millisecond
// SHORT_WAIT is the time before hint of first phrase; should be 1/4 of WAIT
// MAX is the maximum number of phrases to show on one screen
// LIMIT is the maximum number of phrases (< MAX) before splitting into two, shuffled separately; should be odd and ~80% of MAX
// LIMIT is the maximum number of phrases (< MAX) before splitting into two,
// shuffled separately; should be odd and ~80% of MAX
const levels = [
{ MAX: 10, LIMIT: 7, WAIT: 20_000, SHORT_WAIT: 5_000 },
{ MAX: 18, LIMIT: 13, WAIT: 40_000, SHORT_WAIT: 10_000 },
Expand All @@ -51,11 +58,12 @@ const levels = [
{ MAX: 90, LIMIT: 71, WAIT:150_000, SHORT_WAIT: 37_500 },
]

function recite (ayat, title='', lvl=2) {
function recite (content, lvl=2) {

const { MAX, LIMIT, WAIT, SHORT_WAIT } = levels[lvl]
init_board()
recite_init(content)

const current = () => el_p.children.length
const { MAX, LIMIT, WAIT, SHORT_WAIT } = levels[lvl]

let time_start = now_ms()
let noplay_since = now_ms()
Expand Down Expand Up @@ -85,39 +93,23 @@ function recite (ayat, title='', lvl=2) {
if (Qall('.mh').length) { return } // if a mistakes hint is shown
// show hint one minute since last attempt, or 15 seconds since start if haven't played yet
const n = now_ms()
const c = current()
const c = current_idx()
if (n - noplay_since >= WAIT || n - time_start >= SHORT_WAIT && c === 0) {
Qid('w'+c).classList.add('th') /* time hint */
delayed[c] = true
}
}, 1000)

const teacher = el_teacher_input.checked

el_endmsg.hidden = true
el_x.innerHTML = ''
el_p.hidden = false
el_x.hidden = false

el_p.style.color = 'gray'
el_p.style.textAlign = 'center'
el_p.innerText = title
const clean_placeholder = () => {
el_p.style.color = ''
el_p.style.textAlign = ''
el_p.innerText = ''
}

const w = ayat.map(e => e.r(/[A-Z<>]/g, ''))

const words = phrasify_ayat(w)
const words = phrasify(content)
const final_count = words.length

const mistakes = []
const delayed = []
for (let i = 0; i < final_count; ++i) { mistakes[i] = 0; delayed[i] = false }

const done = () => {
if (int != null) { clearInterval(int); int = null }
//
const seen = new Set()
//
range(final_count).forEach(i => {
Expand All @@ -138,6 +130,7 @@ function recite (ayat, title='', lvl=2) {
})
//
el_endmsg.hidden = false
recite_done(content)
confetti.start(1200, 50, 150)
show_selectors()
setTimeout(() => el_ok.focus(), 500)
Expand All @@ -150,19 +143,13 @@ function recite (ayat, title='', lvl=2) {
phrase_mistakes.clear()
noplay_since = now_ms()
Qall('.mh, .th').forEach(e => e.classList.remove('mh', 'th')) // remove hints
if (idx === 0) { clean_placeholder() }
const w = Qid('w'+idx)
w.innerHTML = w.dataset.word
if (w.dataset.word.match(/\u06dd|\ufdfd/)) { audio.next(); audio.play() } // if basmala or end of ayah
w.draggable = false
w.classList.remove('hint')
el_p.append('\u200b', w) // zero width space, to allow a phrase to start on the next line, without additional spacing
do_drop(idx)
if (idx === final_count - 1) { done() } else { next_subset() }
}

const drop = (el) => {
const idx = +el.id.r(/^w/, '')
const c = current()
const c = current_idx()
if (idx === c) {
real_drop(c)
}
Expand Down Expand Up @@ -254,8 +241,4 @@ function recite (ayat, title='', lvl=2) {
ev.preventDefault()
drop(Qid('w'+ev.dataTransfer.getData('text/plain')))
}

audio.set_index(teacher ? 0 : -1)
if (teacher) { audio.play(0) }

}
11 changes: 10 additions & 1 deletion .process.pl
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
.audio.js
.search.js
.selectors.js
.quran.js
.logic.js
.lzma-d-min.js
.z.js
Expand All @@ -40,7 +41,7 @@

local $_ = slurp_stdin;

my @ids = m/id="([^"]+)"/g;
my @ids = (s/<!--.*?-->//gr) =~ /id="([^"]+)"/g;

## PROCESS & MINIFY

Expand Down Expand Up @@ -85,6 +86,8 @@
execute q' cat res/qaris | while read value; do read title; printf "<option value=%s>%s</option>" "$value" "$title"; done '
}sge;

s{(<button.*?)id="(.*?)"}{$1 data-goatcounter-click="$2" id="$2"}g;

# minify html
s/\s+</</g; # note: this changes the behavior of the html; I'm relying on that
s/&spc;/ /g; # for the rarely needed space that would otherwise be removed by the previous rule
Expand All @@ -94,6 +97,12 @@
s/\A //;
s/ \Z//;

s{<(?!svg|circle|line).*?>}{
$&
=~ s/(\b\w+)="([^\s"'`<>=]+)"/$1=$2/gr
=~ s/(\b\w+)=""/$1/gr
}sge;

# style
s{<<style>>}{
sprintf '<style>%s</style>',
Expand Down
102 changes: 102 additions & 0 deletions .quran.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
const aayaat_logic = (function () {

const unmark = (phrase) => phrase
// remove mosħaf formatting signs
.r(/\xa0\u06dd[٠-٩]+(?:\xa0\u06e9)?/, '') // ayah number & sajda if any
.r(/\u06de\xa0/, '') // start of rub el hizb if found
.r(/[\u06D6-\u06DC] /, '') // waqf signs
.r(/\u0305/g, '') // combining overline
.r(/^(.)\u0651/g, '$1') // initial shadda-of-idgham
// remove final tashkeel signs (except shadda)
.r(/[\u06e4-\u06e6]+$/g, '') // madd-monfasel & madd sela
.r(/\u06e1$/, '') // jazm (quranic sukun)
.r(/[\u064e-\u0650]$/, '') // fatha, damma, kasra
.r(/[\u064c\u064d]$/, '') // tanween {damm, kasr}
.r(/[\u08f1\u08f2]$/, '') // open tanween {damm, kasr}
.r(/\u064f\u06e2$/, '') // iqlab tanween damm
.r(/\u0650\u06ed$/, '') // iqlab tanween kasr
.r(/\u064b([اى]?)$/, '$1') // tanween fath
.r(/\u08f0([اى]?)$/, '$1') // open tanween fath
.r(/\u064e\u06e2([اى]?)$/, '$1') // iqlab tanween fath
.r(/\u064e([اى]?)$/, '$1') // just fath, before final alef (either kind), because of tanween (eg, إذا)
.r(/(ى)\u0670$/, '$1') // dagger alef from final alef maqsura (its existence depends on the first letter of the next word)

function phrasify ([ title, aayaat ]) {
// equiv. to: return ayat.flatMap(a => a.split(/(?<=[\u06D6-\u06DC] |\n)/))
// but supports older-ish browsers that don't have flatMap nor lookbehind
// (that "\n" is for the added basmala before the beginning of almost all suar)
const ret = []
const arr = aayaat.map(a => a.replace(/([\u06D6-\u06DC] |\n)/g, '$1X').split('X'))
for (let i = 0; i < arr.length; ++i) { ret.push(...arr[i]) }
return ret
}

const set_placeholder = (title) => {
el_p.style.color = 'gray'
el_p.style.textAlign = 'center'
el_p.innerText = title
}

const clear_placeholder = () => {
el_p.style.color = ''
el_p.style.textAlign = ''
el_p.innerText = ''
}

const repeat_title = 'عبارات الآيات'
const preview_format = (a) => a.join(' ')
.r(/\ufdfd\n/g, '<center>\ufdfd</center>') // center basmala (but not in al-fateha)
.r(/(\u06dd٧٥) (\u06de\xa0بَرَاۤءَةࣱ)/, '$1\n$2') // if previewing sura 8 and sura 9, add a line break between them (b/c there is no basmala)
const preview_put = ([ title, a ]) => {
clear_placeholder()
el_p.append(make_elem('p', { id: 't', innerHTML: preview_format(a) }))
}

const current_idx = () => el_p.children.length

const recite_init = ([ title, aayaat ]) => {
set_placeholder(title)
//
const teacher = el_teacher_input.checked
audio.set_index(teacher ? 0 : -1)
if (teacher) { audio.play(0) }
}

const do_drop = (idx) => {
const w = Qid('w'+idx)
if (idx === 0) { clear_placeholder() }
const word = w.dataset.word
w.innerHTML = word
if (word.match(/\u06dd|\ufdfd/)) { audio.next(); audio.play() } // if basmala or end of ayah
w.draggable = false
w.classList.remove('hint')
if (word === '\ufdfd\n') { // basmala: center it on a line by its own (but not in al-fateha)
const center = make_elem('center')
center.append(w)
el_p.appendChild(center)
}
else {
el_p.append(
word.startsWith('\u06de\xa0بَ') && idx >= 1 // start of sura 9 after sura 8?
? '\n' // force a line break between the end of sura 8 and the beginning of sura 9, in place of the non-existent basmala.
: '\u200b', // zero width space, to allow a phrase to start on the next line, without additional spacing
w)
}
}


const recite_done = () => {}

return {
unmark,
phrasify,
repeat_title,
preview_format,
preview_put,
current_idx,
recite_init,
recite_done,
do_drop,
}

})()
Loading

0 comments on commit 6cbedc3

Please sign in to comment.