Radif per ghazal, matla/maqta labels, pen name set apart; contents grouped by radif letter
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
4c7e0559a7
commit
9a2db49eab
@ -51,11 +51,13 @@ for (const entry of manifest.Poets) {
|
|||||||
const p = await read(`poets${ref.FullUrl}.json`);
|
const p = await read(`poets${ref.FullUrl}.json`);
|
||||||
const searchText = normalise([p.Title, ...p.Verses.map((v: any) => v.Text)].join(' '));
|
const searchText = normalise([p.Title, ...p.Verses.map((v: any) => v.Text)].join(' '));
|
||||||
await client.query(
|
await client.query(
|
||||||
`INSERT INTO poems (id, category_id, poet_id, url, title, full_title, source_url, position, search_text)
|
`INSERT INTO poems (id, category_id, poet_id, url, title, full_title, source_url, position, search_text,
|
||||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
|
radif, matla, maqta, radif_letter)
|
||||||
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)
|
||||||
ON CONFLICT (id) DO UPDATE SET category_id=$2, poet_id=$3, url=$4, title=$5, full_title=$6,
|
ON CONFLICT (id) DO UPDATE SET category_id=$2, poet_id=$3, url=$4, title=$5, full_title=$6,
|
||||||
source_url=$7, position=$8, search_text=$9`,
|
source_url=$7, position=$8, search_text=$9, radif=$10, matla=$11, maqta=$12, radif_letter=$13`,
|
||||||
[p.Id, cat.Id, poet.Id, p.FullUrl, p.Title, p.FullTitle, p.SourceUrl ?? null, i, searchText],
|
[p.Id, cat.Id, poet.Id, p.FullUrl, p.Title, p.FullTitle, p.SourceUrl ?? null, i, searchText,
|
||||||
|
p.Radif ?? null, p.Matla ?? null, p.Maqta ?? null, ref.RadifLetter ?? null],
|
||||||
);
|
);
|
||||||
await client.query('DELETE FROM verses WHERE poem_id = $1', [p.Id]);
|
await client.query('DELETE FROM verses WHERE poem_id = $1', [p.Id]);
|
||||||
if (p.Verses.length) {
|
if (p.Verses.length) {
|
||||||
|
|||||||
@ -70,7 +70,7 @@ app.get<{ Querystring: { url?: string } }>('/api/page', async (req, reply) => {
|
|||||||
GROUP BY c.id ORDER BY c.position, c.id`,
|
GROUP BY c.id ORDER BY c.position, c.id`,
|
||||||
[cat.id],
|
[cat.id],
|
||||||
),
|
),
|
||||||
pool.query('SELECT url, title FROM poems WHERE category_id = $1 ORDER BY position, id', [cat.id]),
|
pool.query('SELECT url, title, radif_letter FROM poems WHERE category_id = $1 ORDER BY position, id', [cat.id]),
|
||||||
]);
|
]);
|
||||||
return {
|
return {
|
||||||
type: cat.parent_id === null ? 'poet' : 'category',
|
type: cat.parent_id === null ? 'poet' : 'category',
|
||||||
|
|||||||
@ -40,6 +40,12 @@ CREATE TABLE IF NOT EXISTS poems (
|
|||||||
position integer NOT NULL DEFAULT 0,
|
position integer NOT NULL DEFAULT 0,
|
||||||
search_text text NOT NULL DEFAULT '' -- normalised title + verses
|
search_text text NOT NULL DEFAULT '' -- normalised title + verses
|
||||||
);
|
);
|
||||||
|
-- ghazal marks (divan-data extension): radif ('' = rhyme only), matla/maqta couplet numbers, and the
|
||||||
|
-- radif letter that groups a divan's contents ("ردیف الف … ی"; NULL outside radif-ordered lists)
|
||||||
|
ALTER TABLE poems ADD COLUMN IF NOT EXISTS radif text;
|
||||||
|
ALTER TABLE poems ADD COLUMN IF NOT EXISTS matla integer;
|
||||||
|
ALTER TABLE poems ADD COLUMN IF NOT EXISTS maqta integer;
|
||||||
|
ALTER TABLE poems ADD COLUMN IF NOT EXISTS radif_letter text;
|
||||||
CREATE INDEX IF NOT EXISTS poems_category ON poems(category_id);
|
CREATE INDEX IF NOT EXISTS poems_category ON poems(category_id);
|
||||||
CREATE INDEX IF NOT EXISTS poems_search ON poems USING gin (search_text gin_trgm_ops);
|
CREATE INDEX IF NOT EXISTS poems_search ON poems USING gin (search_text gin_trgm_ops);
|
||||||
|
|
||||||
|
|||||||
@ -10,15 +10,27 @@ if (!page) return new Response(null, { status: 404, statusText: 'Not found' });
|
|||||||
|
|
||||||
// group verses into couplets / single lines / paragraphs for rendering
|
// group verses into couplets / single lines / paragraphs for rendering
|
||||||
type Verse = { position: string; couplet: number; text: string };
|
type Verse = { position: string; couplet: number; text: string };
|
||||||
const blocks: { kind: 'couplet' | 'single' | 'para'; lines: string[] }[] = [];
|
const blocks: { kind: 'couplet' | 'single' | 'para'; lines: string[]; mark?: string }[] = [];
|
||||||
if (page.type === 'poem') {
|
if (page.type === 'poem') {
|
||||||
|
const { matla, maqta } = page.poem;
|
||||||
for (const v of page.verses as Verse[]) {
|
for (const v of page.verses as Verse[]) {
|
||||||
if (v.position === 'Right') blocks.push({ kind: 'couplet', lines: [v.text] });
|
if (v.position === 'Right') blocks.push({ kind: 'couplet', lines: [v.text],
|
||||||
|
mark: v.couplet === matla ? 'مطلع' : v.couplet === maqta ? 'مقطع' : undefined });
|
||||||
else if (v.position === 'Left' && blocks.at(-1)?.kind === 'couplet' && blocks.at(-1)!.lines.length === 1) blocks.at(-1)!.lines.push(v.text);
|
else if (v.position === 'Left' && blocks.at(-1)?.kind === 'couplet' && blocks.at(-1)!.lines.length === 1) blocks.at(-1)!.lines.push(v.text);
|
||||||
else if (v.position === 'Paragraph') blocks.push({ kind: 'para', lines: [v.text] });
|
else if (v.position === 'Paragraph') blocks.push({ kind: 'para', lines: [v.text] });
|
||||||
else blocks.push({ kind: 'single', lines: [v.text] });
|
else blocks.push({ kind: 'single', lines: [v.text] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// pen name (marked ؔ in the text) set apart; split() with a capture group puts matches at odd indexes
|
||||||
|
const verse = (line: string) => line.split(/(\S*\u0614\S*)/);
|
||||||
|
// contents of a divan grouped by radif letter ("ردیف الف … ی"); ے is filed under ی as in printed divans
|
||||||
|
const LETTER = (l: string) => (l === 'ا' ? 'الف' : l === 'ے' ? 'ی' : l);
|
||||||
|
const groups: { letter: string | null; poems: any[] }[] = [];
|
||||||
|
for (const p of page.poems ?? []) {
|
||||||
|
const l = p.radif_letter ? LETTER(p.radif_letter) : null;
|
||||||
|
if (groups.at(-1)?.letter !== l || !groups.length) groups.push({ letter: l, poems: [] });
|
||||||
|
groups.at(-1)!.poems.push(p);
|
||||||
|
}
|
||||||
const crumbs = page.breadcrumbs ?? [];
|
const crumbs = page.breadcrumbs ?? [];
|
||||||
const title = page.type === 'poem' ? page.poem.title : page.type === 'poet' ? page.poet.nickname : page.category.title;
|
const title = page.type === 'poem' ? page.poem.title : page.type === 'poet' ? page.poet.nickname : page.category.title;
|
||||||
---
|
---
|
||||||
@ -47,16 +59,20 @@ const title = page.type === 'poem' ? page.poem.title : page.type === 'poet' ? pa
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{page.type !== 'poem' && page.poems.length > 0 && (
|
{page.type !== 'poem' && groups.map((g) => (
|
||||||
<ol class="toc">{page.poems.map((p: any) => <li><a href={p.url}>{p.title}</a></li>)}</ol>
|
<>
|
||||||
)}
|
{g.letter && <h2 class="radif-head">ردیف {g.letter}</h2>}
|
||||||
|
<ol class="toc">{g.poems.map((p: any) => <li><a href={p.url}>{p.title}</a></li>)}</ol>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
|
||||||
{page.type === 'poem' && (
|
{page.type === 'poem' && (
|
||||||
<>
|
<>
|
||||||
<h1>{page.poem.title}</h1>
|
<h1>{page.poem.title}</h1>
|
||||||
|
{page.poem.radif != null && <p class="muted radif">{page.poem.radif ? `ردیف: ${page.poem.radif}` : 'غیر مردف'}</p>}
|
||||||
<article class="poem">
|
<article class="poem">
|
||||||
{blocks.map((b) =>
|
{blocks.map((b) =>
|
||||||
b.kind === 'couplet' ? <div class="couplet">{b.lines.map((l) => <p>{l}</p>)}</div>
|
b.kind === 'couplet' ? <div class="couplet" data-mark={b.mark}>{b.lines.map((l) => <p>{verse(l).map((s, i) => i % 2 ? <span class="takhallus">{s}</span> : s)}</p>)}</div>
|
||||||
: b.kind === 'para' ? <p class="para">{b.lines[0]}</p>
|
: b.kind === 'para' ? <p class="para">{b.lines[0]}</p>
|
||||||
: <p class="single">{b.lines[0]}</p>
|
: <p class="single">{b.lines[0]}</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -86,6 +86,10 @@ h1::after { content: ''; display: block; width: 140px; height: 2px; background:
|
|||||||
.couplet { display: grid; grid-template-columns: 1fr 1fr; column-gap: 10%; margin: 0 0 6px; border-radius: 6px; font-size: 1.1rem; line-height: var(--verse-lh); }
|
.couplet { display: grid; grid-template-columns: 1fr 1fr; column-gap: 10%; margin: 0 0 6px; border-radius: 6px; font-size: 1.1rem; line-height: var(--verse-lh); }
|
||||||
.couplet:hover { background: linear-gradient(90deg, transparent, var(--gold-subtle) 15%, var(--gold-subtle) 85%, transparent); }
|
.couplet:hover { background: linear-gradient(90deg, transparent, var(--gold-subtle) 15%, var(--gold-subtle) 85%, transparent); }
|
||||||
.couplet p { margin: 0; text-align: left; }
|
.couplet p { margin: 0; text-align: left; }
|
||||||
|
.couplet[data-mark]::before { content: attr(data-mark); grid-column: 1 / -1; justify-self: center; font-size: .72rem; line-height: 1.6; color: var(--brand); border: 1px solid var(--gold-light); border-radius: 999px; padding: 0 10px; margin-bottom: 2px; }
|
||||||
|
.takhallus { color: var(--brand); }
|
||||||
|
.radif { text-align: center; margin-top: -6px; }
|
||||||
|
.radif-head { text-align: center; font-size: 1.05rem; color: var(--brand); margin: 22px 0 4px; }
|
||||||
.couplet p + p { text-align: right; }
|
.couplet p + p { text-align: right; }
|
||||||
.couplet::after { content: '✤'; grid-column: 1 / -1; text-align: center; color: var(--brand); font-size: .72rem; opacity: .75; line-height: 1.2; }
|
.couplet::after { content: '✤'; grid-column: 1 / -1; text-align: center; color: var(--brand); font-size: .72rem; opacity: .75; line-height: 1.2; }
|
||||||
.single { text-align: center; margin: 0 0 6px; font-size: 1.1rem; line-height: var(--verse-lh); }
|
.single { text-align: center; margin: 0 0 6px; font-size: 1.1rem; line-height: var(--verse-lh); }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user