using File_Folder_Helper.Models; using Microsoft.Extensions.Logging; using System.Collections.ObjectModel; using System.Text; using System.Text.RegularExpressions; namespace File_Folder_Helper.Helpers; internal static partial class HelperKanbanMetadata { [GeneratedRegex("([A-Z]+(.))")] private static partial Regex UpperCase(); [GeneratedRegex("[\\s!?.,@:;|\\\\/\"'`£$%\\^&*{}[\\]()<>~#+\\-=_¬]+")] private static partial Regex InvalidCharacter(); private record Record(FileInfo FileInfo, string Group, int GroupCount, int ItemLineNumber); 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 void TestParamCases() { // cSpell:disable 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("я надеюсь, что это сработает"); } // cSpell:restore private static List GetCollectionFromIndex(string sourceDirectory, ReadOnlyCollection lines) { List results = []; string line; FileInfo fileInfo; string[] segments; int groupCount = 0; string? group = null; for (int i = 0; i < lines.Count; i++) { line = lines[i]; if (line.Length < 4) continue; if (line[..3] == "## ") { group = line[3..]; groupCount += 1; continue; } if (group is null || line[..3] != "- [" || line[^1] != ')') continue; segments = line.Split("]("); if (segments.Length != 2) continue; fileInfo = new(Path.Combine(sourceDirectory, segments[1][..^1])); if (!fileInfo.Exists) continue; results.Add(new(fileInfo, group, groupCount, i)); } return results; } private static void WriteKanbanBoardFile(string directory, List records, string h1) { string? last = null; List results = [h1]; foreach (Record record in records) { if (last is null || record.Group != last) { results.Add(string.Empty); results.Add($"## {record.Group}"); results.Add(string.Empty); } results.Add($"- [ ] {Path.GetFileNameWithoutExtension(record.FileInfo.Name)}"); last = record.Group; } string file = Path.Combine(directory, "index.knb.md"); if (File.Exists(file)) { string allText = File.ReadAllText(file); if (string.Join(Environment.NewLine, results) == allText) results.Clear(); } if (results.Count > 0) File.WriteAllText(file, string.Join(Environment.NewLine, results)); } private static void WriteKanbanBoardYmlView(string directory, List records, string kanbanIndexH1) { List results = [kanbanIndexH1, string.Empty]; string h1; TimeSpan timeSpan; List lines; LineNumber lineNumber; Record[] sorted = (from l in records orderby l.GroupCount, l.FileInfo.LastWriteTime descending select l).ToArray(); foreach (Record record in sorted) { if (record.ItemLineNumber == 0) throw new NotSupportedException(); (lines, lineNumber) = HelperMarkdown.GetStatusAndFrontMatterYamlEndLineNumbers(record.FileInfo); if (lines.Count == 0) continue; timeSpan = new(record.FileInfo.LastWriteTime.Ticks - record.FileInfo.CreationTime.Ticks); h1 = lineNumber.H1 is null ? Path.GetFileNameWithoutExtension(record.FileInfo.Name) : lines[lineNumber.H1.Value]; results.Add($"#{h1}"); results.Add(string.Empty); results.Add("```yaml"); results.Add($"CreationTime: {record.FileInfo.CreationTime:yyyy-MM-dd}"); results.Add($"LastWriteTime: {record.FileInfo.LastWriteTime:yyyy-MM-dd}"); results.Add($"TotalDays: {Math.Round(timeSpan.TotalDays, 2)}"); if (lineNumber.FrontMatterYamlEnd is not null && lines.Count >= lineNumber.FrontMatterYamlEnd.Value) { for (int i = 0; i < lineNumber.FrontMatterYamlEnd; i++) { if (lines[i] == "---") continue; results.Add(lines[i]); } } results.Add($"status: \"{record.GroupCount}-{record.Group}\""); results.Add("```"); results.Add(string.Empty); } string file = Path.Combine(directory, "index.yml.md"); if (File.Exists(file)) { string allText = File.ReadAllText(file); if (string.Join(Environment.NewLine, results) == allText) results.Clear(); } if (results.Count > 0) File.WriteAllText(file, string.Join(Environment.NewLine, results)); } internal static void SetMetadata(string sourceDirectory, ReadOnlyCollection kanbanIndexFileLines, LineNumber kanbanIndexFileLineNumber, ReadOnlyCollection gitOthersModifiedAndDeletedExcludingStandardFiles) { bool? match; bool gitCheck; string? paramCase; string statusLine; List lines; LineNumber lineNumber; string? directory = Path.GetDirectoryName(sourceDirectory); List records = GetCollectionFromIndex(sourceDirectory, kanbanIndexFileLines); if (directory is not null && kanbanIndexFileLineNumber.H1 is not null) { string checkDirectory = Path.Combine(directory, ".vscode", "helper"); if (Directory.Exists(checkDirectory)) { WriteKanbanBoardFile(checkDirectory, records, kanbanIndexFileLines[kanbanIndexFileLineNumber.H1.Value]); WriteKanbanBoardYmlView(checkDirectory, records, kanbanIndexFileLines[kanbanIndexFileLineNumber.H1.Value]); } } foreach (Record record in records) { if (record.ItemLineNumber == 0) throw new NotSupportedException(); (lines, lineNumber) = HelperMarkdown.GetStatusAndFrontMatterYamlEndLineNumbers(record.FileInfo); if (lines.Count == 0) continue; statusLine = $"status: \"{record.GroupCount}-{record.Group}\""; paramCase = lineNumber.H1 is null ? null : GetParamCase(lines[lineNumber.H1.Value]); match = lineNumber.H1 is null || paramCase is null ? null : Path.GetFileNameWithoutExtension(record.FileInfo.Name) == paramCase; if (lineNumber.FrontMatterYamlEnd is null) throw new NotSupportedException($"{nameof(SetMetadata)} must be executed first!"); if (lineNumber.H1 is not null && paramCase is not null && match is not null && !match.Value) lines[lineNumber.H1.Value] = $"# {paramCase}"; if (lineNumber.Status is null) lines.Insert(lineNumber.FrontMatterYamlEnd.Value, statusLine); else { if ((match is null || match.Value) && lines[lineNumber.Status.Value] == statusLine) continue; lines[lineNumber.Status.Value] = statusLine; } gitCheck = gitOthersModifiedAndDeletedExcludingStandardFiles.Contains(record.FileInfo.FullName); if (!gitCheck) continue; File.WriteAllLines(record.FileInfo.FullName, lines); } } internal static void SetMetadata(ILogger logger, string sourceDirectory) { TestParamCases(); string fullPath = Path.GetFullPath(sourceDirectory); if (!Directory.Exists(fullPath)) _ = Directory.CreateDirectory(fullPath); string indexFile = Path.Combine(fullPath, "index.md"); if (!File.Exists(indexFile)) logger.LogInformation("<{indexFile}> doesn't exist!", indexFile); else { FileInfo fileInfo = new(indexFile); (List lines, LineNumber lineNumber) = HelperMarkdown.GetStatusAndFrontMatterYamlEndLineNumbers(fileInfo); SetMetadata(fullPath, new(lines), lineNumber, gitOthersModifiedAndDeletedExcludingStandardFiles: new([])); } } }