From 9a2db49eab22b7fe2ef002095ed4ad583e4b9cc7 Mon Sep 17 00:00:00 2001 From: Anas Rashid Date: Thu, 8 Oct 2026 21:27:43 +0200 Subject: [PATCH] Radif per ghazal, matla/maqta labels, pen name set apart; contents grouped by radif letter Co-Authored-By: Claude Opus 5.5 --- api/src/import.ts | 10 ++++++---- api/src/server.ts | 2 +- db/schema.sql | 6 ++++++ web/src/pages/[...path].astro | 28 ++++++++++++++++++++++------ web/src/styles/global.css | 4 ++++ 5 files changed, 39 insertions(+), 11 deletions(-) diff --git a/api/src/import.ts b/api/src/import.ts index 1786aded..3b5c8e72 100644 --- a/api/src/import.ts +++ b/api/src/import.ts @@ -51,11 +51,13 @@ for (const entry of manifest.Poets) { const p = await read(`poets${ref.FullUrl}.json`); const searchText = normalise([p.Title, ...p.Verses.map((v: any) => v.Text)].join(' ')); await client.query( - `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) + `INSERT INTO poems (id, category_id, poet_id, url, title, full_title, source_url, position, search_text, + 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, - source_url=$7, position=$8, search_text=$9`, - [p.Id, cat.Id, poet.Id, p.FullUrl, p.Title, p.FullTitle, p.SourceUrl ?? null, i, searchText], + 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.Radif ?? null, p.Matla ?? null, p.Maqta ?? null, ref.RadifLetter ?? null], ); await client.query('DELETE FROM verses WHERE poem_id = $1', [p.Id]); if (p.Verses.length) { diff --git a/api/src/server.ts b/api/src/server.ts index 335232ed..33899e30 100644 --- a/api/src/server.ts +++ b/api/src/server.ts @@ -70,7 +70,7 @@ app.get<{ Querystring: { url?: string } }>('/api/page', async (req, reply) => { GROUP BY c.id ORDER BY c.position, c.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 { type: cat.parent_id === null ? 'poet' : 'category', diff --git a/db/schema.sql b/db/schema.sql index c94aa7e8..bbf4c6ed 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -40,6 +40,12 @@ CREATE TABLE IF NOT EXISTS poems ( position integer NOT NULL DEFAULT 0, 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_search ON poems USING gin (search_text gin_trgm_ops); diff --git a/web/src/pages/[...path].astro b/web/src/pages/[...path].astro index 94241bfd..4163a9ba 100644 --- a/web/src/pages/[...path].astro +++ b/web/src/pages/[...path].astro @@ -10,15 +10,27 @@ if (!page) return new Response(null, { status: 404, statusText: 'Not found' }); // group verses into couplets / single lines / paragraphs for rendering 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') { + const { matla, maqta } = page.poem; 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 === 'Paragraph') blocks.push({ kind: 'para', 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 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 ))} )} - {page.type !== 'poem' && page.poems.length > 0 && ( -
    {page.poems.map((p: any) =>
  1. {p.title}
  2. )}
- )} + {page.type !== 'poem' && groups.map((g) => ( + <> + {g.letter &&

ردیف {g.letter}

} +
    {g.poems.map((p: any) =>
  1. {p.title}
  2. )}
+ + ))} {page.type === 'poem' && ( <>

{page.poem.title}

+ {page.poem.radif != null &&

{page.poem.radif ? `ردیف: ${page.poem.radif}` : 'غیر مردف'}

}
{blocks.map((b) => - b.kind === 'couplet' ?
{b.lines.map((l) =>

{l}

)}
+ b.kind === 'couplet' ?
{b.lines.map((l) =>

{verse(l).map((s, i) => i % 2 ? {s} : s)}

)}
: b.kind === 'para' ?

{b.lines[0]}

:

{b.lines[0]}

)} diff --git a/web/src/styles/global.css b/web/src/styles/global.css index 33c97727..700e0d09 100644 --- a/web/src/styles/global.css +++ b/web/src/styles/global.css @@ -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:hover { background: linear-gradient(90deg, transparent, var(--gold-subtle) 15%, var(--gold-subtle) 85%, transparent); } .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::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); }