file-folder-helper/ADO2024/PI2/Helper-2024-06-23.cs
2025-02-24 17:46:47 -07:00

625 lines
27 KiB
C#

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,
ReadOnlyCollection<string> DestinationDirectories,
string DirectoryFilter,
string Done,
string IndexFile,
string SearchPattern,
string SubTasks,
string SourceDirectory,
ReadOnlyCollection<string> Tasks);
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(Input))]
private partial class InputSourceGenerationContext : JsonSerializerContext
{
}
private static Record GetRecord(Input input, FileInfo fileInfo)
{
Record result;
string line;
int? stopLine = null;
int? subTasksLine = null;
int? codeInsidersLine = null;
LineNumber lineNumber = HelperMarkdown.GetLineNumbers(fileInfo);
for (int i = 0; i < lineNumber.Lines.Count; i++)
{
line = lineNumber.Lines[i];
if (line.StartsWith(input.CodeInsiders) && line[^1] == ')')
codeInsidersLine = i;
if (line != 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<Record> GetRecords(Input input)
{
List<Record> results = [];
Record record;
FileInfo fileInfo;
string sourceDirectory = input.SourceDirectory;
ReadOnlyCollection<string> 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<string> 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<SubTaskLine> GetSubTaskLines(Input input, bool? foundDone, string fallbackLine, Record record)
{
List<SubTaskLine> 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<H1AndParamCase> 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<H1AndParamCase> GetH1ParamCaseCollection(Input input, ReadOnlyCollection<string> lines)
{
List<H1AndParamCase> 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<H1AndParamCase> h1ParamCaseCollection)
{
foreach (H1AndParamCase h1ParamCase in h1ParamCaseCollection)
FileWriteAllText(Path.Combine(directory, $"{h1ParamCase.ParamCase}.md"), $"# {h1ParamCase.H1}");
}
private static string WriteAndGetIndexFile(string h1, string verifiedDirectory, ReadOnlyCollection<H1AndParamCase> h1ParamCaseCollection)
{
string result;
string[] indexLines = GetIndexLines(h1, h1ParamCaseCollection);
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<SubTaskLine> GetSubTaskLines(Input input, FileInfo fileInfo, LineNumber lineNumber)
{
List<SubTaskLine> results = [];
char done;
FileInfo f;
Record record;
bool doneValue;
string[] segments;
string fallbackLine;
bool? foundDone = null;
ReadOnlyCollection<SubTaskLine> 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<string> args)
{
string indexFile = args[5];
string searchPattern = args[2];
string directoryFilter = args[8];
string done = args[7].Replace('_', ' ');
string subTasks = args[3].Replace('_', ' ');
string codeInsiders = args[4].Replace('_', ' ');
string sourceDirectory = Path.GetFullPath(args[0]);
string[] tasks = args[6].Split(',').Select(l => l.Replace('_', ' ')).ToArray();
long? afterEpochTotalMilliseconds = args.Count < 11 ? null : long.Parse(args[10]);
string[] destinationDirectories = args.Count < 10 ? [] : args.Count < 12 ? [Path.GetFullPath(args[9])] : [Path.GetFullPath(args[9]), Path.GetFullPath(args[11])];
Input input = new(AfterEpochTotalMilliseconds: afterEpochTotalMilliseconds,
CodeInsiders: codeInsiders,
DestinationDirectories: destinationDirectories.AsReadOnly(),
DirectoryFilter: directoryFilter,
Done: done,
IndexFile: indexFile,
SearchPattern: searchPattern,
SubTasks: subTasks,
SourceDirectory: sourceDirectory,
Tasks: tasks.AsReadOnly());
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 || input.DestinationDirectories.Count == 0)
result = null;
else
{
if (!input.DestinationDirectories.Any(checkDirectory.Contains))
result = null;
else
{
if (record.LineNumber.H1 is null)
result = null;
else
{
string segment = Path.GetFileName(checkDirectory);
string h1 = record.LineNumber.Lines[record.LineNumber.H1.Value];
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<H1AndParamCase> h1ParamCaseCollection = GetH1ParamCaseCollection(input, record.LineNumber.Lines);
if (h1ParamCaseCollection.Count == 0)
result = null;
else
{
DateTime dateTime = utcEpochDateTime.AddMilliseconds(check).ToLocalTime();
string seasonName = GetSeasonName(dateTime.DayOfYear);
ReadOnlyCollection<string> directoryNames = HelperDirectory.GetDirectoryNames(checkDirectory);
if (!directoryNames.Contains(dateTime.Year.ToString()) || !directoryNames.Contains($"{dateTime.Year}-{seasonName}") || !directoryNames.Contains(check.ToString()))
result = null;
else
result = WriteAndGetIndexFile(h1, checkDirectory, h1ParamCaseCollection);
}
}
}
}
}
return result;
}
private static bool FileWrite(Record record, List<string> newLines, double percent)
{
bool result = false;
if (record.StopLine is not null && record.SubTasksLine is not null)
{
string contents;
string progressLine;
List<string> 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(ILogger<Worker> logger, Input input, Record record, string codeInsidersLine)
{
FileInfo result;
string? indexFile;
List<string> results;
string raw = codeInsidersLine[input.CodeInsiders.Length..^1];
string checkDirectory = $"{raw[..2].ToUpper()}{raw[2..]}";
if (!Directory.Exists(checkDirectory))
{
if (input.DestinationDirectories.Count > 0 && input.DestinationDirectories.Any(checkDirectory.Contains))
_ = 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);
else
logger.LogInformation("<{checkDirectory}>", checkDirectory);
}
}
result = results.Count == 0 ? new(Path.Combine(checkDirectory, input.IndexFile)) : new(results[0]);
return result;
}
internal static void UpdateSubTasksInMarkdownFiles(ILogger<Worker> logger, List<string> args)
{
bool reload;
int allCount;
int lineCheck;
double percent;
double doneCount;
FileInfo fileInfo;
List<Record> records;
LineNumber lineNumber;
List<string> newLines;
bool reloadAny = false;
string? checkDirectory;
string codeInsidersLine;
List<string> oldLines = [];
Input input = GetInput(args);
string fileNameWithoutExtension;
ReadOnlyCollection<SubTaskLine> 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(logger, 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;
}
}
}