- Reading mode (the book button in a poem's, book's or poet's action row): the text, the breadcrumbs, the poet's name and the logo; no search, icons, buttons, couplet tools, tags or footer; larger verse. Kept across pages until the ✕ in the header; switched on before paint (no flash); only on readable pages, so search and tools are unaffected - For Safari Reader, Firefox Reader View and Chrome's reading mode: a poem is one <article> (title, poet, text), with <meta name="author"> and schema.org CreativeWork data - Printing shows the text and its source only Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
558 lines
39 KiB
Plaintext
558 lines
39 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; author?: string; jsonld?: object }
|
||
// author and jsonld describe a work for search engines and the browser's own reader view
|
||
const { title, description = 'اردو کا کلاسیکی ادب', q = '', author, jsonld } = Astro.props;
|
||
const fullTitle = title ? `${title} · دیوان` : 'دیوان · اردو کا کلاسیکی ادب';
|
||
import Icon from '../components/Icon.astro';
|
||
import logo from '../../public/logo/turanj.svg?raw'; // inline, so the page can turn its rays
|
||
---
|
||
<!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" />
|
||
<link rel="icon" href="/favicon-32.png" type="image/png" sizes="32x32" />
|
||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||
<link rel="manifest" href="/site.webmanifest" />
|
||
<meta name="theme-color" content="#fbf6ea" media="(prefers-color-scheme: light)" />
|
||
<meta name="theme-color" content="#14120f" media="(prefers-color-scheme: dark)" />
|
||
<!-- link previews (WhatsApp, Facebook, X, Slack): the logo and headline -->
|
||
<meta property="og:site_name" content="دیوان" />
|
||
<meta property="og:type" content="website" />
|
||
<meta property="og:locale" content="ur_PK" />
|
||
<meta property="og:title" content={fullTitle} />
|
||
<meta property="og:description" content={description} />
|
||
<meta property="og:url" content={new URL(Astro.url.pathname, Astro.url.origin).href} />
|
||
<meta property="og:image" content={new URL('/og-image.png', Astro.url.origin).href} />
|
||
<meta property="og:image:width" content="1200" />
|
||
<meta property="og:image:height" content="630" />
|
||
<meta property="og:image:alt" content="دیوان: اردو کا کلاسیکی ادب" />
|
||
<meta name="twitter:card" content="summary_large_image" />
|
||
{author && <meta name="author" content={author} />}
|
||
{jsonld && <script type="application/ld+json" set:html={JSON.stringify(jsonld).replace(/</g, '\\u003c')} />}
|
||
<script is:inline>
|
||
// font, palette and light/dark applied before paint (Naskh, sepia and the system's light/dark 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;
|
||
const pal = localStorage.getItem('divan-palette'); // sepia when unset
|
||
if (pal) document.documentElement.dataset.palette = pal;
|
||
if (document.cookie.includes('divan-digits=latn')) document.documentElement.dataset.digits = 'latn'; // Western numerals
|
||
if (localStorage.getItem('divan-reading')) document.documentElement.dataset.reading = ''; // reading mode, kept across pages
|
||
} catch (e) {}
|
||
</script>
|
||
</head>
|
||
<body data-user={Astro.locals.user ? '1' : undefined}>
|
||
<header class="site-header">
|
||
<div class="wrap">
|
||
<a class="wordmark" href="/" title="سرورق"><Fragment set:html={logo.replace('<svg ', '<svg class="logo" ')} /></a>
|
||
<form class="search-form" action="/search" method="get" role="search">
|
||
<input type="search" name="q" id="q" data-urdu value={q} placeholder="شعر، لفظ یا عبارت تلاش کریں" aria-label="تلاش" autocomplete="off" />
|
||
<button type="button" id="rom-toggle" class="in-box rom" 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>
|
||
<button type="submit" class="in-box go" aria-label="تلاش کریں" title="تلاش کریں">
|
||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="10.5" cy="10.5" r="6.5"/><path d="m15.5 15.5 5 5"/></svg>
|
||
</button>
|
||
</form>
|
||
<button type="button" class="icon-btn reading-exit" data-reading-toggle title="مطالعے کے انداز سے باہر" aria-label="مطالعے کے انداز سے باہر"><Icon name="close" /></button>
|
||
<nav class="toggles" aria-label="سائٹ">
|
||
{Astro.locals.user?.role === 'admin' && <a class="icon-btn" href="/admin" title="ایڈمن" aria-label="ایڈمن"><Icon name="admin" /></a>}
|
||
{['mod-l2', 'mod-l1', 'admin'].includes(Astro.locals.user?.role ?? '') && <a class="icon-btn" href="/mod" title="موڈریشن" aria-label="موڈریشن"><Icon name="edit" /></a>}
|
||
{Astro.locals.user && <a class="icon-btn" href="/library" title="محفوظ شدہ" aria-label="محفوظ شدہ"><Icon name="bookmark" /></a>}
|
||
{Astro.locals.user && <a class="icon-btn" href="/notes" title="میرے نوٹس" aria-label="میرے نوٹس"><Icon name="note" /></a>}
|
||
{Astro.locals.user && <a class="icon-btn" href="/words" title="ذخیرۂ الفاظ" aria-label="ذخیرۂ الفاظ"><span class="glyph-icon"><Icon name="words" /><span aria-hidden="true">ع</span></span></a>}
|
||
{Astro.locals.user
|
||
? <a class="icon-btn" href="/account" title={`اکاؤنٹ: ${Astro.locals.user.full_name || Astro.locals.user.email}`} aria-label="اکاؤنٹ"><Icon name="user" /></a>
|
||
: <a class="icon-btn" href={`/signin?next=${encodeURIComponent(Astro.url.pathname)}`} title="لاگ ان" aria-label="لاگ ان"><Icon name="login" /></a>}
|
||
<details class="display-menu">
|
||
<summary class="icon-btn" title="خط اور رنگ" aria-label="خط اور رنگ"><Icon name="settings" /></summary>
|
||
<div class="display-panel">
|
||
<p>خط</p>
|
||
<div class="seg"><button type="button" data-font="naskh">نسخ</button><button type="button" data-font="nastaliq" class="f-nastaliq">نستعلیق</button></div>
|
||
<p>رنگ</p>
|
||
<div class="swatches">
|
||
{[['', 'سپیا', '#fbf6ea', '#bf3f2a'], ['teal', 'فیروزی', '#e2f0ee', '#0b4f6c'], ['lapis', 'نیلگوں', '#e6ebf8', '#1d3b8b'],
|
||
['green', 'سبز', '#e5eede', '#1f5d3a'], ['rose', 'گلابی', '#f5e6ea', '#6b2c5e']].map(([v, name, a, b]) => (
|
||
<button type="button" data-palette={v} title={name} aria-label={name} style={`--a: ${a}; --b: ${b}`}><span></span>{name}</button>
|
||
))}
|
||
</div>
|
||
<p>ہندسے</p>
|
||
<div class="seg"><button type="button" data-digits="">۱۲۳ مشرقی</button><button type="button" data-digits="latn">123 مغربی</button></div>
|
||
<p>روشنی</p>
|
||
<div class="seg">
|
||
<button type="button" data-theme-pick="light"><Icon name="sun" size={16} /> روشن</button>
|
||
<button type="button" data-theme-pick="dark"><Icon name="moon" size={16} /> تاریک</button>
|
||
<button type="button" data-theme-pick=""><Icon name="theme" size={16} /> خودکار</button>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
{Astro.locals.user && (
|
||
<form method="post" action="/account" class="inline-form">
|
||
<input type="hidden" name="act" value="signout" />
|
||
<button class="icon-btn danger" title="لاگ آؤٹ" aria-label="لاگ آؤٹ"><Icon name="logout" /></button>
|
||
</form>
|
||
)}
|
||
</nav>
|
||
</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="saveword" class="plus" title="ذخیرۂ الفاظ میں ڈالیں">ذخیرۂ الفاظ</button>
|
||
<button type="button" data-act="bookmark" title="یہ عبارت محفوظ کریں">محفوظ کریں</button>
|
||
<button type="button" data-act="note" 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 class="footer-logo" href="/" title="سرورق"><img src="/logo/turanj.svg" width="56" height="56" alt="دیوان" /></a>
|
||
<nav class="footer-links" aria-label="سائٹ کے بارے میں">
|
||
<div><h2>دیوان</h2><ul>
|
||
<li><a href="/about"><Icon name="info" size={18} /><span>دیوان کے بارے میں<small>مقصد اور طریقۂ کار</small></span></a></li>
|
||
<li><a href="/tags"><Icon name="tag" size={18} /><span>ٹیگ<small>موضوعات سے کلام</small></span></a></li>
|
||
</ul></div>
|
||
<div><h2>ماخذ</h2><ul>
|
||
<li><a href="https://ur.wikisource.org" rel="noopener"><Icon name="dict" size={18} /><span>ویکی ماخذ<small>متن · عوامی ملکیت</small></span><Icon name="external" size={13} class="ext" /></a></li>
|
||
<li><a href="https://ur.wikipedia.org" rel="noopener"><Icon name="globe" size={18} /><span>ویکیپیڈیا<small>شعرا کے تعارف</small></span><Icon name="external" size={13} class="ext" /></a></li>
|
||
</ul></div>
|
||
<div><h2>کھلا منبع</h2><ul>
|
||
<li><a href="https://github.com/anas-rashid/divan-data" rel="noopener"><Icon name="database" size={18} /><span>ڈیٹا<small>تمام متن، آزادانہ</small></span><Icon name="external" size={13} class="ext" /></a></li>
|
||
<li><a href="https://github.com/anas-rashid/divan" rel="noopener"><Icon name="code" size={18} /><span>کوڈ<small>سائٹ کا سورس</small></span><Icon name="external" size={13} class="ext" /></a></li>
|
||
</ul></div>
|
||
</nav>
|
||
<p class="made-in">
|
||
<svg class="flag" viewBox="0 0 30 20" width="27" height="18" role="img" aria-label="پاکستان کا پرچم">
|
||
<rect width="30" height="20" fill="#fff" /><rect x="7.5" width="22.5" height="20" fill="#01411c" />
|
||
<circle cx="19.2" cy="10" r="6" fill="#fff" /><circle cx="20.6" cy="8.7" r="5.4" fill="#01411c" />
|
||
<polygon fill="#fff" points="23.65,5.99 23.12,7.39 24.25,8.39 22.75,8.32 22.15,9.69 21.75,8.25 20.26,8.11 21.51,7.28 21.19,5.82 22.36,6.75" />
|
||
</svg>
|
||
ایک پاکستانی کی طرف سے، پاکستان کے لیے
|
||
</p>
|
||
</div>
|
||
</footer>
|
||
<script is:inline>
|
||
const root = document.documentElement;
|
||
// display settings: reading font, colour palette, light/dark/automatic; each choice is remembered
|
||
const panel = document.querySelector('.display-menu');
|
||
const store = (k, v) => { try { v ? localStorage.setItem(k, v) : localStorage.removeItem(k); } catch (e) {} };
|
||
const marks = () => {
|
||
const on = { font: root.classList.contains('nastaliq') ? 'nastaliq' : 'naskh', palette: root.dataset.palette ?? '', theme: root.dataset.theme ?? '' };
|
||
panel.querySelectorAll('[data-font]').forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.font === on.font)));
|
||
panel.querySelectorAll('[data-palette]').forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.palette === on.palette)));
|
||
panel.querySelectorAll('[data-theme-pick]').forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.themePick === on.theme)));
|
||
panel.querySelectorAll('[data-digits]').forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.digits === (root.dataset.digits ?? ''))));
|
||
};
|
||
panel.addEventListener('click', (e) => {
|
||
const b = e.target.closest('button'); if (!b) return;
|
||
if (b.dataset.font) { root.classList.toggle('nastaliq', b.dataset.font === 'nastaliq'); store('divan-font', b.dataset.font); }
|
||
if (b.dataset.palette != null) { if (b.dataset.palette) root.dataset.palette = b.dataset.palette; else delete root.dataset.palette; store('divan-palette', b.dataset.palette); }
|
||
if (b.dataset.digits != null) {
|
||
// numerals: a cookie, so the server sends pages with them; switching back to Eastern reloads the page
|
||
document.cookie = `divan-digits=${b.dataset.digits}; path=/; max-age=${b.dataset.digits ? 31536000 : 0}; samesite=lax`;
|
||
if (!b.dataset.digits) { delete root.dataset.digits; location.reload(); return; }
|
||
root.dataset.digits = 'latn'; westernDigits(document.body);
|
||
}
|
||
if (b.dataset.themePick != null) { if (b.dataset.themePick) root.dataset.theme = b.dataset.themePick; else delete root.dataset.theme; store('divan-theme', b.dataset.themePick); }
|
||
marks();
|
||
|
||
});
|
||
document.addEventListener('click', (e) => { if (panel.open && !panel.contains(e.target)) panel.open = false; }); // close when clicking elsewhere
|
||
// Western numerals also in what scripts add later (word sidebar, page numbers, suggestions)
|
||
function westernDigits(node) {
|
||
const walk = document.createTreeWalker(node, NodeFilter.SHOW_TEXT);
|
||
for (let t = walk.nextNode(); t; t = walk.nextNode()) if (/[۰-۹]/.test(t.data)) t.data = t.data.replace(/[۰-۹]/g, (d) => String(d.charCodeAt(0) - 0x6f0));
|
||
}
|
||
new MutationObserver((list) => {
|
||
if (root.dataset.digits !== 'latn') return;
|
||
for (const m of list) for (const n of m.addedNodes) n.nodeType === 3 ? westernDigits(n.parentNode) : n.nodeType === 1 && westernDigits(n);
|
||
}).observe(document.body, { childList: true, subtree: true });
|
||
marks();
|
||
// 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');
|
||
const kbHome = [kb.parentElement, kb.nextSibling]; // the keyboard's place in the header
|
||
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();
|
||
// the Urdu keyboard and Roman typing work on the search box and on any input marked data-urdu
|
||
// (e.g. the author filter): whichever was focused last
|
||
let target = q;
|
||
document.addEventListener('focusin', (e) => { if (e.target.matches?.('[data-urdu]')) target = e.target; });
|
||
// a page may redraw its boxes (e.g. the home sections editor): find the box again by its id
|
||
const live = () => (target.isConnected ? target : (target = (target.id && document.getElementById(target.id)) || q));
|
||
const typed = () => target.dispatchEvent(new Event('input', { bubbles: true })); // so suggestions update
|
||
const insert = (text) => {
|
||
live();
|
||
// rich editors (the content editor) take typed text through the browser's own editing command
|
||
if (target.isContentEditable) { target.focus(); document.execCommand('insertText', false, text); return; }
|
||
const s = target.selectionStart ?? target.value.length, e = target.selectionEnd ?? target.value.length;
|
||
target.setRangeText(text, s, e, 'end'); target.focus(); typed();
|
||
};
|
||
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') {
|
||
live();
|
||
if (target.isContentEditable) { target.focus(); document.execCommand('delete'); return; }
|
||
const s = target.selectionStart, e = target.selectionEnd;
|
||
target.setRangeText('', s === e ? Math.max(0, s - 1) : s, e, 'end'); target.focus(); typed();
|
||
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)); target.focus();
|
||
};
|
||
romBtn.onclick = () => { romBtn.setAttribute('aria-pressed', String(romBtn.getAttribute('aria-pressed') !== 'true')); target.focus(); };
|
||
// other inputs can open the same keyboard / Roman typing with their own buttons
|
||
document.addEventListener('click', (e) => {
|
||
const b = e.target.closest('[data-urdu-kb], [data-urdu-rom]'); if (!b) return;
|
||
target = document.getElementById(b.dataset.urduKb ?? b.dataset.urduRom)?.querySelector('[contenteditable]') ?? document.getElementById(b.dataset.urduKb ?? b.dataset.urduRom);
|
||
// in an open dialog (the note box) the header can't be reached: the keyboard moves under the box, and back when it closes
|
||
const box = b.dataset.urduKb && target.closest('dialog');
|
||
if (box) target.closest('.in-box-field')?.after(kb); else if (b.dataset.urduKb) kbHome[0].insertBefore(kb, kbHome[1]);
|
||
(b.dataset.urduKb ? kbBtn : romBtn).click();
|
||
b.setAttribute('aria-pressed', (b.dataset.urduKb ? kbBtn : romBtn).getAttribute('aria-pressed'));
|
||
});
|
||
document.addEventListener('close', (ev) => { // a dialog closing takes nothing of the header's with it
|
||
if (!ev.target.contains?.(kb)) return;
|
||
kb.hidden = true; kbBtn.setAttribute('aria-pressed', 'false'); kbHome[0].insertBefore(kb, kbHome[1]);
|
||
ev.target.querySelectorAll('[data-urdu-kb]').forEach((x) => x.setAttribute('aria-pressed', 'false'));
|
||
}, true);
|
||
document.addEventListener('keydown', (ev) => {
|
||
if (!ev.target.matches?.('[data-urdu]')) return;
|
||
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 = '';
|
||
// reader's library: bookmarks (poets, books/chapters, poems, couplets, phrases) and the word book (+);
|
||
// signed-out readers go to sign-in
|
||
let source = {}; // the couplet the selected word came from
|
||
async function save(item) {
|
||
if (!document.body.dataset.user) { location.href = '/signin?next=' + encodeURIComponent(location.pathname); return null; }
|
||
const r = await fetch('/api/library', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(item) });
|
||
if (r.status === 401) { location.href = '/signin?next=' + encodeURIComponent(location.pathname); return null; }
|
||
return r.ok ? r.json() : null;
|
||
}
|
||
document.addEventListener('click', async (e) => {
|
||
const b = e.target.closest('[data-save]'); if (!b) return;
|
||
if (getSelection()?.toString().trim()) return; // selecting text, not pressing
|
||
if (b.dataset.confirm && !(await ask(b.dataset.confirm, { danger: true, ok: 'ہٹائیں' }))) return;
|
||
const item = JSON.parse(b.dataset.save), d = await save(item);
|
||
if (d && item.kind === 'phrase') location.reload(); // the highlight is drawn by the page
|
||
else if (d) {
|
||
b.setAttribute('aria-pressed', String(d.saved));
|
||
if (b.classList.contains('save')) b.textContent = d.saved ? 'محفوظ شدہ' : 'محفوظ کریں';
|
||
}
|
||
});
|
||
const wordSave = (w) => { // sidebar button, with the word's saved state
|
||
const b = el('button', 'wordbook', null, { type: 'button' });
|
||
const set = (on) => { b.setAttribute('aria-pressed', String(on)); b.textContent = on ? 'ذخیرۂ الفاظ میں ہے' : 'ذخیرۂ الفاظ میں ڈالیں'; };
|
||
set(false);
|
||
b.onclick = async () => { const d = await save({ kind: 'word', word: w, ...source }); if (d) set(d.saved); };
|
||
if (document.body.dataset.user) fetch('/api/library?word=' + encodeURIComponent(w)).then((r) => r.ok && r.json()).then((d) => d && set(d.word));
|
||
return b;
|
||
};
|
||
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), wordSave(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();
|
||
// only the reading text (poems and prose, intros, e-book text, search snippets, library couplets), never the
|
||
// site's menus, buttons, tabs or forms
|
||
const at = sel && !sel.isCollapsed && sel.anchorNode?.parentElement;
|
||
const inText = at && main.contains(at) && at.closest('article, .intro, .ebook-text, .snippet, .lib-couplet')
|
||
&& !at.closest('form, input, textarea, [contenteditable], button, nav, .admin-nav');
|
||
selText = inText ? sel.toString().replace(/\u0614/g, '').trim() : '';
|
||
if (!selText) return hideMenu();
|
||
const w = oneWord(selText);
|
||
const block = sel.anchorNode.parentElement?.closest('[data-c]');
|
||
source = block ? { poemId: Number(block.closest('[data-poem]')?.dataset.poem) || undefined, couplet: Number(block.dataset.c) } : {};
|
||
menu.querySelector('[data-act=saveword]').hidden = !w;
|
||
menu.querySelector('[data-act=bookmark]').hidden = menu.querySelector('[data-act=note]').hidden = !(source.poemId && Number.isInteger(source.couplet));
|
||
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 'bookmark': save({ kind: 'phrase', poemId: source.poemId, couplet: source.couplet, phrase: selText })
|
||
.then((d) => d && (d.saved ? location.reload() : flash(b, 'پہلے سے محفوظ تھی، ہٹا دی'))); break;
|
||
case 'note': hideMenu(); document.dispatchEvent(new CustomEvent('divan:note', { detail: { couplet: source.couplet, phrase: selText } })); break; // the poem page opens its note box
|
||
case 'saveword': save({ kind: 'word', word: oneWord(selText), ...source }).then((d) => d && flash(b, d.saved ? 'ذخیرۂ الفاظ میں ڈال دیا' : 'ذخیرۂ الفاظ سے نکال دیا')); 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();
|
||
});
|
||
|
||
</script>
|
||
<!-- shared UI: the confirm dialog (instead of the browser's English confirm), toasts, busy buttons -->
|
||
<dialog class="ask" id="ask" aria-labelledby="ask-msg">
|
||
<form method="dialog">
|
||
<p id="ask-msg"></p>
|
||
<div class="ask-actions"><button class="btn primary" value="ok">ہاں</button><button class="btn" value="">نہیں</button></div>
|
||
</form>
|
||
</dialog>
|
||
<div class="toasts" aria-live="polite"></div>
|
||
<script is:inline>
|
||
// reading mode: only the text, the breadcrumbs and the logo; remembered across pages until switched off
|
||
document.addEventListener('click', (e) => {
|
||
if (!e.target.closest('[data-reading-toggle]')) return;
|
||
const root = document.documentElement, on = !('reading' in root.dataset);
|
||
const flip = () => { on ? (root.dataset.reading = '') : delete root.dataset.reading; };
|
||
document.startViewTransition ? document.startViewTransition(flip) : flip();
|
||
try { on ? localStorage.setItem('divan-reading', '1') : localStorage.removeItem('divan-reading'); } catch (err) {}
|
||
});
|
||
</script>
|
||
<script is:inline>
|
||
(() => {
|
||
const dlg = document.getElementById('ask'), ok = dlg.querySelector('[value=ok]');
|
||
// ask(message, {danger, ok}) -> Promise<boolean>; the destructive answer is red and named for what it does
|
||
window.ask = (message, o = {}) => new Promise((done) => {
|
||
dlg.querySelector('#ask-msg').textContent = message;
|
||
ok.textContent = o.ok || 'ہاں'; ok.classList.toggle('danger', !!o.danger); ok.classList.toggle('primary', !o.danger);
|
||
dlg.returnValue = ''; dlg.showModal(); dlg.querySelector('[value=""]').focus();
|
||
dlg.addEventListener('close', () => done(dlg.returnValue === 'ok'), { once: true });
|
||
});
|
||
dlg.addEventListener('click', (e) => { if (e.target === dlg) dlg.close(); }); // a click outside the box is "no"
|
||
// toast(message, kind): slides in, leaves by itself
|
||
const box = document.querySelector('.toasts');
|
||
window.toast = (message, kind = 'ok') => {
|
||
const t = document.createElement('p'); t.className = 'toast ' + kind; t.textContent = message; box.append(t);
|
||
setTimeout(() => { t.classList.add('out'); setTimeout(() => t.remove(), 350); }, 4200); // removed on a timer: with reduced motion there is no animation to end
|
||
};
|
||
// a page's "done" line becomes a toast
|
||
document.querySelectorAll('.form-done').forEach((p) => { if (p.textContent.trim()) { toast(p.textContent.trim()); p.hidden = true; } });
|
||
// forms: data-confirm asks first; a submitting button shows it is busy
|
||
document.addEventListener('submit', async (e) => {
|
||
const f = e.target; if (f.method === 'dialog') return;
|
||
const btn = e.submitter;
|
||
if (f.dataset.confirm && !f.dataset.asked) {
|
||
e.preventDefault();
|
||
if (!(await ask(f.dataset.confirm, { danger: 'danger' in f.dataset, ok: f.dataset.ok }))) return;
|
||
f.dataset.asked = '1'; f.requestSubmit(btn); delete f.dataset.asked; return;
|
||
}
|
||
if (btn && !e.defaultPrevented && !f.hasAttribute('data-js')) btn.setAttribute('aria-busy', 'true');
|
||
});
|
||
addEventListener('pageshow', () => document.querySelectorAll('[aria-busy=true]').forEach((b) => b.removeAttribute('aria-busy'))); // back button
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|