using File_Folder_Helper.Helpers; 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; using System.Text.RegularExpressions; namespace File_Folder_Helper.ADO2024.PI2; internal static partial class Helper20240623 { [GeneratedRegex("([A-Z]+(.))")] private static partial Regex UpperCase(); [GeneratedRegex("[\\s!?.,@:;|\\\\/\"'`£$%\\^&*{}[\\]()<>~#+\\-=_¬]+")] private static partial Regex InvalidCharacter(); private record H1AndParamCase(string H1, string ParamCase); private record SubTaskLine(string Text, bool Done, long? Ticks, int? Line); private record Record(int? CodeInsidersLine, FileInfo FileInfo, LineNumber LineNumber, int? StopLine, int? SubTasksLine); private record Input(long? AfterEpochTotalMilliseconds, string CodeInsiders, string? DestinationDirectory, string DirectoryFilter, string Done, string IndexFile, string SearchPattern, string SubTasks, string SourceDirectory, ReadOnlyCollection Tasks); [JsonSourceGenerationOptions(WriteIndented = true)] [JsonSerializable(typeof(Input))] private partial class InputSourceGenerationContext : JsonSerializerContext { } private static Record GetRecord(Input input, FileInfo fileInfo) { Record result; int? stopLine = null; int? subTasksLine = null; int? codeInsidersLine = null; LineNumber lineNumber = HelperMarkdown.GetLineNumbers(fileInfo); for (int i = 0; i < lineNumber.Lines.Count; i++) { if (lineNumber.Lines[i].StartsWith(input.CodeInsiders) && lineNumber.Lines[i][^1] == '"') { if (lineNumber.Lines.Count > i + 1 && lineNumber.Lines[i + 1] == "```") codeInsidersLine = i; } if (lineNumber.Lines[i] != input.SubTasks) continue; subTasksLine = i; if (codeInsidersLine is null) break; if (lineNumber.Lines.Count > i) { for (int j = i + 1; j < lineNumber.Lines.Count; j++) { if (lineNumber.Lines[j].Length > 0 && lineNumber.Lines[j][0] == '#') { stopLine = j; break; } } } stopLine ??= lineNumber.Lines.Count; break; } result = new(codeInsidersLine, fileInfo, lineNumber, stopLine, subTasksLine); return result; } private static List GetRecords(Input input) { List results = []; Record record; FileInfo fileInfo; string sourceDirectory = input.SourceDirectory; ReadOnlyCollection directoryNames = HelperDirectory.GetDirectoryNames(input.SourceDirectory); if (!directoryNames.Any(l => l.StartsWith(input.DirectoryFilter, StringComparison.CurrentCultureIgnoreCase))) { string directoryName; string[] checkDirectories = Directory.GetDirectories(input.SourceDirectory, "*", SearchOption.TopDirectoryOnly); foreach (string checkDirectory in checkDirectories) { directoryName = Path.GetFileName(checkDirectory); if (directoryName.StartsWith(input.DirectoryFilter, StringComparison.CurrentCultureIgnoreCase)) { sourceDirectory = checkDirectory; break; } } } string[] subDirectories = Directory.GetDirectories(sourceDirectory, "*", SearchOption.TopDirectoryOnly); List files = Directory.GetFiles(sourceDirectory, input.SearchPattern, SearchOption.TopDirectoryOnly).ToList(); foreach (string subDirectory in subDirectories) files.AddRange(Directory.GetFiles(subDirectory, input.SearchPattern, SearchOption.TopDirectoryOnly)); foreach (string file in files) { fileInfo = new(file); record = GetRecord(input, fileInfo); results.Add(record); } return results; } private static string GetParamCase(string value) { string result; StringBuilder stringBuilder = new(value); Match[] matches = UpperCase().Matches(value).ToArray(); for (int i = matches.Length - 1; i > -1; i--) _ = stringBuilder.Insert(matches[i].Index, '-'); string[] segments = InvalidCharacter().Split(stringBuilder.ToString().ToLower()); result = string.Join('-', segments).Trim('-'); return result; } private static ReadOnlyCollection GetSubTaskLines(Input input, bool? foundDone, string fallbackLine, Record record) { List results = []; char done; string line; string text; bool doneValue; SubTaskLine subTaskLine; bool foundSubTasks = false; int tasksZeroLength = input.Tasks[0].Length; long ticks = record.FileInfo.LastWriteTime.Ticks; for (int i = 0; i < record.LineNumber.Lines.Count; i++) { line = record.LineNumber.Lines[i]; if (!foundSubTasks && line == input.SubTasks) foundSubTasks = true; if (!foundSubTasks) continue; if (line.Length <= tasksZeroLength || !line.StartsWith(input.Tasks[0]) || line[tasksZeroLength] is not ' ' and not 'x' || line[tasksZeroLength + 1] != ']') continue; doneValue = foundDone is not null && foundDone.Value; subTaskLine = new($" {line}", doneValue, ticks, i); results.Add(subTaskLine); } doneValue = foundDone is not null && foundDone.Value; if (record.LineNumber.H1 is null) subTaskLine = new(fallbackLine, doneValue, ticks, Line: null); else { done = foundDone is null || !foundDone.Value ? ' ' : 'x'; string codeInsidersLine = record.CodeInsidersLine is null ? string.Empty : $" ~~{record.LineNumber.Lines[record.CodeInsidersLine.Value]}~~"; text = $"- [{done}] {ticks} {record.LineNumber.Lines[record.LineNumber.H1.Value]}{codeInsidersLine}"; subTaskLine = new(text, doneValue, ticks, Line: 0); } results.Add(subTaskLine); return new(results); } private static string GetSeasonName(int dayOfYear) { string result = dayOfYear switch { < 78 => "0.Winter", < 124 => "1.Spring", < 171 => "2.Spring", < 217 => "3.Summer", < 264 => "4.Summer", < 309 => "5.Fall", < 354 => "6.Fall", _ => "7.Winter" }; return result; } private static string[] GetIndexLines(string h1, ReadOnlyCollection h1ParamCaseCollection) => [ "---", "startedColumns:", " - 'In Progress'", "completedColumns:", " - Done", "---", string.Empty, $"# {h1}", string.Empty, "## Backlog", string.Empty, string.Join(Environment.NewLine, h1ParamCaseCollection.Select(l => $"- [{l.ParamCase}](tasks/{l.ParamCase}.md)")), string.Empty, "## Todo", string.Empty, "## In Progress", string.Empty, "## Done", string.Empty ]; private static string[] GetCascadingStyleSheetsLines() => [ ".kanbn-column-done .kanbn-column-task-list {", " border-color: #198038;", "}", string.Empty, ".kanbn-task-data-created {", " display: none;", "}", string.Empty, ".kanbn-task-data-workload {", " display: none;", "}" ]; private static string GetSettingsLines() => /*lang=json,strict*/ """ { "[markdown]": { "editor.wordWrap": "off" }, "cSpell.words": [ "kanbn" ] } """; private static string GetTasksLines(string directory) => /*lang=json,strict*/ """ { "version": "2.0.0", "tasks": [ { "label": "File-Folder-Helper AOT s X Day-Helper-2024-06-23", "type": "shell", "command": "L:/DevOps/Mesa_FI/File-Folder-Helper/bin/Release/net8.0/win-x64/publish/File-Folder-Helper.exe", "args": [ "s", "X", "{}", "Day-Helper-2024-06-23", "*.md", "##_Sub-tasks", "code-insiders", "index.md", "-_[,](", "##_Done", ".kan", "D:/5-Other-Small/Kanban/Year-Season", "316940400000" ], "problemMatcher": [] } ] } """.Replace("{}", directory.Replace('\\', '/')); private static void FileWriteAllText(string path, string contents) { // string checkJson = Regex.Replace(File.ReadAllText(path), @"\s+", " ", RegexOptions.Multiline); // if (Regex.Replace(singletonJson, @"\s+", " ", RegexOptions.Multiline) != checkJson) // File.WriteAllText(path, singletonJson); string old = !File.Exists(path) ? string.Empty : File.ReadAllText(path); if (old != contents) File.WriteAllText(path, contents); } private static void FileWriteAllText(string path, string[] contents) => FileWriteAllText(path, string.Join(Environment.NewLine, contents)); private static ReadOnlyCollection GetH1ParamCaseCollection(Input input, ReadOnlyCollection lines) { List results = []; string h1; string line; string paramCase; bool foundSubTasks = false; H1AndParamCase h1AndParamCase; int tasksZeroLength = input.Tasks[0].Length; for (int i = 0; i < lines.Count; i++) { line = lines[i]; if (!foundSubTasks && line == input.SubTasks) foundSubTasks = true; if (!foundSubTasks) continue; if (line.Length <= tasksZeroLength || !line.StartsWith(input.Tasks[0]) || line[tasksZeroLength] is not ' ' and not 'x' || line[tasksZeroLength + 1] != ']') continue; h1 = line[(tasksZeroLength + 3)..]; if (string.IsNullOrEmpty(h1)) continue; paramCase = GetParamCase(h1); h1AndParamCase = new(h1, paramCase); results.Add(h1AndParamCase); } return results.AsReadOnly(); } private static void CreateFiles(string directory, ReadOnlyCollection h1ParamCaseCollection) { foreach (H1AndParamCase h1ParamCase in h1ParamCaseCollection) FileWriteAllText(Path.Combine(directory, $"{h1ParamCase.ParamCase}.md"), $"# {h1ParamCase.H1}"); } private static string WriteAndGetIndexFile(string destinationDirectory, string h1, long verified, ReadOnlyCollection h1ParamCaseCollection) { string result; string[] indexLines = GetIndexLines(h1, h1ParamCaseCollection); DateTime utcEpochDateTime = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); DateTime dateTime = utcEpochDateTime.AddMilliseconds(verified).ToLocalTime(); string seasonName = GetSeasonName(dateTime.DayOfYear); string verifiedDirectory = Path.Combine(destinationDirectory, $"{dateTime.Year}", $"{dateTime.Year}-{seasonName}", verified.ToString()); string kanbanDirectory = Path.Combine(verifiedDirectory, ".kanbn"); string tasksKanbanDirectory = Path.Combine(kanbanDirectory, "tasks"); if (!Directory.Exists(tasksKanbanDirectory)) _ = Directory.CreateDirectory(tasksKanbanDirectory); string verifiedVisualStudioCodeDirectory = Path.Combine(verifiedDirectory, ".vscode"); if (!Directory.Exists(verifiedVisualStudioCodeDirectory)) _ = Directory.CreateDirectory(verifiedVisualStudioCodeDirectory); result = Path.Combine(kanbanDirectory, "index.md"); CreateFiles(tasksKanbanDirectory, h1ParamCaseCollection); FileWriteAllText(result, indexLines); FileWriteAllText(Path.Combine(kanbanDirectory, "board.css"), GetCascadingStyleSheetsLines()); FileWriteAllText(Path.Combine(verifiedVisualStudioCodeDirectory, "settings.json"), GetSettingsLines()); FileWriteAllText(Path.Combine(verifiedVisualStudioCodeDirectory, "tasks.json"), GetTasksLines(verifiedDirectory)); return result; } private static ReadOnlyCollection GetSubTaskLines(Input input, FileInfo fileInfo, LineNumber lineNumber) { List results = []; char done; FileInfo f; Record record; bool doneValue; string[] segments; string fallbackLine; bool? foundDone = null; ReadOnlyCollection subTaskLines; for (int i = 0; i < lineNumber.Lines.Count; i++) { if (lineNumber.Lines[i] == input.Done) foundDone = true; segments = lineNumber.Lines[i].Split(input.Tasks[1]); doneValue = foundDone is not null && foundDone.Value; if (segments.Length > 2 || !segments[0].StartsWith(input.Tasks[0])) continue; done = foundDone is null || !foundDone.Value ? ' ' : 'x'; fallbackLine = $"- [{done}] {segments[0][input.Tasks[0].Length..]} ~~FallbackLine~~"; if (string.IsNullOrEmpty(fileInfo.DirectoryName)) continue; f = new(Path.GetFullPath(Path.Combine(fileInfo.DirectoryName, segments[1][..^1]))); if (!f.Exists) { results.Add(new(fallbackLine, doneValue, Ticks: null, Line: null)); continue; } record = GetRecord(input, f); if (lineNumber.H1 is not null && record.LineNumber.H1 is not null) { string a = lineNumber.Lines[lineNumber.H1.Value]; string b = record.LineNumber.Lines[record.LineNumber.H1.Value]; if (b != a) { if (b != a) { } } } subTaskLines = GetSubTaskLines(input, doneValue, fallbackLine, record); for (int j = subTaskLines.Count - 1; j >= 0; j--) results.Add(subTaskLines[j]); } return results.AsReadOnly(); } private static Input GetInput(List args) { string indexFile = args[5]; string searchPattern = args[2]; string directoryFilter = args[8]; string codeInsiders = $"{args[4]} \""; string done = args[7].Replace('_', ' '); string subTasks = args[3].Replace('_', ' '); string sourceDirectory = Path.GetFullPath(args[0]); string? destinationDirectory = args.Count < 8 ? null : Path.GetFullPath(args[9]); long? afterEpochTotalMilliseconds = args.Count < 9 ? null : long.Parse(args[10]); ReadOnlyCollection tasks = args[6].Split(',').Select(l => l.Replace('_', ' ')).ToArray().AsReadOnly(); Input input = new(afterEpochTotalMilliseconds, codeInsiders, destinationDirectory, directoryFilter, done, indexFile, searchPattern, subTasks, sourceDirectory, tasks); if (input.Tasks[0] != "- [" || input.Tasks[1] != "](") throw new Exception(JsonSerializer.Serialize(input, InputSourceGenerationContext.Default.Input)); return input; } private static string? MaybeWriteAndGetIndexFile(Input input, Record record, string? checkDirectory) { string? result; if (string.IsNullOrEmpty(checkDirectory) || input.AfterEpochTotalMilliseconds is null || string.IsNullOrEmpty(input.DestinationDirectory) || !checkDirectory.Contains(input.DestinationDirectory)) result = null; else { if (record.LineNumber.H1 is null) result = null; else { string segment = Path.GetFileName(checkDirectory); DateTime utcEpochDateTime = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); long utcEpochTotalMilliseconds = (long)Math.Floor(DateTime.UtcNow.Subtract(utcEpochDateTime).TotalMilliseconds); if (!long.TryParse(segment, out long check) || check < input.AfterEpochTotalMilliseconds || check > utcEpochTotalMilliseconds) result = null; else { ReadOnlyCollection h1ParamCaseCollection = GetH1ParamCaseCollection(input, record.LineNumber.Lines); if (h1ParamCaseCollection.Count == 0) result = null; else result = WriteAndGetIndexFile(input.DestinationDirectory, record.LineNumber.Lines[record.LineNumber.H1.Value], check, h1ParamCaseCollection); } } } return result; } private static bool FileWrite(Record record, List newLines, double percent) { bool result = false; if (record.StopLine is not null && record.SubTasksLine is not null) { string contents; string progressLine; List resultLines; resultLines = record.LineNumber.Lines.ToList(); if (record.LineNumber.FrontMatterYamlEnd is not null) { progressLine = $"progress: {percent}"; if (record.LineNumber.Progress is not null) resultLines[record.LineNumber.Progress.Value] = progressLine; else { resultLines.Insert(record.LineNumber.FrontMatterYamlEnd.Value, progressLine); contents = string.Join(Environment.NewLine, resultLines); FileWriteAllText(record.FileInfo.FullName, contents); result = true; } if (!result && record.LineNumber.Completed is null && percent > 99.9) { resultLines.Insert(record.LineNumber.FrontMatterYamlEnd.Value, $"completed: {DateTime.Now:yyyy-MM-dd}"); contents = string.Join(Environment.NewLine, resultLines); FileWriteAllText(record.FileInfo.FullName, contents); result = true; } if (!result && record.LineNumber.Completed is not null && percent < 99.9) { resultLines.RemoveAt(record.LineNumber.Completed.Value); contents = string.Join(Environment.NewLine, resultLines); FileWriteAllText(record.FileInfo.FullName, contents); result = true; } } if (!result) { for (int i = record.StopLine.Value - 1; i > record.SubTasksLine.Value + 1; i--) resultLines.RemoveAt(i); if (record.StopLine.Value == record.LineNumber.Lines.Count && resultLines[^1].Length == 0) resultLines.RemoveAt(resultLines.Count - 1); for (int i = 0; i < newLines.Count; i++) resultLines.Insert(record.SubTasksLine.Value + 1 + i, newLines[i]); resultLines.Insert(record.SubTasksLine.Value + 1, string.Empty); contents = string.Join(Environment.NewLine, resultLines); FileWriteAllText(record.FileInfo.FullName, contents); } } return result; } private static FileInfo GetIndexFileInfo(Input input, Record record, string codeInsidersLine) { FileInfo result; string? indexFile; List results; string? checkDirectory = codeInsidersLine[input.CodeInsiders.Length..^1]; if (!Directory.Exists(checkDirectory)) { if (!string.IsNullOrEmpty(input.DestinationDirectory) && checkDirectory.Contains(input.DestinationDirectory)) _ = Directory.CreateDirectory(checkDirectory); } if (!Directory.Exists(checkDirectory)) results = []; else { results = Directory.GetFiles(checkDirectory, input.IndexFile, SearchOption.AllDirectories).ToList(); if (results.Count != 1) { for (int i = results.Count - 1; i > -1; i--) { if (!results[i].Contains(input.DirectoryFilter, StringComparison.CurrentCultureIgnoreCase)) results.RemoveAt(i); } } if (results.Count == 0) { indexFile = MaybeWriteAndGetIndexFile(input, record, checkDirectory); if (!string.IsNullOrEmpty(indexFile)) results.Add(indexFile); } } result = results.Count == 0 ? new(Path.Combine(checkDirectory, input.IndexFile)) : new(results[0]); return result; } internal static void UpdateSubTasksInMarkdownFiles(ILogger logger, List args) { bool reload; int allCount; int lineCheck; double percent; double doneCount; FileInfo fileInfo; List records; LineNumber lineNumber; List newLines; bool reloadAny = false; string? checkDirectory; string codeInsidersLine; List oldLines = []; Input input = GetInput(args); string fileNameWithoutExtension; ReadOnlyCollection subTaskLines; for (int z = 0; z < 9; z++) { records = GetRecords(input); foreach (Record record in from l in records orderby l.SubTasksLine is null, l.CodeInsidersLine is null select l) { if (record.SubTasksLine is null) continue; fileNameWithoutExtension = Path.GetFileNameWithoutExtension(record.FileInfo.FullName); if (record.CodeInsidersLine is not null) logger.LogInformation("<{file}> has [{subTasks}]", fileNameWithoutExtension, input.SubTasks); else { logger.LogWarning("<{file}> has [{subTasks}] but doesn't have [{codeInsiders}]!", fileNameWithoutExtension, input.SubTasks, input.CodeInsiders); continue; } if (record.StopLine is null) continue; codeInsidersLine = record.LineNumber.Lines[record.CodeInsidersLine.Value]; fileInfo = GetIndexFileInfo(input, record, codeInsidersLine); if (!fileInfo.Exists) { checkDirectory = codeInsidersLine[input.CodeInsiders.Length..^1]; logger.LogError("<{checkDirectory}> doesn't have a [{indexFile}]", Path.GetFileName(checkDirectory), input.IndexFile); continue; } oldLines.Clear(); checkDirectory = fileInfo.DirectoryName; lineNumber = HelperMarkdown.GetLineNumbers(fileInfo); subTaskLines = GetSubTaskLines(input, fileInfo, lineNumber); if (subTaskLines.Count == 0) continue; lineCheck = 0; for (int i = record.SubTasksLine.Value + 1; i < record.StopLine.Value - 1; i++) oldLines.Add(record.LineNumber.Lines[i]); if (subTaskLines.Any(l => l.Ticks is null)) newLines = (from l in subTaskLines select l.Text).ToList(); else newLines = (from l in subTaskLines orderby l.Done descending, l.Ticks, l.Line select l.Text).ToList(); if (subTaskLines.Count == 0) percent = 0; else { allCount = (from l in subTaskLines where l.Line is not null && l.Line.Value == 0 select 1).Count(); doneCount = (from l in subTaskLines where l.Line is not null && l.Line.Value == 0 && l.Done select 1).Count(); // done = allCount != doneCount ? ' ' : 'x'; percent = allCount == 0 ? 0 : Math.Round(doneCount / allCount, 3); // newLines.Insert(0, $"- [{done}] Sub-tasks {doneCount} of {allCount} [{percent * 100}%]"); } if (newLines.Count == oldLines.Count) { for (int i = 0; i < newLines.Count; i++) { if (newLines[i] != record.LineNumber.Lines[record.SubTasksLine.Value + 1 + i]) continue; lineCheck++; } if (lineCheck == newLines.Count) continue; } if (string.IsNullOrEmpty(checkDirectory)) continue; checkDirectory = Path.Combine(checkDirectory, DateTime.Now.Ticks.ToString()); _ = Directory.CreateDirectory(checkDirectory); Thread.Sleep(500); Directory.Delete(checkDirectory); reload = FileWrite(record, newLines, percent); if (!reloadAny && reload) reloadAny = true; } if (!reloadAny) break; } } }