using System.Text.RegularExpressions; using Journal.Core.Models; namespace Journal.Core.Services.Entries; public static partial class JournalParser { [GeneratedRegex(@"(?:\*\*Date:\*\*|\*\*Date:|Date:)\s*(.+)")] private static partial Regex DatePattern(); [GeneratedRegex(@"^\#\#+\s*(.*)$")] private static partial Regex SectionHeaderPattern(); [GeneratedRegex(@"^\s*[-*]\s*\[([xX ])\]\s*(.*)$")] private static partial Regex CheckboxPattern(); [GeneratedRegex(@"^(!\w+)\s*((?:@\S+\s*)?)(?:\s*((?:#\S+\s*)*))?\s*$")] private static partial Regex FragmentHeaderPattern(); [GeneratedRegex(@"^!\w+\s*")] private static partial Regex FragmentBoundaryPattern(); public static JournalEntry ParseJournalContent(string content, string fileStem) { ArgumentNullException.ThrowIfNull(content); return new JournalEntry( date: ExtractDate(content, fileStem), rawContent: content, sections: ParseSections(content), fragments: ParseFragments(content)); } public static string ExtractDate(string content, string fileStem) { ArgumentNullException.ThrowIfNull(content); if (string.IsNullOrWhiteSpace(fileStem)) throw new ArgumentException("File stem is required", nameof(fileStem)); var match = DatePattern().Match(content); if (match.Success) { var parsed = match.Groups[1].Value.Trim(); if (!string.IsNullOrWhiteSpace(parsed)) return parsed; } return fileStem.Trim(); } public static Dictionary ParseSections(string content) { ArgumentNullException.ThrowIfNull(content); var parsedSections = new Dictionary(); string? currentSectionTitle = null; var currentSectionContent = new List(); var currentSectionCheckboxes = new Dictionary(); var lines = content.Split(["\r\n", "\n", "\r"], StringSplitOptions.None); foreach (var line in lines) { var sectionHeaderMatch = SectionHeaderPattern().Match(line.Trim()); if (sectionHeaderMatch.Success) { if (currentSectionTitle is not null) { parsedSections[currentSectionTitle] = new ParsedSection( currentSectionTitle, currentSectionContent, currentSectionCheckboxes); } var headerText = sectionHeaderMatch.Groups[1].Value.Trim(); var foundTitle = FindCanonicalSectionTitle(headerText); if (foundTitle is not null) { currentSectionTitle = foundTitle; currentSectionContent = []; currentSectionCheckboxes = []; } else { currentSectionTitle = null; currentSectionContent = []; currentSectionCheckboxes = []; } continue; } if (currentSectionTitle is not null) { var checkboxMatch = CheckboxPattern().Match(line); if (checkboxMatch.Success) { var isChecked = checkboxMatch.Groups[1].Value.Trim().Equals("x", StringComparison.OrdinalIgnoreCase); var checkboxText = checkboxMatch.Groups[2].Value.Trim(); currentSectionCheckboxes[checkboxText] = isChecked; } currentSectionContent.Add(line); } } if (currentSectionTitle is not null) { parsedSections[currentSectionTitle] = new ParsedSection( currentSectionTitle, currentSectionContent, currentSectionCheckboxes); } return parsedSections; } public static List ParseFragments(string content) { ArgumentNullException.ThrowIfNull(content); var fragments = new List(); var lines = content.Split(["\r\n", "\n", "\r"], StringSplitOptions.None); for (var i = 0; i < lines.Length; i++) { var headerMatch = FragmentHeaderPattern().Match(lines[i]); if (!headerMatch.Success) continue; var type = headerMatch.Groups[1].Value.Trim(); var timeToken = headerMatch.Groups[2].Value.Trim().TrimStart('@'); var tagsToken = headerMatch.Groups[3].Value.Trim(); var descriptionLines = new List(); var j = i + 1; while (j < lines.Length && !FragmentBoundaryPattern().IsMatch(lines[j])) { descriptionLines.Add(lines[j]); j++; } var description = string.Join("\n", descriptionLines).Trim(); if (!string.IsNullOrWhiteSpace(description)) { var fragment = new Fragment(type, description); if (!string.IsNullOrWhiteSpace(timeToken) && DateTimeOffset.TryParse(timeToken, out var parsedTime)) fragment.Time = parsedTime; if (!string.IsNullOrWhiteSpace(tagsToken)) { fragment.Tags = [ .. tagsToken.Split(' ', StringSplitOptions.RemoveEmptyEntries) .Where(t => t.StartsWith('#')) .Select(t => t.Trim().TrimStart('#')) .Where(t => !string.IsNullOrWhiteSpace(t)) ]; } fragments.Add(fragment); } i = j - 1; } return fragments; } private static string? FindCanonicalSectionTitle(string headerText) { foreach (var title in SectionTitles.Canonical) { if (headerText.Contains(title, StringComparison.OrdinalIgnoreCase)) return title; } return null; } }