file-folder-helper/Helpers/HelperMarkdown.cs

623 lines
26 KiB
C#

using Humanizer;
using System.Text;
using System.Text.Json;
namespace File_Folder_Helper.Helpers;
internal static partial class HelperMarkdown
{
/// <summary>
/// Determines a 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;
}
internal static string[] GetFiles(Models.AppSettings appSettings, string directory)
{
string[] files = Directory.GetFiles(directory, "*.md", SearchOption.AllDirectories).
Where(l => !appSettings.Exclude.Any(m => l.Contains(m))).ToArray();
return files;
}
private static (string type, string h1) GetTypeAndH1(Models.AppSettings appSettings, string h1, List<string> lines, LineNumber lineNumber)
{
string type = lineNumber.Type is null ? appSettings.DefaultNoteType : lines[lineNumber.Type.Value].Replace("type: ", string.Empty);
string h1FromFile = lineNumber.H1 is null ? h1 : lines[lineNumber.H1.Value][2..];
return (type, h1FromFile);
}
internal static (List<string>, LineNumber) GetStatusAndMetaEndLineNumbers(FileInfo fileInfo)
{
string line;
int? h1LineNumber = null;
int? typeLineNumber = null;
int? statusLineNumber = null;
int? createdLineNumber = null;
int? updatedLineNumber = null;
int? metaEndLineNumber = 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 (line[..3] == "---")
{
metaEndLineNumber = 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[..2] == "# ")
{
h1LineNumber = i;
continue;
}
}
LineNumber lineNumber = new(createdLineNumber,
h1LineNumber,
metaEndLineNumber,
statusLineNumber,
typeLineNumber,
updatedLineNumber);
return (lines.ToList(), lineNumber);
}
internal static List<(MarkdownFile, string[])> GetCollection(Models.AppSettings appSettings, string[] files)
{
List<(MarkdownFile, string[])> results = new();
string h1;
string type;
FileInfo fileInfo;
List<string> lines;
LineNumber lineNumber;
MarkdownFile markdownFile;
string fileNameWithoutExtension;
foreach (string file in files)
{
fileInfo = new(file);
if (fileInfo.DirectoryName is null)
continue;
(lines, lineNumber) = GetStatusAndMetaEndLineNumbers(fileInfo);
fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileInfo.FullName);
h1 = fileNameWithoutExtension.ToLower().Hyphenate();
if (lines.Any())
(type, h1) = GetTypeAndH1(appSettings, h1, lines, lineNumber);
else
{
type = "note";
File.WriteAllLines(file, new string[] { "---", $"type: {type}", "---", string.Empty, $"# {h1}" });
lines = File.ReadAllLines(file).ToList();
}
markdownFile = new(file, fileInfo.DirectoryName, fileInfo.Name, fileNameWithoutExtension, fileInfo.Extension, fileInfo.CreationTime, fileInfo.LastWriteTime, lineNumber, type, h1);
results.Add(new(markdownFile, lines.ToArray()));
}
return results;
}
internal static bool SetFrontMatterAndH1(Models.AppSettings appSettings, List<(MarkdownFile, string[])> collection)
{
bool result = false;
string h1Line;
string typeLine;
string createdLine;
string updatedLine;
DateTime creationDateTime;
string createdLineCompare;
string updatedLineCompare;
List<string> results = new();
foreach ((MarkdownFile markdownFile, string[] lines) in collection)
{
if (markdownFile.FileName == "board.md")
continue;
if (!lines.Any())
continue;
results.Clear();
results.AddRange(lines);
creationDateTime = markdownFile.CreationDateTime > markdownFile.LastWriteDateTime ? markdownFile.LastWriteDateTime : markdownFile.CreationDateTime;
typeLine = $"type: {appSettings.DefaultNoteType}";
h1Line = $"# {markdownFile.FileNameWithoutExtension}";
createdLineCompare = $"created: {creationDateTime.ToUniversalTime():yyyy-MM-dd}";
createdLine = $"created: {creationDateTime.ToUniversalTime():yyyy-MM-ddTHH:mm:ss.fffZ}";
updatedLineCompare = $"updated: {markdownFile.LastWriteDateTime.ToUniversalTime():yyyy-MM-dd}";
updatedLine = $"updated: {markdownFile.LastWriteDateTime.ToUniversalTime():yyyy-MM-ddTHH:mm:ss.fffZ}";
if (markdownFile.LineNumber.MetaEnd 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, "---");
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.MetaEnd.Value + 1, string.Empty);
results.Insert(markdownFile.LineNumber.MetaEnd.Value + 1, h1Line);
results.Insert(markdownFile.LineNumber.MetaEnd.Value + 1, string.Empty);
}
if (markdownFile.LineNumber.Type is null)
results.Insert(markdownFile.LineNumber.MetaEnd.Value, typeLine);
if (markdownFile.LineNumber.Updated is null)
results.Insert(markdownFile.LineNumber.MetaEnd.Value, updatedLine);
else
{
if (results[markdownFile.LineNumber.Updated.Value].Contains('$'))
continue;
if (results[markdownFile.LineNumber.Updated.Value][..updatedLineCompare.Length] == updatedLineCompare)
continue;
results[markdownFile.LineNumber.Updated.Value] = updatedLine;
}
if (markdownFile.LineNumber.Created is null)
results.Insert(markdownFile.LineNumber.MetaEnd.Value, createdLine);
else if (results[markdownFile.LineNumber.Created.Value][..createdLineCompare.Length] != createdLineCompare)
results[markdownFile.LineNumber.Created.Value] = createdLine;
}
if (!result)
result = true;
File.WriteAllLines(markdownFile.File, results);
File.SetLastWriteTime(markdownFile.File, markdownFile.LastWriteDateTime);
}
return result;
}
internal static bool CircularReference(List<(MarkdownFile, string[])> collection)
{
bool result = false;
string line;
string check;
bool circularReference;
foreach ((MarkdownFile markdownFile, string[] lines) in collection)
{
if (lines.Length < 1)
continue;
circularReference = false;
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;
if (!circularReference)
circularReference = true;
}
if (circularReference)
{
if (!result)
result = true;
File.WriteAllLines(markdownFile.File, lines);
}
}
return result;
}
internal static bool FindReplace(List<(MarkdownFile, string[])> collection)
{
bool result = false;
bool found;
string line;
string check;
foreach ((MarkdownFile markdownFile, string[] lines) in collection)
{
if (lines.Length < 1)
continue;
found = false;
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)
{
if (!result)
result = true;
File.WriteAllLines(markdownFile.File, lines);
}
}
return result;
}
private static Dictionary<string, List<MarkdownFile>> GetKeyValuePairs(List<(MarkdownFile MarkdownFile, string[] Lines)> collection)
{
Dictionary<string, List<MarkdownFile>> results = new();
List<MarkdownFile>? markdownFiles;
foreach ((MarkdownFile markdownFile, _) in collection)
{
if (!results.TryGetValue(markdownFile.FileNameWithoutExtension, out markdownFiles))
{
results.Add(markdownFile.FileNameWithoutExtension, new());
if (!results.TryGetValue(markdownFile.FileNameWithoutExtension, out markdownFiles))
throw new NotSupportedException();
}
markdownFiles.Add(markdownFile);
}
return results;
}
private static (string?, string?) GetRelativePath(Dictionary<string, List<MarkdownFile>> keyValuePairs, MarkdownFile markdownFile, string file)
{
string? result;
string? match;
string? title;
List<MarkdownFile>? markdownFiles;
string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file);
if (keyValuePairs.TryGetValue(fileNameWithoutExtension, out markdownFiles))
{
if (markdownFiles.Count != 1)
(match, title) = (null, null);
else
(match, title) = (markdownFiles.First().File, markdownFiles.First().H1);
}
else
{
if (!keyValuePairs.TryGetValue(fileNameWithoutExtension.ToLower(), out markdownFiles))
(match, title) = (null, null);
else
{
if (markdownFiles.Count != 1)
(match, title) = (null, null);
else
(match, title) = (markdownFiles.First().File, markdownFiles.First().H1);
}
}
if (match is null)
{
List<string> files = new();
List<string> fileNames = new();
foreach (KeyValuePair<string, List<MarkdownFile>> keyValuePair in keyValuePairs)
{
foreach (MarkdownFile keyValue in keyValuePair.Value)
{
files.Add(keyValue.File);
fileNames.Add(keyValue.FileNameWithoutExtension);
}
}
string[] matches = fileNames.Where(l => l.Length == fileNameWithoutExtension.Length && l.Contains(fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase)).ToArray();
if (matches.Length == 1)
match = matches.First();
else
{
string checkName = fileNameWithoutExtension.ToLower().Replace("%20", "-").Replace(' ', '-');
matches = fileNames.Where(l => l.Length == checkName.Length && l.Contains(checkName, StringComparison.OrdinalIgnoreCase)).ToArray();
if (matches.Length == 1)
match = matches.First();
else
{
if (!matches.Any())
match = null;
else
{
checkName = matches.First();
matches = files.Where(l => l.Contains(checkName, StringComparison.OrdinalIgnoreCase)).ToArray();
if (matches.Length == 1)
match = matches.First();
else
{
checkName = $"{checkName}{markdownFile.Extension}";
matches = files.Where(l => l.EndsWith(checkName, StringComparison.OrdinalIgnoreCase)).ToArray();
if (matches.Length == 1)
match = matches.First();
else
{
checkName = $"\\{checkName}";
matches = files.Where(l => l.EndsWith(checkName, StringComparison.OrdinalIgnoreCase)).ToArray();
if (matches.Length == 1)
match = matches.First();
else
match = null;
}
}
}
}
}
}
result = match is null ? null : Path.GetRelativePath(markdownFile.Directory, Path.GetFullPath(match));
return (result, title);
}
internal static bool ConvertToRelativePath(List<(MarkdownFile MarkdownFile, string[] Lines)> collection)
{
bool result = false;
bool write;
string line;
string after;
string before;
string? title;
string[] segmentsA;
string[] segmentsB;
string[] segmentsC;
string? relativePath;
Dictionary<string, List<MarkdownFile>> keyValuePairs = GetKeyValuePairs(collection);
foreach ((MarkdownFile markdownFile, string[] lines) in collection)
{
if (lines.Length < 1)
continue;
write = false;
for (int i = 0; i < lines.Length; i++)
{
segmentsA = lines[i].Split("]]");
if (segmentsA.Length is not 2 or 3)
continue;
segmentsB = segmentsA.First().Split("[[");
if (segmentsB.Length is not 2 or 3)
continue;
after = segmentsA.Last();
before = segmentsB.First();
segmentsC = segmentsB.Last().Split('|');
(relativePath, title) = GetRelativePath(keyValuePairs, markdownFile, segmentsC.First());
if (relativePath is null)
continue;
if (title is null)
{
title = segmentsC.Last().Humanize(LetterCasing.Title);
if (title.Length != segmentsC.Last().Length)
title = segmentsC.Last();
}
line = $"{before}[{title}]({relativePath.Replace('\\', '/')}){after}";
if (lines[i] == line)
continue;
lines[i] = line;
if (!write)
write = true;
}
if (write)
{
if (!result)
result = true;
File.WriteAllLines(markdownFile.File, lines);
}
}
return result;
}
internal static bool ConvertFileToSlugName(List<(MarkdownFile MarkdownFile, string[] Lines)> collection)
{
bool result = false;
bool write;
string h1;
string file;
string line;
string? title;
string h1Check;
string fileName;
string checkName;
string? directory;
string[] segmentsA;
string[] segmentsB;
string[] segmentsC;
string checkFileName;
string segmentsALast;
string segmentsBFirst;
string relativeDirectory;
string formattedRelativeDirectory;
Dictionary<string, List<MarkdownFile>> keyValuePairs = GetKeyValuePairs(collection);
foreach ((MarkdownFile markdownFile, string[] lines) in collection)
{
if (markdownFile.FileNameWithoutExtension == "index")
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.Last();
if (segmentsALast.StartsWith("http:") || segmentsALast.StartsWith("https:") || segmentsALast.StartsWith("rdp:") || segmentsALast.StartsWith("onenote:"))
continue;
segmentsB = segmentsALast.Split(")");
if (segmentsB.Length != 2)
continue;
segmentsBFirst = segmentsB.First();
file = Path.GetFullPath(Path.Combine(markdownFile.Directory, segmentsBFirst));
fileName = Path.GetFileName(file);
directory = Path.GetDirectoryName(file);
if (string.IsNullOrEmpty(directory))
continue;
relativeDirectory = segmentsBFirst[..^fileName.Length];
formattedRelativeDirectory = relativeDirectory.Replace(" ", "%20");
checkFileName = fileName.ToLower().Replace("%20", "-").Replace(' ', '-');
checkName = Path.Combine(directory, checkFileName);
segmentsC = segmentsA.First().Split('[');
(_, title) = GetRelativePath(keyValuePairs, markdownFile, file);
if (title is null)
{
title = segmentsC.Last().Humanize(LetterCasing.Title);
if (title.Length != segmentsC.Last().Length)
title = segmentsC.Last();
}
line = $"{segmentsC.First()}[{title}]({Path.Combine(formattedRelativeDirectory, checkFileName)}){segmentsB.Last()}";
if (lines[i] == line)
continue;
if (fileName.Contains(' ') || fileName.Contains("%20"))
{
if (!File.Exists(file))
continue;
if (File.Exists(checkName))
continue;
File.Move(file, checkName);
}
else if (fileName != fileName.ToLower())
{
if (file != checkName)
{
if (!File.Exists(file))
continue;
File.Move(file, checkName);
}
}
lines[i] = line;
if (!write)
write = true;
}
if (write)
{
if (!result)
result = true;
File.WriteAllLines(markdownFile.File, lines);
}
}
if (!result)
{
foreach ((MarkdownFile markdownFile, string[] lines) in collection)
{
if (markdownFile.LineNumber.H1 is not null)
{
h1 = lines[markdownFile.LineNumber.H1.Value];
if (h1.Length > 2)
{
h1Check = $"# {h1[2..].Humanize(LetterCasing.Title)}";
if (h1Check.Length == h1.Length && h1Check != h1)
{
lines[markdownFile.LineNumber.H1.Value] = h1Check;
if (!result)
result = true;
File.WriteAllLines(markdownFile.File, lines);
}
}
}
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 (!result)
result = true;
File.Move(markdownFile.File, checkName);
}
}
return result;
}
internal static void MarkdownWikiLinkVerification(Models.AppSettings appSettings, string argsZero)
{
string fullPath = Path.GetFullPath(argsZero);
List<(MarkdownFile MarkdownFile, string[] Lines)> collection;
collection = GetCollection(appSettings, GetFiles(appSettings, fullPath));
if (SetFrontMatterAndH1(appSettings, collection))
collection = GetCollection(appSettings, GetFiles(appSettings, fullPath));
if (CircularReference(collection))
collection = GetCollection(appSettings, GetFiles(appSettings, fullPath));
if (FindReplace(collection))
collection = GetCollection(appSettings, GetFiles(appSettings, fullPath));
if (ConvertToRelativePath(collection))
collection = GetCollection(appSettings, GetFiles(appSettings, fullPath));
if (ConvertFileToSlugName(collection))
collection = GetCollection(appSettings, GetFiles(appSettings, fullPath));
string directory = Path.Combine(Environment.CurrentDirectory, ".vscode");
if (!Directory.Exists(directory))
{
string json;
MarkdownFile markdownFile = collection.First().MarkdownFile;
json = JsonSerializer.Serialize(markdownFile, MarkdownFileSourceGenerationContext.Default.MarkdownFile);
if (json != "{}")
{
json = JsonSerializer.Serialize(collection.Select(l => l.MarkdownFile).ToArray(), MarkdownFileCollectionSourceGenerationContext.Default.MarkdownFileArray);
File.WriteAllText($"{DateTime.Now.Ticks}.json", json);
}
}
}
}