divan/api/src/owned.ts
Anas Rashid 93825fe5d9 Publishing commits to divan-data git (#34); public names only; 'last edited' on poems
- api/src/git.ts: each published version is a commit in the divan-data checkout (queued, one at a
  time): the moderator as author by public name with a placeholder email, the summary and version
  in the message, Reviewed-by / Approved-by / Divan-Revision / Divan-Version trailers;
  DIVAN_GIT_PUSH=1 pushes. The commit id is kept with the revision.
- Privacy fix: published files and commits carry public names (profile name, else 'موڈریٹر <id>'),
  never email addresses (divan-data is public).
- Poem pages: 'دیوان کا ورژن …' with who edited, reviewed and published, the date, and a link to
  the commit diff (DIVAN_DATA_COMMIT_URL).
- deploy/sync.sh: pull --rebase so publishing commits are kept.
- Admins are super moderators (documented).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 00:37:39 +02:00

53 lines
3.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Divan-owned content (#30): works edited in Divan are kept in divan-data's divan/ folder, so the daily
// Wikisource sync never overwrites them (export_divan.py prefers them). Each work is two files:
// divan/<work url>.dtx Divan text, the readable source (see divantext.ts, docs/content-model.md)
// divan/<work url>.json generated from it: Title, Verses (the site's verse layout), Edited (who, when)
// Publishing (#34) calls writeOwned and commits both; fromPoem gives the starting text for an edit.
// node src/owned.ts <divan-data dir> <work url> <file.dtx> [editor] write one by hand
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { parse, toVerses } from './divantext.ts';
type Verse = { Position: string; Text: string; CoupletIndex: number };
// a work (ganjoor-data poem JSON) as Divan text: verse in <poem> (blank line between couplets, stanzas and
// lines), prose as paragraphs
export function fromPoem(poem: { Title: string; Verses: Verse[]; SourceUrl?: string }, meta: Record<string, string> = {}) {
const head = Object.entries({ عنوان: poem.Title, ...meta, ...(poem.SourceUrl && { ماخذ: 'ویکی ماخذ', ماخذ_ربط: poem.SourceUrl }) })
.map(([k, v]) => `| ${k} = ${v}`).join('\n');
const units = new Map<number, Verse[]>();
for (const v of poem.Verses) (units.get(v.CoupletIndex) ?? units.set(v.CoupletIndex, []).get(v.CoupletIndex)!).push(v);
const parts: string[] = [];
let verse: string[] = [];
const flush = () => { if (verse.length) parts.push(`<poem>\n${verse.join('\n\n')}\n</poem>`); verse = []; };
for (const u of units.values()) {
if (u[0].Position === 'Paragraph') { flush(); parts.push(u.map((v) => v.Text).join('\n')); }
else if (u[0].Position === 'Heading') { flush(); parts.push(`${'='.repeat((u[0] as any).Level ?? 2)} ${u[0].Text} ${'='.repeat((u[0] as any).Level ?? 2)}`); }
else verse.push(u.map((v) => v.Text).join('\n'));
}
flush();
return `{{دیوان\n${head}\n}}\n${parts.join('\n\n')}\n`;
}
// write a Divan-owned work (the .dtx and the generated .json); returns the paths written
export async function writeOwned(dataDir: string, url: string, dtx: string,
edited: { by: string; at?: string; version?: number; revision?: number; reviewedBy?: string | null; publishedBy?: string }) {
const doc = parse(dtx);
const verses = toVerses(doc);
if (!verses.length) throw new Error('the text has no verses or paragraphs');
if (!/^[\w/-]+$/.test(url) || url.includes('..')) throw new Error(`not a work url: ${url}`);
const base = join(dataDir, 'divan', url.replace(/^\/+|\/+$/g, ''));
await mkdir(dirname(base), { recursive: true });
const json = { FullUrl: '/' + url.replace(/^\/+/, ''), Title: doc.meta['عنوان'] ?? '', Verses: verses.map(({ VOrder, ...v }) => ({ VOrder, ...v, SectionIndex1: 0 })),
Edited: { ...edited, at: edited.at ?? new Date().toISOString() } };
await writeFile(base + '.dtx', dtx.endsWith('\n') ? dtx : dtx + '\n');
await writeFile(base + '.json', JSON.stringify(json, null, 1) + '\n');
return [base + '.dtx', base + '.json'];
}
if (import.meta.url === `file://${process.argv[1]}`) {
const [dir, url, file, by = 'divan'] = process.argv.slice(2);
if (!dir || !url || !file) { console.error('usage: node src/owned.ts <divan-data dir> <work url> <file.dtx> [editor]'); process.exit(1); }
console.log((await writeOwned(dir, url, await readFile(file, 'utf8'), { by })).join('\n'));
}