From be10ecebde338ca9dfc893a5320e5adba2048f55 Mon Sep 17 00:00:00 2001 From: Pavan Maddula Date: Mon, 3 Aug 2026 15:59:46 +0530 Subject: [PATCH 1/2] feat: add Markdown export for conference and workshop pages --- .gitignore | 3 + site/package.json | 2 +- site/src/components/MarkdownExport.astro | 141 +++++++++++++++++++++++ site/src/lib/markdown.ts | 91 +++++++++++++++ site/src/pages/conference/[conf].astro | 3 + site/src/pages/workshop/[slug].astro | 3 + 6 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 site/src/components/MarkdownExport.astro create mode 100644 site/src/lib/markdown.ts diff --git a/.gitignore b/.gitignore index 20048e0..715aa4c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ node_modules/ site/node_modules/ site/dist/ +site/.astro/ +site/public/pagefind/ +site/public/pagefind-papers/ .DS_Store *.log diff --git a/site/package.json b/site/package.json index 07f6100..8afc794 100644 --- a/site/package.json +++ b/site/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "astro dev", - "build": "astro build && pagefind --site dist --root-selector '[data-pf-ws]' && pagefind --site dist --output-subdir pagefind-papers --root-selector '[data-pf-papers]'", + "build": "astro build && pagefind --site dist --root-selector \"[data-pf-ws]\" && pagefind --site dist --output-subdir pagefind-papers --root-selector \"[data-pf-papers]\" && node -e \"fs.cpSync('dist/pagefind', 'public/pagefind', {recursive: true}); fs.cpSync('dist/pagefind-papers', 'public/pagefind-papers', {recursive: true})\"", "preview": "astro preview" }, "dependencies": { diff --git a/site/src/components/MarkdownExport.astro b/site/src/components/MarkdownExport.astro new file mode 100644 index 0000000..5c83d62 --- /dev/null +++ b/site/src/components/MarkdownExport.astro @@ -0,0 +1,141 @@ +--- +export interface Props { + content: string; + filename?: string; + variant?: 'button' | 'link'; +} + +const { content, filename, variant = 'button' } = Astro.props; +// We encode the content so we can safely embed it in data attributes +const encodedContent = encodeURIComponent(content); +--- + +{variant === 'link' ? ( + +) : ( +
+ + + {filename && ( + + )} +
+)} + + + + diff --git a/site/src/lib/markdown.ts b/site/src/lib/markdown.ts new file mode 100644 index 0000000..dbf896c --- /dev/null +++ b/site/src/lib/markdown.ts @@ -0,0 +1,91 @@ +import { topicById } from './data'; + +export function formatWorkshop(w: any, confName: string): string { + const parts: string[] = []; + + parts.push(`## ${w.name}`); + + if (w.statusLabel) { + parts.push(`Status\n${w.statusLabel}`); + } + + if (w.deadlineWallClock) { + parts.push(`Submission Deadline\n${w.deadlineWallClock}`); + } else { + parts.push(`Submission Deadline\nTBA`); + } + + if (w.notificationDateLabel) { + parts.push(`Notification\n${w.notificationDateLabel}`); + } + + if (w.workshopDateLabel) { + parts.push(`Workshop Date\n${w.workshopDateLabel}`); + } + + if (w.topics && w.topics.length > 0) { + parts.push(`Topics\n${w.topics.map((t: string) => `- ${topicById.get(t)?.label ?? t}`).join('\n')}`); + } + + if (w.website) { + parts.push(`Website\n[${w.website}](${w.website})`); + } + + if (w.openreview_venue_id) { + const url = `https://openreview.net/group?id=${w.openreview_venue_id}`; + parts.push(`OpenReview\n[${url}](${url})`); + } + + if (w.notes) { + parts.push(`Description\n${w.notes}`); + } + + return parts.join('\n\n'); +} + +export function formatConferenceYear(conf: any, year: number, wsList: any[]): string { + const confFull = conf.full_name || conf.name; + + const header = [ + `# ${confFull} ${year} Workshops`, + `Generated from AI Workshop Tracker`, + ``, + `Conference: ${conf.name}`, + `Edition: ${year}`, + ``, + `Generated:`, + new Date().toISOString().split('T')[0], + ``, + `Total Workshops: ${wsList.length}`, + ``, + `---` + ].join('\n'); + + const body = wsList.map(w => formatWorkshop(w, conf.name)).join('\n\n---\n\n'); + + return `${header}\n\n${body}`; +} + +export function formatSingleWorkshopInfo(w: any, conf: any): string { + const confFull = conf.full_name || conf.name; + + const header = [ + `# ${w.name}`, + `Generated from AI Workshop Tracker`, + ``, + `Conference: ${confFull}`, + `Edition: ${w.year}`, + ``, + `Generated:`, + new Date().toISOString().split('T')[0], + ``, + `---` + ].join('\n'); + + // Strip the '## ' from the first line since the header already has '# w.name' + const bodyParts = formatWorkshop(w, conf.name).split('\n\n'); + bodyParts.shift(); + const body = bodyParts.join('\n\n'); + + return `${header}\n\n${body}`; +} diff --git a/site/src/pages/conference/[conf].astro b/site/src/pages/conference/[conf].astro index 4aecb8c..73e9af0 100644 --- a/site/src/pages/conference/[conf].astro +++ b/site/src/pages/conference/[conf].astro @@ -2,6 +2,8 @@ import Base from '../../components/Base.astro'; import { workshops, conferences } from '../../lib/data'; import { href, REPO_URL } from '../../lib/site'; +import MarkdownExport from '../../components/MarkdownExport.astro'; +import { formatConferenceYear } from '../../lib/markdown'; // One hub page per conference, generated from the same data the rest of the // site uses. Intentionally carries NO data-pf-ws / data-pf-papers attributes, @@ -144,6 +146,7 @@ const statusLines = (w: any): string[] => {yearGroups.map(([year, ws]) => (

{confName} {year}

+
    {ws.map((w) => (
  • diff --git a/site/src/pages/workshop/[slug].astro b/site/src/pages/workshop/[slug].astro index ed79c38..5d3fbac 100644 --- a/site/src/pages/workshop/[slug].astro +++ b/site/src/pages/workshop/[slug].astro @@ -2,6 +2,8 @@ import Base from '../../components/Base.astro'; import { workshops, conferenceById, topicById, loadPaperCache } from '../../lib/data'; import { href, REPO_URL, CALENDAR_ENABLED } from '../../lib/site'; +import MarkdownExport from '../../components/MarkdownExport.astro'; +import { formatSingleWorkshopInfo } from '../../lib/markdown'; export function getStaticPaths() { return workshops.map((w) => ({ params: { slug: w.slug }, props: { w } })); @@ -152,6 +154,7 @@ const breadcrumbLd = { {CALENDAR_ENABLED && w.deadlineUtcMs && Add deadline to calendar (.ics)} See all {conf.name} workshops → ✎ Edit this entry + {isSeed && (

    From 47b349e8c716a2d9e17b2d1b25b45361e1ce09a7 Mon Sep 17 00:00:00 2001 From: Pavan Maddula Date: Fri, 7 Aug 2026 01:52:25 +0530 Subject: [PATCH 2/2] feat: per-workshop Markdown export with abstract deadlines, history, and theme fixes --- .gitignore | 3 - scripts/markdown_test.mjs | 84 +++++++++++++++++ site/package.json | 2 +- site/src/components/MarkdownExport.astro | 94 ++++++++++--------- site/src/lib/markdown.ts | 111 ++++++++++++++++------- 5 files changed, 207 insertions(+), 87 deletions(-) create mode 100644 scripts/markdown_test.mjs diff --git a/.gitignore b/.gitignore index 715aa4c..20048e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,6 @@ node_modules/ site/node_modules/ site/dist/ -site/.astro/ -site/public/pagefind/ -site/public/pagefind-papers/ .DS_Store *.log diff --git a/scripts/markdown_test.mjs b/scripts/markdown_test.mjs new file mode 100644 index 0000000..153cb96 --- /dev/null +++ b/scripts/markdown_test.mjs @@ -0,0 +1,84 @@ +#!/usr/bin/env node +/** + * Unit tests for site/src/lib/markdown.ts formatting logic. + * Run: node scripts/markdown_test.mjs + */ +import { formatWorkshop, formatConferenceYear, formatSingleWorkshopInfo, isAutoTopicsNote } from '../site/src/lib/markdown.ts'; + +let failed = 0; +function check(label, got, expect) { + const ok = typeof expect === 'boolean' ? got === expect : (got.includes ? got.includes(expect) : got === expect); + if (!ok) { + failed++; + console.error(`✗ ${label}\n Got: ${JSON.stringify(got)}\n Expected to match: ${JSON.stringify(expect)}`); + } else { + console.log(`✓ ${label}`); + } +} + +// 1. Test auto topics note filtering +check('isAutoTopicsNote identifies exact auto-suggested note', + isAutoTopicsNote('Topics were auto-suggested and may be imprecise — edits welcome.'), true); +check('isAutoTopicsNote identifies historical auto note', + isAutoTopicsNote('Auto-imported from the OpenReview venue record on 2026-06-20 — please verify and enrich (topics are keyword-guessed).'), true); +check('isAutoTopicsNote identifies SEED DATA note', + isAutoTopicsNote('SEED DATA: unverified entry'), true); +check('isAutoTopicsNote allows legitimate custom description', + isAutoTopicsNote('This workshop focuses on efficient LLM reasoning.'), false); + +// 2. Test formatWorkshop with abstract deadline, bold key-values, and filtered notes +const sampleWs = { + name: 'Efficient Reasoning Workshop', + statusLabel: 'Open call', + abstractDeadlineWallClock: 'Aug 20, 2026 23:59 UTC', + abstractDeadlinePassed: false, + deadlineWallClock: 'Aug 29, 2026 23:59 UTC', + notificationDateLabel: 'Sep 20, 2026', + workshopDateLabel: 'Oct 25, 2026', + topics: ['llms', 'efficiency'], + website: 'https://example.com/ws', + openreview_venue_id: 'NeurIPS.cc/2026/Workshop/ER', + notes: 'Topics were auto-suggested and may be imprecise — edits welcome.', + deadlineChange: { + kind: 'extended', + days: 5, + fromWallClock: 'Aug 24, 2026 23:59 UTC', + }, +}; + +const output = formatWorkshop(sampleWs, 'NeurIPS'); + +check('formatWorkshop includes H2 header', output, '## Efficient Reasoning Workshop'); +check('formatWorkshop formats bold Status label', output, '- **Status:** Open call'); +check('formatWorkshop includes abstract deadline', output, '- **Abstract Deadline:** Aug 20, 2026 23:59 UTC'); +check('formatWorkshop formats bold Submission Deadline', output, '- **Submission Deadline:** Aug 29, 2026 23:59 UTC'); +check('formatWorkshop formats bold Website link', output, '- **Website:** [https://example.com/ws](https://example.com/ws)'); +check('formatWorkshop formats bold OpenReview link', output, '- **OpenReview:** [https://openreview.net/group?id=NeurIPS.cc/2026/Workshop/ER](https://openreview.net/group?id=NeurIPS.cc/2026/Workshop/ER)'); +check('formatWorkshop formats Deadline History', output, '**Deadline History:** Extended by 5 days (previously Aug 24, 2026 23:59 UTC)'); +check('formatWorkshop excludes auto-suggested maintenance notes from Description', output.includes('Description'), false); + +// 3. Test custom notes inclusion +const wsWithCustomNotes = { + ...sampleWs, + notes: 'We welcome papers on efficient attention mechanisms.', +}; +const outputCustom = formatWorkshop(wsWithCustomNotes, 'NeurIPS'); +check('formatWorkshop includes valid custom description', outputCustom, '**Description:**\nWe welcome papers on efficient attention mechanisms.'); + +// 4. Test formatConferenceYear +const confObj = { name: 'NeurIPS', full_name: 'Neural Information Processing Systems' }; +const confOutput = formatConferenceYear(confObj, 2026, [sampleWs]); +check('formatConferenceYear contains conference header', confOutput, '# Neural Information Processing Systems 2026 Workshops'); +check('formatConferenceYear contains Total Workshops count', confOutput, 'Total Workshops: 1'); + +// 5. Test formatSingleWorkshopInfo +const singleOutput = formatSingleWorkshopInfo(sampleWs, confObj); +check('formatSingleWorkshopInfo contains single workshop header', singleOutput, '# Efficient Reasoning Workshop'); +check('formatSingleWorkshopInfo contains Conference field', singleOutput, 'Conference: Neural Information Processing Systems'); + +if (failed > 0) { + console.error(`\nTest suite failed with ${failed} failure(s).`); + process.exit(1); +} else { + console.log('\nAll markdown tests passed successfully!'); +} diff --git a/site/package.json b/site/package.json index 8afc794..aff33ad 100644 --- a/site/package.json +++ b/site/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "astro dev", - "build": "astro build && pagefind --site dist --root-selector \"[data-pf-ws]\" && pagefind --site dist --output-subdir pagefind-papers --root-selector \"[data-pf-papers]\" && node -e \"fs.cpSync('dist/pagefind', 'public/pagefind', {recursive: true}); fs.cpSync('dist/pagefind-papers', 'public/pagefind-papers', {recursive: true})\"", + "build": "astro build && pagefind --site dist --root-selector \"[data-pf-ws]\" && pagefind --site dist --output-subdir pagefind-papers --root-selector \"[data-pf-papers]\"", "preview": "astro preview" }, "dependencies": { diff --git a/site/src/components/MarkdownExport.astro b/site/src/components/MarkdownExport.astro index 5c83d62..2ca527d 100644 --- a/site/src/components/MarkdownExport.astro +++ b/site/src/components/MarkdownExport.astro @@ -1,26 +1,30 @@ --- export interface Props { - content: string; + content?: string; + exportUrl?: string; filename?: string; variant?: 'button' | 'link'; } -const { content, filename, variant = 'button' } = Astro.props; -// We encode the content so we can safely embed it in data attributes -const encodedContent = encodeURIComponent(content); +const { content, exportUrl, filename, variant = 'button' } = Astro.props; +const encodedContent = content ? encodeURIComponent(content) : undefined; --- {variant === 'link' ? ( - ) : (

    - - - {filename && ( + + {exportUrl && filename ? ( + + ⬇ Download .md + + ) : filename && ( @@ -29,8 +33,6 @@ const encodedContent = encodeURIComponent(content); )}