Empty file ISO Add date back for just .kanbn Removed HardcodedFileSearchAndSort Sync with 01-23 JsonToTsv System.Text.Json White-List Ready to move to Move Helper Remove Whitelist Force Start At Check for .git directory before ls Optional Allow root for unc path nuget bump PreVerify EnforceCodeStyleInBuild dotnet_analyzer_diagnostic HelperGit searchDelegate Host File AlertIfNewDeviceIsConnected AOT SetFrontMatterAndH1 Match Error Unknown with better logging Undo 04-05 WriteAppendToHostConfFile MonA IsKanbanIndex Dotnet Format Pre-commit NPM CreateWindowsShortcut Working directory Split description Copy tests Ready to test Delete after a couple of days GitConfigCleanUp knb Files
1369 lines
61 KiB
C#
1369 lines
61 KiB
C#
using File_Folder_Helper.Models;
|
|
using Microsoft.Extensions.Logging;
|
|
using System.Collections.ObjectModel;
|
|
using System.Globalization;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace File_Folder_Helper.Helpers;
|
|
|
|
internal static partial class HelperMarkdown
|
|
{
|
|
|
|
private record Input(string? Destination,
|
|
string Source,
|
|
string? StartAt);
|
|
|
|
private record Record(string Directory,
|
|
string File,
|
|
string[] Lines);
|
|
|
|
private record MarkdownFile(DateTime CreationDateTime,
|
|
string Directory,
|
|
string Extension,
|
|
string File,
|
|
string FileName,
|
|
string FileNameWithoutExtension,
|
|
string H1,
|
|
bool IsKanbanIndex,
|
|
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
|
|
{
|
|
}
|
|
|
|
private static void SetRecursiveLines(AppSettings appSettings, ILogger<Worker> logger, ReadOnlyDictionary<string, List<MarkdownFileAndLines>> keyValuePairs, string linkTitle, MarkdownFile markdownFile, string[] lines, List<char> indentations, List<string> 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<string> GetFrontMatterLines(string[] parsedLines)
|
|
{
|
|
List<string> results = [];
|
|
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;
|
|
}
|
|
|
|
/// <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>
|
|
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;
|
|
}
|
|
|
|
[GeneratedRegex("(~~)?(#)([a-zA-Z0-9]{6})(~~)?( )")]
|
|
private static partial Regex HtmlColor();
|
|
|
|
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 (string?, Dictionary<string, JsonElement>?, List<string>) Get(List<string> jsonLines)
|
|
{
|
|
string? result;
|
|
List<string> results;
|
|
Dictionary<string, JsonElement>? 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.Count == 0)
|
|
{
|
|
result = null;
|
|
keyValuePairs = null;
|
|
}
|
|
return (result, keyValuePairs, results);
|
|
}
|
|
|
|
internal static (List<string>, 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 (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 (h1LineNumber is null && line.Length > 2 && line[0] == '#' && line[1] == ' ')
|
|
{
|
|
h1LineNumber = i;
|
|
continue;
|
|
}
|
|
}
|
|
LineNumber lineNumber = new(createdLineNumber,
|
|
h1LineNumber,
|
|
frontMatterYamlEndLineNumber,
|
|
statusLineNumber,
|
|
typeLineNumber,
|
|
updatedLineNumber);
|
|
return (lines.ToList(), lineNumber);
|
|
}
|
|
|
|
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> 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 (string?, Dictionary<string, JsonElement>?, string[]) Get(int frontMatterYamlEnd, string[] lines)
|
|
{
|
|
string? result;
|
|
List<string> results;
|
|
Dictionary<string, JsonElement>? keyValuePairs;
|
|
string[] segments;
|
|
string[] segmentsB;
|
|
string segmentsLast;
|
|
string segmentsFirst;
|
|
List<string> jsonLines = [];
|
|
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(char.IsNumber))
|
|
jsonLines.Add($"{segmentsLast},");
|
|
else
|
|
{
|
|
segmentsB = segmentsLast.Split('.');
|
|
if (segmentsB.Length == 2 && segmentsB[0].Length < 7 && segmentsB[^1].Length < 7 && segmentsB[0].All(char.IsNumber) && segmentsB[^1].All(char.IsNumber))
|
|
jsonLines.Add($"{segmentsLast},");
|
|
else if (!segmentsLast.Contains('[') && !segmentsLast.Contains('{'))
|
|
jsonLines.Add($"\"{segmentsLast}\",");
|
|
else
|
|
{
|
|
jsonLines.Clear();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
jsonLines.Clear();
|
|
break;
|
|
}
|
|
}
|
|
if (jsonLines.Count > 0)
|
|
(result, keyValuePairs, results) = Get(jsonLines);
|
|
else
|
|
(result, keyValuePairs, results) = (null, null, []);
|
|
return (result, keyValuePairs, results.ToArray());
|
|
}
|
|
|
|
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)
|
|
{
|
|
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<string, MarkdownFileAndLines> GetRelativeToCollection(AppSettings appSettings, Input input, string[] files, bool force)
|
|
{
|
|
Dictionary<string, MarkdownFileAndLines> results = [];
|
|
string h1;
|
|
string key;
|
|
string type;
|
|
FileInfo fileInfo;
|
|
bool isKanbanIndex;
|
|
List<string> 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.Count > 0)
|
|
(type, h1) = GetTypeAndH1(appSettings, h1, lines, lineNumber);
|
|
else
|
|
{
|
|
type = appSettings.DefaultNoteType;
|
|
File.WriteAllLines(file, ["---", $"type: \"{type}\"", "---", string.Empty, $"# {h1}"]);
|
|
lines = File.ReadAllLines(file).ToList();
|
|
}
|
|
isKanbanIndex = fileNameWithoutExtension == "index" && type == "Kanban";
|
|
markdownFile = new(fileInfo.CreationTime,
|
|
fileInfo.DirectoryName,
|
|
fileInfo.Extension,
|
|
file,
|
|
fileInfo.Name,
|
|
fileNameWithoutExtension,
|
|
h1,
|
|
isKanbanIndex,
|
|
fileInfo.LastWriteTime,
|
|
lineNumber,
|
|
type);
|
|
if (force || input.StartAt is null || file.StartsWith(input.StartAt))
|
|
results.Add(key, new(markdownFile, lines.ToArray()));
|
|
else
|
|
results.Add(key, new(markdownFile, []));
|
|
}
|
|
return new(results);
|
|
}
|
|
|
|
private static ReadOnlyDictionary<string, List<Card>> GetColumnsToCards(Input input, ReadOnlyDictionary<string, MarkdownFileAndLines> relativeToCollection)
|
|
{
|
|
Dictionary<string, List<Card>> results = [];
|
|
Card card;
|
|
string key;
|
|
string[] lines;
|
|
string[] segmentsA;
|
|
string? column = null;
|
|
List<Card> cards = [];
|
|
List<string> allKeys = [];
|
|
MarkdownFile markdownFile;
|
|
MarkdownExtra markdownExtra;
|
|
MarkdownFileAndLines? markdownFileAndLines;
|
|
foreach (KeyValuePair<string, MarkdownFileAndLines> relativeTo in relativeToCollection)
|
|
allKeys.Add(relativeTo.Key);
|
|
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 (!File.Exists(markdownFile.File))
|
|
continue;
|
|
for (int i = 0; i < lines.Length; i++)
|
|
{
|
|
if (lines[i].Length < 4 || lines[i][0] != '#' || lines[i][1] != '#' || lines[i][2] != ' ')
|
|
continue;
|
|
if (cards.Count > 1)
|
|
{
|
|
if (column is null)
|
|
throw new NullReferenceException(nameof(column));
|
|
results.Add(column, cards);
|
|
cards = [];
|
|
}
|
|
column = lines[i][3..].TrimEnd();
|
|
if (lines.Length == i + 1)
|
|
continue;
|
|
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 (!allKeys.Remove(key))
|
|
continue;
|
|
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);
|
|
cards.Add(card);
|
|
}
|
|
}
|
|
foreach (string notLinkedKey in allKeys)
|
|
{
|
|
key = notLinkedKey;
|
|
if (!relativeToCollection.TryGetValue(key, out markdownFileAndLines))
|
|
continue;
|
|
if (markdownFileAndLines.MarkdownFile.LineNumber.FrontMatterYamlEnd is null)
|
|
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);
|
|
cards.Add(card);
|
|
}
|
|
if (cards.Count > 1)
|
|
{
|
|
column = "Not Linked";
|
|
results.Add(column, cards);
|
|
cards = [];
|
|
}
|
|
}
|
|
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? startAt = null;
|
|
string? destination = null;
|
|
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] == '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 (startAt is not null && startAt.Length < source.Length)
|
|
throw new Exception($"Start at directory <{startAt}> must be a subdirectory!");
|
|
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, startAt);
|
|
return result;
|
|
}
|
|
|
|
private static int ConvertFrontMatterToJsonFriendly(ReadOnlyDictionary<string, MarkdownFileAndLines> relativeToCollection)
|
|
{
|
|
int result = 0;
|
|
List<string> results = [];
|
|
bool write;
|
|
string[] lines;
|
|
MarkdownFile markdownFile;
|
|
string[] frontMatterYamlLines;
|
|
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.LineNumber.FrontMatterYamlEnd is null)
|
|
continue;
|
|
(_, _, frontMatterYamlLines) = Get(markdownFile.LineNumber.FrontMatterYamlEnd.Value, lines);
|
|
if (frontMatterYamlLines.Length == 0)
|
|
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<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;
|
|
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<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;
|
|
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 int ConvertToRelativePath(ILogger<Worker> logger, 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;
|
|
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(AppSettings appSettings, ILogger<Worker> logger, 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 (!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.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)
|
|
{
|
|
File.WriteAllLines(markdownFile.File, lines);
|
|
result += 1;
|
|
}
|
|
}
|
|
if (result == 0)
|
|
result = ConvertFileToSlugName(relativeToCollection);
|
|
return result;
|
|
}
|
|
|
|
private static ReadOnlyDictionary<string, MarkdownFileAndLines> GetRelativeToCollection(AppSettings appSettings, Input input, bool force = false)
|
|
{
|
|
ReadOnlyDictionary<string, MarkdownFileAndLines> results;
|
|
string[] files = GetFiles(appSettings, input.Source);
|
|
results = GetRelativeToCollection(appSettings, input, files, force);
|
|
return new(results);
|
|
}
|
|
|
|
private static List<MarkdownFileAndLines> GetRecursiveLines(AppSettings appSettings, Input input, ILogger<Worker> logger, ReadOnlyDictionary<string, MarkdownFileAndLines> relativeToCollection)
|
|
{
|
|
List<MarkdownFileAndLines> results = [];
|
|
string[] lines;
|
|
List<char> indentations;
|
|
MarkdownFile markdownFile;
|
|
List<string> recursiveLines;
|
|
ReadOnlyDictionary<string, List<MarkdownFileAndLines>> keyValuePairs = GetKeyValuePairs(relativeToCollection);
|
|
foreach (KeyValuePair<string, MarkdownFileAndLines> relativeTo in relativeToCollection)
|
|
{
|
|
if (relativeTo.Value.Lines.Length == 0)
|
|
continue;
|
|
if (input.StartAt is null || !relativeTo.Value.MarkdownFile.File.StartsWith(input.StartAt) || Path.GetFileName(relativeTo.Value.MarkdownFile.Directory) != Path.GetFileName(input.StartAt))
|
|
continue;
|
|
indentations = [];
|
|
recursiveLines = [];
|
|
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(Input input, List<MarkdownFileAndLines> markdownFileAndLinesCollection)
|
|
{
|
|
foreach (MarkdownFileAndLines markdownFileAndLines in markdownFileAndLinesCollection)
|
|
{
|
|
if (input.Destination is null)
|
|
continue;
|
|
File.WriteAllLines(Path.Combine(input.Destination, markdownFileAndLines.MarkdownFile.FileName), markdownFileAndLines.Lines);
|
|
}
|
|
}
|
|
|
|
private static void SaveColumnToCards(Input input, ReadOnlyDictionary<string, MarkdownFileAndLines> relativeToCollection)
|
|
{
|
|
if (string.IsNullOrEmpty(input.StartAt) || string.IsNullOrEmpty(input.Destination))
|
|
throw new NotSupportedException();
|
|
ReadOnlyDictionary<string, List<Card>> columnsToCards;
|
|
int kanbanIndexFiles = (from l in relativeToCollection where l.Value.MarkdownFile.IsKanbanIndex select 1).Sum();
|
|
if (kanbanIndexFiles == 1)
|
|
{
|
|
string jsonFile = Path.Combine(input.Destination, $"{nameof(columnsToCards)}.json");
|
|
if (File.Exists(jsonFile))
|
|
File.Delete(jsonFile);
|
|
columnsToCards = GetColumnsToCards(input, relativeToCollection);
|
|
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, List<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, Input input, ReadOnlyDictionary<string, MarkdownFileAndLines> relativeToCollection, ReadOnlyCollection<string> gitOthersModifiedAndDeletedExcludingStandardFiles)
|
|
{
|
|
int result = 0;
|
|
List<string> results = [];
|
|
bool gitCheck;
|
|
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;
|
|
results.AddRange(lines);
|
|
typeLine = $"type: \"{appSettings.DefaultNoteType}\"";
|
|
h1Line = $"# {markdownFile.FileNameWithoutExtension}";
|
|
creationDateTime = markdownFile.CreationDateTime > markdownFile.LastWriteDateTime ? markdownFile.LastWriteDateTime : markdownFile.CreationDateTime;
|
|
gitCheck = gitOthersModifiedAndDeletedExcludingStandardFiles.Contains(markdownFile.File);
|
|
createdLine = $"created: \"{creationDateTime.ToUniversalTime():yyyy-MM-ddTHH:mm:ss.fffZ}\"";
|
|
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, "---");
|
|
if (gitCheck)
|
|
{
|
|
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 (gitCheck)
|
|
{
|
|
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;
|
|
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 = false;
|
|
Input input = GetInput(args);
|
|
ReadOnlyDictionary<string, MarkdownFileAndLines> relativeToCollection = GetRelativeToCollection(appSettings, input);
|
|
ReadOnlyCollection<string> gitOthersModifiedAndDeletedExcludingStandardFiles = HelperGit.GetOthersModifiedAndDeletedExcludingStandardFiles(input.Source, usePathCombine, cancellationToken);
|
|
updated = SetFrontMatterAndH1(appSettings, input, relativeToCollection, gitOthersModifiedAndDeletedExcludingStandardFiles);
|
|
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) && !string.IsNullOrEmpty(input.Destination))
|
|
{
|
|
relativeToCollection = GetRelativeToCollection(appSettings, input, force: true);
|
|
List<MarkdownFileAndLines> markdownFileAndLinesCollection = GetRecursiveLines(appSettings, input, logger, relativeToCollection);
|
|
Write(input, markdownFileAndLinesCollection);
|
|
}
|
|
if (!string.IsNullOrEmpty(input.StartAt) && !string.IsNullOrEmpty(input.Destination))
|
|
SaveColumnToCards(input, relativeToCollection);
|
|
}
|
|
|
|
} |