using System.Text; namespace MeetingAssistant.MeetingNotes; public sealed class MeetingArtifactFrontmatter { public string Title { get; set; } = ""; public string? OneLiner { get; set; } public DateTimeOffset? StartTime { get; set; } public DateTimeOffset? EndTime { get; set; } public List? Attendees { get; set; } public List? Projects { get; set; } public string Meeting { get; set; } = ""; public string Transcript { get; set; } = ""; public string AssistantContext { get; set; } = ""; public string Summary { get; set; } = ""; public string? State { get; set; } public string? Agenda { get; set; } public DateTimeOffset? ScheduledEnd { get; set; } } public static class MeetingArtifactFrontmatterRenderer { public static string Render( MeetingArtifactFrontmatter frontmatter, string body) { var builder = new StringBuilder(); builder.AppendLine("---"); AppendScalar(builder, "title", frontmatter.Title); AppendScalarIfNotEmpty(builder, "oneliner", frontmatter.OneLiner); AppendDateTime(builder, "start_time", frontmatter.StartTime); AppendDateTime(builder, "end_time", frontmatter.EndTime); AppendListIfNotNull(builder, "attendees", frontmatter.Attendees); AppendListIfNotNull(builder, "projects", frontmatter.Projects); AppendQuotedIfNotEmpty(builder, "meeting", frontmatter.Meeting); AppendQuotedIfNotEmpty(builder, "transcript", frontmatter.Transcript); AppendQuotedIfNotEmpty(builder, "assistant_context", frontmatter.AssistantContext); AppendQuotedIfNotEmpty(builder, "summary", frontmatter.Summary); if (!string.IsNullOrWhiteSpace(frontmatter.State)) { AppendScalar(builder, "state", frontmatter.State); } if (frontmatter.Agenda is not null) { AppendBlockScalar(builder, "agenda", frontmatter.Agenda); } if (frontmatter.ScheduledEnd is not null) { AppendDateTime(builder, "scheduled_end", frontmatter.ScheduledEnd); } builder.AppendLine("---"); builder.AppendLine(); builder.Append(body.TrimStart('\r', '\n')); return builder.ToString(); } public static (string Frontmatter, string Body) Split(string content) { var document = MarkdownDocumentParser.SplitOptional(content); return (document.Frontmatter, document.Body); } public static MeetingArtifactFrontmatter Create( MeetingSessionArtifacts artifacts, MeetingNote meetingNote, string title, string sourceNotePath, string? state = null) { return Create( artifacts, title, meetingNote.Frontmatter.StartTime, meetingNote.Frontmatter.EndTime, sourceNotePath, state); } public static MeetingArtifactFrontmatter Create( MeetingSessionArtifacts artifacts, string title, DateTimeOffset? startTime, DateTimeOffset? endTime, string sourceNotePath, string? state = null) { return new MeetingArtifactFrontmatter { Title = title, StartTime = startTime, EndTime = endTime, Meeting = LinkUnlessSelf(artifacts.MeetingNotePath, sourceNotePath, "Meeting Note"), Transcript = LinkUnlessSelf(artifacts.TranscriptPath, sourceNotePath, "Transcript"), AssistantContext = LinkUnlessSelf(artifacts.AssistantContextPath, sourceNotePath, "Assistant Context"), Summary = LinkUnlessSelf(artifacts.SummaryPath, sourceNotePath, "Summary"), State = state }; } public static string DefaultTitle(MeetingNote meetingNote, string fallback) { return string.IsNullOrWhiteSpace(meetingNote.Frontmatter.Title) ? fallback : meetingNote.Frontmatter.Title; } private static void AppendScalar(StringBuilder builder, string key, string? value) { builder.Append(key); builder.Append(": "); builder.AppendLine(string.IsNullOrWhiteSpace(value) ? "\"\"" : EscapeListItem(value)); } private static void AppendScalarIfNotEmpty(StringBuilder builder, string key, string? value) { if (string.IsNullOrWhiteSpace(value)) { return; } AppendScalar(builder, key, value); } private static void AppendQuoted(StringBuilder builder, string key, string value) { builder.Append(key); builder.Append(": "); builder.AppendLine(EscapeQuoted(value)); } private static void AppendQuotedIfNotEmpty(StringBuilder builder, string key, string? value) { if (string.IsNullOrWhiteSpace(value)) { return; } AppendQuoted(builder, key, value); } private static void AppendDateTime(StringBuilder builder, string key, DateTimeOffset? value) { builder.Append(key); builder.Append(": "); builder.AppendLine(value is null ? "\"\"" : EscapeQuoted(value.Value.ToString("O"))); } private static void AppendListIfNotNull(StringBuilder builder, string key, IReadOnlyList? values) { if (values is null) { return; } builder.Append(key); builder.AppendLine(":"); foreach (var value in values) { builder.Append("- "); builder.AppendLine(EscapeListItem(value)); } } private static void AppendBlockScalar(StringBuilder builder, string key, string value) { if (string.IsNullOrEmpty(value)) { builder.Append(key); builder.AppendLine(": \"\""); return; } builder.Append(key); builder.AppendLine(": |-"); foreach (var line in value.Replace("\r\n", "\n", StringComparison.Ordinal).Replace('\r', '\n').Split('\n')) { builder.Append(" "); builder.AppendLine(line); } } private static string EscapeQuoted(string value) { return $"\"{value.Replace("\"", "\\\"", StringComparison.Ordinal)}\""; } private static string EscapeListItem(string value) { return value.Contains(':', StringComparison.Ordinal) || value.StartsWith("[", StringComparison.Ordinal) ? EscapeQuoted(value) : value; } private static string LinkUnlessSelf(string targetPath, string sourceNotePath, string label) { return IsSamePath(targetPath, sourceNotePath) ? "" : ObsidianLink.ToWikiLink(targetPath, sourceNotePath, label); } private static bool IsSamePath(string first, string second) { return string.Equals( Path.GetFullPath(first), Path.GetFullPath(second), StringComparison.OrdinalIgnoreCase); } }