331 lines
20 KiB
Plaintext
331 lines
20 KiB
Plaintext
---
|
||
// fonts are self-hosted (no Google Fonts request; works with Brave Shields and offline)
|
||
import '@fontsource/noto-naskh-arabic/400.css';
|
||
import '@fontsource/noto-naskh-arabic/700.css';
|
||
import '@fontsource/noto-nastaliq-urdu/400.css';
|
||
import '@fontsource/noto-nastaliq-urdu/700.css';
|
||
import '../styles/global.css';
|
||
interface Props { title?: string; description?: string; q?: string }
|
||
const { title, description = 'اردو کی کلاسیکی شاعری اور نثر', q = '' } = Astro.props;
|
||
const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو کی کلاسیکی شاعری اور نثر';
|
||
---
|
||
<!doctype html>
|
||
<html lang="ur-PK" dir="rtl">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>{fullTitle}</title>
|
||
<meta name="description" content={description} />
|
||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||
<script is:inline>
|
||
// font + theme choice applied before paint (Naskh and system theme by default)
|
||
try {
|
||
// ?font=nastaliq|naskh in the URL sets the choice (shareable links)
|
||
const f = new URLSearchParams(location.search).get('font');
|
||
if (f === 'nastaliq' || f === 'naskh') localStorage.setItem('divan-font', f);
|
||
if (localStorage.getItem('divan-font') === 'nastaliq') document.documentElement.classList.add('nastaliq');
|
||
const t = localStorage.getItem('divan-theme');
|
||
if (t) document.documentElement.dataset.theme = t;
|
||
} catch (e) {}
|
||
</script>
|
||
</head>
|
||
<body>
|
||
<header class="site-header">
|
||
<div class="wrap">
|
||
<a class="wordmark" href="/">دیوان</a>
|
||
<form class="search-form" action="/search" method="get" role="search">
|
||
<input type="search" name="q" id="q" value={q} placeholder="شعر، لفظ یا عبارت تلاش کریں" aria-label="تلاش" autocomplete="off" />
|
||
<button type="button" id="rom-toggle" class="in-box" aria-label="رومن حروف سے اردو" aria-pressed="false" title="رومن حروف سے اردو ٹائپنگ (dil → دل)">اب</button>
|
||
<button type="button" id="kb-toggle" class="in-box" aria-label="اردو کی بورڈ" aria-pressed="false" title="اردو کی بورڈ">
|
||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true"><rect x="2" y="6" width="20" height="12" rx="2"/><path d="M6 10h.01M10 10h.01M14 10h.01M18 10h.01M7 14h10" stroke-linecap="round" stroke-width="2"/></svg>
|
||
</button>
|
||
</form>
|
||
<div class="toggles">
|
||
{Astro.locals.user
|
||
? <a class="account-link" href="/account" title={Astro.locals.user.email}>اکاؤنٹ</a>
|
||
: <a class="account-link" href={`/signin?next=${encodeURIComponent(Astro.url.pathname)}`}>لاگ ان</a>}
|
||
<button type="button" id="font-toggle" aria-label="خط بدلیں">نسخ</button>
|
||
<button type="button" id="theme-toggle" aria-label="روشن یا تاریک">◐</button>
|
||
</div>
|
||
</div>
|
||
<div id="kb" class="wrap kb" hidden></div>
|
||
</header>
|
||
<main class="wrap"><slot /></main>
|
||
<div id="selmenu" class="selmenu" role="toolbar" aria-label="منتخب عبارت" hidden>
|
||
<button type="button" data-act="meaning" title="معنی اور تلفظ">معنی</button>
|
||
<button type="button" data-act="search" title="دیوان میں تلاش">تلاش</button>
|
||
<button type="button" data-act="copy" title="حوالے کے ساتھ کاپی">کاپی</button>
|
||
<button type="button" data-act="share" title="اس عبارت کا لنک">شیئر</button>
|
||
<button type="button" data-act="google" title="گوگل میں تلاش">گوگل</button>
|
||
</div>
|
||
<aside id="dict" class="dict" hidden aria-live="polite">
|
||
<button type="button" class="dict-close" aria-label="بند کریں">✕</button>
|
||
<div class="dict-body"></div>
|
||
</aside>
|
||
<footer class="site-footer">
|
||
<div class="wrap">
|
||
متن: <a href="https://ur.wikisource.org">ویکی ماخذ</a> (عوامی ملکیت) · تعارف: <a href="https://ur.wikipedia.org">ویکیپیڈیا</a> ·
|
||
<a href="https://github.com/anas-rashid/divan-data">ڈیٹا</a> · <a href="https://github.com/anas-rashid/divan">کوڈ</a>
|
||
</div>
|
||
</footer>
|
||
<script is:inline>
|
||
const root = document.documentElement;
|
||
const fontBtn = document.getElementById('font-toggle');
|
||
const label = () => (fontBtn.textContent = root.classList.contains('nastaliq') ? 'نستعلیق' : 'نسخ');
|
||
label();
|
||
fontBtn.onclick = () => {
|
||
root.classList.toggle('nastaliq');
|
||
try { localStorage.setItem('divan-font', root.classList.contains('nastaliq') ? 'nastaliq' : 'naskh'); } catch (e) {}
|
||
label();
|
||
};
|
||
// Urdu on-screen keyboard, Gboard/Windows Urdu layout
|
||
const BASE = [[...'طصھدٹپتبجح'], [...'مورنلہاکی'], [...'قفےسشغع']];
|
||
const SHIFT = [['ظ','ض','ذ','ڈ','ث','ّ','ۃ','ـ','چ','خ'], ['ژ','ز','ڑ','ں','ۂ','ء','آ','گ',''], ['','','ۓ','','ؤ','ئ','']];
|
||
// Roman -> Urdu (phonetic) typing, its own toggle
|
||
const PHONETIC = {
|
||
q:'ق', w:'و', e:'ع', r:'ر', t:'ت', y:'ے', u:'ء', i:'ی', o:'ہ', p:'پ',
|
||
a:'ا', s:'س', d:'د', f:'ف', g:'گ', h:'ح', j:'ج', k:'ک', l:'ل',
|
||
z:'ز', x:'ش', c:'چ', v:'ط', b:'ب', n:'ن', m:'م',
|
||
A:'آ', T:'ٹ', D:'ڈ', R:'ڑ', N:'ں', Z:'ذ', S:'ص', G:'غ', H:'ھ', K:'خ', C:'ث', V:'ظ', X:'ژ', J:'ض', I:'ئ', O:'ۂ', Y:'ؤ',
|
||
',':'،', '.':'۔', '?':'؟', ';':'؛',
|
||
};
|
||
const q = document.getElementById('q'), kb = document.getElementById('kb'), kbBtn = document.getElementById('kb-toggle'), romBtn = document.getElementById('rom-toggle');
|
||
let shift = false;
|
||
const key = (ch) => ch ? `<button type="button" data-ch="${ch}">${ch}</button>` : '';
|
||
const render = () => {
|
||
const L = shift ? SHIFT : BASE;
|
||
kb.innerHTML = `<div>${L[0].map((c) => key(c)).join('')}</div><div>${L[1].map((c) => key(c)).join('')}</div>` +
|
||
`<div><button type="button" data-act="shift" class="fn${shift ? ' on' : ''}" aria-label="شفٹ">⇧</button>${L[2].map((c) => key(c)).join('')}` +
|
||
'<button type="button" data-act="bs" class="fn" aria-label="مٹائیں">⌫</button></div>' +
|
||
`<div>${key('،')}<button type="button" data-ch=" " class="wide">فاصلہ</button>${key('۔')}${key('؟')}</div>`;
|
||
};
|
||
render();
|
||
const insert = (text) => {
|
||
const s = q.selectionStart ?? q.value.length, e = q.selectionEnd ?? q.value.length;
|
||
q.setRangeText(text, s, e, 'end'); q.focus();
|
||
};
|
||
kb.onmousedown = (ev) => ev.preventDefault(); // keep focus/caret in the search box
|
||
kb.onclick = (ev) => {
|
||
const b = ev.target.closest('button'); if (!b) return;
|
||
if (b.dataset.act === 'shift') { shift = !shift; render(); return; }
|
||
if (b.dataset.act === 'bs') {
|
||
const s = q.selectionStart, e = q.selectionEnd;
|
||
q.setRangeText('', s === e ? Math.max(0, s - 1) : s, e, 'end'); q.focus();
|
||
return;
|
||
}
|
||
insert(b.dataset.ch);
|
||
if (shift) { shift = false; render(); } // one-shot shift, as on Gboard
|
||
};
|
||
kbBtn.onclick = () => {
|
||
kb.hidden = !kb.hidden; kbBtn.setAttribute('aria-pressed', String(!kb.hidden)); q.focus();
|
||
};
|
||
romBtn.onclick = () => { romBtn.setAttribute('aria-pressed', String(romBtn.getAttribute('aria-pressed') !== 'true')); q.focus(); };
|
||
q.addEventListener('keydown', (ev) => {
|
||
if (romBtn.getAttribute('aria-pressed') !== 'true' || ev.ctrlKey || ev.metaKey || ev.altKey) return;
|
||
const ch = PHONETIC[ev.key];
|
||
if (ch) { ev.preventDefault(); insert(ch); }
|
||
});
|
||
|
||
// word sidebar: select one word in the text -> Wiktionary meanings and pronunciation (data shown as text only)
|
||
const dict = document.getElementById('dict'), dictBody = dict.querySelector('.dict-body'), main = document.querySelector('main');
|
||
const LANG = { ur: 'اردو', fa: 'فارسی', ar: 'عربی' };
|
||
const POS = { Noun: 'اسم', 'Proper noun': 'اسم معرفہ', Verb: 'فعل', Adjective: 'صفت', Adverb: 'متعلق فعل', Pronoun: 'ضمیر',
|
||
Postposition: 'حرف جار', Preposition: 'حرف جار', Conjunction: 'حرف عطف', Interjection: 'حرف ندا', Particle: 'حرف',
|
||
Translation: 'ترجمہ (اردو ویکی لغت)', Phrase: 'فقرہ', Proverb: 'کہاوت', Name: 'اسم معرفہ' };
|
||
const el = (tag, cls, text, attrs) => {
|
||
const e = document.createElement(tag);
|
||
if (cls) e.className = cls;
|
||
if (text != null) e.textContent = text;
|
||
for (const k in attrs ?? {}) e.setAttribute(k, attrs[k]);
|
||
return e;
|
||
};
|
||
let current = '';
|
||
async function showWord(w) {
|
||
if (w === current && !dict.hidden) return;
|
||
current = w; dict.hidden = false; document.body.classList.add('dict-open');
|
||
dictBody.replaceChildren(el('h2', null, w), el('p', 'muted', 'تلاش جاری ہے…'));
|
||
let d = null;
|
||
// v: bump when the response format changes (responses are browser-cached for a day)
|
||
try { const r = await fetch('/api/word?v=6&w=' + encodeURIComponent(w)); if (r.ok) d = await r.json(); } catch (e) {}
|
||
if (w !== current) return; // a newer selection won
|
||
const buttons = (items) => { // clickable words
|
||
const box = el('div', 'similar');
|
||
for (const s of items) {
|
||
const b = el('button', null, s.title, { type: 'button', title: s.langs.map((c) => LANG[c]).join('، ') });
|
||
b.onclick = () => showWord(s.title);
|
||
box.append(b);
|
||
}
|
||
return box;
|
||
};
|
||
const words = (title, items, into) => { if (items.length) into.push(el('h3', null, title), buttons(items)); };
|
||
const list = (items, cls, attrs) => { const ol = el('ol', cls, null, attrs); items.forEach((x) => ol.append(el('li', null, x))); return ol; };
|
||
const out = [el('h2', null, w)];
|
||
if (!d || !d.found) {
|
||
out.push(el('p', 'muted', 'ویکی لغت میں یہ لفظ نہیں ملا۔'));
|
||
words('ملتے جلتے الفاظ', [...(d?.similar ?? []), ...(d?.variants ?? [])], out);
|
||
dictBody.replaceChildren(...out);
|
||
return;
|
||
}
|
||
// one section per language, in order اردو، فارسی، عربی; each says where its meanings come from.
|
||
// A Persian or Arabic word is explained first by its Urdu translations, then English, then its own language.
|
||
for (const l of d.langs) {
|
||
const sec = el('section', 'dict-lang');
|
||
sec.append(el('h3', null, LANG[l.code]));
|
||
// collapsible extra (more meanings, examples, long etymologies, word lists)
|
||
const more = (label, ...children) => {
|
||
const d = el('details', 'more');
|
||
d.append(el('summary', null, label), ...children);
|
||
return d;
|
||
};
|
||
const tagged = (tags) => (tags?.length ? ` (${tags.join(', ')})` : '');
|
||
// a pronunciation line: vowelled form, transliteration, every IPA (with dialect labels), every recording
|
||
const pron = (r) => {
|
||
const pr = el('p', 'pron');
|
||
if (r.form) pr.append(el('span', 'form', r.form), ' ');
|
||
if (r.tr) pr.append(el('bdi', 'en', r.tr));
|
||
r.ipa.forEach((i) => pr.append(' ', el('bdi', 'ipa', i.ipa + tagged(i.tags), { title: i.tags.join(', ') })));
|
||
r.audio.forEach((a) => {
|
||
if (!a.url.startsWith('https://upload.wikimedia.org/')) return;
|
||
const b = el('button', 'play', '🔊', { type: 'button', 'aria-label': 'تلفظ سنیں', title: a.tags.join(', ') });
|
||
b.onclick = () => new Audio(a.url).play();
|
||
pr.append(' ', b);
|
||
});
|
||
return pr;
|
||
};
|
||
// one meaning: gloss with labels; usage examples (text, romanisation, translation) folded
|
||
const sense = (x) => {
|
||
const li = el('li', null, x.gloss + tagged(x.tags));
|
||
if (x.examples.length) {
|
||
const ex = x.examples.map((e) => {
|
||
const p = el('p', 'example');
|
||
p.append(el('span', 'ex-text', e.text, { dir: 'auto' }));
|
||
if (e.roman) p.append(el('br'), el('i', null, e.roman));
|
||
if (e.english) p.append(el('br'), e.english);
|
||
return p;
|
||
});
|
||
li.append(more(`مثالیں (${x.examples.length})`, ...ex));
|
||
}
|
||
return li;
|
||
};
|
||
const senses = (defs) => {
|
||
const ol = el('ol', 'en', null, { dir: 'ltr', lang: 'en' });
|
||
defs.slice(0, 4).forEach((x) => ol.append(sense(x)));
|
||
if (defs.length <= 4) return [ol];
|
||
const rest = el('ol', 'en', null, { dir: 'ltr', lang: 'en', start: '5' });
|
||
defs.slice(4).forEach((x) => rest.append(sense(x)));
|
||
return [ol, more(`مزید معانی (${defs.length - 4})`, rest)];
|
||
};
|
||
const urdu = () => {
|
||
if (!l.urdu?.length) return;
|
||
sec.append(el('p', 'pos', 'اردو میں · انگریزی ویکی لغت کے واسطے سے'));
|
||
for (const e of l.urdu) {
|
||
const p = el('p', 'via');
|
||
p.append(e.urdu.join('، '), ' ', el('bdi', 'en muted', `(${e.gloss})`));
|
||
sec.append(p);
|
||
}
|
||
};
|
||
// readings: the same spelling with different short vowels (ملک: مُلک، مَلِک، مِلک), each with its meanings
|
||
const english = () => {
|
||
if (l.readings.length > 1) sec.append(el('p', 'pos', `${l.readings.length} قراءتیں (حرکات کے فرق سے)`));
|
||
for (const r of l.readings) {
|
||
const box = el('div', l.readings.length > 1 ? 'reading' : null);
|
||
box.append(pron(r));
|
||
for (const s of r.senses) box.append(el('p', 'pos', `انگریزی میں · ${POS[s.pos.charAt(0).toUpperCase() + s.pos.slice(1)] ?? s.pos}`), ...senses(s.defs));
|
||
sec.append(box);
|
||
}
|
||
if (l.translation.length) sec.append(el('p', 'pos', POS.Translation), list(l.translation, 'en', { dir: 'ltr', lang: 'en' }));
|
||
};
|
||
if (l.code !== 'ur') { urdu(); english(); }
|
||
if (l.meanings.length) {
|
||
const label = el('p', 'pos');
|
||
label.append(`معانی: ${LANG[l.code]} ویکی لغت` + (l.origin ? ` · اصل: ${l.origin}` : '') + ' · ');
|
||
if (l.source) label.append(el('a', null, 'ماخذ', { href: l.source, target: '_blank', rel: 'noopener' }));
|
||
const own = list(l.meanings.slice(0, 6), null, { lang: l.code });
|
||
sec.append(label, own);
|
||
if (l.meanings.length > 6) sec.append(more(`مزید معانی (${l.meanings.length - 6})`, list(l.meanings.slice(6), null, { lang: l.code, start: '7' })));
|
||
}
|
||
if (l.code === 'ur') { english(); urdu(); }
|
||
for (const [label, items] of [['مترادفات', l.synonyms], ['مشتقات', l.derived], ['متعلقہ الفاظ', l.related]]) {
|
||
if (!items.length) continue;
|
||
const box = buttons(items.map((t) => ({ title: t, langs: [l.code] })));
|
||
sec.append(...(items.length > 8 ? [more(`${label} (${items.length})`, box)] : [el('p', 'pos', label), box]));
|
||
}
|
||
for (const ety of l.etymologies) {
|
||
const text = el('p', 'en ety', ety, { dir: 'ltr', lang: 'en' });
|
||
sec.append(...(ety.length > 220 ? [more('اشتقاق', text)] : [el('p', 'pos', 'اشتقاق'), text]));
|
||
}
|
||
if (l.en) {
|
||
const p = el('p', 'pos');
|
||
p.append(el('a', null, 'انگریزی ویکی لغت', { href: l.en, target: '_blank', rel: 'noopener' }));
|
||
sec.append(p);
|
||
}
|
||
out.push(sec);
|
||
}
|
||
words('ملتے جلتے الفاظ · دیگر حروفِ علت', d.variants ?? [], out); // same consonants, other long vowels
|
||
const src = el('p', 'src muted');
|
||
src.append('ویکی لغت (Wiktionary) · ');
|
||
src.append('CC BY-SA');
|
||
dictBody.replaceChildren(...out, src);
|
||
}
|
||
const TRIM = /["'«»()\[\]،۔؟؛!:.,\-–—_]+|^\s+|\s+$/g; // punctuation and dashes anywhere, outer spaces
|
||
// selection in the reading text: Ganjoor-style menu by the selection; a single word also opens the sidebar
|
||
const menu = document.getElementById('selmenu');
|
||
const oneWord = (t) => { const w = t.replace(TRIM, '').replace(/\u0614/g, ''); return /^[\p{Script=Arabic}\p{M}\u200C]{1,40}$/u.test(w) ? w : null; };
|
||
let selText = '', selTimer;
|
||
const hideMenu = () => { menu.hidden = true; };
|
||
document.addEventListener('selectionchange', () => {
|
||
clearTimeout(selTimer);
|
||
selTimer = setTimeout(() => {
|
||
const sel = getSelection();
|
||
const inText = sel && !sel.isCollapsed && main.contains(sel.anchorNode) && !sel.anchorNode.parentElement?.closest('form, input, textarea');
|
||
selText = inText ? sel.toString().replace(/\u0614/g, '').trim() : '';
|
||
if (!selText) return hideMenu();
|
||
const w = oneWord(selText);
|
||
menu.querySelector('[data-act=meaning]').hidden = !w;
|
||
menu.hidden = false;
|
||
// below the selection, centred, kept inside the viewport (absolute: moves with the page)
|
||
const r = sel.getRangeAt(0).getBoundingClientRect(), m = menu.getBoundingClientRect();
|
||
menu.style.top = `${r.bottom + scrollY + 8}px`;
|
||
menu.style.left = `${Math.max(8, Math.min(innerWidth - m.width - 8, r.left + r.width / 2 - m.width / 2)) + scrollX}px`;
|
||
if (w) showWord(w);
|
||
}, 350);
|
||
});
|
||
menu.onmousedown = (e) => e.preventDefault(); // keep the selection
|
||
const cite = () => { // "text\n— poet، title\nlink" for copying
|
||
const art = document.querySelector('[data-cite]');
|
||
return `${selText}\n— ${art?.dataset.cite ?? document.title}\n${location.origin}${location.pathname}`;
|
||
};
|
||
const flash = (b, msg) => { const t = b.textContent; b.textContent = msg; setTimeout(() => (b.textContent = t), 1200); };
|
||
menu.onclick = async (e) => {
|
||
const b = e.target.closest('button'); if (!b) return;
|
||
const q = /\s/.test(selText) ? `"${selText}"` : selText, enc = encodeURIComponent(selText);
|
||
switch (b.dataset.act) {
|
||
case 'meaning': showWord(oneWord(selText)); break;
|
||
case 'search': location.href = `/search?q=${encodeURIComponent(q)}`; break;
|
||
case 'google': open(`https://www.google.com/search?q=${enc}`, '_blank', 'noopener'); break;
|
||
case 'copy':
|
||
try { await navigator.clipboard.writeText(cite()); flash(b, 'کاپی ہو گیا'); } catch (err) { flash(b, 'ناکام'); }
|
||
break;
|
||
case 'share': { // this page with the selection highlighted (?hl=), as search results link
|
||
const url = `${location.origin}${location.pathname}?hl=${encodeURIComponent(q)}`;
|
||
if (navigator.share) navigator.share({ title: document.title, text: selText, url }).catch(() => {});
|
||
else try { await navigator.clipboard.writeText(url); flash(b, 'لنک کاپی ہو گیا'); } catch (err) { flash(b, 'ناکام'); }
|
||
}
|
||
}
|
||
};
|
||
const closeDict = () => { dict.hidden = true; current = ''; document.body.classList.remove('dict-open'); };
|
||
dict.querySelector('.dict-close').onclick = closeDict;
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key !== 'Escape') return;
|
||
if (!menu.hidden) hideMenu(); else if (!dict.hidden) closeDict();
|
||
});
|
||
|
||
document.getElementById('theme-toggle').onclick = () => {
|
||
const dark = root.dataset.theme ? root.dataset.theme === 'dark' : matchMedia('(prefers-color-scheme: dark)').matches;
|
||
root.dataset.theme = dark ? 'light' : 'dark';
|
||
try { localStorage.setItem('divan-theme', root.dataset.theme); } catch (e) {}
|
||
};
|
||
</script>
|
||
</body>
|
||
</html>
|