Add Git overlay, operation tools, and virtualized preview so large folders stay responsive.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
258
src/Explorer.Application/MarkdownParser.cs
Normal file
258
src/Explorer.Application/MarkdownParser.cs
Normal file
@@ -0,0 +1,258 @@
|
||||
namespace Explorer.Application;
|
||||
|
||||
public sealed record MarkdownDocument(IReadOnlyList<MarkdownBlock> Blocks, IReadOnlyList<MarkdownHeading> Headings);
|
||||
|
||||
public abstract record MarkdownBlock;
|
||||
|
||||
public sealed record MarkdownHeading(int Level, string Text, string Id) : MarkdownBlock;
|
||||
|
||||
public sealed record MarkdownParagraph(string Text) : MarkdownBlock;
|
||||
|
||||
public sealed record MarkdownList(bool Ordered, IReadOnlyList<string> Items) : MarkdownBlock;
|
||||
|
||||
public sealed record MarkdownCode(string Language, string Text) : MarkdownBlock;
|
||||
|
||||
public sealed record MarkdownTable(IReadOnlyList<string> Headers, IReadOnlyList<IReadOnlyList<string>> Rows) : MarkdownBlock;
|
||||
|
||||
public sealed record MarkdownRule : MarkdownBlock;
|
||||
|
||||
public sealed record MarkdownQuote(string Text) : MarkdownBlock;
|
||||
|
||||
public static class MarkdownParser
|
||||
{
|
||||
public static MarkdownDocument Parse(string text)
|
||||
{
|
||||
var lines = (text ?? "").Replace("\r\n", "\n").Replace('\r', '\n').Split('\n');
|
||||
var blocks = new List<MarkdownBlock>();
|
||||
var headings = new List<MarkdownHeading>();
|
||||
var ids = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var i = 0;
|
||||
while (i < lines.Length)
|
||||
{
|
||||
var raw = lines[i];
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
{
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (raw.StartsWith("```", StringComparison.Ordinal))
|
||||
{
|
||||
var lang = raw[3..].Trim();
|
||||
i++;
|
||||
var body = new List<string>();
|
||||
while (i < lines.Length && !lines[i].StartsWith("```", StringComparison.Ordinal))
|
||||
{
|
||||
body.Add(lines[i]);
|
||||
i++;
|
||||
}
|
||||
|
||||
if (i < lines.Length)
|
||||
{
|
||||
i++;
|
||||
}
|
||||
|
||||
blocks.Add(new MarkdownCode(lang, string.Join("\n", body)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsRule(raw))
|
||||
{
|
||||
blocks.Add(new MarkdownRule());
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
var heading = ParseHeading(raw);
|
||||
if (heading is not null)
|
||||
{
|
||||
var id = UniqueId(Slug(heading.Text), ids);
|
||||
var block = heading with { Id = id };
|
||||
blocks.Add(block);
|
||||
if (block.Level is 2 or 3)
|
||||
{
|
||||
headings.Add(block);
|
||||
}
|
||||
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (raw.TrimStart().StartsWith('|'))
|
||||
{
|
||||
var table = ReadTable(lines, ref i);
|
||||
if (table is not null)
|
||||
{
|
||||
blocks.Add(table);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (LooksLikeList(raw, out var ordered))
|
||||
{
|
||||
var items = new List<string>();
|
||||
while (i < lines.Length && LooksLikeList(lines[i], out var next) && next == ordered)
|
||||
{
|
||||
items.Add(ListText(lines[i]));
|
||||
i++;
|
||||
}
|
||||
|
||||
blocks.Add(new MarkdownList(ordered, items));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (raw.StartsWith("> ", StringComparison.Ordinal) || raw == ">")
|
||||
{
|
||||
var quote = new List<string>();
|
||||
while (i < lines.Length && (lines[i].StartsWith("> ", StringComparison.Ordinal) || lines[i] == ">"))
|
||||
{
|
||||
quote.Add(lines[i].StartsWith("> ", StringComparison.Ordinal) ? lines[i][2..] : "");
|
||||
i++;
|
||||
}
|
||||
|
||||
blocks.Add(new MarkdownQuote(string.Join(" ", quote)));
|
||||
continue;
|
||||
}
|
||||
|
||||
var para = new List<string> { raw.Trim() };
|
||||
i++;
|
||||
while (i < lines.Length
|
||||
&& !string.IsNullOrWhiteSpace(lines[i])
|
||||
&& ParseHeading(lines[i]) is null
|
||||
&& !IsRule(lines[i])
|
||||
&& !lines[i].StartsWith("```", StringComparison.Ordinal)
|
||||
&& !LooksLikeList(lines[i], out _)
|
||||
&& !lines[i].TrimStart().StartsWith('|'))
|
||||
{
|
||||
para.Add(lines[i].Trim());
|
||||
i++;
|
||||
}
|
||||
|
||||
blocks.Add(new MarkdownParagraph(string.Join(" ", para)));
|
||||
}
|
||||
|
||||
return new MarkdownDocument(blocks, headings);
|
||||
}
|
||||
|
||||
private static MarkdownHeading? ParseHeading(string line)
|
||||
{
|
||||
if (!line.StartsWith('#') || line.Length < 3)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var level = 0;
|
||||
while (level < line.Length && line[level] == '#' && level < 6)
|
||||
{
|
||||
level++;
|
||||
}
|
||||
|
||||
if (level == 0 || level >= line.Length || line[level] != ' ')
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new MarkdownHeading(level, line[(level + 1)..].Trim(), "");
|
||||
}
|
||||
|
||||
private static MarkdownTable? ReadTable(string[] lines, ref int i)
|
||||
{
|
||||
var header = SplitRow(lines[i]);
|
||||
if (header.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
i++;
|
||||
if (i >= lines.Length || !IsAlignmentRow(lines[i]))
|
||||
{
|
||||
return new MarkdownTable(header, []);
|
||||
}
|
||||
|
||||
i++;
|
||||
var rows = new List<IReadOnlyList<string>>();
|
||||
while (i < lines.Length && lines[i].TrimStart().StartsWith('|'))
|
||||
{
|
||||
rows.Add(SplitRow(lines[i]));
|
||||
i++;
|
||||
}
|
||||
|
||||
return new MarkdownTable(header, rows);
|
||||
}
|
||||
|
||||
private static List<string> SplitRow(string line)
|
||||
{
|
||||
var trimmed = line.Trim();
|
||||
if (trimmed.StartsWith('|'))
|
||||
{
|
||||
trimmed = trimmed[1..];
|
||||
}
|
||||
|
||||
if (trimmed.EndsWith('|'))
|
||||
{
|
||||
trimmed = trimmed[..^1];
|
||||
}
|
||||
|
||||
return trimmed.Split('|').Select(c => c.Trim()).ToList();
|
||||
}
|
||||
|
||||
private static bool IsAlignmentRow(string line)
|
||||
=> line.Contains('|') && line.All(c => c is '|' or ':' or '-' or ' ' or '\t');
|
||||
|
||||
private static bool IsRule(string line)
|
||||
{
|
||||
var t = line.Trim();
|
||||
return t is "---" or "***" or "___" || (t.Length >= 3 && t.All(c => c == '-' || c == '*' || c == '_'));
|
||||
}
|
||||
|
||||
private static bool LooksLikeList(string line, out bool ordered)
|
||||
{
|
||||
ordered = false;
|
||||
var t = line.TrimStart();
|
||||
if (t.StartsWith("- ", StringComparison.Ordinal) || t.StartsWith("* ", StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var dot = t.IndexOf(". ", StringComparison.Ordinal);
|
||||
if (dot > 0 && t[..dot].All(char.IsDigit))
|
||||
{
|
||||
ordered = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string ListText(string line)
|
||||
{
|
||||
var t = line.TrimStart();
|
||||
if (t.StartsWith("- ", StringComparison.Ordinal) || t.StartsWith("* ", StringComparison.Ordinal))
|
||||
{
|
||||
return t[2..].Trim();
|
||||
}
|
||||
|
||||
var dot = t.IndexOf(". ", StringComparison.Ordinal);
|
||||
return dot > 0 ? t[(dot + 2)..].Trim() : t;
|
||||
}
|
||||
|
||||
private static string Slug(string text)
|
||||
{
|
||||
var chars = text.Where(c => char.IsLetterOrDigit(c) || c is ' ' or '-').ToArray();
|
||||
return new string(chars).Trim().Replace(' ', '-').ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static string UniqueId(string slug, HashSet<string> ids)
|
||||
{
|
||||
var id = string.IsNullOrEmpty(slug) ? "section" : slug;
|
||||
var n = 2;
|
||||
var candidate = id;
|
||||
while (!ids.Add(candidate))
|
||||
{
|
||||
candidate = id + "-" + n;
|
||||
n++;
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user