UNPKG

protoml-parser

Version:

ProtoML is a lightweight, declarative markup language designed for writing and structuring meeting protocols, notes and task lists in a human-readable and machine-parseable format.

157 lines (135 loc) 4.79 kB
const { convertHtmlToMarkdown, getVisibleMetaEntries, isProbablyHtml, renderStructuredReference, renderTemplate, stripHtml, } = require("./renderUtils"); function formatKeyValueList(items) { return items.map(([key, value]) => `- **${key}:** ${stripHtml(value)}`).join("\n"); } function renderObjectSection(lines, title) { if (!lines.length) return []; return [`## ${title}`, ...lines, ""]; } function renderMeetingLine(line) { const source = String(line || ""); if (!source.trim()) return ""; return isProbablyHtml(source) ? convertHtmlToMarkdown(source) : stripHtml(source); } function renderMarkdown(ast, options = {}) { const lines = []; const title = renderTemplate( ast.meta?.protocol || "Protocol - {{date}}", { ...(ast.meta || {}), date: ast.meta?.date || "Untitled" } ); const meetingTitle = renderTemplate( ast.meta?.meeting_title || "Rendered Meeting", ast.meta || {} ); lines.push(`# ${stripHtml(title)}`); lines.push(""); const metaEntries = getVisibleMetaEntries(ast, options); if (metaEntries.length) { lines.push("## Meta"); lines.push(formatKeyValueList(metaEntries)); lines.push(""); } if (ast.participants && Object.keys(ast.participants).length) { lines.push("## Participants"); for (const [, participant] of Object.entries(ast.participants)) { const alias = participant.alias ? ` (${participant.alias})` : ""; const email = participant.email ? ` - ${participant.email}` : ""; lines.push(`- ${participant.name}${alias}${email}`); } lines.push(""); } if (ast.subjects && Object.keys(ast.subjects).length) { lines.push("## Subjects"); for (const [id, subject] of Object.entries(ast.subjects)) { lines.push(`- ${id}: ${stripHtml(subject)}`); } lines.push(""); } if (ast.tag_stats && Object.keys(ast.tag_stats).length) { lines.push("## Tags"); for (const [id, stat] of Object.entries(ast.tag_stats)) { lines.push(`- ${id}: ${stripHtml(stat.label)} (total=${stat.total}, open=${stat.open}, done=${stat.done})`); } lines.push(""); } if (ast.signatures && Object.keys(ast.signatures).length) { lines.push(...renderObjectSection( Object.entries(ast.signatures).map(([id, signature]) => { const parts = [signature.name, signature.role, signature.date].filter(Boolean); const notes = signature.note ? ` - ${stripHtml(signature.note)}` : ""; return `- ${id}: ${parts.join(" | ")}${notes}`; }), "Signatures" )); } if (ast.approvals && Object.keys(ast.approvals).length) { lines.push(...renderObjectSection( Object.entries(ast.approvals).map(([id, approval]) => { const parts = [approval.label, approval.status, approval.by, approval.date].filter(Boolean); const notes = approval.notes ? ` - ${stripHtml(approval.notes)}` : ""; return `- ${id}: ${parts.join(" | ")}${notes}`; }), "Approvals" )); } if (Array.isArray(ast.tasks) && ast.tasks.length) { lines.push("## Tasks"); for (const task of ast.tasks) { const marker = task.done ? "[x]" : "[ ]"; const meta = []; if (task.assigned_to?.name) meta.push(`assigned=${task.assigned_to.name}`); if (task.subject?.label) meta.push(`subject=${stripHtml(task.subject.label)}`); if (task.tag?.label) meta.push(`tag=${stripHtml(task.tag.label)}`); lines.push(`- ${marker} ${stripHtml(task.text)}`); if (meta.length) { lines.push(` - ${meta.join(" | ")}`); } } lines.push(""); } if (Array.isArray(ast.notes) && ast.notes.length) { lines.push("## Notes"); for (const note of ast.notes) { lines.push(`- ${stripHtml(note)}`); } lines.push(""); } if (Array.isArray(ast.references) && ast.references.length) { lines.push("## References"); for (const entry of ast.references) { lines.push(`- ${renderStructuredReference(entry, true)}`); } lines.push(""); } if (Array.isArray(ast.attachments) && ast.attachments.length) { lines.push("## Attachments"); for (const entry of ast.attachments) { lines.push(`- ${renderStructuredReference(entry, true)}`); } lines.push(""); } if (Array.isArray(ast.meeting) && ast.meeting.length) { lines.push(`## ${stripHtml(meetingTitle)}`); lines.push(""); for (const line of ast.meeting) { const text = renderMeetingLine(line); if (!text) { lines.push(""); continue; } lines.push(text); if (!text.startsWith("#") && !text.startsWith("- ") && !text.startsWith("> ")) { lines.push(""); } } } return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim() + "\n"; } module.exports = renderMarkdown;