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) => - {p.title}
)}
- )}
+ {page.type !== 'poem' && groups.map((g) => (
+ <>
+ {g.letter && ردیف {g.letter}
}
+ {g.poems.map((p: any) => - {p.title}
)}
+ >
+ ))}
{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); }