file-folder-helper/Helpers/HelperMarkdown.cs
2025-02-27 14:45:50 -07:00

1241 lines
57 KiB
C#

using File_Folder_Helper.Models;
using Microsoft.Extensions.Logging;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using YamlDotNet.Serialization;
namespace File_Folder_Helper.Helpers;
internal static partial class HelperMarkdown
{
private record Input(string? Destination,
string Source,
bool UseProcessStart);
private record Record(string Directory,
string File,
string[] Lines);
private record MarkdownFile(DateTime CreationDateTime,
string Directory,
string Extension,
string File,
string FileName,
string FileNameWithoutExtension,
ReadOnlyDictionary<string, object> FrontMatterYaml,
string H1,
bool IsGitOthersModifiedAndDeletedExcludingStandard,
bool IsKanbanIndex,
bool IsKanbanMarkdown,
DateTime LastWriteDateTime,
LineNumber LineNumber,
string Type);
private record MarkdownFileAndLines(MarkdownFile MarkdownFile,
string[] Lines);
private record MarkdownExtra(ReadOnlyCollection<string>? Assignees,
string? Effort,
ReadOnlyCollection<H2HexColor>? H2HexColorCollection,
ReadOnlyCollection<H2NoCheckboxes>? H2NoCheckboxesCollection,
ReadOnlyCollection<H2WithCheckboxes>? H2WithCheckboxesCollection,
string? RequestedDateTime);
private record MarkdownFileH1AndRelativePath(MarkdownFile? MarkdownFile, string[]? Lines, string? H1, string? RelativePath);
[JsonSourceGenerationOptions(WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
[JsonSerializable(typeof(Dictionary<string, JsonElement>))]
internal partial class DictionaryStringAndJsonElementSourceGenerationContext : JsonSerializerContext
{
}
[GeneratedRegex("(~~)?(#)([a-zA-Z0-9]{6})(~~)?( )")]
private static partial Regex HtmlColor();
private static MarkdownExtra GetMarkdownExtra(MarkdownFileAndLines markdownFileAndLines)
{
MarkdownExtra result;
int skip;
Match match;
string line;
int completed;
int notCompleted;
List<string> lines;
string? effort = null;
List<string> assignees = [];
string? requestedDateTime = null;
ReadOnlyCollection<Group> groups;
List<H2HexColor> h2HexColors = [];
List<H2NoCheckboxes> h2NoCheckboxes = [];
List<H2WithCheckboxes> h2WithCheckboxes = [];
if (markdownFileAndLines.MarkdownFile.LineNumber.FrontMatterYamlEnd is not null)
{
for (int i = 1; i < markdownFileAndLines.Lines.Length; i++)
{
line = markdownFileAndLines.Lines[i];
if (line.Length < 3)
continue;
if (line.Length > 8 && line[..8] == "effort: ")
{
effort = line[7..].Trim().Trim('"');
continue;
}
if (line.Length > 10 && line[..10] == "assigned: ")
{
foreach (string item in line[10..].Split(',', StringSplitOptions.RemoveEmptyEntries))
assignees.Add(item.Trim().Trim('"'));
continue;
}
if (line.Length > 11 && line[..11] == "requested: ")
{
requestedDateTime = line[10..].Trim().Trim('"');
continue;
}
if (line.Length > 3 && line[0] == '#' && line[1] == '#' && line[2] == ' ')
{
completed = 0;
notCompleted = 0;
match = HtmlColor().Match(line[3..]);
if (line.Length > 3 && match.Success)
{
groups = match.Groups.AsReadOnly();
skip = 3 + groups.Skip(1).Sum(l => l.Length);
h2HexColors.Add(new(line[skip..], $"#{groups.First(l => l.Value.Length == 6)}"));
continue;
}
lines = [];
if (i + 1 == markdownFileAndLines.Lines.Length)
continue;
for (int j = i + 1; j < markdownFileAndLines.Lines.Length; j++)
{
line = markdownFileAndLines.Lines[j];
if (line.Length == 0)
continue;
if (line.Length > 2 && line[0] == '#')
break;
lines.Add(line);
if (line.Length < 5 || line[0] != '-' || line[1] != ' ' || line[2] != '[')
continue;
if (line[3] == ' ' && line[4] == ']')
notCompleted++;
else if (line[3] is 'x' or 'X' && line[4] == ']')
completed++;
}
if (completed != 0 || notCompleted != 0)
h2WithCheckboxes.Add(new(completed,
markdownFileAndLines.Lines[i][3..],
notCompleted,
notCompleted + completed));
else if (lines.Count > 0)
h2NoCheckboxes.Add(new(markdownFileAndLines.Lines[i][3..], new(lines)));
continue;
}
}
}
result = new(new(assignees), effort, new(h2HexColors), new(h2NoCheckboxes), new(h2WithCheckboxes), requestedDateTime);
return result;
}
private static List<MarkdownFileAndLines> GetMarkdownFileAndLines(string file, List<MarkdownFileAndLines> markdownFiles)
{
List<MarkdownFileAndLines> results = [];
List<string> distinct = [];
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 FileInfo[] GetFiles(AppSettings appSettings, DirectoryInfo directoryInfo, SearchOption searchOption) =>
directoryInfo.GetFiles("*.md", searchOption).Where(l => !appSettings.ExcludeDirectoryNames.Any(m => l.FullName.Contains(m))).ToArray();
/// <summary>
/// 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.
/// </summary>
/// <param name="filename">The text file to analyze.</param>
/// <returns>The detected encoding.</returns>
private 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;
}
private static ReadOnlyCollection<string> GetFromMatterYamlLines(ReadOnlyCollection<string> lines, LineNumber lineNumber)
{
List<string> results = [];
if (lineNumber.FrontMatterYamlEnd is not null && lines.Count >= lineNumber.FrontMatterYamlEnd.Value)
{
for (int i = 1; i < lineNumber.FrontMatterYamlEnd.Value; i++)
results.Add(lines[i]);
}
return new(results);
}
private static List<string> GetKeys(ReadOnlyDictionary<string, MarkdownFileAndLines> relativeToCollection)
{
List<string> results = [];
MarkdownFile markdownFile;
foreach (KeyValuePair<string, MarkdownFileAndLines> relativeTo in relativeToCollection)
{
markdownFile = relativeTo.Value.MarkdownFile;
if (markdownFile.IsKanbanMarkdown)
continue;
results.Add(relativeTo.Key);
}
return results;
}
private static void SetCards(Input input, ReadOnlyDictionary<string, MarkdownFileAndLines> relativeToCollection, List<Card> notLinkedKey, MarkdownFile markdownFile, string[] lines, List<Card> cards, List<string> allKeys, int i)
{
Card card;
string key;
string[] segmentsA;
MarkdownExtra markdownExtra;
MarkdownFileAndLines? markdownFileAndLines;
for (int j = i + 1; j < lines.Length; j++)
{
if (lines[j].Length < 5)
continue;
if (lines[j].Length >= 4 && lines[j][0] == '#' && lines[j][1] == '#' && lines[j][2] == ' ')
break;
segmentsA = lines[j].Split("](");
if (segmentsA.Length != 2 || segmentsA[1][^1] != ')')
continue;
key = Path.GetRelativePath(input.Source, Path.Combine(markdownFile.Directory, segmentsA[1][..^1]));
if (!relativeToCollection.TryGetValue(key, out markdownFileAndLines))
continue;
markdownExtra = GetMarkdownExtra(markdownFileAndLines);
card = new(markdownExtra.Assignees,
markdownFileAndLines.MarkdownFile.CreationDateTime,
markdownFileAndLines.MarkdownFile.Directory,
markdownExtra.Effort,
markdownFileAndLines.MarkdownFile.Extension,
markdownFileAndLines.MarkdownFile.File,
markdownFileAndLines.MarkdownFile.FileName,
markdownFileAndLines.MarkdownFile.FileNameWithoutExtension,
markdownFileAndLines.MarkdownFile.H1,
markdownExtra.H2HexColorCollection,
markdownExtra.H2NoCheckboxesCollection,
markdownExtra.H2WithCheckboxesCollection,
markdownFileAndLines.MarkdownFile.LastWriteDateTime,
markdownFileAndLines.MarkdownFile.LineNumber,
markdownExtra.RequestedDateTime,
markdownFileAndLines.MarkdownFile.Type);
if (allKeys.Remove(key))
cards.Add(card);
else
notLinkedKey.Add(card);
}
}
private static List<MarkdownFileAndLines> Distinct(IEnumerable<MarkdownFileAndLines>? markdownFileAndLinesCollection)
{
List<MarkdownFileAndLines> results = [];
if (markdownFileAndLinesCollection is not null)
{
List<string> distinct = [];
foreach (MarkdownFileAndLines markdownFileAndLines in markdownFileAndLinesCollection)
{
if (distinct.Contains(markdownFileAndLines.MarkdownFile.File))
continue;
distinct.Add(markdownFileAndLines.MarkdownFile.File);
results.Add(markdownFileAndLines);
}
}
return results;
}
private static List<MarkdownFileAndLines> GetMarkdownFileAndLines(ReadOnlyDictionary<string, List<MarkdownFileAndLines>> keyValuePairs)
{
List<MarkdownFileAndLines> results = [];
foreach (KeyValuePair<string, List<MarkdownFileAndLines>> keyValuePair in keyValuePairs)
{
foreach (MarkdownFileAndLines markdownFileAndLines in keyValuePair.Value)
results.Add(markdownFileAndLines);
}
return results;
}
private static ReadOnlyCollection<FileInfo> GetFiles(AppSettings appSettings, Input input)
{
List<FileInfo> results = [];
List<FileInfo> collection = [];
DirectoryInfo sourceDirectoryInfo = new(input.Source);
DirectoryInfo[] directories = sourceDirectoryInfo.GetDirectories("*", SearchOption.AllDirectories);
foreach (DirectoryInfo directoryInfo in directories)
{
collection.Clear();
if (!directoryInfo.Exists || (!string.IsNullOrEmpty(directoryInfo.LinkTarget) && !Directory.Exists(directoryInfo.LinkTarget)))
continue;
collection.AddRange(GetFiles(appSettings, directoryInfo, SearchOption.TopDirectoryOnly));
foreach (FileInfo file in collection)
results.Add(file);
}
return new(results);
}
internal static LineNumber GetLineNumbers(FileInfo fileInfo)
{
string line;
int? h1LineNumber = null;
int? typeLineNumber = null;
int? statusLineNumber = null;
int? createdLineNumber = null;
int? updatedLineNumber = null;
int? progressLineNumber = null;
int? completedLineNumber = null;
int? startedColumnsLineNumber = null;
int? completedColumnsLineNumber = null;
int? frontMatterYamlEndLineNumber = null;
Encoding? encoding = GetEncoding(fileInfo.FullName) ?? Encoding.Default;
string[] lines = File.ReadAllLines(fileInfo.FullName, encoding);
for (int i = 0; i < lines.Length; i++)
{
line = lines[i];
if (line.Length < 3)
continue;
if (i == 0 && line[..3] == "---")
continue;
if (h1LineNumber is null && 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 (line.Length > 10 && line[..10] == "progress: ")
{
progressLineNumber = i;
continue;
}
if (line.Length > 11 && line[..11] == "completed: ")
{
completedLineNumber = i;
continue;
}
if (line.Length > 14 && line[..14] == "startedColumns")
{
startedColumnsLineNumber = i;
continue;
}
if (line.Length > 16 && line[..16] == "completedColumns")
{
completedColumnsLineNumber = i;
continue;
}
if (h1LineNumber is null && line.Length > 2 && line[0] == '#' && line[1] == ' ')
{
h1LineNumber = i;
continue;
}
}
LineNumber lineNumber = new(Created: createdLineNumber,
Completed: completedLineNumber,
CompletedColumns: completedColumnsLineNumber,
H1: h1LineNumber,
FrontMatterYamlEnd: frontMatterYamlEndLineNumber,
Lines: lines.AsReadOnly(),
Progress: progressLineNumber,
Status: statusLineNumber,
StartedColumns: startedColumnsLineNumber,
Type: typeLineNumber,
Updated: updatedLineNumber);
return lineNumber;
}
private static Dictionary<string, object> GetFromMatterYaml(ReadOnlyCollection<string> frontMatterYamlLines)
{
Dictionary<string, object> results = [];
string[] segments;
foreach (string line in frontMatterYamlLines.OrderBy(l => l))
{
segments = line.Split(": ");
if (segments.Length != 2)
{
results.Clear();
break;
}
if (segments[1] is "''" or "\"\"")
results.Add(segments[0], string.Empty);
else if (segments[1] == "[]")
results.Add(segments[0], Array.Empty<string>());
else
results.Add(segments[0], segments[1].Trim());
}
return results;
}
private static ReadOnlyDictionary<string, object> GetFromMatterYaml(ReadOnlyCollection<string> lines, LineNumber lineNumber)
{
Dictionary<string, object> results = [];
#pragma warning disable IL3050
IDeserializer deserializer = new DeserializerBuilder().Build();
#pragma warning restore IL3050
ReadOnlyCollection<string> frontMatterYamlLines = GetFromMatterYamlLines(lines, lineNumber);
if (!frontMatterYamlLines.Any(l => l.StartsWith(' ')))
results = GetFromMatterYaml(frontMatterYamlLines);
else
{
string frontMatterYaml = string.Join(Environment.NewLine, frontMatterYamlLines);
Dictionary<string, object>? keyValuePairs = deserializer.Deserialize<Dictionary<string, object>>(frontMatterYaml);
if (keyValuePairs is null)
results = GetFromMatterYaml(frontMatterYamlLines);
else
{
foreach (string key in keyValuePairs.Keys.OrderBy(l => l))
results.Add(key, keyValuePairs[key]);
}
}
return new(results);
}
private static ReadOnlyDictionary<string, List<MarkdownFileAndLines>> GetKeyValuePairs(ReadOnlyDictionary<string, MarkdownFileAndLines> relativeToCollection)
{
Dictionary<string, List<MarkdownFileAndLines>> results = [];
MarkdownFile markdownFile;
string fileNameWithoutExtension;
string fileNameWithoutExtensionB;
List<MarkdownFileAndLines>? markdownFiles;
foreach (KeyValuePair<string, MarkdownFileAndLines> relativeTo in relativeToCollection)
{
markdownFile = relativeTo.Value.MarkdownFile;
if (!results.TryGetValue(relativeTo.Key, out markdownFiles))
{
results.Add(relativeTo.Key, []);
if (!results.TryGetValue(relativeTo.Key, out markdownFiles))
throw new NotSupportedException();
}
markdownFiles.Add(relativeTo.Value);
}
foreach (KeyValuePair<string, MarkdownFileAndLines> 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, []);
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, []);
if (!results.TryGetValue(fileNameWithoutExtension, out markdownFiles))
throw new NotSupportedException();
}
if (fileNameWithoutExtensionB == markdownFile.FileNameWithoutExtension)
continue;
if (!results.TryGetValue(fileNameWithoutExtensionB, out markdownFiles))
{
results.Add(fileNameWithoutExtensionB, []);
if (!results.TryGetValue(fileNameWithoutExtensionB, out markdownFiles))
throw new NotSupportedException();
}
markdownFiles.Add(relativeTo.Value);
}
foreach (KeyValuePair<string, MarkdownFileAndLines> relativeTo in relativeToCollection)
{
markdownFile = relativeTo.Value.MarkdownFile;
if (!results.TryGetValue(markdownFile.H1, out markdownFiles))
{
results.Add(markdownFile.H1, []);
if (!results.TryGetValue(markdownFile.H1, out markdownFiles))
throw new NotSupportedException();
}
markdownFiles.Add(relativeTo.Value);
}
return new(results);
}
private static int ConvertFileToSlugName(ReadOnlyDictionary<string, MarkdownFileAndLines> relativeToCollection)
{
int result = 0;
string h1;
string h1Check;
string[] lines;
string checkName;
string checkFileName;
MarkdownFile markdownFile;
foreach (KeyValuePair<string, MarkdownFileAndLines> relativeTo in relativeToCollection)
{
if (relativeTo.Value.Lines.Length == 0)
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)
{
if (!markdownFile.IsGitOthersModifiedAndDeletedExcludingStandard)
continue;
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;
if (!markdownFile.IsGitOthersModifiedAndDeletedExcludingStandard)
continue;
File.Move(markdownFile.File, checkName);
result += 1;
}
return result;
}
private static MarkdownFileAndLines? GetKanbanIndexMarkdownFileAndLines(ReadOnlyDictionary<string, MarkdownFileAndLines> keyValuePairs)
{
MarkdownFile markdownFile;
MarkdownFileAndLines? result = null;
foreach (KeyValuePair<string, MarkdownFileAndLines> keyValuePair in keyValuePairs)
{
markdownFile = keyValuePair.Value.MarkdownFile;
if (markdownFile.IsKanbanIndex)
{
if (result is not null)
{
result = null;
break;
}
result = keyValuePair.Value;
}
}
return result;
}
private static ReadOnlyDictionary<string, List<Card>> GetColumnsToCards(Input input, ReadOnlyDictionary<string, MarkdownFileAndLines> relativeToCollection, MarkdownFileAndLines markdownFileAndLines)
{
Dictionary<string, List<Card>> results = [];
string? column;
string[] lines;
List<Card> cards = [];
List<Card> notLinkedKey = [];
lines = markdownFileAndLines.Lines;
List<string> allKeys = GetKeys(relativeToCollection);
for (int i = 0; i < lines.Length; i++)
{
if (lines[i].Length < 4 || lines[i][0] != '#' || lines[i][1] != '#' || lines[i][2] != ' ')
continue;
column = lines[i][3..].TrimEnd();
if (lines.Length == i + 1)
continue;
SetCards(input, relativeToCollection, notLinkedKey, markdownFileAndLines.MarkdownFile, lines, cards, allKeys, i);
results.Add(column, cards);
cards = [];
}
if (notLinkedKey.Count > 1)
results.Add("Not Linked", notLinkedKey);
return new(results);
}
private static MarkdownFileAndLines? GetMarkdownFile(ReadOnlyDictionary<string, List<MarkdownFileAndLines>> keyValuePairs, MarkdownFile markdownFile, string file)
{
MarkdownFileAndLines? result;
List<MarkdownFileAndLines>? 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<MarkdownFileAndLines> matches;
matches = markdownFileAndLinesCollection is null ? [] : 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<string> args)
{
Input result;
string? destination = null;
bool useProcessStart = false;
string source = Path.GetFullPath(args[0]);
for (int i = 1; i < args.Count; i++)
{
if (args[i].Length == 2 && i + 1 < args.Count)
{
if (args[i][1] == 'u')
useProcessStart = args[i + 1] == "true";
else if (args[i][1] == 'd')
destination = Path.GetFullPath(args[i + 1]);
i++;
}
}
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(destination, source, useProcessStart);
return result;
}
private static ReadOnlyDictionary<string, MarkdownFileAndLines> GetRelativeToCollection(AppSettings appSettings, Input input, ReadOnlyCollection<string> gitOthersModifiedAndDeletedExcludingStandardFiles)
{
Dictionary<string, MarkdownFileAndLines> results = [];
string h1;
string key;
string type;
bool isKanbanIndex;
bool isWithinSource;
bool isKanbanMarkdown;
LineNumber lineNumber;
MarkdownFile markdownFile;
string fileNameWithoutExtension;
ReadOnlyCollection<string> lines;
ReadOnlyDictionary<string, object> frontMatterYaml;
bool isGitOthersModifiedAndDeletedExcludingStandard;
ReadOnlyCollection<FileInfo> files = GetFiles(appSettings, input);
foreach (FileInfo fileInfo in files)
{ // cSpell:disable
if (fileInfo.DirectoryName is null)
continue;
key = Path.GetRelativePath(input.Source, fileInfo.FullName);
isWithinSource = fileInfo.FullName.Contains(input.Source);
isGitOthersModifiedAndDeletedExcludingStandard = gitOthersModifiedAndDeletedExcludingStandardFiles.Contains(fileInfo.FullName);
if (!isWithinSource && results.ContainsKey(key))
continue;
lineNumber = GetLineNumbers(fileInfo);
lines = lineNumber.Lines;
fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileInfo.FullName);
h1 = fileNameWithoutExtension.ToLower().Replace("%20", "-").Replace(' ', '-');
frontMatterYaml = GetFromMatterYaml(lines, lineNumber);
if (lines.Count > 0)
(type, h1) = GetTypeAndH1(appSettings, h1, lines, lineNumber);
else
{
if (!isGitOthersModifiedAndDeletedExcludingStandard)
continue;
type = appSettings.DefaultNoteType;
File.WriteAllLines(fileInfo.FullName, ["---", $"type: {type}\"", "---", string.Empty, $"# {h1}"]);
lines = File.ReadAllLines(fileInfo.FullName).AsReadOnly();
}
isKanbanMarkdown = fileInfo.Name.EndsWith(".knb.md");
isKanbanIndex = fileNameWithoutExtension == "index" && type.StartsWith("kanb", StringComparison.OrdinalIgnoreCase);
markdownFile = new(fileInfo.CreationTime,
fileInfo.DirectoryName,
fileInfo.Extension,
fileInfo.FullName,
fileInfo.Name,
fileNameWithoutExtension,
frontMatterYaml,
h1,
isGitOthersModifiedAndDeletedExcludingStandard,
isKanbanIndex,
isKanbanMarkdown,
fileInfo.LastWriteTime,
lineNumber,
type);
results.Add(key, new(markdownFile, lines.ToArray()));
} // cSpell:restore
return new(results);
}
private static int SortFrontMatter(ReadOnlyDictionary<string, MarkdownFileAndLines> relativeToCollection)
{
int result = 0;
List<string> results = [];
string[] lines;
string frontMatterYaml;
MarkdownFile markdownFile;
string[] frontMatterYamlLines;
#pragma warning disable IL3050
ISerializer serializer = new SerializerBuilder().WithIndentedSequences().Build();
#pragma warning restore IL3050
foreach (KeyValuePair<string, MarkdownFileAndLines> relativeTo in relativeToCollection)
{
results.Clear();
if (relativeTo.Value.Lines.Length < 2)
continue;
lines = relativeTo.Value.Lines;
markdownFile = relativeTo.Value.MarkdownFile;
if (markdownFile.IsKanbanMarkdown)
continue;
if (!markdownFile.IsGitOthersModifiedAndDeletedExcludingStandard)
continue;
if (markdownFile.LineNumber.FrontMatterYamlEnd is null)
continue;
frontMatterYaml = serializer.Serialize(markdownFile.FrontMatterYaml).Trim();
frontMatterYamlLines = frontMatterYaml.Split(Environment.NewLine);
results.Add("---");
results.AddRange(frontMatterYamlLines);
for (int i = markdownFile.LineNumber.FrontMatterYamlEnd.Value; i < lines.Length; i++)
results.Add(lines[i]);
if (results.Count == lines.Length && string.Join('\r', lines) == string.Join('\r', results))
continue;
File.WriteAllLines(markdownFile.File, results);
File.SetLastWriteTime(markdownFile.File, markdownFile.LastWriteDateTime);
result += 1;
}
return result;
}
private static int CircularReference(ILogger<Worker> logger, ReadOnlyDictionary<string, MarkdownFileAndLines> relativeToCollection)
{
int result = 0;
string line;
string check;
string[] lines;
bool circularReference;
MarkdownFile markdownFile;
foreach (KeyValuePair<string, MarkdownFileAndLines> relativeTo in relativeToCollection)
{
if (relativeTo.Value.Lines.Length == 0)
continue;
circularReference = false;
lines = relativeTo.Value.Lines;
markdownFile = relativeTo.Value.MarkdownFile;
if (!markdownFile.IsGitOthersModifiedAndDeletedExcludingStandard)
continue;
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)
continue;
File.WriteAllLines(markdownFile.File, lines);
result += 1;
}
return result;
}
private static int FindReplace(ReadOnlyDictionary<string, MarkdownFileAndLines> relativeToCollection)
{
int result = 0;
bool found;
string line;
string check;
string[] lines;
MarkdownFile markdownFile;
foreach (KeyValuePair<string, MarkdownFileAndLines> relativeTo in relativeToCollection)
{
if (relativeTo.Value.Lines.Length == 0)
continue;
found = false;
lines = relativeTo.Value.Lines;
markdownFile = relativeTo.Value.MarkdownFile;
if (!markdownFile.IsGitOthersModifiedAndDeletedExcludingStandard)
continue;
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)
continue;
File.WriteAllLines(markdownFile.File, lines);
result += 1;
}
return result;
}
private static int ConvertToRelativePath(ILogger<Worker> logger, Input input, ReadOnlyDictionary<string, MarkdownFileAndLines> relativeToCollection)
{
int result = 0;
bool write;
string line;
string[] lines;
string[] segmentsA;
string[] segmentsB;
string[] segmentsC;
MarkdownFile markdownFile;
MarkdownFileH1AndRelativePath markdownFileH1AndRelativePath;
ReadOnlyDictionary<string, List<MarkdownFileAndLines>> keyValuePairs = GetKeyValuePairs(relativeToCollection);
foreach (KeyValuePair<string, MarkdownFileAndLines> relativeTo in relativeToCollection)
{
if (relativeTo.Value.Lines.Length == 0)
continue;
write = false;
lines = relativeTo.Value.Lines;
markdownFile = relativeTo.Value.MarkdownFile;
if (!input.UseProcessStart && !markdownFile.IsGitOthersModifiedAndDeletedExcludingStandard)
continue;
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.FileName);
if (input.UseProcessStart && File.Exists(markdownFile.File))
_ = Process.Start(new ProcessStartInfo(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", "VSCodium", "VSCodium.exe"), markdownFile.File) { WorkingDirectory = input.Source });
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)
continue;
if (!markdownFile.IsGitOthersModifiedAndDeletedExcludingStandard)
continue;
File.WriteAllLines(markdownFile.File, lines);
result += 1;
}
return result;
}
private static int ConvertFileToSlugName(AppSettings appSettings, ILogger<Worker> logger, Input input, ReadOnlyDictionary<string, MarkdownFileAndLines> 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<string, List<MarkdownFileAndLines>> keyValuePairs = GetKeyValuePairs(relativeToCollection);
foreach (KeyValuePair<string, MarkdownFileAndLines> relativeTo in relativeToCollection)
{
if (relativeTo.Value.Lines.Length == 0)
continue;
lines = relativeTo.Value.Lines;
markdownFile = relativeTo.Value.MarkdownFile;
if (markdownFile.IsKanbanIndex)
continue;
if (!input.UseProcessStart && !markdownFile.IsGitOthersModifiedAndDeletedExcludingStandard)
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.FileName);
if (input.UseProcessStart && File.Exists(markdownFile.File))
_ = Process.Start(new ProcessStartInfo(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", "VSCodium", "VSCodium.exe"), markdownFile.File) { WorkingDirectory = input.Source });
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.Equals(fileName, StringComparison.CurrentCultureIgnoreCase))
{
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)
continue;
if (!markdownFile.IsGitOthersModifiedAndDeletedExcludingStandard)
continue;
File.WriteAllLines(markdownFile.File, lines);
result += 1;
}
if (result == 0)
result = ConvertFileToSlugName(relativeToCollection);
return result;
}
private static void SaveColumnToCards(Input input, ReadOnlyDictionary<string, MarkdownFileAndLines> relativeToCollection)
{
if (string.IsNullOrEmpty(input.Destination))
throw new NotSupportedException();
MarkdownFileAndLines? markdownFileAndLines = GetKanbanIndexMarkdownFileAndLines(relativeToCollection);
if (markdownFileAndLines is not null && File.Exists(markdownFileAndLines.MarkdownFile.File))
{
ReadOnlyDictionary<string, List<Card>> columnsToCards;
string jsonFile = Path.Combine(input.Destination, $"{nameof(columnsToCards)}.json");
if (File.Exists(jsonFile))
File.Delete(jsonFile);
columnsToCards = GetColumnsToCards(input, relativeToCollection, markdownFileAndLines);
if (columnsToCards.Count == 0)
File.WriteAllText(jsonFile, "{}");
else
{
string json = JsonSerializer.Serialize(columnsToCards, ColumnsAndCardsSourceGenerationContext.Default.ReadOnlyDictionaryStringListCard);
File.WriteAllText(jsonFile, json);
}
}
}
private static (string type, string h1) GetTypeAndH1(AppSettings appSettings, string h1, ReadOnlyCollection<string> lines, LineNumber lineNumber)
{
string type = lineNumber.Type is null ? appSettings.DefaultNoteType : lines[lineNumber.Type.Value][5..].Trim().Trim('"');
string h1FromFile = lineNumber.H1 is null ? h1 : lines[lineNumber.H1.Value][2..];
return (type, h1FromFile);
}
private static int SetFrontMatterAndH1(AppSettings appSettings, ReadOnlyCollection<string> gitOthersModifiedAndDeletedExcludingStandardFiles, ReadOnlyDictionary<string, MarkdownFileAndLines> relativeToCollection)
{
int result = 0;
List<string> results = [];
string h1Line;
string[] lines;
string typeLine;
TimeSpan timeSpan;
string createdLine;
string updatedLine;
string lineDateTime;
DateTime checkDateTime;
DateTime creationDateTime;
MarkdownFile markdownFile;
string lineCreationFormat = "yyyy-MM-ddTHH:mm:ss.fffZ";
foreach (KeyValuePair<string, MarkdownFileAndLines> relativeTo in relativeToCollection)
{
if (relativeTo.Value.Lines.Length == 0)
continue;
results.Clear();
lines = relativeTo.Value.Lines;
markdownFile = relativeTo.Value.MarkdownFile;
if (markdownFile.IsKanbanMarkdown)
continue;
results.AddRange(lines);
typeLine = $"type: {appSettings.DefaultNoteType}";
h1Line = $"# {markdownFile.FileNameWithoutExtension}";
creationDateTime = markdownFile.CreationDateTime > markdownFile.LastWriteDateTime ? markdownFile.LastWriteDateTime : markdownFile.CreationDateTime;
createdLine = $"created: {creationDateTime.ToUniversalTime():yyyy-MM-ddTHH:mm:ss.fffZ}";
updatedLine = $"updated: {markdownFile.LastWriteDateTime.ToUniversalTime():yyyy-MM-ddTHH:mm:ss.fffZ}";
if (markdownFile.IsKanbanIndex)
HelperKanbanMetadata.SetMetadata(markdownFile.Directory, markdownFile.LineNumber, gitOthersModifiedAndDeletedExcludingStandardFiles);
if (markdownFile.LineNumber.FrontMatterYamlEnd is null)
{
if (markdownFile.LineNumber.H1 is not null)
results.Insert(0, string.Empty);
else
{
results.Insert(0, string.Empty);
results.Insert(0, h1Line);
results.Insert(0, string.Empty);
}
results.Insert(0, "---");
if (markdownFile.IsGitOthersModifiedAndDeletedExcludingStandard)
{
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.IsGitOthersModifiedAndDeletedExcludingStandard)
{
if (markdownFile.LineNumber.Updated is null)
results.Insert(markdownFile.LineNumber.FrontMatterYamlEnd.Value, updatedLine);
else
{
lineDateTime = results[markdownFile.LineNumber.Updated.Value].Split(": ")[1].Trim('"');
if (!DateTime.TryParseExact(lineDateTime, lineCreationFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out checkDateTime))
results[markdownFile.LineNumber.Updated.Value] = updatedLine;
else
{
timeSpan = new(checkDateTime.Ticks - markdownFile.LastWriteDateTime.Ticks);
if (timeSpan.TotalDays is > 1 or < -1)
results[markdownFile.LineNumber.Updated.Value] = updatedLine;
}
}
if (markdownFile.LineNumber.Created is null)
results.Insert(markdownFile.LineNumber.FrontMatterYamlEnd.Value, createdLine);
else
{
lineDateTime = results[markdownFile.LineNumber.Created.Value].Split(": ")[1].Trim('"');
if (!DateTime.TryParseExact(lineDateTime, lineCreationFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out checkDateTime))
results[markdownFile.LineNumber.Created.Value] = createdLine;
else
{
timeSpan = new(checkDateTime.Ticks - creationDateTime.Ticks);
if (timeSpan.TotalDays > 1)
results[markdownFile.LineNumber.Created.Value] = createdLine;
if (timeSpan.TotalDays < -1)
File.SetCreationTime(markdownFile.File, checkDateTime);
}
}
}
}
if (results.Count == lines.Length && string.Join('\r', lines) == string.Join('\r', results))
continue;
if (!markdownFile.IsGitOthersModifiedAndDeletedExcludingStandard)
continue;
File.WriteAllLines(markdownFile.File, results);
File.SetLastWriteTime(markdownFile.File, markdownFile.LastWriteDateTime);
result += 1;
}
return result;
}
private static MarkdownFileH1AndRelativePath GetRelativePath(ReadOnlyDictionary<string, List<MarkdownFileAndLines>> 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<Worker> logger, List<string> args, CancellationToken cancellationToken)
{
int updated;
bool usePathCombine = true;
Input input = GetInput(args);
ReadOnlyCollection<string> gitOthersModifiedAndDeletedExcludingStandardFiles = HelperGit.GetOthersModifiedAndDeletedExcludingStandardFiles(input.Source, usePathCombine, cancellationToken);
ReadOnlyDictionary<string, MarkdownFileAndLines> relativeToCollection = GetRelativeToCollection(appSettings, input, gitOthersModifiedAndDeletedExcludingStandardFiles);
updated = SetFrontMatterAndH1(appSettings, gitOthersModifiedAndDeletedExcludingStandardFiles, relativeToCollection);
if (updated != 0)
{
relativeToCollection = GetRelativeToCollection(appSettings, input, gitOthersModifiedAndDeletedExcludingStandardFiles);
logger.LogInformation("{updated} Markdown file(s) were updated", updated);
}
updated = SortFrontMatter(relativeToCollection);
if (updated != 0)
{
relativeToCollection = GetRelativeToCollection(appSettings, input, gitOthersModifiedAndDeletedExcludingStandardFiles);
logger.LogInformation("{updated} Markdown file(s) were updated", updated);
}
updated = CircularReference(logger, relativeToCollection);
if (updated != 0)
{
relativeToCollection = GetRelativeToCollection(appSettings, input, gitOthersModifiedAndDeletedExcludingStandardFiles);
logger.LogInformation("{updated} Markdown file(s) were updated", updated);
}
updated = FindReplace(relativeToCollection);
if (updated != 0)
{
relativeToCollection = GetRelativeToCollection(appSettings, input, gitOthersModifiedAndDeletedExcludingStandardFiles);
logger.LogInformation("{updated} Markdown file(s) were updated", updated);
}
updated = ConvertToRelativePath(logger, input, relativeToCollection);
if (updated != 0)
{
relativeToCollection = GetRelativeToCollection(appSettings, input, gitOthersModifiedAndDeletedExcludingStandardFiles);
logger.LogInformation("{updated} Markdown file(s) were updated", updated);
}
updated = ConvertFileToSlugName(appSettings, logger, input, relativeToCollection);
if (updated != 0)
{
relativeToCollection = GetRelativeToCollection(appSettings, input, gitOthersModifiedAndDeletedExcludingStandardFiles);
logger.LogInformation("{updated} Markdown file(s) were updated", updated);
}
if (!string.IsNullOrEmpty(input.Destination))
SaveColumnToCards(input, relativeToCollection);
}
}