Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions scripts/markdown_test.mjs
Original file line number Diff line number Diff line change
@@ -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!');
}
2 changes: 1 addition & 1 deletion site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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]\"",
"preview": "astro preview"
},
"dependencies": {
Expand Down
139 changes: 139 additions & 0 deletions site/src/components/MarkdownExport.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
---
export interface Props {
content?: string;
exportUrl?: string;
filename?: string;
variant?: 'button' | 'link';
}

const { content, exportUrl, filename, variant = 'button' } = Astro.props;
const encodedContent = content ? encodeURIComponent(content) : undefined;
---

{variant === 'link' ? (
<button type="button" class="btn-copy as-link markdown-export-btn" data-md={encodedContent} data-url={exportUrl}>
<span class="btn-text">📋 Copy as Markdown</span>
</button>
) : (
<div class="markdown-export">
<button type="button" class="btn-copy markdown-export-btn" data-md={encodedContent} data-url={exportUrl}>
<span class="btn-text">📋 Copy as Markdown</span>
</button>

{exportUrl && filename ? (
<a href={exportUrl} download={filename} class="btn-download markdown-export-btn">
⬇ Download .md
</a>
) : filename && (
<button type="button" class="btn-download markdown-export-btn" data-filename={filename} data-md={encodedContent}>
⬇ Download .md
</button>
)}
</div>
)}

<script>
document.addEventListener('click', async (e) => {
const target = e.target as HTMLElement | null;
if (!target) return;

const copyBtn = target.closest('.btn-copy');
if (copyBtn) {
try {
let md = '';
const url = copyBtn.getAttribute('data-url');
const inlineData = copyBtn.getAttribute('data-md');

if (url) {
const res = await fetch(url);
md = await res.text();
} else if (inlineData) {
md = decodeURIComponent(inlineData);
}

if (!md) return;

await navigator.clipboard.writeText(md);

const textSpan = copyBtn.querySelector('.btn-text') || copyBtn;
const originalText = textSpan.textContent;
textSpan.textContent = '✓ Copied!';
(textSpan as HTMLElement).style.color = 'var(--accent, #10b981)';
setTimeout(() => {
textSpan.textContent = originalText;
(textSpan as HTMLElement).style.color = '';
}, 2000);
} catch (err) {
console.error('Failed to copy: ', err);
}
return;
}

const dlBtn = target.closest('button.btn-download');
if (dlBtn) {
const md = decodeURIComponent(dlBtn.getAttribute('data-md') || '');
const filename = dlBtn.getAttribute('data-filename') || 'export.md';

const blob = new Blob([md], { type: 'text/markdown;charset=utf-8' });
const url = window.URL.createObjectURL(blob);

const a = document.createElement('a');
a.href = url;
a.download = filename;
a.style.display = 'none';
document.body.appendChild(a);
a.click();

setTimeout(() => {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 1000);
}
});
</script>

<style>
.markdown-export {
display: flex;
gap: 0.5rem;
margin: 1rem 0;
flex-wrap: wrap;
}
.markdown-export button,
.markdown-export a.btn-download {
display: inline-flex;
align-items: center;
padding: 0.4rem 0.8rem;
font-size: 0.9rem;
font-family: var(--font-body, inherit);
color: var(--ink, inherit);
background: var(--surface, transparent);
border: 1px solid var(--line, currentColor);
border-radius: 4px;
cursor: pointer;
text-decoration: none;
transition: border-color 0.2s, background-color 0.2s, color 0.2s;
}
.markdown-export button:hover,
.markdown-export a.btn-download:hover {
border-color: var(--accent);
background: var(--accent-soft);
}

/* Link variant styles */
button.as-link {
background: none;
border: none;
padding: 0;
font-size: inherit;
font-family: inherit;
color: var(--accent);
text-decoration: underline;
text-underline-offset: 2px;
cursor: pointer;
transition: opacity 0.2s;
}
button.as-link:hover {
opacity: 0.8;
}
</style>
132 changes: 132 additions & 0 deletions site/src/lib/markdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { topicById } from './data.ts';

export const AUTO_TOPICS_NOTE = 'Topics were auto-suggested and may be imprecise — edits welcome.';

/**
* True if `notes` is the bot's auto-suggested-topics note or a seed note,
* so we drop it from the exported Description.
*/
export function isAutoTopicsNote(notes?: string | null): boolean {
if (!notes || typeof notes !== 'string') return false;
const trimmed = notes.trim();
if (trimmed === AUTO_TOPICS_NOTE) return true;
if (trimmed.includes('auto-suggested and may be imprecise')) return true;
if (trimmed.includes('SEED DATA')) return true;
if (/^Auto-imported from the OpenReview venue record on \d{4}-\d{2}-\d{2} — please verify and enrich \(topics are keyword-guessed\)\.?$/.test(trimmed)) return true;
return false;
}

export function formatWorkshop(w: any, confName?: string): string {
const parts: string[] = [];

parts.push(`## ${w.name}`);

const details: string[] = [];

if (w.statusLabel) {
details.push(`- **Status:** ${w.statusLabel}`);
}

if (w.abstractDeadlineWallClock) {
const passedTag = w.abstractDeadlinePassed ? ' (closed)' : '';
details.push(`- **Abstract Deadline:** ${w.abstractDeadlineWallClock}${passedTag}`);
}

if (w.deadlineWallClock) {
details.push(`- **Submission Deadline:** ${w.deadlineWallClock}`);
} else {
details.push(`- **Submission Deadline:** TBA`);
}

if (w.notificationDateLabel) {
details.push(`- **Notification:** ${w.notificationDateLabel}`);
}

if (w.workshopDateLabel) {
details.push(`- **Workshop Date:** ${w.workshopDateLabel}`);
}

if (w.website) {
details.push(`- **Website:** [${w.website}](${w.website})`);
}

if (w.openreview_venue_id) {
const url = `https://openreview.net/group?id=${w.openreview_venue_id}`;
details.push(`- **OpenReview:** [${url}](${url})`);
}

if (details.length > 0) {
parts.push(details.join('\n'));
}

if (w.topics && w.topics.length > 0) {
const topicLabels = w.topics.map((t: string) => `- ${topicById.get(t)?.label ?? t}`).join('\n');
parts.push(`**Topics:**\n${topicLabels}`);
}

if (w.deadlineChange) {
if (w.deadlineChange.kind === 'extended') {
parts.push(`**Deadline History:** Extended by ${w.deadlineChange.days} ${w.deadlineChange.days === 1 ? 'day' : 'days'} (previously ${w.deadlineChange.fromWallClock})`);
} else if (w.deadlineChange.kind === 'earlier') {
parts.push(`**Deadline History:** Moved ${w.deadlineChange.days} ${w.deadlineChange.days === 1 ? 'day' : 'days'} earlier (previously ${w.deadlineChange.fromWallClock})`);
} else if (w.deadlineChange.kind === 'announced') {
parts.push(`**Deadline History:** Deadline just announced`);
}
} else if (Array.isArray(w.deadlineHistoryView) && w.deadlineHistoryView.length > 1) {
const historyLines = w.deadlineHistoryView.map((h: any, i: number) => {
const tag = i === w.deadlineHistoryView.length - 1 ? 'first recorded' : 'changed';
return `- ${h.recordedLabel}: ${h.wallClock ?? 'no date published'} (${tag})`;
}).join('\n');
parts.push(`**Deadline History:**\n${historyLines}`);
}

if (w.notes && !isAutoTopicsNote(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 genDate = new Date().toISOString().split('T')[0];

const header = [
`# ${confFull} ${year} Workshops`,
`Generated from AI Workshop Tracker`,
``,
`Conference: ${conf.name}`,
`Edition: ${year}`,
`Generated: ${genDate}`,
`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 genDate = new Date().toISOString().split('T')[0];

const header = [
`# ${w.name}`,
`Generated from AI Workshop Tracker`,
``,
`Conference: ${confFull}`,
`Edition: ${w.year}`,
`Generated: ${genDate}`,
``,
`---`
].join('\n');

// Strip the '## ' line from formatWorkshop output since 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}`;
}
3 changes: 3 additions & 0 deletions site/src/pages/conference/[conf].astro
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -144,6 +146,7 @@ const statusLines = (w: any): string[] =>
{yearGroups.map(([year, ws]) => (
<section class="conf-year">
<h2>{confName} {year}</h2>
<MarkdownExport content={formatConferenceYear(conf, year, ws)} filename={`${conf.id}-${year}-workshops.md`} />
<ul class="conf-ws-list">
{ws.map((w) => (
<li>
Expand Down
Loading