diff --git a/.vscode/HelperMarkdown.cs b/.vscode/HelperMarkdown.cs new file mode 100644 index 0000000..b73db84 --- /dev/null +++ b/.vscode/HelperMarkdown.cs @@ -0,0 +1,1186 @@ +using File_Folder_Helper.Models; +using Microsoft.Extensions.Logging; +using System.Collections.ObjectModel; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace File_Folder_Helper.Helpers; + +internal static partial class HelperMarkdown +{ + + private record Record(string Source, string? StartAt, string? Destination); + + private record MarkdownFileAndLines(MarkdownFile MarkdownFile, string[] Lines); + + private record MarkdownFileH1AndRelativePath(MarkdownFile? MarkdownFile, string[]? Lines, string? H1, string? RelativePath); + + [JsonSourceGenerationOptions(WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] + [JsonSerializable(typeof(Dictionary))] + internal partial class DictionaryStringAndJsonElementSourceGenerationContext : JsonSerializerContext + { + } + + private static Record GetRecord(List args) + { + Record result; + string? startAt = null; + string? destination = null; + for (int i = 1; i < args.Count; i++) + { + if (args[i].Length == 2 && i + 1 < args.Count) + { + if (args[i][1] == 's') + startAt = Path.GetFullPath(args[i + 1]); + else if (args[i][1] == 'd') + destination = Path.GetFullPath(args[i + 1]); + i++; + } + } + if (startAt is not null && !Directory.Exists(startAt)) + throw new Exception($"Start at directory <{startAt}> doesn't exist!"); + if (destination is not null) + { + string? root = Path.GetPathRoot(destination); + if (root is null || !Directory.Exists(root)) + throw new NotSupportedException($"This method requires frontMatterYamlLines valid -d path <{root}>!"); + if (!Directory.Exists(destination)) + _ = Directory.CreateDirectory(destination); + } + result = new(Path.GetFullPath(args[0]), startAt, destination); + return result; + } + + /// + /// Determines files text file's encoding by analyzing its byte order mark (BOM). + /// Defaults to ASCII when detection of the text file's endianness fails. + /// + /// The text file to analyze. + /// The detected encoding. + internal static Encoding? GetEncoding(string filename) + { + Encoding? result; + byte[] bom = new byte[4]; + using FileStream file = new(filename, FileMode.Open, FileAccess.Read); + _ = file.Read(bom, 0, 4); + if (bom[0] == 0x2b && bom[1] == 0x2f && bom[2] == 0x76) +#pragma warning disable SYSLIB0001 + result = Encoding.UTF7; +#pragma warning restore SYSLIB0001 + if (bom[0] == 0xef && bom[1] == 0xbb && bom[2] == 0xbf) + result = Encoding.UTF8; + if (bom[0] == 0xff && bom[1] == 0xfe && bom[2] == 0 && bom[3] == 0) + result = Encoding.UTF32; //UTF-32LE + if (bom[0] == 0xff && bom[1] == 0xfe) + result = Encoding.Unicode; //UTF-16LE + if (bom[0] == 0xfe && bom[1] == 0xff) + result = Encoding.BigEndianUnicode; //UTF-16BE + if (bom[0] == 0 && bom[1] == 0 && bom[2] == 0xfe && bom[3] == 0xff) + result = new UTF32Encoding(true, true); //UTF-32BE + else + result = null; + return result; + } + + internal static string[] GetFiles(AppSettings appSettings, string directory) + { + string[] results = Directory.GetFiles(directory, "*.md", SearchOption.AllDirectories). + Where(l => !appSettings.ExcludeDirectoryNames.Any(m => l.Contains(m))).ToArray(); + return results; + } + + private static (string type, string h1) GetTypeAndH1(AppSettings appSettings, string h1, List lines, LineNumber lineNumber) + { + string type = lineNumber.Type is null ? appSettings.DefaultNoteType : lines[lineNumber.Type.Value].Replace("type: ", string.Empty); + string h1FromFile = lineNumber.H1 is null ? h1 : lines[lineNumber.H1.Value][2..]; + return (type, h1FromFile); + } + + internal static (List, LineNumber) GetStatusAndFrontMatterYamlEndLineNumbers(FileInfo fileInfo) + { + string line; + int? h1LineNumber = null; + int? typeLineNumber = null; + int? statusLineNumber = null; + int? createdLineNumber = null; + int? updatedLineNumber = null; + int? frontMatterYamlEndLineNumber = null; + Encoding? encoding = GetEncoding(fileInfo.FullName) ?? Encoding.Default; + string[] lines = File.ReadAllLines(fileInfo.FullName, encoding); + for (int i = 1; i < lines.Length; i++) + { + line = lines[i]; + if (line.Length < 3) + continue; + if (line[..3] == "---") + { + frontMatterYamlEndLineNumber = i; + continue; + } + if (line.Length > 6 && line[..6] == "type: ") + { + typeLineNumber = i; + continue; + } + if (line.Length > 8 && line[..8] == "status: ") + { + statusLineNumber = i; + continue; + } + if (line.Length > 9 && line[..9] == "created: ") + { + createdLineNumber = i; + continue; + } + if (line.Length > 9 && line[..9] == "updated: ") + { + updatedLineNumber = i; + continue; + } + if (h1LineNumber is null && line.Length > 2 && line[..2] == "# ") + { + h1LineNumber = i; + continue; + } + } + LineNumber lineNumber = new(createdLineNumber, + h1LineNumber, + frontMatterYamlEndLineNumber, + statusLineNumber, + typeLineNumber, + updatedLineNumber); + return (lines.ToList(), lineNumber); + } + + private static ReadOnlyDictionary GetRelativeToCollection(AppSettings appSettings, Record record, string[] files, bool force) + { + Dictionary results = new(); + string h1; + string key; + string type; + FileInfo fileInfo; + List lines; + LineNumber lineNumber; + MarkdownFile markdownFile; + string fileNameWithoutExtension; + foreach (string file in files) + { + fileInfo = new(file); + if (fileInfo.DirectoryName is null) + continue; + key = Path.GetRelativePath(record.Source, file); + (lines, lineNumber) = GetStatusAndFrontMatterYamlEndLineNumbers(fileInfo); + fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileInfo.FullName); + h1 = fileNameWithoutExtension.ToLower().Replace("%20", "-").Replace(' ', '-'); + if (lines.Any()) + (type, h1) = GetTypeAndH1(appSettings, h1, lines, lineNumber); + else + { + type = appSettings.DefaultNoteType; + File.WriteAllLines(file, new string[] { "---", $"type: \"{type}\"", "---", string.Empty, $"# {h1}" }); + lines = File.ReadAllLines(file).ToList(); + } + markdownFile = new(file, fileInfo.DirectoryName, fileInfo.Name, fileNameWithoutExtension, fileInfo.Extension, fileInfo.CreationTime, fileInfo.LastWriteTime, lineNumber, type, h1); + if (force || record.StartAt is null || file.StartsWith(record.StartAt)) + results.Add(key, new(markdownFile, lines.ToArray())); + else + results.Add(key, new(markdownFile, Array.Empty())); + } + return new(results); + } + + private static ReadOnlyDictionary GetRelativeToCollection(AppSettings appSettings, Record record, bool force = false) + { + ReadOnlyDictionary results; + string[] files = GetFiles(appSettings, record.Source); + results = GetRelativeToCollection(appSettings, record, files, force); + return new(results); + } + + private static int SetFrontMatterAndH1(AppSettings appSettings, ReadOnlyDictionary relativeToCollection) + { + int result = 0; + List results = new(); + string h1Line; + string[] lines; + string typeLine; + string createdLine; + string updatedLine; + DateTime creationDateTime; + MarkdownFile markdownFile; + string createdLineCompare; + string updatedLineCompare; + foreach (KeyValuePair relativeTo in relativeToCollection) + { + if (!relativeTo.Value.Lines.Any()) + continue; + results.Clear(); + lines = relativeTo.Value.Lines; + markdownFile = relativeTo.Value.MarkdownFile; + results.AddRange(lines); + creationDateTime = markdownFile.CreationDateTime > markdownFile.LastWriteDateTime ? markdownFile.LastWriteDateTime : markdownFile.CreationDateTime; + typeLine = $"type: \"{appSettings.DefaultNoteType}\""; + h1Line = $"# {markdownFile.FileNameWithoutExtension}"; + createdLineCompare = $"created: \"{creationDateTime.ToUniversalTime():yyyy-MM-dd}T"; + createdLine = $"created: \"{creationDateTime.ToUniversalTime():yyyy-MM-ddTHH:mm:ss.fffZ}\""; + updatedLineCompare = $"updated: \"{markdownFile.LastWriteDateTime.ToUniversalTime():yyyy-MM-dd}T"; + updatedLine = $"updated: \"{markdownFile.LastWriteDateTime.ToUniversalTime():yyyy-MM-ddTHH:mm:ss.fffZ}\""; + if (markdownFile.LineNumber.FrontMatterYamlEnd is null) + { + if (markdownFile.LineNumber.H1 is null) + { + results.Insert(0, string.Empty); + results.Insert(0, h1Line); + results.Insert(0, string.Empty); + } + results.Insert(0, "---"); + results.Insert(0, updatedLine); + results.Insert(0, createdLine); + results.Insert(0, typeLine); + results.Insert(0, "---"); + } + else + { + if (markdownFile.LineNumber.H1 is null) + { + results.Insert(markdownFile.LineNumber.FrontMatterYamlEnd.Value + 1, string.Empty); + results.Insert(markdownFile.LineNumber.FrontMatterYamlEnd.Value + 1, h1Line); + results.Insert(markdownFile.LineNumber.FrontMatterYamlEnd.Value + 1, string.Empty); + } + if (markdownFile.LineNumber.Type is null) + results.Insert(markdownFile.LineNumber.FrontMatterYamlEnd.Value, typeLine); + if (markdownFile.LineNumber.Updated is null) + results.Insert(markdownFile.LineNumber.FrontMatterYamlEnd.Value, updatedLine); + else + { + if (results[markdownFile.LineNumber.Updated.Value].Contains('$')) + continue; + if (results[markdownFile.LineNumber.Updated.Value][..updatedLineCompare.Length] == updatedLineCompare) + continue; + results[markdownFile.LineNumber.Updated.Value] = updatedLine; + } + if (markdownFile.LineNumber.Created is null) + results.Insert(markdownFile.LineNumber.FrontMatterYamlEnd.Value, createdLine); + else if (results[markdownFile.LineNumber.Created.Value][..createdLineCompare.Length] != createdLineCompare) + results[markdownFile.LineNumber.Created.Value] = createdLine; + } + File.WriteAllLines(markdownFile.File, results); + File.SetLastWriteTime(markdownFile.File, markdownFile.LastWriteDateTime); + result += 1; + } + return result; + } + + private static List GetFrontMatterLines(string[] parsedLines) + { + List results = new(); + string afterTrim; + string[] segments; + StringBuilder stringBuilder = new(); + for (int i = 0; i < parsedLines.Length; i++) + { + afterTrim = parsedLines[i].Trim(); + if (string.IsNullOrEmpty(afterTrim) || afterTrim[0] is '{' or '}') + continue; + segments = afterTrim.Split(": "); + if (segments.Length != 2) + { + if (results[^1][^1] == '[') + { + _ = stringBuilder.Clear(); + _ = stringBuilder.Append(results[^1]); + results.RemoveAt(results.Count - 1); + for (int j = i; j < parsedLines.Length; j++) + { + i = j; + afterTrim = parsedLines[j].Trim(); + if (afterTrim == "],") + _ = stringBuilder.Append(afterTrim[..^1]); + else if (afterTrim[^1] == ',') + _ = stringBuilder.Append(afterTrim).Append(' '); + else + _ = stringBuilder.Append(afterTrim); + if (afterTrim is "]" or "],") + { + results.Add(stringBuilder.ToString()); + break; + } + } + continue; + } + results.Clear(); + break; + } + if (afterTrim[^1] != ',') + results.Add(afterTrim[1..].Replace("\": ", ": ")); + else + results.Add(afterTrim[1..^1].Replace("\": ", ": ")); + } + return results; + } + + private static (string?, Dictionary?, List) Get(List jsonLines) + { + string? result; + List results; + Dictionary? keyValuePairs; + string jsonLinesLast = jsonLines[^1]; + jsonLines.RemoveAt(jsonLines.Count - 1); + jsonLines.Add(jsonLinesLast[..^1]); + jsonLines.Insert(0, "{"); + jsonLines.Add("}"); + result = string.Join(Environment.NewLine, jsonLines); + keyValuePairs = JsonSerializer.Deserialize(result, DictionaryStringAndJsonElementSourceGenerationContext.Default.DictionaryStringJsonElement); + if (keyValuePairs is null) + throw new NullReferenceException(nameof(keyValuePairs)); + result = JsonSerializer.Serialize(keyValuePairs, DictionaryStringAndJsonElementSourceGenerationContext.Default.DictionaryStringJsonElement); + string[] parsedLines = result.Split(Environment.NewLine).ToArray(); + results = GetFrontMatterLines(parsedLines); + if (!results.Any()) + { + result = null; + keyValuePairs = null; + } + return (result, keyValuePairs, results); + } + + private static (string?, Dictionary?, string[]) Get(int frontMatterYamlEnd, string[] lines) + { + string? result; + List results; + Dictionary? keyValuePairs; + string[] segments; + string[] segmentsB; + string segmentsLast; + string segmentsFirst; + List jsonLines = new(); + for (int i = 0; i < frontMatterYamlEnd; i++) + { + if (lines[i] == "---") + continue; + segments = lines[i].Split(": "); + if (segments.Length != 2) + { + jsonLines.Clear(); + break; + } + segmentsLast = segments[^1].Trim(); + segmentsFirst = segments[0].Trim(); + if (string.IsNullOrEmpty(segmentsLast)) + continue; + if (segmentsFirst[0] == '"' && segmentsFirst[^1] == '"') + jsonLines.Add($"{segmentsFirst}: "); + else if (segmentsFirst[0] == '\'' && segmentsFirst[^1] == '\'') + jsonLines.Add($"\"{segmentsFirst[1..^1]}\": "); + else + jsonLines.Add($"\"{segmentsFirst}\": "); + if (segmentsLast == "[]") + jsonLines.RemoveAt(jsonLines.Count - 1); + else if (segmentsLast.Length > 4 && segmentsLast[0] == '[' && segmentsLast[^1] == ']' && segmentsLast[1] == '"' && segmentsLast[^2] == '"') + jsonLines.Add($"{segmentsLast},"); + else if (segmentsLast[0] == '"' && segmentsLast[^1] == '"') + jsonLines.Add($"{segmentsLast},"); + else if (segmentsLast[0] == '"' && segmentsLast[^1] == '"') + jsonLines.Add($"\"{segmentsLast[1..^1]}\""); + else if (!segmentsLast.Contains('"') && !segmentsLast.Contains('\'')) + { + if (segmentsLast is "true" or "false") + jsonLines.Add($"{segmentsLast},"); + else if (DateTime.TryParse(segmentsLast, out DateTime dateTime)) + jsonLines.Add($"\"{segmentsLast}\","); + else if (segmentsLast.All(l => char.IsNumber(l))) + jsonLines.Add($"{segmentsLast},"); + else + { + segmentsB = segmentsLast.Split('.'); + if (segmentsB.Length == 2 && segmentsB[0].Length < 7 && segmentsB[^1].Length < 7 && segmentsB[0].All(l => char.IsNumber(l)) && segmentsB[^1].All(l => char.IsNumber(l))) + jsonLines.Add($"{segmentsLast},"); + else if (!segmentsLast.Contains('[') && !segmentsLast.Contains('{')) + jsonLines.Add($"\"{segmentsLast}\","); + else + { + jsonLines.Clear(); + break; + } + } + } + else + { + jsonLines.Clear(); + break; + } + } + if (jsonLines.Any()) + (result, keyValuePairs, results) = Get(jsonLines); + else + (result, keyValuePairs, results) = (null, null, new()); + return (result, keyValuePairs, results.ToArray()); + } + + private static int ConvertFrontMatterToJsonFriendly(ReadOnlyDictionary relativeToCollection) + { + int result = 0; + List results = new(); + bool write; + string[] lines; + MarkdownFile markdownFile; + string[] frontMatterYamlLines; + foreach (KeyValuePair relativeTo in relativeToCollection) + { + if (!relativeTo.Value.Lines.Any()) + continue; + results.Clear(); + lines = relativeTo.Value.Lines; + markdownFile = relativeTo.Value.MarkdownFile; + if (markdownFile.LineNumber.FrontMatterYamlEnd is null) + continue; + (_, _, frontMatterYamlLines) = Get(markdownFile.LineNumber.FrontMatterYamlEnd.Value, lines); + if (!frontMatterYamlLines.Any()) + continue; + results.Add("---"); + results.AddRange(frontMatterYamlLines); + results.Add("---"); + for (int i = markdownFile.LineNumber.FrontMatterYamlEnd.Value + 1; i < lines.Length; i++) + results.Add(lines[i]); + if (results.Count == lines.Length) + { + write = false; + for (int i = 0; i < lines.Length; i++) + { + if (results[i] == lines[i]) + continue; + write = true; + break; + } + if (!write) + continue; + } + File.WriteAllLines(markdownFile.File, results); + File.SetLastWriteTime(markdownFile.File, markdownFile.LastWriteDateTime); + result += 1; + } + return result; + } + + private static int CircularReference(ILogger logger, ReadOnlyDictionary relativeToCollection) + { + int result = 0; + string line; + string check; + string[] lines; + bool circularReference; + MarkdownFile markdownFile; + foreach (KeyValuePair relativeTo in relativeToCollection) + { + if (!relativeTo.Value.Lines.Any()) + continue; + circularReference = false; + lines = relativeTo.Value.Lines; + markdownFile = relativeTo.Value.MarkdownFile; + for (int i = 0; i < lines.Length; i++) + { + check = $"[[{markdownFile.FileNameWithoutExtension}]]"; + if (!lines[i].Contains(check)) + continue; + line = lines[i].Replace(check, $"~~{markdownFile.FileName}~~"); + if (lines[i] == line) + continue; + lines[i] = line; + if (!circularReference) + circularReference = true; + } + for (int i = 0; i < lines.Length; i++) + { + check = $"{markdownFile.FileNameWithoutExtension}|{markdownFile.FileNameWithoutExtension}]]"; + if (!lines[i].Contains(check)) + continue; + line = lines[i].Replace(check, $"~~{markdownFile.FileName}~~"); + if (lines[i] == line) + continue; + lines[i] = line; + if (!circularReference) + circularReference = true; + } + for (int i = 0; i < lines.Length; i++) + { + check = $"[{markdownFile.FileNameWithoutExtension}]({markdownFile.FileName})"; + if (!lines[i].Contains(check)) + continue; + line = lines[i].Replace(check, $"~~{markdownFile.FileName}~~"); + if (lines[i] == line) + continue; + lines[i] = line; + logger.LogInformation("circular reference for <{file}>", markdownFile.FileName); + if (!circularReference) + circularReference = true; + } + if (circularReference) + { + File.WriteAllLines(markdownFile.File, lines); + result += 1; + } + } + return result; + } + + private static int FindReplace(ReadOnlyDictionary relativeToCollection) + { + int result = 0; + bool found; + string line; + string check; + string[] lines; + MarkdownFile markdownFile; + foreach (KeyValuePair relativeTo in relativeToCollection) + { + if (!relativeTo.Value.Lines.Any()) + continue; + found = false; + lines = relativeTo.Value.Lines; + markdownFile = relativeTo.Value.MarkdownFile; + for (int i = 0; i < lines.Length; i++) + { + check = $"[[K-A/"; + if (!lines[i].Contains(check)) + continue; + line = lines[i].Replace(check, "[[.kanbn/Archive/"); + if (lines[i] == line) + continue; + lines[i] = line; + if (!found) + found = true; + } + for (int i = 0; i < lines.Length; i++) + { + check = $"[[K-T/"; + if (!lines[i].Contains(check)) + continue; + line = lines[i].Replace(check, "[[.kanbn/Tasks/"); + if (lines[i] == line) + continue; + lines[i] = line; + if (!found) + found = true; + } + if (found) + { + File.WriteAllLines(markdownFile.File, lines); + result += 1; + } + } + return result; + } + + private static ReadOnlyDictionary> GetKeyValuePairs(ReadOnlyDictionary relativeToCollection) + { + Dictionary> results = new(); + MarkdownFile markdownFile; + string fileNameWithoutExtension; + string fileNameWithoutExtensionB; + List? markdownFiles; + foreach (KeyValuePair relativeTo in relativeToCollection) + { + markdownFile = relativeTo.Value.MarkdownFile; + if (!results.TryGetValue(relativeTo.Key, out markdownFiles)) + { + results.Add(relativeTo.Key, new()); + if (!results.TryGetValue(relativeTo.Key, out markdownFiles)) + throw new NotSupportedException(); + } + markdownFiles.Add(relativeTo.Value); + } + foreach (KeyValuePair relativeTo in relativeToCollection) + { + markdownFile = relativeTo.Value.MarkdownFile; + fileNameWithoutExtension = markdownFile.FileNameWithoutExtension.ToLower(); + fileNameWithoutExtensionB = fileNameWithoutExtension.Replace("%20", "-").Replace(' ', '-'); + if (!results.TryGetValue(markdownFile.FileNameWithoutExtension, out markdownFiles)) + { + results.Add(markdownFile.FileNameWithoutExtension, new()); + if (!results.TryGetValue(markdownFile.FileNameWithoutExtension, out markdownFiles)) + throw new NotSupportedException(); + } + markdownFiles.Add(relativeTo.Value); + if (fileNameWithoutExtension == markdownFile.FileNameWithoutExtension) + continue; + if (!results.TryGetValue(fileNameWithoutExtension, out markdownFiles)) + { + results.Add(fileNameWithoutExtension, new()); + if (!results.TryGetValue(fileNameWithoutExtension, out markdownFiles)) + throw new NotSupportedException(); + } + if (fileNameWithoutExtensionB == markdownFile.FileNameWithoutExtension) + continue; + if (!results.TryGetValue(fileNameWithoutExtensionB, out markdownFiles)) + { + results.Add(fileNameWithoutExtensionB, new()); + if (!results.TryGetValue(fileNameWithoutExtensionB, out markdownFiles)) + throw new NotSupportedException(); + } + markdownFiles.Add(relativeTo.Value); + } + foreach (KeyValuePair relativeTo in relativeToCollection) + { + markdownFile = relativeTo.Value.MarkdownFile; + if (!results.TryGetValue(markdownFile.H1, out markdownFiles)) + { + results.Add(markdownFile.H1, new()); + if (!results.TryGetValue(markdownFile.H1, out markdownFiles)) + throw new NotSupportedException(); + } + markdownFiles.Add(relativeTo.Value); + } + return new(results); + } + + private static List GetMarkdownFileAndLines(string file, List markdownFiles) + { + List results = new(); + List distinct = new(); + string? directory = Path.GetDirectoryName(file); + foreach (MarkdownFileAndLines markdownFileAndLines in markdownFiles) + { + if (string.IsNullOrEmpty(directory) || markdownFileAndLines.MarkdownFile.Directory != directory) + continue; + if (distinct.Contains(markdownFileAndLines.MarkdownFile.File)) + continue; + distinct.Add(markdownFileAndLines.MarkdownFile.File); + results.Add(markdownFileAndLines); + } + return results; + } + + private static List GetMarkdownFileAndLines(ReadOnlyDictionary> keyValuePairs) + { + List results = new(); + foreach (KeyValuePair> keyValuePair in keyValuePairs) + { + foreach (MarkdownFileAndLines markdownFileAndLines in keyValuePair.Value) + results.Add(markdownFileAndLines); + } + return results; + } + + private static List Distinct(IEnumerable? markdownFileAndLinesCollection) + { + List results = new(); + if (markdownFileAndLinesCollection is not null) + { + List distinct = new(); + foreach (MarkdownFileAndLines markdownFileAndLines in markdownFileAndLinesCollection) + { + if (distinct.Contains(markdownFileAndLines.MarkdownFile.File)) + continue; + distinct.Add(markdownFileAndLines.MarkdownFile.File); + results.Add(markdownFileAndLines); + } + } + return results; + } + + private static MarkdownFileAndLines? GetMarkdownFile(ReadOnlyDictionary> keyValuePairs, MarkdownFile markdownFile, string file) + { + MarkdownFileAndLines? result; + List? markdownFileAndLinesCollection; + string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file); + if (!keyValuePairs.TryGetValue(fileNameWithoutExtension, out markdownFileAndLinesCollection)) + _ = keyValuePairs.TryGetValue(fileNameWithoutExtension.ToLower(), out markdownFileAndLinesCollection); + markdownFileAndLinesCollection = Distinct(markdownFileAndLinesCollection); + if (markdownFileAndLinesCollection is not null && markdownFileAndLinesCollection.Count == 1) + result = markdownFileAndLinesCollection[0]; + else + { + List matches; + matches = markdownFileAndLinesCollection is null ? new() : GetMarkdownFileAndLines(file, markdownFileAndLinesCollection); + if (matches.Count == 1) + result = matches[0]; + else + { + markdownFileAndLinesCollection = GetMarkdownFileAndLines(keyValuePairs); + matches = Distinct(markdownFileAndLinesCollection.Where(l => l.MarkdownFile.FileNameWithoutExtension.Length == fileNameWithoutExtension.Length && l.MarkdownFile.FileNameWithoutExtension.Contains(fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase))); + if (matches.Count == 1) + result = matches[0]; + else + { + string checkName = fileNameWithoutExtension.ToLower().Replace("%20", "-").Replace(' ', '-'); + matches = Distinct(markdownFileAndLinesCollection.Where(l => l.MarkdownFile.FileNameWithoutExtension.Length == checkName.Length && l.MarkdownFile.FileNameWithoutExtension.Contains(checkName, StringComparison.OrdinalIgnoreCase))); + if (matches.Count == 1) + result = matches[0]; + else if (matches.Count == 0) + result = null; + else + { + checkName = matches[0].MarkdownFile.FileNameWithoutExtension; + matches = Distinct(markdownFileAndLinesCollection.Where(l => l.MarkdownFile.File.Contains(checkName, StringComparison.OrdinalIgnoreCase))); + if (matches.Count == 1) + result = matches[0]; + else + { + checkName = $"{checkName}{markdownFile.Extension}"; + matches = Distinct(markdownFileAndLinesCollection.Where(l => l.MarkdownFile.File.EndsWith(checkName, StringComparison.OrdinalIgnoreCase))); + if (matches.Count == 1) + result = matches[0]; + else + { + checkName = $"\\{checkName}"; + matches = Distinct(markdownFileAndLinesCollection.Where(l => l.MarkdownFile.File.EndsWith(checkName, StringComparison.OrdinalIgnoreCase))); + if (matches.Count == 1) + result = matches[0]; + else + result = null; + } + } + } + } + } + } + return result; + } + + private static MarkdownFileH1AndRelativePath GetRelativePath(ReadOnlyDictionary> keyValuePairs, MarkdownFile markdownFile, string file) + { + MarkdownFileAndLines? result = GetMarkdownFile(keyValuePairs, markdownFile, file); + return new(result?.MarkdownFile, result?.Lines, result?.MarkdownFile.H1, result is null ? null : Path.GetRelativePath(markdownFile.Directory, Path.GetFullPath(result.MarkdownFile.File))); + } + + private static int ConvertToRelativePath(ILogger logger, ReadOnlyDictionary relativeToCollection) + { + int result = 0; + bool write; + string line; + string[] lines; + string[] segmentsA; + string[] segmentsB; + string[] segmentsC; + MarkdownFile markdownFile; + MarkdownFileH1AndRelativePath markdownFileH1AndRelativePath; + ReadOnlyDictionary> keyValuePairs = GetKeyValuePairs(relativeToCollection); + foreach (KeyValuePair relativeTo in relativeToCollection) + { + if (!relativeTo.Value.Lines.Any()) + continue; + write = false; + lines = relativeTo.Value.Lines; + markdownFile = relativeTo.Value.MarkdownFile; + for (int i = 0; i < lines.Length; i++) + { + segmentsA = lines[i].Split("]]"); + if (segmentsA.Length is not 2 or 3) + continue; + segmentsB = segmentsA[0].Split("[["); + if (segmentsB.Length is not 2 or 3) + continue; + segmentsC = segmentsB[^1].Split('|'); + markdownFileH1AndRelativePath = GetRelativePath(keyValuePairs, markdownFile, segmentsC[0]); + if (markdownFileH1AndRelativePath.MarkdownFile is null || markdownFileH1AndRelativePath.H1 is null || markdownFileH1AndRelativePath.RelativePath is null) + { + logger.LogInformation("Didn't find {line} in <{file}>", lines[i], markdownFile.FileNameWithoutExtension); + continue; + } + line = $"{segmentsB[0]}[{markdownFileH1AndRelativePath.H1}]({markdownFileH1AndRelativePath.RelativePath.Replace('\\', '/')}){segmentsA[^1]}"; + if (lines[i] == line) + continue; + lines[i] = line; + if (!write) + write = true; + } + if (write) + { + File.WriteAllLines(markdownFile.File, lines); + result += 1; + } + } + return result; + } + + private static int ConvertFileToSlugName(ReadOnlyDictionary relativeToCollection) + { + int result = 0; + string h1; + string h1Check; + string[] lines; + string checkName; + string checkFileName; + MarkdownFile markdownFile; + foreach (KeyValuePair relativeTo in relativeToCollection) + { + if (!relativeTo.Value.Lines.Any()) + continue; + lines = relativeTo.Value.Lines; + markdownFile = relativeTo.Value.MarkdownFile; + if (markdownFile.LineNumber.H1 is not null) + { + h1 = lines[markdownFile.LineNumber.H1.Value]; + if (h1.Length > 2) + { + h1Check = $"# {h1[2..]}"; + if (h1Check.Length == h1.Length && h1Check != h1) + { + lines[markdownFile.LineNumber.H1.Value] = h1Check; + File.WriteAllLines(markdownFile.File, lines); + result += 1; + } + } + } + checkFileName = markdownFile.FileName.ToLower().Replace("%20", "-").Replace(' ', '-'); + if (checkFileName == markdownFile.FileName) + continue; + if (!File.Exists(markdownFile.File)) + continue; + checkName = Path.Combine(markdownFile.Directory, checkFileName); + if (checkName == markdownFile.File) + continue; + File.Move(markdownFile.File, checkName); + result += 1; + } + return result; + } + + private static int ConvertFileToSlugName(AppSettings appSettings, ILogger logger, ReadOnlyDictionary relativeToCollection) + { + int result = 0; + bool write; + string file; + string line; + string[] lines; + string fileName; + string checkName; + string? directory; + string[] segmentsA; + string[] segmentsB; + string[] segmentsC; + string checkFileName; + string segmentsALast; + string segmentsBFirst; + MarkdownFile markdownFile; + MarkdownFileH1AndRelativePath markdownFileH1AndRelativePath; + ReadOnlyDictionary> keyValuePairs = GetKeyValuePairs(relativeToCollection); + foreach (KeyValuePair relativeTo in relativeToCollection) + { + if (!relativeTo.Value.Lines.Any()) + continue; + lines = relativeTo.Value.Lines; + markdownFile = relativeTo.Value.MarkdownFile; + if (markdownFile.FileNameWithoutExtension == "index" && markdownFile.Directory.EndsWith(".kanbn")) + continue; + if (!File.Exists(markdownFile.File)) + continue; + write = false; + for (int i = 0; i < lines.Length; i++) + { + segmentsA = lines[i].Split("]("); + if (segmentsA.Length != 2) + continue; + segmentsALast = segmentsA[^1]; + if (appSettings.ExcludeSchemes.Any(l => segmentsALast.StartsWith(l))) + continue; + segmentsB = segmentsALast.Split(")"); + if (segmentsB.Length != 2) + continue; + segmentsBFirst = segmentsB[0]; + file = Path.GetFullPath(Path.Combine(markdownFile.Directory, segmentsBFirst)); + fileName = Path.GetFileName(file); + directory = Path.GetDirectoryName(file); + if (string.IsNullOrEmpty(directory)) + continue; + checkFileName = fileName.ToLower().Replace("%20", "-").Replace(' ', '-'); + checkName = Path.Combine(directory, checkFileName); + segmentsC = segmentsA[0].Split('['); + markdownFileH1AndRelativePath = GetRelativePath(keyValuePairs, markdownFile, file); + if (markdownFileH1AndRelativePath.MarkdownFile is null || markdownFileH1AndRelativePath.H1 is null || markdownFileH1AndRelativePath.RelativePath is null) + { + logger.LogInformation("Didn't find {line} in <{file}>", lines[i], markdownFile.FileNameWithoutExtension); + continue; + } + line = $"{string.Join('[', segmentsC, 0, segmentsC.Length - 1)}[{markdownFileH1AndRelativePath.H1}]({markdownFileH1AndRelativePath.RelativePath.Replace('\\', '/')}){segmentsB[^1]}"; + if (lines[i] == line) + continue; + if (fileName.Contains(' ') || fileName.Contains("%20")) + { + if (!File.Exists(file)) + { + logger.LogInformation("Didn't find <{file}>", file); + continue; + } + if (File.Exists(checkName)) + continue; + File.Move(file, checkName); + } + else if (fileName != fileName.ToLower()) + { + if (file != checkName) + { + if (!File.Exists(file)) + { + logger.LogInformation("Didn't find <{file}>", file); + continue; + } + File.Move(file, checkName); + } + } + lines[i] = line; + if (!write) + write = true; + } + if (write) + { + File.WriteAllLines(markdownFile.File, lines); + result += 1; + } + } + if (result == 0) + result = ConvertFileToSlugName(relativeToCollection); + return result; + } + + private static void SetRecursiveLines(AppSettings appSettings, ILogger logger, ReadOnlyDictionary> keyValuePairs, string linkTitle, MarkdownFile markdownFile, string[] lines, List indentations, List recursiveLines) + { + if (recursiveLines is null) + throw new Exception(); + string file; + string[] segmentsA; + string[] segmentsB; + string segmentsALast; + bool fencedCodeBlock = false; + string indentation = new(indentations.ToArray()); + MarkdownFileH1AndRelativePath markdownFileH1AndRelativePath; + for (int i = 0; i < lines.Length; i++) + { + if (indentations.Count > 15) + { + recursiveLines.Add("```Error```"); + break; + } + if (lines[i].Length < 1) + continue; + if (lines[i].Length > 4 && lines[i][..3] == "```") + fencedCodeBlock = !fencedCodeBlock; + if (fencedCodeBlock) + continue; + if (lines[i][0] == '#') + { + if (lines[i] == $"# {linkTitle}") + continue; + recursiveLines.Add($"{indentation}{lines[i]}"); + continue; + } + segmentsA = lines[i].Split("]("); + if (segmentsA.Length != 2) + continue; + segmentsALast = segmentsA[^1]; + if (appSettings.ExcludeSchemes.Any(l => segmentsALast.StartsWith(l))) + continue; + segmentsB = segmentsALast.Split(")"); + if (segmentsB.Length != 2) + continue; + file = Path.GetFullPath(Path.Combine(markdownFile.Directory, segmentsB[0])); + markdownFileH1AndRelativePath = GetRelativePath(keyValuePairs, markdownFile, file); + if (markdownFileH1AndRelativePath.MarkdownFile is null || markdownFileH1AndRelativePath.H1 is null || markdownFileH1AndRelativePath.RelativePath is null) + { + recursiveLines.Add($"???{indentation}{lines[i]}"); + logger.LogInformation("Didn't find {line} in <{file}>", lines[i], markdownFile.FileNameWithoutExtension); + continue; + } + if (markdownFileH1AndRelativePath.Lines is null) + continue; + indentations.Add('\t'); + recursiveLines.Add($"{indentation}{lines[i]}"); + SetRecursiveLines(appSettings, logger, keyValuePairs, segmentsA[0].Split('[')[^1], markdownFileH1AndRelativePath.MarkdownFile, markdownFileH1AndRelativePath.Lines, indentations, recursiveLines); + } + if (indentations.Count > 0) + indentations.RemoveAt(0); + } + + private static List GetRecursiveLines(AppSettings appSettings, Record record, ILogger logger, ReadOnlyDictionary relativeToCollection) + { + List results = new(); + string[] lines; + List indentations; + MarkdownFile markdownFile; + List recursiveLines; + ReadOnlyDictionary> keyValuePairs = GetKeyValuePairs(relativeToCollection); + foreach (KeyValuePair relativeTo in relativeToCollection) + { + if (!relativeTo.Value.Lines.Any()) + continue; + if (record.StartAt is null || !relativeTo.Value.MarkdownFile.File.StartsWith(record.StartAt) || Path.GetFileName(relativeTo.Value.MarkdownFile.Directory) != Path.GetFileName(record.StartAt)) + continue; + indentations = new(); + recursiveLines = new(); + lines = relativeTo.Value.Lines; + markdownFile = relativeTo.Value.MarkdownFile; + SetRecursiveLines(appSettings, logger, keyValuePairs, markdownFile.FileNameWithoutExtension, markdownFile, lines, indentations, recursiveLines); + results.Add(new(relativeTo.Value.MarkdownFile, recursiveLines.ToArray())); + } + return results; + } + + private static void Write(Record record, List markdownFileAndLinesCollection) + { + foreach (MarkdownFileAndLines markdownFileAndLines in markdownFileAndLinesCollection) + { + if (record.Destination is null) + continue; + File.WriteAllLines(Path.Combine(record.Destination, markdownFileAndLines.MarkdownFile.FileName), markdownFileAndLines.Lines); + } + } + + internal static void MarkdownWikiLinkVerification(AppSettings appSettings, ILogger logger, List args) + { + int updated; + Record record = GetRecord(args); + ReadOnlyDictionary relativeToCollection; + relativeToCollection = GetRelativeToCollection(appSettings, record); + updated = SetFrontMatterAndH1(appSettings, relativeToCollection); + if (updated != 0) + { + relativeToCollection = GetRelativeToCollection(appSettings, record); + logger.LogInformation("{updated} Markdown file(s) were updated", updated); + } + updated = ConvertFrontMatterToJsonFriendly(relativeToCollection); + if (updated != 0) + { + relativeToCollection = GetRelativeToCollection(appSettings, record); + logger.LogInformation("{updated} Markdown file(s) were updated", updated); + } + updated = CircularReference(logger, relativeToCollection); + if (updated != 0) + { + relativeToCollection = GetRelativeToCollection(appSettings, record); + logger.LogInformation("{updated} Markdown file(s) were updated", updated); + } + updated = FindReplace(relativeToCollection); + if (updated != 0) + { + relativeToCollection = GetRelativeToCollection(appSettings, record); + logger.LogInformation("{updated} Markdown file(s) were updated", updated); + } + updated = ConvertToRelativePath(logger, relativeToCollection); + if (updated != 0) + { + relativeToCollection = GetRelativeToCollection(appSettings, record); + logger.LogInformation("{updated} Markdown file(s) were updated", updated); + } + updated = ConvertFileToSlugName(appSettings, logger, relativeToCollection); + if (updated != 0) + { + relativeToCollection = GetRelativeToCollection(appSettings, record); + logger.LogInformation("{updated} Markdown file(s) were updated", updated); + } + if (!string.IsNullOrEmpty(record.StartAt)) + { + relativeToCollection = GetRelativeToCollection(appSettings, record, force: true); + List markdownFileAndLinesCollection = GetRecursiveLines(appSettings, record, logger, relativeToCollection); + if (!string.IsNullOrEmpty(record.Destination)) + Write(record, markdownFileAndLinesCollection); + } + string directory = Path.Combine(Environment.CurrentDirectory, ".vscode"); + if (!Directory.Exists(directory)) + { + string json; + MarkdownFile markdownFile = relativeToCollection.ElementAt(0).Value.MarkdownFile; + json = JsonSerializer.Serialize(markdownFile, MarkdownFileSourceGenerationContext.Default.MarkdownFile); + if (json != "{}") + { + json = JsonSerializer.Serialize(relativeToCollection.Select(l => l.Value.MarkdownFile).ToArray(), MarkdownFileCollectionSourceGenerationContext.Default.MarkdownFileArray); + File.WriteAllText($"{DateTime.Now.Ticks}.json", json); + } + } + } + + private static List<(string, string, string[])> GetWithLinksForHugo(AppSettings appSettings, Record record) + { + List<(string, string, string[])> results = new(); + string file; + string line; + string[] lines; + string fileName; + string? directory; + string[] segmentsA; + string[] segmentsB; + string relativeFile; + string segmentsALast; + string segmentsBFirst; + MarkdownFile markdownFile; + int sourceDirectoryLength = record.Source.Length; + ReadOnlyDictionary relativeToCollection = GetRelativeToCollection(appSettings, record); + foreach (KeyValuePair relativeTo in relativeToCollection) + { + if (!relativeTo.Value.Lines.Any()) + continue; + lines = relativeTo.Value.Lines; + markdownFile = relativeTo.Value.MarkdownFile; + if (record.Destination is null) + continue; + if (markdownFile.File.Length < sourceDirectoryLength) + continue; + if (!File.Exists(markdownFile.File)) + continue; + fileName = $"{record.Destination}{markdownFile.File[sourceDirectoryLength..]}"; + directory = Path.GetDirectoryName(fileName); + if (string.IsNullOrEmpty(directory)) + continue; + for (int i = 0; i < lines.Length; i++) + { + segmentsA = lines[i].Split("]("); + if (segmentsA.Length != 2) + continue; + segmentsALast = segmentsA[^1]; + if (appSettings.ExcludeSchemes.Any(l => segmentsALast.StartsWith(l))) + continue; + segmentsB = segmentsALast.Split(")"); + if (segmentsB.Length != 2) + continue; + segmentsBFirst = segmentsB[0]; + if (!segmentsBFirst.EndsWith(".md")) + file = Path.GetFullPath(Path.Combine(markdownFile.Directory, segmentsBFirst)); + else + file = Path.GetFullPath(Path.Combine(markdownFile.Directory, segmentsBFirst[..^3])); + relativeFile = Path.GetRelativePath(record.Source, file).Replace('\\', '/'); + line = $"{segmentsA[0]}]({relativeFile}){segmentsB[^1]}"; + if (lines[i] == line) + throw new NotSupportedException($"Line {i} shouldn't match with {line}"); + lines[i] = line; + } + results.Add((directory, fileName, lines)); + } + return results; + } + + private static List GetDistinct(List<(string, string, string[])> collection) + { + List results = new(); + foreach ((string directory, _, _) in collection) + { + if (results.Contains(directory)) + continue; + results.Add(directory); + } + return results; + } + + private static void CreateMissingDirectories(List directories) + { + foreach (string directory in directories) + { + if (!Directory.Exists(directory)) + _ = Directory.CreateDirectory(directory); + } + } + + internal static void MarkdownConvertLinksForHugo(AppSettings appSettings, ILogger logger, List args) + { + Record record = GetRecord(args); + if (string.IsNullOrEmpty(record.Destination)) + throw new NotSupportedException("This method requires frontMatterYamlLines -d path!"); + List<(string, string, string[])> collection = GetWithLinksForHugo(appSettings, record); + if (!collection.Any()) + logger.LogInformation("No files?"); + List distinct = GetDistinct(collection); + CreateMissingDirectories(distinct); + foreach ((_, string file, string[] lines) in collection) + File.WriteAllLines(file, lines); + } + +} \ No newline at end of file diff --git a/Helpers/HelperCreateNoteFiles.cs b/Helpers/HelperCreateNoteFiles.cs index 703ca6e..540e370 100644 --- a/Helpers/HelperCreateNoteFiles.cs +++ b/Helpers/HelperCreateNoteFiles.cs @@ -10,6 +10,73 @@ internal static partial class HelperCreateNoteFiles [GeneratedRegex("[^a-z0-9-]")] private static partial Regex AlphaNumOnly(); + private static string? GetTags(string tagsText) + { + string? result; + StringBuilder stringBuilder = new(); + if (string.IsNullOrEmpty(tagsText)) + result = null; + else + { + string[] segments; + _ = stringBuilder.AppendLine("tags:"); + string[] tags = tagsText.Split(';', StringSplitOptions.RemoveEmptyEntries); + foreach (string tag in tags) + { + segments = tag.Split(':'); + _ = stringBuilder.AppendLine($"- '{segments.First()}'"); + } + result = stringBuilder.ToString().Trim(); + } + return result; + } + + private static string? GetLinks(string type, string linksText) + { + string? result; + StringBuilder stringBuilder = new(); + if (!string.IsNullOrEmpty(linksText)) + result = null; + else + { + string linkLower; + string[] segments; + string[] links = linksText.Split(';', StringSplitOptions.RemoveEmptyEntries); + foreach (string link in links) + { + segments = link.Split(':'); + linkLower = AlphaNumOnly().Replace(segments.First().Trim().ToLower(), "-").Replace("--", "-"); + if (segments.Length == 1) + _ = stringBuilder.AppendLine($"- [[{type}/{linkLower}]]"); + else if (segments.Length == 2) + _ = stringBuilder.AppendLine($"- [{type}/{linkLower}]({segments.Last()})"); + else + continue; + } + result = stringBuilder.ToString().Trim(); + } + return result; + } + + private static string? GetAttributes(string[] columns, string[]? headerColumns, int expectedCount) + { + string? result; + if (headerColumns is null || columns.Length <= expectedCount) + result = null; + else + { + StringBuilder stringBuilder = new(); + for (int j = expectedCount; j < columns.Length; j++) + { + if (headerColumns.Length <= j) + continue; + _ = stringBuilder.AppendLine($"{headerColumns[j].Trim()}: '{columns[j].Trim()}'"); + } + result = stringBuilder.ToString().Trim(); + } + return result; + } + private static void CleanExistingFiles(string directory, long ticks) { string check; @@ -100,73 +167,6 @@ internal static partial class HelperCreateNoteFiles } } - private static string? GetTags(string tagsText) - { - string? result; - StringBuilder stringBuilder = new(); - if (string.IsNullOrEmpty(tagsText)) - result = null; - else - { - string[] segments; - _ = stringBuilder.AppendLine("tags:"); - string[] tags = tagsText.Split(';', StringSplitOptions.RemoveEmptyEntries); - foreach (string tag in tags) - { - segments = tag.Split(':'); - _ = stringBuilder.AppendLine($"- '{segments.First()}'"); - } - result = stringBuilder.ToString().Trim(); - } - return result; - } - - private static string? GetLinks(string type, string linksText) - { - string? result; - StringBuilder stringBuilder = new(); - if (!string.IsNullOrEmpty(linksText)) - result = null; - else - { - string linkLower; - string[] segments; - string[] links = linksText.Split(';', StringSplitOptions.RemoveEmptyEntries); - foreach (string link in links) - { - segments = link.Split(':'); - linkLower = AlphaNumOnly().Replace(segments.First().Trim().ToLower(), "-").Replace("--", "-"); - if (segments.Length == 1) - _ = stringBuilder.AppendLine($"- [[{type}/{linkLower}]]"); - else if (segments.Length == 2) - _ = stringBuilder.AppendLine($"- [{type}/{linkLower}]({segments.Last()})"); - else - continue; - } - result = stringBuilder.ToString().Trim(); - } - return result; - } - - private static string? GetAttributes(string[] columns, string[]? headerColumns, int expectedCount) - { - string? result; - if (headerColumns is null || columns.Length <= expectedCount) - result = null; - else - { - StringBuilder stringBuilder = new(); - for (int j = expectedCount; j < columns.Length; j++) - { - if (headerColumns.Length <= j) - continue; - _ = stringBuilder.AppendLine($"{headerColumns[j].Trim()}: '{columns[j].Trim()}'"); - } - result = stringBuilder.ToString().Trim(); - } - return result; - } - private static void CreateImportFiles(long ticks, List importFiles) { bool csv; diff --git a/Helpers/HelperGenealogicalDataCommunication.cs b/Helpers/HelperGenealogicalDataCommunication.cs index fc36a0a..1484f7b 100644 --- a/Helpers/HelperGenealogicalDataCommunication.cs +++ b/Helpers/HelperGenealogicalDataCommunication.cs @@ -29,44 +29,60 @@ internal static partial class HelperGenealogicalDataCommunication char Sex, char First); - [GeneratedRegex("[\\\\,\\/,\\:,\\*,\\?,\\\",\\<,\\>,\\|]")] - private static partial Regex WindowsFileSystem(); + private record PersonExport(long Id, + ReadOnlyCollection Lines, + string PersonKeyFormatted, + char[] AgeCollection, + DateTime DateTime, + long PersonKey); - private static Input GetInput(List args) + private record Family(string? Title, + string? Id, + string? Index, + string PersonName, + Person Person, + long PersonKey, + string? LineTwo); + + private static string? GetFaceBook(Person person) => + person.Birth?.Continue.Where(l => !l.Contains("profile.php?id=") && l.StartsWith("https://www.facebook.com/")).Select(l => l[25..].Split('/')[0]).FirstOrDefault(); + + private static string? GetFaceBookId(Person person) => + person.Birth?.Continue.Where(l => l.StartsWith("https://www.facebook.com/profile.php?id=")).Select(l => l[40..].Split('&')[0]).FirstOrDefault(); + + private static ReadOnlyDictionary> Convert(Dictionary> keyValuePairs) { - Input result; - string? destination = null; - string? singletonDirectory = null; - string genealogicalDataCommunicationRootDirectory = Path.GetFullPath(args[0]); - string fileName = Path.GetFileName(genealogicalDataCommunicationRootDirectory); - string[] files = Directory.GetFiles(genealogicalDataCommunicationRootDirectory, $"{fileName}.ged", SearchOption.TopDirectoryOnly); - string genealogicalDataCommunicationDirectory = Path.Combine(genealogicalDataCommunicationRootDirectory, fileName); - if (!Directory.Exists(genealogicalDataCommunicationDirectory)) - _ = Directory.CreateDirectory(genealogicalDataCommunicationDirectory); - for (int i = 1; i < args.Count; i++) - { - if (args[i].Length == 2 && i + 1 < args.Count) - { - if (args[i][1] == 's') - singletonDirectory = Path.GetFullPath(args[i + 1]); - else if (args[i][1] == 'd') - destination = Path.GetFullPath(args[i + 1]); - i++; - } - } - string? genealogicalDataCommunicationFile = files.Length != 1 ? null : files[0]; - if (destination is not null) - { - string? root = Path.GetPathRoot(destination); - if (root is null || !Directory.Exists(root)) - throw new NotSupportedException($"This method requires frontMatterYamlLines valid -d path <{root}>!"); - if (!Directory.Exists(destination)) - _ = Directory.CreateDirectory(destination); - } - result = new(genealogicalDataCommunicationDirectory, genealogicalDataCommunicationFile, singletonDirectory, destination); - return result; + Dictionary> results = new(); + foreach (KeyValuePair> keyValuePair in keyValuePairs) + results.Add(keyValuePair.Key, new(keyValuePair.Value)); + return new(results); } + private static Dictionary>> Convert(Dictionary>> keyValuePairs) + { + Dictionary>> results = new(); + foreach (KeyValuePair>> keyValuePair in keyValuePairs) + results.Add(keyValuePair.Key, new(keyValuePair.Value)); + return new(results); + } + + private static ReadOnlyCollection GetObjectCollection(Person? person) + { + List results; + if (person is null) + results = new() { "Id", "First-Name", "Last-Name", "Birth-Date", "Sex", "Address", "City", "State", "Zip", "Phone", "E-mail", "Facebook", "Facebook-Id", "Comment", "U-Id" }; + else + { + string? facebook = GetFaceBook(person); + string? facebookId = GetFaceBookId(person); + results = new() { person.Id, person.Name?.Given, person.Name?.Sur, person.Birth?.Date.ToString(), person.Sex, null, null, "NM", null, null, null, facebook, facebookId, null, person.UId }; + } + return new(results); + } + + private static string GetKey(Family family) => + $"{family.Id}-{family.Index}".Trim('-'); + private static ReadOnlyCollection GetHeaderLines(string startsWith, string[] sourceLines) { List results = new(); @@ -79,14 +95,6 @@ internal static partial class HelperGenealogicalDataCommunication return new(results); } - private static ReadOnlyDictionary> Convert(Dictionary> keyValuePairs) - { - Dictionary> results = new(); - foreach (KeyValuePair> keyValuePair in keyValuePairs) - results.Add(keyValuePair.Key, new(keyValuePair.Value)); - return new(results); - } - private static long? GetId(string line) { long? result; @@ -95,102 +103,79 @@ internal static partial class HelperGenealogicalDataCommunication return result; } - private static GenealogicalDataCommunicationCollections GetGenealogicalDataCommunicationCollections(Input input) + private static Dictionary>> Convert(ReadOnlyCollection distinctSortedKKeys) { - GenealogicalDataCommunicationCollections result; - long? id; - List lines = new(); - const string startsWith = "0 @"; - List footerLines = new(); - Dictionary> keyValuePairs = new(); - List> familyGroupLines = new(); - string[] sourceLines = string.IsNullOrEmpty(input.GenealogicalDataCommunicationFile) || !File.Exists(input.GenealogicalDataCommunicationFile) ? Array.Empty() : File.ReadAllLines(input.GenealogicalDataCommunicationFile); - ReadOnlyCollection headerLines = GetHeaderLines(startsWith, sourceLines); - for (int i = headerLines.Count; i < sourceLines.Length; i++) + Dictionary>> results = new(); + ReadOnlyCollection collection; + List>? objectCollection; + foreach (string key in distinctSortedKKeys) { - if (!sourceLines[i].StartsWith(startsWith)) + if (results.ContainsKey(key)) continue; - if (sourceLines[i].EndsWith("@ SOUR") || sourceLines[i].EndsWith("@ SUBM") || sourceLines[i].EndsWith("@ OBJE") || sourceLines[i].EndsWith("@ REPO")) - continue; - lines.Add(sourceLines[i]); - if (sourceLines[i].EndsWith("@ FAM")) + if (!results.TryGetValue(key, out objectCollection)) { - lines.Clear(); - for (int j = sourceLines.Length - 1; j >= i; j--) - { - lines.Add(sourceLines[j]); - if (sourceLines[j][0] == '0') - { - if (!sourceLines[j].EndsWith("@ FAM")) - footerLines.AddRange(lines); - else - { - lines.Reverse(); - familyGroupLines.Add(new(lines.ToArray())); - } - lines.Clear(); - } - } - familyGroupLines.Reverse(); - footerLines.Reverse(); - break; + results.Add(key, new()); + if (!results.TryGetValue(key, out objectCollection)) + throw new NotSupportedException(); } - else if (sourceLines[i].EndsWith("@ INDI")) - { - id = GetId(sourceLines[i]); - for (int j = i + 1; j < sourceLines.Length; j++) - { - if (sourceLines[j].StartsWith(startsWith)) - break; - lines.Add(sourceLines[j]); - } - if (id is null) - throw new Exception(string.Join(Environment.NewLine, lines)); - keyValuePairs.Add(id.Value, new()); - if (lines.Count == 0) - continue; - keyValuePairs[id.Value].AddRange(lines); - lines.Clear(); - } - else - throw new NotSupportedException(); + collection = GetObjectCollection(person: null); + objectCollection.Add(collection); } - ReadOnlyDictionary> individualsToLines = Convert(keyValuePairs); - result = new(headerLines, individualsToLines, new(familyGroupLines), new(footerLines)); - return result; + return results; } - private static PersonHour GetPersonHour(string personDisplayDirectoryName, int hour) => - hour == 0 ? new('U', 'U', 'U') : - hour == 1 ? new('U', 'U', 'U') : - hour == 2 ? new('U', 'U', 'U') : - hour == 3 ? new('A', 'U', 'Y') : - hour == 4 ? new('A', 'F', 'Y') : - hour == 5 ? new('A', 'M', 'Y') : - hour == 6 ? new('A', 'F', 'N') : - hour == 7 ? new('A', 'M', 'N') : - hour == 13 ? new('D', 'U', 'Y') : - hour == 14 ? new('D', 'F', 'Y') : - hour == 15 ? new('D', 'M', 'Y') : - hour == 16 ? new('D', 'F', 'N') : - hour == 17 ? new('D', 'M', 'N') : - throw new NotImplementedException(personDisplayDirectoryName); - - private static string GetHourGroup(string personDisplayDirectoryName, int hour) => - hour == 0 ? "Unknown-Unknown-Unknown" : - hour == 1 ? "Unknown-Unknown-Unknown" : - hour == 2 ? "Unknown-Unknown-Unknown" : - hour == 3 ? "Alive-Unknown-Yes" : - hour == 4 ? "Alive-Female-Yes" : - hour == 5 ? "Alive-Male-Yes" : - hour == 6 ? "Alive-Female-No" : - hour == 7 ? "Alive-Male-No" : - hour == 13 ? "Dead-Unknown-Yes" : - hour == 14 ? "Dead-Female-Yes" : - hour == 15 ? "Dead-Male-Yes" : - hour == 16 ? "Dead-Female-No" : - hour == 17 ? "Dead-Male-No" : - throw new NotImplementedException(personDisplayDirectoryName); + private static Dictionary> GetTxtFileCollection(Input input) + { + Dictionary> results = new(); + string[] lines; + string[] directories; + string directoryName; + string? sourceDirectory; + string? parentDirectory; + List? collectionA; + List? collectionB; + string siblingDirectoryName; + string[] files = input.SingletonDirectory is null || !Directory.Exists(input.SingletonDirectory) ? Array.Empty() : Directory.GetFiles(input.SingletonDirectory, "*.txt", SearchOption.AllDirectories); + foreach (string file in files) + { + sourceDirectory = Path.GetDirectoryName(file); + if (sourceDirectory is null) + continue; + parentDirectory = Path.GetDirectoryName(sourceDirectory); + if (parentDirectory is null) + continue; + lines = File.ReadAllLines(file); + if (lines.Length != 1 || lines.Length == 2 && string.IsNullOrEmpty(lines[1])) + continue; + directoryName = Path.GetFileName(sourceDirectory); + if (!results.TryGetValue(directoryName, out collectionA)) + { + results.Add(directoryName, new()); + if (!results.TryGetValue(directoryName, out collectionA)) + throw new Exception(); + } + collectionA.Add(lines[0]); + directories = Directory.GetDirectories(parentDirectory, "*", SearchOption.TopDirectoryOnly); + foreach (string directory in directories) + { + siblingDirectoryName = Path.GetFileName(directory); + collectionA.Add(siblingDirectoryName); + } + foreach (string directory in directories) + { + siblingDirectoryName = Path.GetFileName(directory); + if (!results.TryGetValue(siblingDirectoryName, out collectionB)) + { + results.Add(siblingDirectoryName, new()); + if (!results.TryGetValue(siblingDirectoryName, out collectionB)) + throw new Exception(); + } + collectionB.AddRange(collectionA); + collectionB.Add(lines[0]); + } + } + return results; + } private static (int, Name) GetName(ReadOnlyCollection lines, int i) { @@ -353,67 +338,14 @@ internal static partial class HelperGenealogicalDataCommunication return (i, result); } - private static Dictionary> GetTxtFileCollection(Input input) - { - Dictionary> results = new(); - string[] lines; - string[] directories; - string directoryName; - string? sourceDirectory; - string? parentDirectory; - List? collectionA; - List? collectionB; - string siblingDirectoryName; - string[] files = input.SingletonDirectory is null || !Directory.Exists(input.SingletonDirectory) ? Array.Empty() : Directory.GetFiles(input.SingletonDirectory, "*.txt", SearchOption.AllDirectories); - foreach (string file in files) - { - sourceDirectory = Path.GetDirectoryName(file); - if (sourceDirectory is null) - continue; - parentDirectory = Path.GetDirectoryName(sourceDirectory); - if (parentDirectory is null) - continue; - lines = File.ReadAllLines(file); - if (lines.Length != 1 || lines.Length == 2 && string.IsNullOrEmpty(lines[1])) - continue; - directoryName = Path.GetFileName(sourceDirectory); - if (!results.TryGetValue(directoryName, out collectionA)) - { - results.Add(directoryName, new()); - if (!results.TryGetValue(directoryName, out collectionA)) - throw new Exception(); - } - collectionA.Add(lines[0]); - directories = Directory.GetDirectories(parentDirectory, "*", SearchOption.TopDirectoryOnly); - foreach (string directory in directories) - { - siblingDirectoryName = Path.GetFileName(directory); - collectionA.Add(siblingDirectoryName); - } - foreach (string directory in directories) - { - siblingDirectoryName = Path.GetFileName(directory); - if (!results.TryGetValue(siblingDirectoryName, out collectionB)) - { - results.Add(siblingDirectoryName, new()); - if (!results.TryGetValue(siblingDirectoryName, out collectionB)) - throw new Exception(); - } - collectionB.AddRange(collectionA); - collectionB.Add(lines[0]); - } - } - return results; - } - - private static string[] GetNewLines(string[] lines, Birth? birth) + private static string[] GetNewLines(ReadOnlyCollection lines, Birth? birth) { List results = new(); string six; string text; string seven; List @continue = birth is null ? new() : birth.Continue.ToList(); - for (int i = 0; i < lines.Length; i++) + for (int i = 0; i < lines.Count; i++) { if (birth is null) throw new NotSupportedException(); @@ -425,7 +357,7 @@ internal static partial class HelperGenealogicalDataCommunication { if (six != "1 BIRT") continue; - for (int j = i + 1; j < lines.Length; j++) + for (int j = i + 1; j < lines.Count; j++) { if (lines[j].Length < 7) throw new NotImplementedException(); @@ -451,23 +383,228 @@ internal static partial class HelperGenealogicalDataCommunication return results.ToArray(); } - private static Dictionary GetPeople(Input input, GenealogicalDataCommunicationCollections genealogicalDataCommunicationCollections) + [GeneratedRegex("[\\\\,\\/,\\:,\\*,\\?,\\\",\\<,\\>,\\|]")] + private static partial Regex WindowsFileSystem(); + + private static List GetIdsWhenPersonHasTitle(ReadOnlyDictionary people) + { + List results = new(); + foreach (KeyValuePair keyValuePair in people) + { + if (keyValuePair.Value.Title is null) + continue; + results.Add(keyValuePair.Key); + } + return results; + } + + private static ReadOnlyCollection GetRelations(ReadOnlyCollection> familyGroupLines) + { + List results = new(); + int id; + string relation; + string[] segments; + ReadOnlyCollection familyLines; + for (int i = 0; i < familyGroupLines.Count; i++) + { + familyLines = familyGroupLines[i]; + for (int j = 0; j < familyLines.Count; j++) + { + segments = familyLines[j].Split('@'); + if (segments[0].Length < 3 || segments.Length != 3) + continue; + if (!int.TryParse(segments[1][1..], out id)) + continue; + relation = segments[0][2..].Trim(); + if (j + 1 >= familyLines.Count || familyLines[j + 1].Length < 3 || familyLines[j + 1][..3] != "2 _") + results.Add(new(i, relation, id, null)); + else + results.Add(new(i, relation, id, familyLines[j + 1][2..])); + } + } + return new(results.OrderBy(l => l.FamilyIndex).ToArray()); + } + + private static ReadOnlyCollection GetDistinctSortedKeys(List familyCollection, char personTitleFilter) + { + string[] results; + string key; + List<(string? Index, string Key)> collection = new(); + foreach (Family family in familyCollection) + { + if (family.Id is null) + continue; + if (string.IsNullOrEmpty(family.Title) || family.Title[0] != personTitleFilter) + continue; + key = GetKey(family); + collection.Add((family.Index, key)); + } + results = (from l in collection orderby l.Key, l.Index?.Length descending select l.Key).Distinct().ToArray(); + return new(results); + } + + private static string GetHourGroup(string personDisplayDirectoryName, int hour) => + hour == 0 ? "Unknown-Unknown-Unknown" : + hour == 1 ? "Unknown-Unknown-Unknown" : + hour == 2 ? "Unknown-Unknown-Unknown" : + hour == 3 ? "Alive-Unknown-Yes" : + hour == 4 ? "Alive-Female-Yes" : + hour == 5 ? "Alive-Male-Yes" : + hour == 6 ? "Alive-Female-No" : + hour == 7 ? "Alive-Male-No" : + hour == 13 ? "Dead-Unknown-Yes" : + hour == 14 ? "Dead-Female-Yes" : + hour == 15 ? "Dead-Male-Yes" : + hour == 16 ? "Dead-Female-No" : + hour == 17 ? "Dead-Male-No" : + throw new NotImplementedException(personDisplayDirectoryName); + + private static (int, TimeSpan) GetAge(long minuendTicks, long subtrahendTicks) + { + TimeSpan result; + int years = 0; + DateTime check = new(subtrahendTicks); + for (int i = 0; i < int.MaxValue; i++) + { + check = check.AddYears(1); + if (check.Ticks > minuendTicks) + break; + years += 1; + } + result = new(minuendTicks - check.AddYears(-1).Ticks); + return (years, result); + } + + private static string? GetYearGroup(string year) => + !int.TryParse(year[2..], out int part) ? null : string.Concat(year[..^2], part < 50 ? "--" : "++"); + + private static Input GetInput(List args) + { + Input result; + string? destination = null; + string? singletonDirectory = null; + string genealogicalDataCommunicationRootDirectory = Path.GetFullPath(args[0]); + string fileName = Path.GetFileName(genealogicalDataCommunicationRootDirectory); + string[] files = Directory.GetFiles(genealogicalDataCommunicationRootDirectory, $"{fileName}.ged", SearchOption.TopDirectoryOnly); + string genealogicalDataCommunicationDirectory = Path.Combine(genealogicalDataCommunicationRootDirectory, fileName); + if (!Directory.Exists(genealogicalDataCommunicationDirectory)) + _ = Directory.CreateDirectory(genealogicalDataCommunicationDirectory); + for (int i = 1; i < args.Count; i++) + { + if (args[i].Length == 2 && i + 1 < args.Count) + { + if (args[i][1] == 's') + singletonDirectory = Path.GetFullPath(args[i + 1]); + else if (args[i][1] == 'd') + destination = Path.GetFullPath(args[i + 1]); + i++; + } + } + string? genealogicalDataCommunicationFile = files.Length != 1 ? null : files[0]; + if (destination is not null) + { + string? root = Path.GetPathRoot(destination); + if (root is null || !Directory.Exists(root)) + throw new NotSupportedException($"This method requires frontMatterYamlLines valid -d path <{root}>!"); + if (!Directory.Exists(destination)) + _ = Directory.CreateDirectory(destination); + } + result = new(genealogicalDataCommunicationDirectory, genealogicalDataCommunicationFile, singletonDirectory, destination); + return result; + } + + private static GenealogicalDataCommunicationCollections GetGenealogicalDataCommunicationCollections(Input input) + { + GenealogicalDataCommunicationCollections result; + long? id; + List lines = new(); + const string startsWith = "0 @"; + List footerLines = new(); + Dictionary> keyValuePairs = new(); + List> familyGroupLines = new(); + string[] sourceLines = string.IsNullOrEmpty(input.GenealogicalDataCommunicationFile) || !File.Exists(input.GenealogicalDataCommunicationFile) ? Array.Empty() : File.ReadAllLines(input.GenealogicalDataCommunicationFile); + ReadOnlyCollection headerLines = GetHeaderLines(startsWith, sourceLines); + for (int i = headerLines.Count; i < sourceLines.Length; i++) + { + if (!sourceLines[i].StartsWith(startsWith)) + continue; + if (sourceLines[i].EndsWith("@ SOUR") || sourceLines[i].EndsWith("@ SUBM") || sourceLines[i].EndsWith("@ OBJE") || sourceLines[i].EndsWith("@ REPO")) + continue; + lines.Add(sourceLines[i]); + if (sourceLines[i].EndsWith("@ FAM")) + { + lines.Clear(); + for (int j = sourceLines.Length - 1; j >= i; j--) + { + lines.Add(sourceLines[j]); + if (sourceLines[j][0] == '0') + { + if (!sourceLines[j].EndsWith("@ FAM")) + footerLines.AddRange(lines); + else + { + lines.Reverse(); + familyGroupLines.Add(new(lines.ToArray())); + } + lines.Clear(); + } + } + familyGroupLines.Reverse(); + footerLines.Reverse(); + break; + } + else if (sourceLines[i].EndsWith("@ INDI")) + { + id = GetId(sourceLines[i]); + for (int j = i + 1; j < sourceLines.Length; j++) + { + if (sourceLines[j].StartsWith(startsWith)) + break; + lines.Add(sourceLines[j]); + } + if (id is null) + throw new Exception(string.Join(Environment.NewLine, lines)); + keyValuePairs.Add(id.Value, new()); + if (lines.Count == 0) + continue; + keyValuePairs[id.Value].AddRange(lines); + lines.Clear(); + } + else + throw new NotSupportedException(); + } + ReadOnlyDictionary> individualsToLines = Convert(keyValuePairs); + result = new(headerLines, individualsToLines, new(familyGroupLines), new(footerLines)); + return result; + } + + private static ReadOnlyDictionary GetPeople(Input input, GenealogicalDataCommunicationCollections genealogicalDataCommunicationCollections) { Dictionary results = new(); + long? id; + char? sex; + Name? name; string six; + string? uId; + Birth? birth; + Death? death; Person person; - long? id = null; - char? sex = null; - Name? name = null; - string? uId = null; - Birth? birth = null; - Death? death = null; - Change? change = null; - bool? moreAdded = null; + string? title; + Change? change; + bool? moreAdded; ReadOnlyCollection lines; Dictionary> keyValuePairs = GetTxtFileCollection(input); foreach (KeyValuePair> keyValuePair in genealogicalDataCommunicationCollections.IndividualsToLines) { + id = null; + sex = null; + name = null; + uId = null; + birth = null; + death = null; + title = null; + change = null; + moreAdded = null; lines = keyValuePair.Value; for (int i = 0; i < lines.Count; i++) { @@ -493,6 +630,8 @@ internal static partial class HelperGenealogicalDataCommunication (i, birth, moreAdded) = GetBirth(keyValuePairs, lines, i); else if (six == "1 DEAT") (i, death) = GetDeath(lines, i); + else if (six == "1 TITL") + title = lines[i].Length == 6 ? null : lines[i][7..]; else if (six == "1 CHAN") (i, change) = GetChange(lines, i); else if (six is "1 FAMC" or "1 FAMS") @@ -504,15 +643,15 @@ internal static partial class HelperGenealogicalDataCommunication if (id is null) throw new Exception(string.Join(Environment.NewLine, lines)); if (moreAdded is null || !moreAdded.Value) - person = new(id.Value, name, sex, uId, birth, death, change, lines.ToArray()); + person = new(id.Value, name, sex, uId, birth, title, death, change, lines.ToArray()); else - person = new(id.Value, name, sex, uId, birth, death, change, GetNewLines(lines.ToArray(), birth)); + person = new(id.Value, name, sex, uId, birth, title, death, change, GetNewLines(lines, birth)); results.Add(id.Value, person); } - return results; + return new(results); } - private static (ReadOnlyDictionary, ReadOnlyDictionary, ReadOnlyDictionary, ReadOnlyCollection<(long, ReadOnlyCollection, string, char[], DateTime, long)>) GetCollections(AppSettings appSettings, Dictionary people) + private static (ReadOnlyDictionary, ReadOnlyDictionary, ReadOnlyDictionary, ReadOnlyCollection) GetCollections(AppSettings appSettings, ReadOnlyDictionary people) { long personKey; char[] ageCollection; @@ -520,7 +659,7 @@ internal static partial class HelperGenealogicalDataCommunication Dictionary idToPersonKey = new(); Dictionary idToGivenName = new(); int length = appSettings.PersonBirthdayFormat.Length; - List<(long, ReadOnlyCollection, string, char[], DateTime, long)> collection = new(); + List collection = new(); foreach (KeyValuePair keyValuePair in people) { if (keyValuePair.Value.Birth?.Note is null) @@ -536,41 +675,42 @@ internal static partial class HelperGenealogicalDataCommunication idToName.Add(keyValuePair.Key, WindowsFileSystem().Replace(keyValuePair.Value.Name.ForwardSlashFull, "_")); ageCollection = keyValuePair.Value.Birth.Continue.Count == 0 ? Array.Empty() : keyValuePair.Value.Birth.Continue[0].ToArray(); idToGivenName.Add(keyValuePair.Key, string.IsNullOrEmpty(keyValuePair.Value.Name.Given) ? WindowsFileSystem().Replace(keyValuePair.Value.Name.ForwardSlashFull, "_") : WindowsFileSystem().Replace(keyValuePair.Value.Name.Given, "_")); - collection.Add((keyValuePair.Key, new(keyValuePair.Value.Lines), keyValuePair.Value.Birth.Note, ageCollection, dateTime, personKey)); + collection.Add(new(keyValuePair.Key, new(keyValuePair.Value.Lines), keyValuePair.Value.Birth.Note, ageCollection, dateTime, personKey)); } return (new(idToPersonKey), new(idToName), new(idToGivenName), new(collection)); } - private static void ExportFamilies(AppSettings appSettings, Input input, GenealogicalDataCommunicationCollections genealogicalDataCommunicationCollections, ReadOnlyDictionary idToPersonKey, ReadOnlyDictionary idToName, ReadOnlyDictionary idToGivenName) + private static List GetFamilyCollection(ReadOnlyCollection> familyGroupLines, ReadOnlyDictionary people, ReadOnlyDictionary idToPersonKey, ReadOnlyDictionary idToName, ReadOnlyDictionary idToGivenName) { + List results = new(); string? name; long personKey; - DateTime dateTime; + Person? person; string? givenName; string familyIndex; - string? destinationRoot; const string wife = "WIFE"; + string? familyTitle = null; const string child = "CHIL"; - string destinationDirectory; const string husband = "HUSB"; string wifeName = string.Empty; string? lastFamilyIndex = null; string husbandName = string.Empty; - ReadOnlyCollection genealogicalDataCommunicationRelations = GetRelations(genealogicalDataCommunicationCollections.FamilyGroupLines); + List family = GetIdsWhenPersonHasTitle(people); + ReadOnlyCollection genealogicalDataCommunicationRelations = GetRelations(familyGroupLines); foreach (GenealogicalDataCommunicationRelation genealogicalDataCommunicationRelation in genealogicalDataCommunicationRelations) { if (idToName.Count == 0 || idToGivenName.Count == 0) break; - destinationRoot = Path.GetDirectoryName(input.GenealogicalDataCommunicationFile); - if (string.IsNullOrEmpty(destinationRoot)) - continue; if (!idToName.TryGetValue(genealogicalDataCommunicationRelation.Id, out name)) continue; + if (!people.TryGetValue(genealogicalDataCommunicationRelation.Id, out person)) + continue; if (!idToGivenName.TryGetValue(genealogicalDataCommunicationRelation.Id, out givenName)) continue; familyIndex = genealogicalDataCommunicationRelation.FamilyIndex.ToString("0000"); if (lastFamilyIndex is not null && lastFamilyIndex != familyIndex) { + familyTitle = null; wifeName = string.Empty; husbandName = string.Empty; } @@ -578,43 +718,107 @@ internal static partial class HelperGenealogicalDataCommunication if (genealogicalDataCommunicationRelation.Relation == husband) { husbandName = givenName; + if (person.Title is not null) + familyTitle = person.Title; continue; } if (genealogicalDataCommunicationRelation.Relation == wife) { wifeName = givenName; + if (person.Title is not null) + familyTitle = person.Title; continue; } if (genealogicalDataCommunicationRelation.Relation != child) continue; if (!idToPersonKey.TryGetValue(genealogicalDataCommunicationRelation.Id, out personKey)) continue; - dateTime = new(personKey); - destinationDirectory = Path.Combine(destinationRoot, string.Concat($"{husbandName}-{wifeName}".Trim('-'), "-", familyIndex), name, dateTime.ToString(appSettings.PersonBirthdayFormat)); - if (!Directory.Exists(destinationDirectory)) - _ = Directory.CreateDirectory(destinationDirectory); - File.WriteAllText(Path.Combine(destinationDirectory, ".txt"), genealogicalDataCommunicationRelation.LineTwo); + if (person.Title is not null) + familyTitle = person.Title; + _ = family.Remove(genealogicalDataCommunicationRelation.Id); + results.Add(new(familyTitle, $"{husbandName}-{wifeName}".Trim('-'), familyIndex, name, person, personKey, genealogicalDataCommunicationRelation.LineTwo)); } + foreach (KeyValuePair keyValuePair in people) + { + if (!family.Contains(keyValuePair.Key)) + continue; + if (!idToName.TryGetValue(keyValuePair.Key, out name)) + continue; + if (!idToPersonKey.TryGetValue(keyValuePair.Key, out personKey)) + continue; + if (!idToGivenName.TryGetValue(keyValuePair.Key, out givenName)) + continue; + if (!family.Remove(keyValuePair.Key)) + continue; + results.Add(new(keyValuePair.Value.Title, givenName, null, name, keyValuePair.Value, personKey, null)); + } + if (family.Count > 0) + throw new NotSupportedException(); foreach (GenealogicalDataCommunicationRelation genealogicalDataCommunicationRelation in genealogicalDataCommunicationRelations) { if (idToName.Count == 0 || idToGivenName.Count == 0) break; - destinationRoot = Path.GetDirectoryName(input.GenealogicalDataCommunicationFile); - if (string.IsNullOrEmpty(destinationRoot)) - continue; if (!idToName.TryGetValue(genealogicalDataCommunicationRelation.Id, out name)) continue; + if (!people.TryGetValue(genealogicalDataCommunicationRelation.Id, out person)) + continue; if (!idToPersonKey.TryGetValue(genealogicalDataCommunicationRelation.Id, out personKey)) continue; - dateTime = new(personKey); - destinationDirectory = Path.Combine(destinationRoot, "A-A-0000", name, dateTime.ToString(appSettings.PersonBirthdayFormat)); - if (!Directory.Exists(destinationDirectory)) - _ = Directory.CreateDirectory(destinationDirectory); - File.WriteAllText(Path.Combine(destinationDirectory, ".txt"), genealogicalDataCommunicationRelation.LineTwo); + results.Add(new(null, null, null, name, person, personKey, genealogicalDataCommunicationRelation.LineTwo)); + } + return results; + } + + private static ReadOnlyDictionary>> GetKeyValuePairs(List familyCollection, char personTitleFilter) + { + Dictionary>> results; + Dictionary>> keyValuePairs; + string id; + string key; + ReadOnlyCollection collection; + List>? objectCollection; + ReadOnlyCollection distinctSortedKeys = GetDistinctSortedKeys(familyCollection, personTitleFilter); + keyValuePairs = Convert(distinctSortedKeys); + foreach (Family family in familyCollection) + { + if (family.Id is null) + continue; + if (string.IsNullOrEmpty(family.Title) || family.Title[0] != personTitleFilter) + continue; + id = family.Person.Id.ToString(); + key = GetKey(family); + if (!keyValuePairs.TryGetValue(key, out objectCollection)) + throw new NotSupportedException(); + collection = GetObjectCollection(family.Person); + objectCollection.Add(collection); + } + results = Convert(keyValuePairs); + return new(results); + } + + private static void WriteJsonFiles(AppSettings appSettings, Input input, GenealogicalDataCommunicationCollections genealogicalDataCommunicationCollections, ReadOnlyDictionary people, List familyCollection) + { + string json; + if (people.Count != genealogicalDataCommunicationCollections.IndividualsToLines.Count) + throw new NotSupportedException(); + JsonSerializerOptions jsonSerializerOptions = new() { WriteIndented = true }; + ReadOnlyDictionary>> keyValuePairs; + json = JsonSerializer.Serialize(new(people), PeopleSourceGenerationContext.Default.DictionaryInt64Person); + File.WriteAllText(Path.Combine(input.GenealogicalDataCommunicationDirectory, "people.json"), json); + Dictionary? result = JsonSerializer.Deserialize(json, PeopleSourceGenerationContext.Default.DictionaryInt64Person); + if (result is null) + throw new NullReferenceException(nameof(result)); + json = JsonSerializer.Serialize(genealogicalDataCommunicationCollections.FamilyGroupLines, jsonSerializerOptions); + File.WriteAllText(Path.Combine(input.GenealogicalDataCommunicationDirectory, "family.json"), json); + foreach (char personTitleFilter in appSettings.PersonTitleFilters) + { + keyValuePairs = GetKeyValuePairs(familyCollection, personTitleFilter); + json = JsonSerializer.Serialize(keyValuePairs, jsonSerializerOptions); + File.WriteAllText(Path.Combine(input.GenealogicalDataCommunicationDirectory, $"{personTitleFilter}.json"), json); } } - private static void WriteGenealogicalDataCommunicationCollections(ILogger logger, Input input, GenealogicalDataCommunicationCollections genealogicalDataCommunicationCollections, Dictionary people) + private static void WriteGenealogicalDataCommunicationCollections(ILogger logger, Input input, GenealogicalDataCommunicationCollections genealogicalDataCommunicationCollections, ReadOnlyDictionary people) { List lines = new(); List allLines = new(); @@ -654,26 +858,27 @@ internal static partial class HelperGenealogicalDataCommunication File.WriteAllLines(Path.Combine(input.GenealogicalDataCommunicationDirectory, "e.ged"), allLines); } - private static string? GetYearGroup(string year) => - !int.TryParse(year[2..], out int part) ? null : string.Concat(year[..^2], part < 50 ? "--" : "++"); - - private static (int, TimeSpan) GetAge(long minuendTicks, long subtrahendTicks) + private static void ExportFamilies(AppSettings appSettings, Input input, List familyCollection) { - TimeSpan result; - int years = 0; - DateTime check = new(subtrahendTicks); - for (int i = 0; i < int.MaxValue; i++) + string directory; + DateTime dateTime; + string? destinationRoot; + string destinationDirectory; + foreach (Family family in familyCollection) { - check = check.AddYears(1); - if (check.Ticks > minuendTicks) - break; - years += 1; + destinationRoot = Path.GetDirectoryName(input.GenealogicalDataCommunicationFile); + if (string.IsNullOrEmpty(destinationRoot)) + continue; + dateTime = new(family.PersonKey); + directory = family.Id is null ? "A-A-0000" : $"{family.Title ?? "O"}-{family.Id}-{family.Index}".Trim('-'); + destinationDirectory = Path.Combine(destinationRoot, directory, family.PersonName, dateTime.ToString(appSettings.PersonBirthdayFormat)); + if (!Directory.Exists(destinationDirectory)) + _ = Directory.CreateDirectory(destinationDirectory); + File.WriteAllText(Path.Combine(destinationDirectory, $"{family.PersonName}.txt"), family.LineTwo); } - result = new(minuendTicks - check.AddYears(-1).Ticks); - return (years, result); } - private static void Export(Input input, long ticks, ReadOnlyDictionary idToName, ReadOnlyCollection<(long, ReadOnlyCollection, string, char[], DateTime, long)> collection) + private static void Export(Input input, long ticks, ReadOnlyDictionary idToName, ReadOnlyCollection personExportCollection) { int age; string text; @@ -687,27 +892,27 @@ internal static partial class HelperGenealogicalDataCommunication List distinct = new(); List duplicates = new(); string personDisplayDirectoryName; - foreach ((long id, ReadOnlyCollection lines, string personKeyFormatted, char[] ageCollection, DateTime dateTime, long personKey) in collection) + foreach (PersonExport personExport in personExportCollection) { if (input.Destination is null) break; - if (!idToName.TryGetValue(id, out name)) + if (!idToName.TryGetValue(personExport.Id, out name)) continue; - hourGroup = GetHourGroup(name, dateTime.Hour); - (age, _) = GetAge(DateTime.Now.Ticks, personKey); + hourGroup = GetHourGroup(name, personExport.DateTime.Hour); + (age, _) = GetAge(DateTime.Now.Ticks, personExport.PersonKey); for (int i = 1; i < 3; i++) { if (i == 2) { - yearGroup = GetYearGroup(dateTime.Year.ToString()); + yearGroup = GetYearGroup(personExport.DateTime.Year.ToString()); personDisplayDirectoryName = name; if (string.IsNullOrEmpty(yearGroup)) continue; } else if (i == 1) { - yearGroup = ageCollection[0].ToString(); - approximateYears = yearGroup[0] == '^' ? $"^{age}" : new string(ageCollection); + yearGroup = personExport.AgeCollection[0].ToString(); + approximateYears = yearGroup[0] == '^' ? $"^{age}" : new string(personExport.AgeCollection); personDisplayDirectoryName = $"{name}{approximateYears}"; if (distinct.Contains(personDisplayDirectoryName)) { @@ -719,23 +924,23 @@ internal static partial class HelperGenealogicalDataCommunication else throw new NotSupportedException(); rootDirectory = i == 1 ? input.Destination : i == 2 ? input.GenealogicalDataCommunicationDirectory : throw new NotSupportedException(); - directory = Path.Combine(rootDirectory, yearGroup, hourGroup, personDisplayDirectoryName, personKeyFormatted); + directory = Path.Combine(rootDirectory, yearGroup, hourGroup, personDisplayDirectoryName, personExport.PersonKeyFormatted); if (!Directory.Exists(directory)) _ = Directory.CreateDirectory(directory); if (i == 2) { - text = string.Join(Environment.NewLine, lines); + text = string.Join(Environment.NewLine, personExport.Lines); count += 1; File.WriteAllText(Path.Combine(directory, $"{count}-A.pged"), text); } - text = string.Join(Environment.NewLine, lines); + text = string.Join(Environment.NewLine, personExport.Lines); if (!string.IsNullOrEmpty(text)) { count += 1; if (i == 2) File.WriteAllText(Path.Combine(directory, $"{count}-B.pged"), text); else - File.WriteAllText(Path.Combine(directory, $"{personKeyFormatted}.pged"), text); + File.WriteAllText(Path.Combine(directory, $"{personExport.PersonKeyFormatted}.pged"), text); } } } @@ -743,31 +948,60 @@ internal static partial class HelperGenealogicalDataCommunication throw new NotSupportedException(); } - private static ReadOnlyCollection GetRelations(ReadOnlyCollection> familyGroupLines) + [Obsolete] + private static PersonHour GetPersonHour(string personDisplayDirectoryName, int hour) => + hour == 0 ? new('U', 'U', 'U') : + hour == 1 ? new('U', 'U', 'U') : + hour == 2 ? new('U', 'U', 'U') : + hour == 3 ? new('A', 'U', 'Y') : + hour == 4 ? new('A', 'F', 'Y') : + hour == 5 ? new('A', 'M', 'Y') : + hour == 6 ? new('A', 'F', 'N') : + hour == 7 ? new('A', 'M', 'N') : + hour == 13 ? new('D', 'U', 'Y') : + hour == 14 ? new('D', 'F', 'Y') : + hour == 15 ? new('D', 'M', 'Y') : + hour == 16 ? new('D', 'F', 'N') : + hour == 17 ? new('D', 'M', 'N') : + throw new NotImplementedException(personDisplayDirectoryName); + + [Obsolete] + private static string[] GetNewLines(ReadOnlyCollection lines, Name? name, string[] kFiles, string[] mFiles, string[] k2Files, string[] m2Files) { - List results = new(); - int id; - string relation; - string[] segments; - ReadOnlyCollection familyLines; - for (int i = 0; i < familyGroupLines.Count; i++) + List results = new(); + string six; + int? birthLastLine = null; + string slugName = WindowsFileSystem().Replace(string.Concat(name?.ForwardSlashFull), "_"); + string? title = name?.ForwardSlashFull is null ? null : kFiles.Contains(slugName) ? "1 TITL K" : k2Files.Contains(slugName) ? "1 TITL K2" : mFiles.Contains(slugName) ? "1 TITL M" : m2Files.Contains(slugName) ? "1 TITL M2" : null; + for (int i = 0; i < lines.Count; i++) { - familyLines = familyGroupLines[i]; - for (int j = 0; j < familyLines.Count; j++) + if (lines[i].Length < 6) + throw new NotImplementedException(); + results.Add(lines[i]); + six = lines[i][..6]; + if (lines[i][0] == '1') { - segments = familyLines[j].Split('@'); - if (segments[0].Length < 3 || segments.Length != 3) + if (six != "1 BIRT") continue; - if (!int.TryParse(segments[1][1..], out id)) - continue; - relation = segments[0][2..].Trim(); - if (j + 1 >= familyLines.Count || familyLines[j + 1].Length < 3 || familyLines[j + 1][..3] != "2 _") - results.Add(new(i, relation, id, null)); - else - results.Add(new(i, relation, id, familyLines[j + 1][2..])); + for (int j = i + 1; j < lines.Count; j++) + { + results.Add(lines[j]); + if (lines[j].Length < 7) + throw new NotImplementedException(); + if (lines[j][0] == '1') + { + birthLastLine = j; + if (title is not null) + results.Insert(j, title); + break; + } + i++; + } } } - return new(results.OrderBy(l => l.FamilyIndex).ToArray()); + if (title is not null && birthLastLine is null) + results.Add(title); + return results.ToArray(); } internal static void FileSystemToGenealogicalDataCommunication(AppSettings appSettings, ILogger logger, List args) @@ -777,22 +1011,17 @@ internal static partial class HelperGenealogicalDataCommunication logger.LogInformation("{ticks}", ticks); logger.LogInformation("{old} {days} day(s) => {new}", 638258293638438812, 4, new DateTime(638258293638438812).AddDays(4.0001).Ticks); GenealogicalDataCommunicationCollections genealogicalDataCommunicationCollections = GetGenealogicalDataCommunicationCollections(input); - Dictionary people = GetPeople(input, genealogicalDataCommunicationCollections); - if (people.Count != genealogicalDataCommunicationCollections.IndividualsToLines.Count) - throw new NotSupportedException(); - string json = JsonSerializer.Serialize(people, PeopleSourceGenerationContext.Default.DictionaryInt64Person); - File.WriteAllText(Path.Combine(input.GenealogicalDataCommunicationDirectory, ".json"), json); - Dictionary? result = JsonSerializer.Deserialize(json, PeopleSourceGenerationContext.Default.DictionaryInt64Person); - if (result is null) - throw new NullReferenceException(nameof(result)); - (ReadOnlyDictionary idToPersonKey, ReadOnlyDictionary idToName, ReadOnlyDictionary idToGivenName, ReadOnlyCollection<(long, ReadOnlyCollection, string, char[], DateTime, long)> collection) = GetCollections(appSettings, people); + ReadOnlyDictionary people = GetPeople(input, genealogicalDataCommunicationCollections); + (ReadOnlyDictionary idToPersonKey, ReadOnlyDictionary idToName, ReadOnlyDictionary idToGivenName, ReadOnlyCollection personExportCollection) = GetCollections(appSettings, people); if (idToPersonKey.Count != people.Count || idToPersonKey.Count != idToName.Count || idToPersonKey.Count != idToGivenName.Count) throw new NotSupportedException(); + List familyCollection = GetFamilyCollection(genealogicalDataCommunicationCollections.FamilyGroupLines, people, idToPersonKey, idToName, idToGivenName); + WriteJsonFiles(appSettings, input, genealogicalDataCommunicationCollections, people, familyCollection); WriteGenealogicalDataCommunicationCollections(logger, input, genealogicalDataCommunicationCollections, people); if (input.Destination is not null) - ExportFamilies(appSettings, input, genealogicalDataCommunicationCollections, idToPersonKey, idToName, idToGivenName); + ExportFamilies(appSettings, input, familyCollection); if (input.Destination is not null) - Export(input, ticks, idToName, collection); + Export(input, ticks, idToName, personExportCollection); if (string.IsNullOrEmpty(input.GenealogicalDataCommunicationFile)) logger.LogInformation("{file} is null?", input.GenealogicalDataCommunicationDirectory); } diff --git a/Helpers/HelperKanbanMetadata.cs b/Helpers/HelperKanbanMetadata.cs index c476d37..237e264 100644 --- a/Helpers/HelperKanbanMetadata.cs +++ b/Helpers/HelperKanbanMetadata.cs @@ -14,6 +14,44 @@ internal static partial class HelperKanbanMetadata [GeneratedRegex("[\\s!?.,@:;|\\\\/\"'`£$%\\^&*{}[\\]()<>~#+\\-=_¬]+")] private static partial Regex InvalidCharacter(); + private static void TestParamCases() + { + if (GetParamCase("PascalCase") != "pascal-case") + throw new Exception("PascalCase"); + if (GetParamCase("camelCase") != "camel-case") + throw new Exception("camelCase"); + if (GetParamCase("snake_case") != "snake-case") + throw new Exception("snake_case"); + if (GetParamCase("No Case") != "no-case") + throw new Exception("No Case"); + if (GetParamCase("With 2 numbers 3") != "with-2-numbers-3") + throw new Exception("With 2 numbers 3"); + if (GetParamCase("Multiple spaces") != "multiple-spaces") + throw new Exception("Multiple spaces"); + if (GetParamCase("Tab\tCharacter") != "tab-character") + throw new Exception("Tab\tCharacter"); + if (GetParamCase("New\nLine") != "new-line") + throw new Exception("New\nLine"); + if (GetParamCase("Punctuation, Characters") != "punctuation-characters") + throw new Exception("Punctuation, Characters"); + if (GetParamCase("M!o?r.e, @p:u;n|c\\t/u\"a\'t`i£o$n% ^c&h*a{r}a[c]t(e)r ~l#i+k-e= _t¬hese") != "m-o-r-e-p-u-n-c-t-u-a-t-i-o-n-c-h-a-r-a-c-t-e-r-s-l-i-k-e-t-hese") + throw new Exception("M!o?r.e, @p:u;n|c\\t/u\"a\'t`i£o$n% ^c&h*a{r}a[c]t(e)r ~l#i+k-e= _t¬hese"); + if (GetParamCase("This string ends with punctuation!") != "this-string-ends-with-punctuation") + throw new Exception("This string ends with punctuation!"); + if (GetParamCase("?This string starts with punctuation") != "this-string-starts-with-punctuation") + throw new Exception("?This string starts with punctuation"); + if (GetParamCase("#This string has punctuation at both ends&") != "this-string-has-punctuation-at-both-ends") + throw new Exception("#This string has punctuation at both ends&"); + if (GetParamCase("軟件 測試") != "軟件-測試") + throw new Exception("軟件 測試"); + if (GetParamCase("実験 試し") != "実験-試し") + throw new Exception("実験 試し"); + if (GetParamCase("יקספּערמענאַל פּרובירן") != "יקספּערמענאַל-פּרובירן") + throw new Exception("יקספּערמענאַל פּרובירן"); + if (GetParamCase("я надеюсь, что это сработает") != "я-надеюсь-что-это-сработает") + throw new Exception("я надеюсь, что это сработает"); + } + private static List<(int, int, string, FileInfo)> GetCollectionFromIndex(string sourceDirectory, string[] lines) { List<(int, int, string, FileInfo)> results = new(); @@ -58,44 +96,6 @@ internal static partial class HelperKanbanMetadata return result; } - private static void TestParamCases() - { - if (GetParamCase("PascalCase") != "pascal-case") - throw new Exception("PascalCase"); - if (GetParamCase("camelCase") != "camel-case") - throw new Exception("camelCase"); - if (GetParamCase("snake_case") != "snake-case") - throw new Exception("snake_case"); - if (GetParamCase("No Case") != "no-case") - throw new Exception("No Case"); - if (GetParamCase("With 2 numbers 3") != "with-2-numbers-3") - throw new Exception("With 2 numbers 3"); - if (GetParamCase("Multiple spaces") != "multiple-spaces") - throw new Exception("Multiple spaces"); - if (GetParamCase("Tab\tCharacter") != "tab-character") - throw new Exception("Tab\tCharacter"); - if (GetParamCase("New\nLine") != "new-line") - throw new Exception("New\nLine"); - if (GetParamCase("Punctuation, Characters") != "punctuation-characters") - throw new Exception("Punctuation, Characters"); - if (GetParamCase("M!o?r.e, @p:u;n|c\\t/u\"a\'t`i£o$n% ^c&h*a{r}a[c]t(e)r ~l#i+k-e= _t¬hese") != "m-o-r-e-p-u-n-c-t-u-a-t-i-o-n-c-h-a-r-a-c-t-e-r-s-l-i-k-e-t-hese") - throw new Exception("M!o?r.e, @p:u;n|c\\t/u\"a\'t`i£o$n% ^c&h*a{r}a[c]t(e)r ~l#i+k-e= _t¬hese"); - if (GetParamCase("This string ends with punctuation!") != "this-string-ends-with-punctuation") - throw new Exception("This string ends with punctuation!"); - if (GetParamCase("?This string starts with punctuation") != "this-string-starts-with-punctuation") - throw new Exception("?This string starts with punctuation"); - if (GetParamCase("#This string has punctuation at both ends&") != "this-string-has-punctuation-at-both-ends") - throw new Exception("#This string has punctuation at both ends&"); - if (GetParamCase("軟件 測試") != "軟件-測試") - throw new Exception("軟件 測試"); - if (GetParamCase("実験 試し") != "実験-試し") - throw new Exception("実験 試し"); - if (GetParamCase("יקספּערמענאַל פּרובירן") != "יקספּערמענאַל-פּרובירן") - throw new Exception("יקספּערמענאַל פּרובירן"); - if (GetParamCase("я надеюсь, что это сработает") != "я-надеюсь-что-это-сработает") - throw new Exception("я надеюсь, что это сработает"); - } - internal static void SetMetadata(ILogger log, AppSettings appSettings, string sourceDirectory) { bool? match; diff --git a/Helpers/HelperMarkdown.cs b/Helpers/HelperMarkdown.cs index b73db84..89e5bb8 100644 --- a/Helpers/HelperMarkdown.cs +++ b/Helpers/HelperMarkdown.cs @@ -10,9 +10,16 @@ namespace File_Folder_Helper.Helpers; internal static partial class HelperMarkdown { - private record Record(string Source, string? StartAt, string? Destination); + private record Input(string Source, + string? StartAt, + string? Destination); - private record MarkdownFileAndLines(MarkdownFile MarkdownFile, string[] Lines); + private record Record(string Directory, + string File, + string[] Lines); + + private record MarkdownFileAndLines(MarkdownFile MarkdownFile, + string[] Lines); private record MarkdownFileH1AndRelativePath(MarkdownFile? MarkdownFile, string[]? Lines, string? H1, string? RelativePath); @@ -22,34 +29,110 @@ internal static partial class HelperMarkdown { } - private static Record GetRecord(List args) + private static void SetRecursiveLines(AppSettings appSettings, ILogger logger, ReadOnlyDictionary> keyValuePairs, string linkTitle, MarkdownFile markdownFile, string[] lines, List indentations, List recursiveLines) { - Record result; - string? startAt = null; - string? destination = null; - for (int i = 1; i < args.Count; i++) + if (recursiveLines is null) + throw new Exception(); + string file; + string[] segmentsA; + string[] segmentsB; + string segmentsALast; + bool fencedCodeBlock = false; + string indentation = new(indentations.ToArray()); + MarkdownFileH1AndRelativePath markdownFileH1AndRelativePath; + for (int i = 0; i < lines.Length; i++) { - if (args[i].Length == 2 && i + 1 < args.Count) + if (indentations.Count > 15) { - if (args[i][1] == 's') - startAt = Path.GetFullPath(args[i + 1]); - else if (args[i][1] == 'd') - destination = Path.GetFullPath(args[i + 1]); - i++; + recursiveLines.Add("```Error```"); + break; } + if (lines[i].Length < 1) + continue; + if (lines[i].Length > 4 && lines[i][..3] == "```") + fencedCodeBlock = !fencedCodeBlock; + if (fencedCodeBlock) + continue; + if (lines[i][0] == '#') + { + if (lines[i] == $"# {linkTitle}") + continue; + recursiveLines.Add($"{indentation}{lines[i]}"); + continue; + } + segmentsA = lines[i].Split("]("); + if (segmentsA.Length != 2) + continue; + segmentsALast = segmentsA[^1]; + if (appSettings.ExcludeSchemes.Any(l => segmentsALast.StartsWith(l))) + continue; + segmentsB = segmentsALast.Split(")"); + if (segmentsB.Length != 2) + continue; + file = Path.GetFullPath(Path.Combine(markdownFile.Directory, segmentsB[0])); + markdownFileH1AndRelativePath = GetRelativePath(keyValuePairs, markdownFile, file); + if (markdownFileH1AndRelativePath.MarkdownFile is null || markdownFileH1AndRelativePath.H1 is null || markdownFileH1AndRelativePath.RelativePath is null) + { + recursiveLines.Add($"???{indentation}{lines[i]}"); + logger.LogInformation("Didn't find {line} in <{file}>", lines[i], markdownFile.FileNameWithoutExtension); + continue; + } + if (markdownFileH1AndRelativePath.Lines is null) + continue; + indentations.Add('\t'); + recursiveLines.Add($"{indentation}{lines[i]}"); + SetRecursiveLines(appSettings, logger, keyValuePairs, segmentsA[0].Split('[')[^1], markdownFileH1AndRelativePath.MarkdownFile, markdownFileH1AndRelativePath.Lines, indentations, recursiveLines); } - if (startAt is not null && !Directory.Exists(startAt)) - throw new Exception($"Start at directory <{startAt}> doesn't exist!"); - if (destination is not null) + if (indentations.Count > 0) + indentations.RemoveAt(0); + } + + private static List GetFrontMatterLines(string[] parsedLines) + { + List results = new(); + string afterTrim; + string[] segments; + StringBuilder stringBuilder = new(); + for (int i = 0; i < parsedLines.Length; i++) { - string? root = Path.GetPathRoot(destination); - if (root is null || !Directory.Exists(root)) - throw new NotSupportedException($"This method requires frontMatterYamlLines valid -d path <{root}>!"); - if (!Directory.Exists(destination)) - _ = Directory.CreateDirectory(destination); + afterTrim = parsedLines[i].Trim(); + if (string.IsNullOrEmpty(afterTrim) || afterTrim[0] is '{' or '}') + continue; + segments = afterTrim.Split(": "); + if (segments.Length != 2) + { + if (results[^1][^1] == '[') + { + _ = stringBuilder.Clear(); + _ = stringBuilder.Append(results[^1]); + results.RemoveAt(results.Count - 1); + for (int j = i; j < parsedLines.Length; j++) + { + i = j; + afterTrim = parsedLines[j].Trim(); + if (afterTrim == "],") + _ = stringBuilder.Append(afterTrim[..^1]); + else if (afterTrim[^1] == ',') + _ = stringBuilder.Append(afterTrim).Append(' '); + else + _ = stringBuilder.Append(afterTrim); + if (afterTrim is "]" or "],") + { + results.Add(stringBuilder.ToString()); + break; + } + } + continue; + } + results.Clear(); + break; + } + if (afterTrim[^1] != ',') + results.Add(afterTrim[1..].Replace("\": ", ": ")); + else + results.Add(afterTrim[1..^1].Replace("\": ", ": ")); } - result = new(Path.GetFullPath(args[0]), startAt, destination); - return result; + return results; } /// @@ -83,18 +166,46 @@ internal static partial class HelperMarkdown return result; } - internal static string[] GetFiles(AppSettings appSettings, string directory) + private static List GetMarkdownFileAndLines(string file, List markdownFiles) { - string[] results = Directory.GetFiles(directory, "*.md", SearchOption.AllDirectories). - Where(l => !appSettings.ExcludeDirectoryNames.Any(m => l.Contains(m))).ToArray(); + List results = new(); + List distinct = new(); + string? directory = Path.GetDirectoryName(file); + foreach (MarkdownFileAndLines markdownFileAndLines in markdownFiles) + { + if (string.IsNullOrEmpty(directory) || markdownFileAndLines.MarkdownFile.Directory != directory) + continue; + if (distinct.Contains(markdownFileAndLines.MarkdownFile.File)) + continue; + distinct.Add(markdownFileAndLines.MarkdownFile.File); + results.Add(markdownFileAndLines); + } return results; } - private static (string type, string h1) GetTypeAndH1(AppSettings appSettings, string h1, List lines, LineNumber lineNumber) + private static (string?, Dictionary?, List) Get(List jsonLines) { - string type = lineNumber.Type is null ? appSettings.DefaultNoteType : lines[lineNumber.Type.Value].Replace("type: ", string.Empty); - string h1FromFile = lineNumber.H1 is null ? h1 : lines[lineNumber.H1.Value][2..]; - return (type, h1FromFile); + string? result; + List results; + Dictionary? keyValuePairs; + string jsonLinesLast = jsonLines[^1]; + jsonLines.RemoveAt(jsonLines.Count - 1); + jsonLines.Add(jsonLinesLast[..^1]); + jsonLines.Insert(0, "{"); + jsonLines.Add("}"); + result = string.Join(Environment.NewLine, jsonLines); + keyValuePairs = JsonSerializer.Deserialize(result, DictionaryStringAndJsonElementSourceGenerationContext.Default.DictionaryStringJsonElement); + if (keyValuePairs is null) + throw new NullReferenceException(nameof(keyValuePairs)); + result = JsonSerializer.Serialize(keyValuePairs, DictionaryStringAndJsonElementSourceGenerationContext.Default.DictionaryStringJsonElement); + string[] parsedLines = result.Split(Environment.NewLine).ToArray(); + results = GetFrontMatterLines(parsedLines); + if (!results.Any()) + { + result = null; + keyValuePairs = null; + } + return (result, keyValuePairs, results); } internal static (List, LineNumber) GetStatusAndFrontMatterYamlEndLineNumbers(FileInfo fileInfo) @@ -153,196 +264,32 @@ internal static partial class HelperMarkdown return (lines.ToList(), lineNumber); } - private static ReadOnlyDictionary GetRelativeToCollection(AppSettings appSettings, Record record, string[] files, bool force) + private static List Distinct(IEnumerable? markdownFileAndLinesCollection) { - Dictionary results = new(); - string h1; - string key; - string type; - FileInfo fileInfo; - List lines; - LineNumber lineNumber; - MarkdownFile markdownFile; - string fileNameWithoutExtension; - foreach (string file in files) + List results = new(); + if (markdownFileAndLinesCollection is not null) { - fileInfo = new(file); - if (fileInfo.DirectoryName is null) - continue; - key = Path.GetRelativePath(record.Source, file); - (lines, lineNumber) = GetStatusAndFrontMatterYamlEndLineNumbers(fileInfo); - fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileInfo.FullName); - h1 = fileNameWithoutExtension.ToLower().Replace("%20", "-").Replace(' ', '-'); - if (lines.Any()) - (type, h1) = GetTypeAndH1(appSettings, h1, lines, lineNumber); - else + List distinct = new(); + foreach (MarkdownFileAndLines markdownFileAndLines in markdownFileAndLinesCollection) { - type = appSettings.DefaultNoteType; - File.WriteAllLines(file, new string[] { "---", $"type: \"{type}\"", "---", string.Empty, $"# {h1}" }); - lines = File.ReadAllLines(file).ToList(); - } - markdownFile = new(file, fileInfo.DirectoryName, fileInfo.Name, fileNameWithoutExtension, fileInfo.Extension, fileInfo.CreationTime, fileInfo.LastWriteTime, lineNumber, type, h1); - if (force || record.StartAt is null || file.StartsWith(record.StartAt)) - results.Add(key, new(markdownFile, lines.ToArray())); - else - results.Add(key, new(markdownFile, Array.Empty())); - } - return new(results); - } - - private static ReadOnlyDictionary GetRelativeToCollection(AppSettings appSettings, Record record, bool force = false) - { - ReadOnlyDictionary results; - string[] files = GetFiles(appSettings, record.Source); - results = GetRelativeToCollection(appSettings, record, files, force); - return new(results); - } - - private static int SetFrontMatterAndH1(AppSettings appSettings, ReadOnlyDictionary relativeToCollection) - { - int result = 0; - List results = new(); - string h1Line; - string[] lines; - string typeLine; - string createdLine; - string updatedLine; - DateTime creationDateTime; - MarkdownFile markdownFile; - string createdLineCompare; - string updatedLineCompare; - foreach (KeyValuePair relativeTo in relativeToCollection) - { - if (!relativeTo.Value.Lines.Any()) - continue; - results.Clear(); - lines = relativeTo.Value.Lines; - markdownFile = relativeTo.Value.MarkdownFile; - results.AddRange(lines); - creationDateTime = markdownFile.CreationDateTime > markdownFile.LastWriteDateTime ? markdownFile.LastWriteDateTime : markdownFile.CreationDateTime; - typeLine = $"type: \"{appSettings.DefaultNoteType}\""; - h1Line = $"# {markdownFile.FileNameWithoutExtension}"; - createdLineCompare = $"created: \"{creationDateTime.ToUniversalTime():yyyy-MM-dd}T"; - createdLine = $"created: \"{creationDateTime.ToUniversalTime():yyyy-MM-ddTHH:mm:ss.fffZ}\""; - updatedLineCompare = $"updated: \"{markdownFile.LastWriteDateTime.ToUniversalTime():yyyy-MM-dd}T"; - updatedLine = $"updated: \"{markdownFile.LastWriteDateTime.ToUniversalTime():yyyy-MM-ddTHH:mm:ss.fffZ}\""; - if (markdownFile.LineNumber.FrontMatterYamlEnd is null) - { - if (markdownFile.LineNumber.H1 is null) - { - results.Insert(0, string.Empty); - results.Insert(0, h1Line); - results.Insert(0, string.Empty); - } - results.Insert(0, "---"); - results.Insert(0, updatedLine); - results.Insert(0, createdLine); - results.Insert(0, typeLine); - results.Insert(0, "---"); - } - else - { - if (markdownFile.LineNumber.H1 is null) - { - results.Insert(markdownFile.LineNumber.FrontMatterYamlEnd.Value + 1, string.Empty); - results.Insert(markdownFile.LineNumber.FrontMatterYamlEnd.Value + 1, h1Line); - results.Insert(markdownFile.LineNumber.FrontMatterYamlEnd.Value + 1, string.Empty); - } - if (markdownFile.LineNumber.Type is null) - results.Insert(markdownFile.LineNumber.FrontMatterYamlEnd.Value, typeLine); - if (markdownFile.LineNumber.Updated is null) - results.Insert(markdownFile.LineNumber.FrontMatterYamlEnd.Value, updatedLine); - else - { - if (results[markdownFile.LineNumber.Updated.Value].Contains('$')) - continue; - if (results[markdownFile.LineNumber.Updated.Value][..updatedLineCompare.Length] == updatedLineCompare) - continue; - results[markdownFile.LineNumber.Updated.Value] = updatedLine; - } - if (markdownFile.LineNumber.Created is null) - results.Insert(markdownFile.LineNumber.FrontMatterYamlEnd.Value, createdLine); - else if (results[markdownFile.LineNumber.Created.Value][..createdLineCompare.Length] != createdLineCompare) - results[markdownFile.LineNumber.Created.Value] = createdLine; - } - File.WriteAllLines(markdownFile.File, results); - File.SetLastWriteTime(markdownFile.File, markdownFile.LastWriteDateTime); - result += 1; - } - return result; - } - - private static List GetFrontMatterLines(string[] parsedLines) - { - List results = new(); - string afterTrim; - string[] segments; - StringBuilder stringBuilder = new(); - for (int i = 0; i < parsedLines.Length; i++) - { - afterTrim = parsedLines[i].Trim(); - if (string.IsNullOrEmpty(afterTrim) || afterTrim[0] is '{' or '}') - continue; - segments = afterTrim.Split(": "); - if (segments.Length != 2) - { - if (results[^1][^1] == '[') - { - _ = stringBuilder.Clear(); - _ = stringBuilder.Append(results[^1]); - results.RemoveAt(results.Count - 1); - for (int j = i; j < parsedLines.Length; j++) - { - i = j; - afterTrim = parsedLines[j].Trim(); - if (afterTrim == "],") - _ = stringBuilder.Append(afterTrim[..^1]); - else if (afterTrim[^1] == ',') - _ = stringBuilder.Append(afterTrim).Append(' '); - else - _ = stringBuilder.Append(afterTrim); - if (afterTrim is "]" or "],") - { - results.Add(stringBuilder.ToString()); - break; - } - } + if (distinct.Contains(markdownFileAndLines.MarkdownFile.File)) continue; - } - results.Clear(); - break; + distinct.Add(markdownFileAndLines.MarkdownFile.File); + results.Add(markdownFileAndLines); } - if (afterTrim[^1] != ',') - results.Add(afterTrim[1..].Replace("\": ", ": ")); - else - results.Add(afterTrim[1..^1].Replace("\": ", ": ")); } return results; } - private static (string?, Dictionary?, List) Get(List jsonLines) + private static List GetMarkdownFileAndLines(ReadOnlyDictionary> keyValuePairs) { - string? result; - List results; - Dictionary? keyValuePairs; - string jsonLinesLast = jsonLines[^1]; - jsonLines.RemoveAt(jsonLines.Count - 1); - jsonLines.Add(jsonLinesLast[..^1]); - jsonLines.Insert(0, "{"); - jsonLines.Add("}"); - result = string.Join(Environment.NewLine, jsonLines); - keyValuePairs = JsonSerializer.Deserialize(result, DictionaryStringAndJsonElementSourceGenerationContext.Default.DictionaryStringJsonElement); - if (keyValuePairs is null) - throw new NullReferenceException(nameof(keyValuePairs)); - result = JsonSerializer.Serialize(keyValuePairs, DictionaryStringAndJsonElementSourceGenerationContext.Default.DictionaryStringJsonElement); - string[] parsedLines = result.Split(Environment.NewLine).ToArray(); - results = GetFrontMatterLines(parsedLines); - if (!results.Any()) + List results = new(); + foreach (KeyValuePair> keyValuePair in keyValuePairs) { - result = null; - keyValuePairs = null; + foreach (MarkdownFileAndLines markdownFileAndLines in keyValuePair.Value) + results.Add(markdownFileAndLines); } - return (result, keyValuePairs, results); + return results; } private static (string?, Dictionary?, string[]) Get(int frontMatterYamlEnd, string[] lines) @@ -418,6 +365,244 @@ internal static partial class HelperMarkdown return (result, keyValuePairs, results.ToArray()); } + private static ReadOnlyDictionary> GetKeyValuePairs(ReadOnlyDictionary relativeToCollection) + { + Dictionary> results = new(); + MarkdownFile markdownFile; + string fileNameWithoutExtension; + string fileNameWithoutExtensionB; + List? markdownFiles; + foreach (KeyValuePair relativeTo in relativeToCollection) + { + markdownFile = relativeTo.Value.MarkdownFile; + if (!results.TryGetValue(relativeTo.Key, out markdownFiles)) + { + results.Add(relativeTo.Key, new()); + if (!results.TryGetValue(relativeTo.Key, out markdownFiles)) + throw new NotSupportedException(); + } + markdownFiles.Add(relativeTo.Value); + } + foreach (KeyValuePair relativeTo in relativeToCollection) + { + markdownFile = relativeTo.Value.MarkdownFile; + fileNameWithoutExtension = markdownFile.FileNameWithoutExtension.ToLower(); + fileNameWithoutExtensionB = fileNameWithoutExtension.Replace("%20", "-").Replace(' ', '-'); + if (!results.TryGetValue(markdownFile.FileNameWithoutExtension, out markdownFiles)) + { + results.Add(markdownFile.FileNameWithoutExtension, new()); + if (!results.TryGetValue(markdownFile.FileNameWithoutExtension, out markdownFiles)) + throw new NotSupportedException(); + } + markdownFiles.Add(relativeTo.Value); + if (fileNameWithoutExtension == markdownFile.FileNameWithoutExtension) + continue; + if (!results.TryGetValue(fileNameWithoutExtension, out markdownFiles)) + { + results.Add(fileNameWithoutExtension, new()); + if (!results.TryGetValue(fileNameWithoutExtension, out markdownFiles)) + throw new NotSupportedException(); + } + if (fileNameWithoutExtensionB == markdownFile.FileNameWithoutExtension) + continue; + if (!results.TryGetValue(fileNameWithoutExtensionB, out markdownFiles)) + { + results.Add(fileNameWithoutExtensionB, new()); + if (!results.TryGetValue(fileNameWithoutExtensionB, out markdownFiles)) + throw new NotSupportedException(); + } + markdownFiles.Add(relativeTo.Value); + } + foreach (KeyValuePair relativeTo in relativeToCollection) + { + markdownFile = relativeTo.Value.MarkdownFile; + if (!results.TryGetValue(markdownFile.H1, out markdownFiles)) + { + results.Add(markdownFile.H1, new()); + if (!results.TryGetValue(markdownFile.H1, out markdownFiles)) + throw new NotSupportedException(); + } + markdownFiles.Add(relativeTo.Value); + } + return new(results); + } + + private static int ConvertFileToSlugName(ReadOnlyDictionary relativeToCollection) + { + int result = 0; + string h1; + string h1Check; + string[] lines; + string checkName; + string checkFileName; + MarkdownFile markdownFile; + foreach (KeyValuePair relativeTo in relativeToCollection) + { + if (!relativeTo.Value.Lines.Any()) + continue; + lines = relativeTo.Value.Lines; + markdownFile = relativeTo.Value.MarkdownFile; + if (markdownFile.LineNumber.H1 is not null) + { + h1 = lines[markdownFile.LineNumber.H1.Value]; + if (h1.Length > 2) + { + h1Check = $"# {h1[2..]}"; + if (h1Check.Length == h1.Length && h1Check != h1) + { + lines[markdownFile.LineNumber.H1.Value] = h1Check; + File.WriteAllLines(markdownFile.File, lines); + result += 1; + } + } + } + checkFileName = markdownFile.FileName.ToLower().Replace("%20", "-").Replace(' ', '-'); + if (checkFileName == markdownFile.FileName) + continue; + if (!File.Exists(markdownFile.File)) + continue; + checkName = Path.Combine(markdownFile.Directory, checkFileName); + if (checkName == markdownFile.File) + continue; + File.Move(markdownFile.File, checkName); + result += 1; + } + return result; + } + + internal static string[] GetFiles(AppSettings appSettings, string directory) + { + string[] results = Directory.GetFiles(directory, "*.md", SearchOption.AllDirectories). + Where(l => !appSettings.ExcludeDirectoryNames.Any(m => l.Contains(m))).ToArray(); + return results; + } + + private static ReadOnlyDictionary GetRelativeToCollection(AppSettings appSettings, Input input, string[] files, bool force) + { + Dictionary results = new(); + string h1; + string key; + string type; + FileInfo fileInfo; + List lines; + LineNumber lineNumber; + MarkdownFile markdownFile; + string fileNameWithoutExtension; + foreach (string file in files) + { + fileInfo = new(file); + if (fileInfo.DirectoryName is null) + continue; + key = Path.GetRelativePath(input.Source, file); + (lines, lineNumber) = GetStatusAndFrontMatterYamlEndLineNumbers(fileInfo); + fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileInfo.FullName); + h1 = fileNameWithoutExtension.ToLower().Replace("%20", "-").Replace(' ', '-'); + if (lines.Any()) + (type, h1) = GetTypeAndH1(appSettings, h1, lines, lineNumber); + else + { + type = appSettings.DefaultNoteType; + File.WriteAllLines(file, new string[] { "---", $"type: \"{type}\"", "---", string.Empty, $"# {h1}" }); + lines = File.ReadAllLines(file).ToList(); + } + markdownFile = new(file, fileInfo.DirectoryName, fileInfo.Name, fileNameWithoutExtension, fileInfo.Extension, fileInfo.CreationTime, fileInfo.LastWriteTime, lineNumber, type, h1); + if (force || input.StartAt is null || file.StartsWith(input.StartAt)) + results.Add(key, new(markdownFile, lines.ToArray())); + else + results.Add(key, new(markdownFile, Array.Empty())); + } + return new(results); + } + + private static MarkdownFileAndLines? GetMarkdownFile(ReadOnlyDictionary> keyValuePairs, MarkdownFile markdownFile, string file) + { + MarkdownFileAndLines? result; + List? markdownFileAndLinesCollection; + string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file); + if (!keyValuePairs.TryGetValue(fileNameWithoutExtension, out markdownFileAndLinesCollection)) + _ = keyValuePairs.TryGetValue(fileNameWithoutExtension.ToLower(), out markdownFileAndLinesCollection); + markdownFileAndLinesCollection = Distinct(markdownFileAndLinesCollection); + if (markdownFileAndLinesCollection is not null && markdownFileAndLinesCollection.Count == 1) + result = markdownFileAndLinesCollection[0]; + else + { + List matches; + matches = markdownFileAndLinesCollection is null ? new() : GetMarkdownFileAndLines(file, markdownFileAndLinesCollection); + if (matches.Count == 1) + result = matches[0]; + else + { + markdownFileAndLinesCollection = GetMarkdownFileAndLines(keyValuePairs); + matches = Distinct(markdownFileAndLinesCollection.Where(l => l.MarkdownFile.FileNameWithoutExtension.Length == fileNameWithoutExtension.Length && l.MarkdownFile.FileNameWithoutExtension.Contains(fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase))); + if (matches.Count == 1) + result = matches[0]; + else + { + string checkName = fileNameWithoutExtension.ToLower().Replace("%20", "-").Replace(' ', '-'); + matches = Distinct(markdownFileAndLinesCollection.Where(l => l.MarkdownFile.FileNameWithoutExtension.Length == checkName.Length && l.MarkdownFile.FileNameWithoutExtension.Contains(checkName, StringComparison.OrdinalIgnoreCase))); + if (matches.Count == 1) + result = matches[0]; + else if (matches.Count == 0) + result = null; + else + { + checkName = matches[0].MarkdownFile.FileNameWithoutExtension; + matches = Distinct(markdownFileAndLinesCollection.Where(l => l.MarkdownFile.File.Contains(checkName, StringComparison.OrdinalIgnoreCase))); + if (matches.Count == 1) + result = matches[0]; + else + { + checkName = $"{checkName}{markdownFile.Extension}"; + matches = Distinct(markdownFileAndLinesCollection.Where(l => l.MarkdownFile.File.EndsWith(checkName, StringComparison.OrdinalIgnoreCase))); + if (matches.Count == 1) + result = matches[0]; + else + { + checkName = $"\\{checkName}"; + matches = Distinct(markdownFileAndLinesCollection.Where(l => l.MarkdownFile.File.EndsWith(checkName, StringComparison.OrdinalIgnoreCase))); + if (matches.Count == 1) + result = matches[0]; + else + result = null; + } + } + } + } + } + } + return result; + } + + private static Input GetInput(List args) + { + Input result; + string? startAt = null; + string? destination = null; + for (int i = 1; i < args.Count; i++) + { + if (args[i].Length == 2 && i + 1 < args.Count) + { + if (args[i][1] == 's') + startAt = Path.GetFullPath(args[i + 1]); + else if (args[i][1] == 'd') + destination = Path.GetFullPath(args[i + 1]); + i++; + } + } + if (startAt is not null && !Directory.Exists(startAt)) + throw new Exception($"Start at directory <{startAt}> doesn't exist!"); + if (destination is not null) + { + string? root = Path.GetPathRoot(destination); + if (root is null || !Directory.Exists(root)) + throw new NotSupportedException($"This method requires frontMatterYamlLines valid -d path <{root}>!"); + if (!Directory.Exists(destination)) + _ = Directory.CreateDirectory(destination); + } + result = new(Path.GetFullPath(args[0]), startAt, destination); + return result; + } + private static int ConvertFrontMatterToJsonFriendly(ReadOnlyDictionary relativeToCollection) { int result = 0; @@ -572,178 +757,6 @@ internal static partial class HelperMarkdown return result; } - private static ReadOnlyDictionary> GetKeyValuePairs(ReadOnlyDictionary relativeToCollection) - { - Dictionary> results = new(); - MarkdownFile markdownFile; - string fileNameWithoutExtension; - string fileNameWithoutExtensionB; - List? markdownFiles; - foreach (KeyValuePair relativeTo in relativeToCollection) - { - markdownFile = relativeTo.Value.MarkdownFile; - if (!results.TryGetValue(relativeTo.Key, out markdownFiles)) - { - results.Add(relativeTo.Key, new()); - if (!results.TryGetValue(relativeTo.Key, out markdownFiles)) - throw new NotSupportedException(); - } - markdownFiles.Add(relativeTo.Value); - } - foreach (KeyValuePair relativeTo in relativeToCollection) - { - markdownFile = relativeTo.Value.MarkdownFile; - fileNameWithoutExtension = markdownFile.FileNameWithoutExtension.ToLower(); - fileNameWithoutExtensionB = fileNameWithoutExtension.Replace("%20", "-").Replace(' ', '-'); - if (!results.TryGetValue(markdownFile.FileNameWithoutExtension, out markdownFiles)) - { - results.Add(markdownFile.FileNameWithoutExtension, new()); - if (!results.TryGetValue(markdownFile.FileNameWithoutExtension, out markdownFiles)) - throw new NotSupportedException(); - } - markdownFiles.Add(relativeTo.Value); - if (fileNameWithoutExtension == markdownFile.FileNameWithoutExtension) - continue; - if (!results.TryGetValue(fileNameWithoutExtension, out markdownFiles)) - { - results.Add(fileNameWithoutExtension, new()); - if (!results.TryGetValue(fileNameWithoutExtension, out markdownFiles)) - throw new NotSupportedException(); - } - if (fileNameWithoutExtensionB == markdownFile.FileNameWithoutExtension) - continue; - if (!results.TryGetValue(fileNameWithoutExtensionB, out markdownFiles)) - { - results.Add(fileNameWithoutExtensionB, new()); - if (!results.TryGetValue(fileNameWithoutExtensionB, out markdownFiles)) - throw new NotSupportedException(); - } - markdownFiles.Add(relativeTo.Value); - } - foreach (KeyValuePair relativeTo in relativeToCollection) - { - markdownFile = relativeTo.Value.MarkdownFile; - if (!results.TryGetValue(markdownFile.H1, out markdownFiles)) - { - results.Add(markdownFile.H1, new()); - if (!results.TryGetValue(markdownFile.H1, out markdownFiles)) - throw new NotSupportedException(); - } - markdownFiles.Add(relativeTo.Value); - } - return new(results); - } - - private static List GetMarkdownFileAndLines(string file, List markdownFiles) - { - List results = new(); - List distinct = new(); - string? directory = Path.GetDirectoryName(file); - foreach (MarkdownFileAndLines markdownFileAndLines in markdownFiles) - { - if (string.IsNullOrEmpty(directory) || markdownFileAndLines.MarkdownFile.Directory != directory) - continue; - if (distinct.Contains(markdownFileAndLines.MarkdownFile.File)) - continue; - distinct.Add(markdownFileAndLines.MarkdownFile.File); - results.Add(markdownFileAndLines); - } - return results; - } - - private static List GetMarkdownFileAndLines(ReadOnlyDictionary> keyValuePairs) - { - List results = new(); - foreach (KeyValuePair> keyValuePair in keyValuePairs) - { - foreach (MarkdownFileAndLines markdownFileAndLines in keyValuePair.Value) - results.Add(markdownFileAndLines); - } - return results; - } - - private static List Distinct(IEnumerable? markdownFileAndLinesCollection) - { - List results = new(); - if (markdownFileAndLinesCollection is not null) - { - List distinct = new(); - foreach (MarkdownFileAndLines markdownFileAndLines in markdownFileAndLinesCollection) - { - if (distinct.Contains(markdownFileAndLines.MarkdownFile.File)) - continue; - distinct.Add(markdownFileAndLines.MarkdownFile.File); - results.Add(markdownFileAndLines); - } - } - return results; - } - - private static MarkdownFileAndLines? GetMarkdownFile(ReadOnlyDictionary> keyValuePairs, MarkdownFile markdownFile, string file) - { - MarkdownFileAndLines? result; - List? markdownFileAndLinesCollection; - string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file); - if (!keyValuePairs.TryGetValue(fileNameWithoutExtension, out markdownFileAndLinesCollection)) - _ = keyValuePairs.TryGetValue(fileNameWithoutExtension.ToLower(), out markdownFileAndLinesCollection); - markdownFileAndLinesCollection = Distinct(markdownFileAndLinesCollection); - if (markdownFileAndLinesCollection is not null && markdownFileAndLinesCollection.Count == 1) - result = markdownFileAndLinesCollection[0]; - else - { - List matches; - matches = markdownFileAndLinesCollection is null ? new() : GetMarkdownFileAndLines(file, markdownFileAndLinesCollection); - if (matches.Count == 1) - result = matches[0]; - else - { - markdownFileAndLinesCollection = GetMarkdownFileAndLines(keyValuePairs); - matches = Distinct(markdownFileAndLinesCollection.Where(l => l.MarkdownFile.FileNameWithoutExtension.Length == fileNameWithoutExtension.Length && l.MarkdownFile.FileNameWithoutExtension.Contains(fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase))); - if (matches.Count == 1) - result = matches[0]; - else - { - string checkName = fileNameWithoutExtension.ToLower().Replace("%20", "-").Replace(' ', '-'); - matches = Distinct(markdownFileAndLinesCollection.Where(l => l.MarkdownFile.FileNameWithoutExtension.Length == checkName.Length && l.MarkdownFile.FileNameWithoutExtension.Contains(checkName, StringComparison.OrdinalIgnoreCase))); - if (matches.Count == 1) - result = matches[0]; - else if (matches.Count == 0) - result = null; - else - { - checkName = matches[0].MarkdownFile.FileNameWithoutExtension; - matches = Distinct(markdownFileAndLinesCollection.Where(l => l.MarkdownFile.File.Contains(checkName, StringComparison.OrdinalIgnoreCase))); - if (matches.Count == 1) - result = matches[0]; - else - { - checkName = $"{checkName}{markdownFile.Extension}"; - matches = Distinct(markdownFileAndLinesCollection.Where(l => l.MarkdownFile.File.EndsWith(checkName, StringComparison.OrdinalIgnoreCase))); - if (matches.Count == 1) - result = matches[0]; - else - { - checkName = $"\\{checkName}"; - matches = Distinct(markdownFileAndLinesCollection.Where(l => l.MarkdownFile.File.EndsWith(checkName, StringComparison.OrdinalIgnoreCase))); - if (matches.Count == 1) - result = matches[0]; - else - result = null; - } - } - } - } - } - } - return result; - } - - private static MarkdownFileH1AndRelativePath GetRelativePath(ReadOnlyDictionary> keyValuePairs, MarkdownFile markdownFile, string file) - { - MarkdownFileAndLines? result = GetMarkdownFile(keyValuePairs, markdownFile, file); - return new(result?.MarkdownFile, result?.Lines, result?.MarkdownFile.H1, result is null ? null : Path.GetRelativePath(markdownFile.Directory, Path.GetFullPath(result.MarkdownFile.File))); - } - private static int ConvertToRelativePath(ILogger logger, ReadOnlyDictionary relativeToCollection) { int result = 0; @@ -794,49 +807,6 @@ internal static partial class HelperMarkdown return result; } - private static int ConvertFileToSlugName(ReadOnlyDictionary relativeToCollection) - { - int result = 0; - string h1; - string h1Check; - string[] lines; - string checkName; - string checkFileName; - MarkdownFile markdownFile; - foreach (KeyValuePair relativeTo in relativeToCollection) - { - if (!relativeTo.Value.Lines.Any()) - continue; - lines = relativeTo.Value.Lines; - markdownFile = relativeTo.Value.MarkdownFile; - if (markdownFile.LineNumber.H1 is not null) - { - h1 = lines[markdownFile.LineNumber.H1.Value]; - if (h1.Length > 2) - { - h1Check = $"# {h1[2..]}"; - if (h1Check.Length == h1.Length && h1Check != h1) - { - lines[markdownFile.LineNumber.H1.Value] = h1Check; - File.WriteAllLines(markdownFile.File, lines); - result += 1; - } - } - } - checkFileName = markdownFile.FileName.ToLower().Replace("%20", "-").Replace(' ', '-'); - if (checkFileName == markdownFile.FileName) - continue; - if (!File.Exists(markdownFile.File)) - continue; - checkName = Path.Combine(markdownFile.Directory, checkFileName); - if (checkName == markdownFile.File) - continue; - File.Move(markdownFile.File, checkName); - result += 1; - } - return result; - } - private static int ConvertFileToSlugName(AppSettings appSettings, ILogger logger, ReadOnlyDictionary relativeToCollection) { int result = 0; @@ -934,65 +904,15 @@ internal static partial class HelperMarkdown return result; } - private static void SetRecursiveLines(AppSettings appSettings, ILogger logger, ReadOnlyDictionary> keyValuePairs, string linkTitle, MarkdownFile markdownFile, string[] lines, List indentations, List recursiveLines) + private static ReadOnlyDictionary GetRelativeToCollection(AppSettings appSettings, Input input, bool force = false) { - if (recursiveLines is null) - throw new Exception(); - string file; - string[] segmentsA; - string[] segmentsB; - string segmentsALast; - bool fencedCodeBlock = false; - string indentation = new(indentations.ToArray()); - MarkdownFileH1AndRelativePath markdownFileH1AndRelativePath; - for (int i = 0; i < lines.Length; i++) - { - if (indentations.Count > 15) - { - recursiveLines.Add("```Error```"); - break; - } - if (lines[i].Length < 1) - continue; - if (lines[i].Length > 4 && lines[i][..3] == "```") - fencedCodeBlock = !fencedCodeBlock; - if (fencedCodeBlock) - continue; - if (lines[i][0] == '#') - { - if (lines[i] == $"# {linkTitle}") - continue; - recursiveLines.Add($"{indentation}{lines[i]}"); - continue; - } - segmentsA = lines[i].Split("]("); - if (segmentsA.Length != 2) - continue; - segmentsALast = segmentsA[^1]; - if (appSettings.ExcludeSchemes.Any(l => segmentsALast.StartsWith(l))) - continue; - segmentsB = segmentsALast.Split(")"); - if (segmentsB.Length != 2) - continue; - file = Path.GetFullPath(Path.Combine(markdownFile.Directory, segmentsB[0])); - markdownFileH1AndRelativePath = GetRelativePath(keyValuePairs, markdownFile, file); - if (markdownFileH1AndRelativePath.MarkdownFile is null || markdownFileH1AndRelativePath.H1 is null || markdownFileH1AndRelativePath.RelativePath is null) - { - recursiveLines.Add($"???{indentation}{lines[i]}"); - logger.LogInformation("Didn't find {line} in <{file}>", lines[i], markdownFile.FileNameWithoutExtension); - continue; - } - if (markdownFileH1AndRelativePath.Lines is null) - continue; - indentations.Add('\t'); - recursiveLines.Add($"{indentation}{lines[i]}"); - SetRecursiveLines(appSettings, logger, keyValuePairs, segmentsA[0].Split('[')[^1], markdownFileH1AndRelativePath.MarkdownFile, markdownFileH1AndRelativePath.Lines, indentations, recursiveLines); - } - if (indentations.Count > 0) - indentations.RemoveAt(0); + ReadOnlyDictionary results; + string[] files = GetFiles(appSettings, input.Source); + results = GetRelativeToCollection(appSettings, input, files, force); + return new(results); } - private static List GetRecursiveLines(AppSettings appSettings, Record record, ILogger logger, ReadOnlyDictionary relativeToCollection) + private static List GetRecursiveLines(AppSettings appSettings, Input input, ILogger logger, ReadOnlyDictionary relativeToCollection) { List results = new(); string[] lines; @@ -1004,7 +924,7 @@ internal static partial class HelperMarkdown { if (!relativeTo.Value.Lines.Any()) continue; - if (record.StartAt is null || !relativeTo.Value.MarkdownFile.File.StartsWith(record.StartAt) || Path.GetFileName(relativeTo.Value.MarkdownFile.Directory) != Path.GetFileName(record.StartAt)) + if (input.StartAt is null || !relativeTo.Value.MarkdownFile.File.StartsWith(input.StartAt) || Path.GetFileName(relativeTo.Value.MarkdownFile.Directory) != Path.GetFileName(input.StartAt)) continue; indentations = new(); recursiveLines = new(); @@ -1016,82 +936,19 @@ internal static partial class HelperMarkdown return results; } - private static void Write(Record record, List markdownFileAndLinesCollection) + private static void Write(Input input, List markdownFileAndLinesCollection) { foreach (MarkdownFileAndLines markdownFileAndLines in markdownFileAndLinesCollection) { - if (record.Destination is null) + if (input.Destination is null) continue; - File.WriteAllLines(Path.Combine(record.Destination, markdownFileAndLines.MarkdownFile.FileName), markdownFileAndLines.Lines); + File.WriteAllLines(Path.Combine(input.Destination, markdownFileAndLines.MarkdownFile.FileName), markdownFileAndLines.Lines); } } - internal static void MarkdownWikiLinkVerification(AppSettings appSettings, ILogger logger, List args) + private static List GetWithLinksForHugo(AppSettings appSettings, Input input) { - int updated; - Record record = GetRecord(args); - ReadOnlyDictionary relativeToCollection; - relativeToCollection = GetRelativeToCollection(appSettings, record); - updated = SetFrontMatterAndH1(appSettings, relativeToCollection); - if (updated != 0) - { - relativeToCollection = GetRelativeToCollection(appSettings, record); - logger.LogInformation("{updated} Markdown file(s) were updated", updated); - } - updated = ConvertFrontMatterToJsonFriendly(relativeToCollection); - if (updated != 0) - { - relativeToCollection = GetRelativeToCollection(appSettings, record); - logger.LogInformation("{updated} Markdown file(s) were updated", updated); - } - updated = CircularReference(logger, relativeToCollection); - if (updated != 0) - { - relativeToCollection = GetRelativeToCollection(appSettings, record); - logger.LogInformation("{updated} Markdown file(s) were updated", updated); - } - updated = FindReplace(relativeToCollection); - if (updated != 0) - { - relativeToCollection = GetRelativeToCollection(appSettings, record); - logger.LogInformation("{updated} Markdown file(s) were updated", updated); - } - updated = ConvertToRelativePath(logger, relativeToCollection); - if (updated != 0) - { - relativeToCollection = GetRelativeToCollection(appSettings, record); - logger.LogInformation("{updated} Markdown file(s) were updated", updated); - } - updated = ConvertFileToSlugName(appSettings, logger, relativeToCollection); - if (updated != 0) - { - relativeToCollection = GetRelativeToCollection(appSettings, record); - logger.LogInformation("{updated} Markdown file(s) were updated", updated); - } - if (!string.IsNullOrEmpty(record.StartAt)) - { - relativeToCollection = GetRelativeToCollection(appSettings, record, force: true); - List markdownFileAndLinesCollection = GetRecursiveLines(appSettings, record, logger, relativeToCollection); - if (!string.IsNullOrEmpty(record.Destination)) - Write(record, markdownFileAndLinesCollection); - } - string directory = Path.Combine(Environment.CurrentDirectory, ".vscode"); - if (!Directory.Exists(directory)) - { - string json; - MarkdownFile markdownFile = relativeToCollection.ElementAt(0).Value.MarkdownFile; - json = JsonSerializer.Serialize(markdownFile, MarkdownFileSourceGenerationContext.Default.MarkdownFile); - if (json != "{}") - { - json = JsonSerializer.Serialize(relativeToCollection.Select(l => l.Value.MarkdownFile).ToArray(), MarkdownFileCollectionSourceGenerationContext.Default.MarkdownFileArray); - File.WriteAllText($"{DateTime.Now.Ticks}.json", json); - } - } - } - - private static List<(string, string, string[])> GetWithLinksForHugo(AppSettings appSettings, Record record) - { - List<(string, string, string[])> results = new(); + List results = new(); string file; string line; string[] lines; @@ -1103,21 +960,21 @@ internal static partial class HelperMarkdown string segmentsALast; string segmentsBFirst; MarkdownFile markdownFile; - int sourceDirectoryLength = record.Source.Length; - ReadOnlyDictionary relativeToCollection = GetRelativeToCollection(appSettings, record); + int sourceDirectoryLength = input.Source.Length; + ReadOnlyDictionary relativeToCollection = GetRelativeToCollection(appSettings, input); foreach (KeyValuePair relativeTo in relativeToCollection) { if (!relativeTo.Value.Lines.Any()) continue; lines = relativeTo.Value.Lines; markdownFile = relativeTo.Value.MarkdownFile; - if (record.Destination is null) + if (input.Destination is null) continue; if (markdownFile.File.Length < sourceDirectoryLength) continue; if (!File.Exists(markdownFile.File)) continue; - fileName = $"{record.Destination}{markdownFile.File[sourceDirectoryLength..]}"; + fileName = $"{input.Destination}{markdownFile.File[sourceDirectoryLength..]}"; directory = Path.GetDirectoryName(fileName); if (string.IsNullOrEmpty(directory)) continue; @@ -1137,25 +994,25 @@ internal static partial class HelperMarkdown file = Path.GetFullPath(Path.Combine(markdownFile.Directory, segmentsBFirst)); else file = Path.GetFullPath(Path.Combine(markdownFile.Directory, segmentsBFirst[..^3])); - relativeFile = Path.GetRelativePath(record.Source, file).Replace('\\', '/'); + relativeFile = Path.GetRelativePath(input.Source, file).Replace('\\', '/'); line = $"{segmentsA[0]}]({relativeFile}){segmentsB[^1]}"; if (lines[i] == line) throw new NotSupportedException($"Line {i} shouldn't match with {line}"); lines[i] = line; } - results.Add((directory, fileName, lines)); + results.Add(new(directory, fileName, lines)); } return results; } - private static List GetDistinct(List<(string, string, string[])> collection) + private static List GetDistinct(List collection) { List results = new(); - foreach ((string directory, _, _) in collection) + foreach (Record record in collection) { - if (results.Contains(directory)) + if (results.Contains(record.Directory)) continue; - results.Add(directory); + results.Add(record.Directory); } return results; } @@ -1169,18 +1026,168 @@ internal static partial class HelperMarkdown } } + private static (string type, string h1) GetTypeAndH1(AppSettings appSettings, string h1, List lines, LineNumber lineNumber) + { + string type = lineNumber.Type is null ? appSettings.DefaultNoteType : lines[lineNumber.Type.Value].Replace("type: ", string.Empty); + string h1FromFile = lineNumber.H1 is null ? h1 : lines[lineNumber.H1.Value][2..]; + return (type, h1FromFile); + } + + private static int SetFrontMatterAndH1(AppSettings appSettings, ReadOnlyDictionary relativeToCollection) + { + int result = 0; + List results = new(); + string h1Line; + string[] lines; + string typeLine; + string createdLine; + string updatedLine; + DateTime creationDateTime; + MarkdownFile markdownFile; + string createdLineCompare; + string updatedLineCompare; + foreach (KeyValuePair relativeTo in relativeToCollection) + { + if (!relativeTo.Value.Lines.Any()) + continue; + results.Clear(); + lines = relativeTo.Value.Lines; + markdownFile = relativeTo.Value.MarkdownFile; + results.AddRange(lines); + creationDateTime = markdownFile.CreationDateTime > markdownFile.LastWriteDateTime ? markdownFile.LastWriteDateTime : markdownFile.CreationDateTime; + typeLine = $"type: \"{appSettings.DefaultNoteType}\""; + h1Line = $"# {markdownFile.FileNameWithoutExtension}"; + createdLineCompare = $"created: \"{creationDateTime.ToUniversalTime():yyyy-MM-dd}T"; + createdLine = $"created: \"{creationDateTime.ToUniversalTime():yyyy-MM-ddTHH:mm:ss.fffZ}\""; + updatedLineCompare = $"updated: \"{markdownFile.LastWriteDateTime.ToUniversalTime():yyyy-MM-dd}T"; + updatedLine = $"updated: \"{markdownFile.LastWriteDateTime.ToUniversalTime():yyyy-MM-ddTHH:mm:ss.fffZ}\""; + if (markdownFile.LineNumber.FrontMatterYamlEnd is null) + { + if (markdownFile.LineNumber.H1 is null) + { + results.Insert(0, string.Empty); + results.Insert(0, h1Line); + results.Insert(0, string.Empty); + } + results.Insert(0, "---"); + results.Insert(0, updatedLine); + results.Insert(0, createdLine); + results.Insert(0, typeLine); + results.Insert(0, "---"); + } + else + { + if (markdownFile.LineNumber.H1 is null) + { + results.Insert(markdownFile.LineNumber.FrontMatterYamlEnd.Value + 1, string.Empty); + results.Insert(markdownFile.LineNumber.FrontMatterYamlEnd.Value + 1, h1Line); + results.Insert(markdownFile.LineNumber.FrontMatterYamlEnd.Value + 1, string.Empty); + } + if (markdownFile.LineNumber.Type is null) + results.Insert(markdownFile.LineNumber.FrontMatterYamlEnd.Value, typeLine); + if (markdownFile.LineNumber.Updated is null) + results.Insert(markdownFile.LineNumber.FrontMatterYamlEnd.Value, updatedLine); + else + { + if (results[markdownFile.LineNumber.Updated.Value].Contains('$')) + continue; + if (results[markdownFile.LineNumber.Updated.Value][..updatedLineCompare.Length] == updatedLineCompare) + continue; + results[markdownFile.LineNumber.Updated.Value] = updatedLine; + } + if (markdownFile.LineNumber.Created is null) + results.Insert(markdownFile.LineNumber.FrontMatterYamlEnd.Value, createdLine); + else if (results[markdownFile.LineNumber.Created.Value][..createdLineCompare.Length] != createdLineCompare) + results[markdownFile.LineNumber.Created.Value] = createdLine; + } + File.WriteAllLines(markdownFile.File, results); + File.SetLastWriteTime(markdownFile.File, markdownFile.LastWriteDateTime); + result += 1; + } + return result; + } + + private static MarkdownFileH1AndRelativePath GetRelativePath(ReadOnlyDictionary> keyValuePairs, MarkdownFile markdownFile, string file) + { + MarkdownFileAndLines? result = GetMarkdownFile(keyValuePairs, markdownFile, file); + return new(result?.MarkdownFile, result?.Lines, result?.MarkdownFile.H1, result is null ? null : Path.GetRelativePath(markdownFile.Directory, Path.GetFullPath(result.MarkdownFile.File))); + } + + internal static void MarkdownWikiLinkVerification(AppSettings appSettings, ILogger logger, List args) + { + int updated; + Input input = GetInput(args); + ReadOnlyDictionary relativeToCollection; + relativeToCollection = GetRelativeToCollection(appSettings, input); + updated = SetFrontMatterAndH1(appSettings, relativeToCollection); + if (updated != 0) + { + relativeToCollection = GetRelativeToCollection(appSettings, input); + logger.LogInformation("{updated} Markdown file(s) were updated", updated); + } + updated = ConvertFrontMatterToJsonFriendly(relativeToCollection); + if (updated != 0) + { + relativeToCollection = GetRelativeToCollection(appSettings, input); + logger.LogInformation("{updated} Markdown file(s) were updated", updated); + } + updated = CircularReference(logger, relativeToCollection); + if (updated != 0) + { + relativeToCollection = GetRelativeToCollection(appSettings, input); + logger.LogInformation("{updated} Markdown file(s) were updated", updated); + } + updated = FindReplace(relativeToCollection); + if (updated != 0) + { + relativeToCollection = GetRelativeToCollection(appSettings, input); + logger.LogInformation("{updated} Markdown file(s) were updated", updated); + } + updated = ConvertToRelativePath(logger, relativeToCollection); + if (updated != 0) + { + relativeToCollection = GetRelativeToCollection(appSettings, input); + logger.LogInformation("{updated} Markdown file(s) were updated", updated); + } + updated = ConvertFileToSlugName(appSettings, logger, relativeToCollection); + if (updated != 0) + { + relativeToCollection = GetRelativeToCollection(appSettings, input); + logger.LogInformation("{updated} Markdown file(s) were updated", updated); + } + if (!string.IsNullOrEmpty(input.StartAt)) + { + relativeToCollection = GetRelativeToCollection(appSettings, input, force: true); + List markdownFileAndLinesCollection = GetRecursiveLines(appSettings, input, logger, relativeToCollection); + if (!string.IsNullOrEmpty(input.Destination)) + Write(input, markdownFileAndLinesCollection); + } + string directory = Path.Combine(Environment.CurrentDirectory, ".vscode"); + if (!Directory.Exists(directory)) + { + string json; + MarkdownFile markdownFile = relativeToCollection.ElementAt(0).Value.MarkdownFile; + json = JsonSerializer.Serialize(markdownFile, MarkdownFileSourceGenerationContext.Default.MarkdownFile); + if (json != "{}") + { + json = JsonSerializer.Serialize(relativeToCollection.Select(l => l.Value.MarkdownFile).ToArray(), MarkdownFileCollectionSourceGenerationContext.Default.MarkdownFileArray); + File.WriteAllText($"{DateTime.Now.Ticks}.json", json); + } + } + } + internal static void MarkdownConvertLinksForHugo(AppSettings appSettings, ILogger logger, List args) { - Record record = GetRecord(args); - if (string.IsNullOrEmpty(record.Destination)) + Input input = GetInput(args); + if (string.IsNullOrEmpty(input.Destination)) throw new NotSupportedException("This method requires frontMatterYamlLines -d path!"); - List<(string, string, string[])> collection = GetWithLinksForHugo(appSettings, record); + List collection = GetWithLinksForHugo(appSettings, input); if (!collection.Any()) logger.LogInformation("No files?"); List distinct = GetDistinct(collection); CreateMissingDirectories(distinct); - foreach ((_, string file, string[] lines) in collection) - File.WriteAllLines(file, lines); + foreach (Record record in collection) + File.WriteAllLines(record.File, record.Lines); } } \ No newline at end of file diff --git a/Helpers/HelperVSCodePossibleExtension.cs b/Helpers/HelperVSCodePossibleExtension.cs new file mode 100644 index 0000000..0b35f7c --- /dev/null +++ b/Helpers/HelperVSCodePossibleExtension.cs @@ -0,0 +1,274 @@ +using Microsoft.Extensions.Logging; +using System.Collections.ObjectModel; +using System.Text.RegularExpressions; + +namespace File_Folder_Helper.Helpers; + +internal static partial class HelperVSCodePossibleExtension +{ + + private record Method(string Name, + int ParameterCount, + int StartLine, + int EndLine, + int FirstUsedLine); + + [GeneratedRegex(@"(?[A-Z]{1}[A-Za-z_0-9]*)\(")] + private static partial Regex CSharpMethodName(); + + [GeneratedRegex(@"\s[a-zA-Z_]*,")] + private static partial Regex CSharpParameter(); + + [GeneratedRegex(@"\b(public|private|internal|protected)\s\b(static)?\s?\b(partial)?\s?\b(async)?\s?[[\]<,>?a-zA-Z()\s]*\s[A-Z]{1}[a-zA-Z_]+\(.*\)")] + private static partial Regex CSharpMethodLine(); + + private static string? GetName(string line) + { + string? result; + Match match = CSharpMethodName().Match(line); + if (!match.Success) + result = null; + else + result = match.Groups["method"].Value; + return result; + } + + private static int GetStartLine(string[] lines, int i) + { + int result = i; + string line; + for (int j = i - 1; j > -1; j--) + { + line = lines[j].Trim(); + if (!line.StartsWith('[') && !line.StartsWith("/// ")) + break; + result--; + } + return result; + } + + private static int GetParameterCount(string line, string search) + { + int result; + string after = line.Split(search)[^1]; + if (after.StartsWith(')')) + result = 0; + else + { + string[] segments = CSharpParameter().Split(after); + result = segments.Length; + } + return result; + } + + private static int GetLineBlockCount(string line, bool isLinq) + { + int result = 0; + bool ignore = false; + for (int i = 0; i < line.Length; i++) + { + if (line[i] == '\'') + i++; + else if (!isLinq && !ignore && line[i] == '{') + result++; + else if (!isLinq && !ignore && line[i] == '}') + result--; + else if (isLinq && !ignore && line[i] == ';') + result--; + else if (i > 0 && line[i] == '"' && line[i - 1] != '\\') + ignore = !ignore; + } + return result; + } + + private static int? GetFirstUsedLine(string[] lines, int i, string search, string searchNot, string searchWrap, int parameterCount) + { + int? result = null; + string[] segments; + string[] afterSegments; + string lastSegmentBeforeDot; + for (int j = 0; j < lines.Length; j++) + { + if (j == i) + continue; + segments = lines[j].Split(search); + if (segments.Length == 1) + { + segments = lines[j].Split(searchNot); + if (segments.Length == 1) + { + segments = lines[j].Split(searchWrap); + if (segments.Length == 1) + continue; + } + } + lastSegmentBeforeDot = segments[^1].Split(").")[0]; + if (parameterCount == 0) + { + if (lastSegmentBeforeDot.Contains(',')) + continue; + } + else + { + afterSegments = lastSegmentBeforeDot.Split(','); + if (afterSegments.Length != parameterCount) + continue; + } + result = j; + break; + } + return result; + } + + private static ReadOnlyCollection GetMethodLines(ReadOnlyCollection methods) + { + List results = new(); + foreach (Method method in methods) + { + for (int i = method.StartLine; i < method.EndLine + 1; i++) + results.Add(i); + } + return new(results); + } + + private static ReadOnlyCollection GetMethods(string cSharpFile, ILogger logger, string[] lines) + { + List results = new(); + int blocks; + bool isLinq; + int endLine; + string line; + string? name; + int startLine; + string search; + string innerLine; + string searchNot; + string searchWrap; + int parameterCount; + int? firstUsedLine; + string lineSegmentFirst; + for (int i = 0; i < lines.Length; i++) + { + line = lines[i].Trim(); + if (string.IsNullOrEmpty(line)) + continue; + if (line.Length < 5) + continue; + if (line.EndsWith(',')) + continue; + if (!CSharpMethodLine().Match(line).Success) + continue; + name = GetName(line); + search = $" {name}("; + searchNot = $"!{name}("; + searchWrap = $"({name}("; + if (string.IsNullOrEmpty(name)) + continue; + blocks = 0; + startLine = GetStartLine(lines, i); + parameterCount = GetParameterCount(line, search); + isLinq = lines[i + 1].Trim() != "{"; + if (isLinq) + blocks++; + for (int j = i + 1; j < lines.Length; j++) + { + innerLine = lines[j].Trim(); + if (isLinq && string.IsNullOrEmpty(innerLine)) + { + if (line.EndsWith(';')) + blocks--; + } + blocks += GetLineBlockCount(innerLine, isLinq); + if (blocks == 0) + { + endLine = j; + if (lines.Length > j + 1 && string.IsNullOrEmpty(lines[j + 1].Trim())) + endLine++; + firstUsedLine = GetFirstUsedLine(lines, i, search, searchNot, searchWrap, parameterCount); + if (firstUsedLine is null) + { + lineSegmentFirst = line.Split(search)[0]; + if (!lines[i - 1].Trim().StartsWith("[Obsolete")) + { + if (lineSegmentFirst.StartsWith("private")) + logger.LogWarning("<{cSharpFileName}> {name} with {parameterCount} parameter(s) <{line}>", Path.GetFileName(cSharpFile), name, parameterCount, lineSegmentFirst); + else + logger.LogInformation("<{cSharpFileName}> {name} with {parameterCount} parameter(s) <{line}>", Path.GetFileName(cSharpFile), name, parameterCount, lineSegmentFirst); + } + break; + } + if (j > lines.Length - 2) + throw new Exception(); + results.Add(new(name, parameterCount, startLine, endLine, firstUsedLine.Value)); + break; + } + } + } + return new(results.OrderBy(l => l.FirstUsedLine).ToArray()); + } + + private static bool WriteAllLines(string cSharpFile, string[] lines, ReadOnlyCollection methods) + { + bool result; + List results = new(); + ReadOnlyCollection methodLines = GetMethodLines(methods); + int minMethodLines = methodLines.Min(); + for (int i = 0; i < minMethodLines; i++) + results.Add(lines[i]); + foreach (Method method in methods) + { + for (int i = method.StartLine; i < method.EndLine + 1; i++) + results.Add(lines[i]); + } + for (int i = minMethodLines; i < lines.Length; i++) + { + if (methodLines.Contains(i)) + continue; + results.Add(lines[i]); + } + string text = File.ReadAllText(cSharpFile); + string join = string.Join(Environment.NewLine, results); + if (join == text) + result = false; + else + { + result = true; + File.WriteAllText(cSharpFile, join); + } + return result; + } + + private static bool SortFile(ILogger logger, string cSharpFile, string[] lines) + { + bool result; + ReadOnlyCollection methods = GetMethods(cSharpFile, logger, lines); + if (methods.Count == 0) + result = false; + else + result = WriteAllLines(cSharpFile, lines, methods); + return result; + } + + internal static void Sort(ILogger logger, List args) + { + bool result = false; + bool check; + string[] lines; + long ticks = DateTime.Now.Ticks; + logger.LogInformation("{ticks}", ticks); + string[] cSharpFiles = Directory.GetFiles(args[0], "*.cs", SearchOption.TopDirectoryOnly); + for (int i = 0; i < 10; i++) + { + foreach (string cSharpFile in cSharpFiles) + { + lines = File.ReadAllLines(cSharpFile); + check = SortFile(logger, cSharpFile, lines); + if (check && !result) + result = true; + } + if (!result) + break; + } + } + +} \ No newline at end of file diff --git a/Helpers/HelperZipFilesByDate.cs b/Helpers/HelperZipFilesByDate.cs index 25d9640..4493540 100644 --- a/Helpers/HelperZipFilesByDate.cs +++ b/Helpers/HelperZipFilesByDate.cs @@ -11,6 +11,91 @@ internal static partial class HelperZipFilesByDate [GeneratedRegex("[a-zA-Z0-9]{1,}")] private static partial Regex LowerAlphaAlphaAndNumber(); + private static bool SetDateFromZipEntry(ILogger log, string[] zipFiles, string keyFile, string keyFileB, string keyFileC) + { + bool result = false; + string[] files; + string checkFile; + FileInfo fileInfo; + string? zipDirectory; + DateTimeOffset? dateTimeOffset; + foreach (string zipFile in zipFiles) + try + { + dateTimeOffset = null; + fileInfo = new(zipFile); + using ZipArchive zip = ZipFile.Open(zipFile, ZipArchiveMode.Read); + foreach (ZipArchiveEntry zipArchiveEntry in zip.Entries) + { + if (!zipArchiveEntry.Name.EndsWith(keyFile)) + continue; + dateTimeOffset = zipArchiveEntry.LastWriteTime; + break; + } + zipDirectory = Path.GetDirectoryName(zipFile); + if (dateTimeOffset is null || zipDirectory is null) + continue; + if (fileInfo.LastWriteTime != dateTimeOffset.Value.LocalDateTime) + { + File.SetLastWriteTime(fileInfo.FullName, dateTimeOffset.Value.LocalDateTime); + if (!result) + result = true; + } + files = Directory.GetFiles(zipDirectory, $"*{keyFile}", SearchOption.TopDirectoryOnly); + foreach (string file in files) + { + fileInfo = new(file); + if (fileInfo.LastWriteTime != dateTimeOffset.Value.LocalDateTime) + { + File.SetLastWriteTime(fileInfo.FullName, dateTimeOffset.Value.LocalDateTime); + if (!result) + result = true; + } + } + if (string.IsNullOrEmpty(keyFileB)) + continue; + files = Directory.GetFiles(zipDirectory, keyFileB, SearchOption.TopDirectoryOnly); + foreach (string file in files) + { + fileInfo = new(file); + if (fileInfo.LastWriteTime != dateTimeOffset.Value.LocalDateTime) + { + File.SetLastWriteTime(fileInfo.FullName, dateTimeOffset.Value.LocalDateTime); + if (!result) + result = true; + } + } + if (string.IsNullOrEmpty(keyFileC)) + continue; + files = Directory.GetFiles(zipDirectory, keyFileC, SearchOption.TopDirectoryOnly); + foreach (string file in files) + { + fileInfo = new(file); + if (fileInfo.LastWriteTime != dateTimeOffset.Value.LocalDateTime) + { + File.SetLastWriteTime(fileInfo.FullName, dateTimeOffset.Value.LocalDateTime); + if (!result) + result = true; + } + } + } + catch (Exception) + { + log.LogInformation("<{zipFile}> is invalid!", zipFile); + checkFile = string.Concat(zipFile, ".err"); + for (int e = 0; e < short.MaxValue; e++) + { + if (!File.Exists(checkFile)) + break; + checkFile = string.Concat(checkFile, e); + } + try + { File.Move(zipFile, checkFile); } + catch (Exception) { log.LogInformation("<{zipFile}> couldn't be moved!", zipFile); } + } + return result; + } + internal static bool ZipFilesByDate(ILogger log, string sourceDirectory, SearchOption searchOption = SearchOption.TopDirectoryOnly, string dayFormat = "") { bool result = false; @@ -152,91 +237,6 @@ internal static partial class HelperZipFilesByDate return result; } - private static bool SetDateFromZipEntry(ILogger log, string[] zipFiles, string keyFile, string keyFileB, string keyFileC) - { - bool result = false; - string[] files; - string checkFile; - FileInfo fileInfo; - string? zipDirectory; - DateTimeOffset? dateTimeOffset; - foreach (string zipFile in zipFiles) - try - { - dateTimeOffset = null; - fileInfo = new(zipFile); - using ZipArchive zip = ZipFile.Open(zipFile, ZipArchiveMode.Read); - foreach (ZipArchiveEntry zipArchiveEntry in zip.Entries) - { - if (!zipArchiveEntry.Name.EndsWith(keyFile)) - continue; - dateTimeOffset = zipArchiveEntry.LastWriteTime; - break; - } - zipDirectory = Path.GetDirectoryName(zipFile); - if (dateTimeOffset is null || zipDirectory is null) - continue; - if (fileInfo.LastWriteTime != dateTimeOffset.Value.LocalDateTime) - { - File.SetLastWriteTime(fileInfo.FullName, dateTimeOffset.Value.LocalDateTime); - if (!result) - result = true; - } - files = Directory.GetFiles(zipDirectory, $"*{keyFile}", SearchOption.TopDirectoryOnly); - foreach (string file in files) - { - fileInfo = new(file); - if (fileInfo.LastWriteTime != dateTimeOffset.Value.LocalDateTime) - { - File.SetLastWriteTime(fileInfo.FullName, dateTimeOffset.Value.LocalDateTime); - if (!result) - result = true; - } - } - if (string.IsNullOrEmpty(keyFileB)) - continue; - files = Directory.GetFiles(zipDirectory, keyFileB, SearchOption.TopDirectoryOnly); - foreach (string file in files) - { - fileInfo = new(file); - if (fileInfo.LastWriteTime != dateTimeOffset.Value.LocalDateTime) - { - File.SetLastWriteTime(fileInfo.FullName, dateTimeOffset.Value.LocalDateTime); - if (!result) - result = true; - } - } - if (string.IsNullOrEmpty(keyFileC)) - continue; - files = Directory.GetFiles(zipDirectory, keyFileC, SearchOption.TopDirectoryOnly); - foreach (string file in files) - { - fileInfo = new(file); - if (fileInfo.LastWriteTime != dateTimeOffset.Value.LocalDateTime) - { - File.SetLastWriteTime(fileInfo.FullName, dateTimeOffset.Value.LocalDateTime); - if (!result) - result = true; - } - } - } - catch (Exception) - { - log.LogInformation("<{zipFile}> is invalid!", zipFile); - checkFile = string.Concat(zipFile, ".err"); - for (int e = 0; e < short.MaxValue; e++) - { - if (!File.Exists(checkFile)) - break; - checkFile = string.Concat(checkFile, e); - } - try - { File.Move(zipFile, checkFile); } - catch (Exception) { log.LogInformation("<{zipFile}> couldn't be moved!", zipFile); } - } - return result; - } - internal static bool SetDateFromZipEntryForNuspec(ILogger log, string[] files) => SetDateFromZipEntry(log, files, ".nuspec", "icon", "readme"); diff --git a/Models/AppSettings.cs b/Models/AppSettings.cs index 4c3e289..94479e5 100644 --- a/Models/AppSettings.cs +++ b/Models/AppSettings.cs @@ -9,6 +9,7 @@ public record AppSettings(string Company, string[] ExcludeSchemes, string PersonBirthdayFormat, char[] PersonCharacters, + char[] PersonTitleFilters, string WorkingDirectoryName) { diff --git a/Models/Binder/AppSettings.cs b/Models/Binder/AppSettings.cs index 853c92e..0824845 100644 --- a/Models/Binder/AppSettings.cs +++ b/Models/Binder/AppSettings.cs @@ -13,6 +13,7 @@ public class AppSettings public string[]? ExcludeSchemes { get; set; } public string? PersonBirthdayFormat { get; set; } public string? PersonCharacters { get; set; } + public string? PersonTitleFilters { get; set; } public string? WorkingDirectoryName { get; set; } public override string ToString() @@ -36,6 +37,8 @@ public class AppSettings throw new NullReferenceException(nameof(appSettings.PersonBirthdayFormat)); if (appSettings?.PersonCharacters is null) throw new NullReferenceException(nameof(appSettings.PersonCharacters)); + if (appSettings?.PersonTitleFilters is null) + throw new NullReferenceException(nameof(appSettings.PersonTitleFilters)); if (appSettings?.WorkingDirectoryName is null) throw new NullReferenceException(nameof(appSettings.WorkingDirectoryName)); result = new( @@ -45,6 +48,7 @@ public class AppSettings appSettings.ExcludeSchemes, appSettings.PersonBirthdayFormat, appSettings.PersonCharacters.ToArray(), + appSettings.PersonTitleFilters.ToArray(), appSettings.WorkingDirectoryName ); return result; diff --git a/Models/Person.cs b/Models/Person.cs index 115644e..a6c036b 100644 --- a/Models/Person.cs +++ b/Models/Person.cs @@ -8,6 +8,7 @@ public record Person(long Id, char? Sex, string? UId, Birth? Birth, + string? Title, Death? Death, Change? Change, string[] Lines) diff --git a/Worker.cs b/Worker.cs index bb7f6e9..9d11cdd 100644 --- a/Worker.cs +++ b/Worker.cs @@ -43,6 +43,7 @@ public class Worker : BackgroundService ConsoleKey.S, ConsoleKey.T, ConsoleKey.U, + ConsoleKey.V, ConsoleKey.Z, ConsoleKey.Delete }; @@ -94,15 +95,23 @@ public class Worker : BackgroundService _Logger.LogInformation("F) Clipboard (All Directories and File Name Without Extension),"); _Logger.LogInformation("G) Genealogical Data Communication"); _Logger.LogInformation("H) Hardcoded file search and sort,"); + // I _Logger.LogInformation("J) Set Date from Json Entry"); _Logger.LogInformation("K) Kanban support"); _Logger.LogInformation("L) Log Merge (APC Log [0-9(8)]_*.log),"); _Logger.LogInformation("N) Create Note Files,"); _Logger.LogInformation("M) Markdown Wiki Link Verification,"); + // O + // P + // Q _Logger.LogInformation("R) Rename to old, copy, delete old"); _Logger.LogInformation("S) Set Date from Zip Entry"); _Logger.LogInformation("T) Too long rename"); _Logger.LogInformation("U) Links for Hugo"); + _Logger.LogInformation("V) VSCode Hope Sort"); + // W + // X + // Y _Logger.LogInformation("Z) Zip file(s) by date,"); _Logger.LogInformation("Delete) Delete empty directories,"); consoleKey = Console.ReadKey().Key; @@ -153,6 +162,9 @@ public class Worker : BackgroundService case ConsoleKey.U: Helpers.HelperMarkdown.MarkdownConvertLinksForHugo(_AppSettings, _Logger, _Args); break; + case ConsoleKey.V: + Helpers.HelperVSCodePossibleExtension.Sort(_Logger, _Args); + break; case ConsoleKey.Z: _ = Helpers.HelperZipFilesByDate.ZipFilesByDate(_Logger, _Args[0]); break;