using System.Text.RegularExpressions; using Serilog; namespace Charac.Build; internal static partial class DocumentationChecks { public static void Validate(string root) { var documents = Directory.EnumerateFiles(Path.Combine(root, "docs"), "*.md", SearchOption.AllDirectories) .Concat(Directory.EnumerateFiles(root, "*.md", SearchOption.TopDirectoryOnly)) .Concat(Directory.EnumerateFiles(Path.Combine(root, "deploy"), "*.md", SearchOption.AllDirectories)) .ToArray(); var links = new Dictionary>(StringComparer.Ordinal); foreach (var document in documents) { var content = File.ReadAllText(document); if (!content.StartsWith("# ", StringComparison.Ordinal)) { throw new InvalidOperationException($"Document needs a title: {document}"); } var targets = new List(); foreach (Match match in MarkdownLink().Matches(content)) { var link = match.Groups[1].Value; if (link.StartsWith('#') || Uri.TryCreate(link, UriKind.Absolute, out _)) { continue; } var target = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(document)!, Uri.UnescapeDataString(link.Split('#')[0]))); if (!File.Exists(target) && !Directory.Exists(target)) { throw new InvalidOperationException($"Broken link in {document}: {link}"); } targets.Add(target); } links[Path.GetFullPath(document)] = targets; } var visited = new HashSet(StringComparer.Ordinal); var pending = new Queue(); pending.Enqueue(Path.Combine(root, "README.md")); while (pending.TryDequeue(out var current)) { if (visited.Add(current) && links.TryGetValue(current, out var targets)) { foreach (var target in targets) { pending.Enqueue(target); } } } foreach (var document in Directory.EnumerateFiles(Path.Combine(root, "docs"), "*.md", SearchOption.AllDirectories)) { if (!visited.Contains(Path.GetFullPath(document))) { throw new InvalidOperationException($"Document must be reachable from README: {document}"); } } var overview = File.ReadAllText(Path.Combine(root, "docs", "overview.md")); foreach (var document in Directory.EnumerateFiles(Path.Combine(root, "docs", "platforms"), "*.md")) { var expectedLink = $"platforms/{Path.GetFileName(document)}"; if (!overview.Contains($"]({expectedLink})", StringComparison.Ordinal)) { throw new InvalidOperationException($"Platform difference needs an overview link: {expectedLink}"); } } Log.Information("Validated {Count} documents, local link targets, reachability and platform coverage.", documents.Length); } [GeneratedRegex(@"\[[^\]]+\]\(([^)\s]+)\)")] private static partial Regex MarkdownLink(); }