Files
markdig/src/SpecFileGen/Program.cs
Alexandre Mutel b62a12d32d Add support for GitHub alert blocks (#776)
* Add support for GitHub alert blocks

* Fix alert for "must come first in a quote block"

* Fix comment

* Update src/Markdig/Extensions/Alerts/AlertBlockRenderer.cs

Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com>

* Update src/Markdig/MarkdownExtensions.cs

Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com>

* Fix parsing of alert block with multiple children blocks

* Allow null for BlockParser ctor argument of QuoteBlock

---------

Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com>
2024-03-14 08:09:42 +01:00

378 lines
15 KiB
C#

using System.Text;
namespace SpecFileGen;
class Program
{
static readonly string SpecificationsDirectory =
Path.GetFullPath(
Path.Combine(
Path.GetDirectoryName(typeof(Spec).Assembly.Location),
"../../../../Markdig.Tests/"));
enum RendererType
{
Html,
Normalize,
PlainText,
Roundtrip
}
class Spec
{
public readonly string Name;
public readonly string Path;
public readonly string OutputPath;
public readonly string Extensions;
public readonly RendererType RendererType;
public int TestCount;
public Spec(string name, string fileName, string extensions, RendererType rendererType = RendererType.Html)
{
Name = name;
Path = SpecificationsDirectory;
if (rendererType == RendererType.Html) Path += "Specs";
else if (rendererType == RendererType.Normalize) Path += "NormalizeSpecs";
else if (rendererType == RendererType.PlainText) Path += "PlainTextSpecs";
else if (rendererType == RendererType.Roundtrip) Path += "RoundtripSpecs";
Path += "/" + fileName;
OutputPath = System.IO.Path.ChangeExtension(Path, "generated.cs");
Extensions = extensions;
RendererType = rendererType;
}
}
class NormalizeSpec : Spec
{
public NormalizeSpec(string name, string fileName, string extensions)
: base(name, fileName, extensions, rendererType: RendererType.Normalize) { }
}
class PlainTextSpec : Spec
{
public PlainTextSpec(string name, string fileName, string extensions)
: base(name, fileName, extensions, rendererType: RendererType.PlainText) { }
}
class RoundtripSpec : Spec
{
public RoundtripSpec(string name, string fileName, string extensions)
: base(name, fileName, extensions, rendererType: RendererType.Roundtrip) { }
}
// NOTE: Beware of Copy/Pasting spec files - some characters may change (non-breaking space into space)!
static readonly Spec[] Specs = new[]
{
new Spec("CommonMarkSpecs", "CommonMark.md", ""),
new Spec("Alert Blocks", "AlertBlockSpecs.md", "advanced"),
new Spec("Pipe Tables", "PipeTableSpecs.md", "pipetables|advanced"),
new Spec("GFM Pipe Tables", "PipeTableGfmSpecs.md", "gfm-pipetables"),
new Spec("Footnotes", "FootnotesSpecs.md", "footnotes|advanced"),
new Spec("Generic Attributes", "GenericAttributesSpecs.md", "attributes|advanced"),
new Spec("Emphasis Extra", "EmphasisExtraSpecs.md", "emphasisextras|advanced"),
new Spec("Hardline Breaks", "HardlineBreakSpecs.md", "hardlinebreak|advanced+hardlinebreak"),
new Spec("Grid Tables", "GridTableSpecs.md", "gridtables|advanced"),
new Spec("Custom Containers", "CustomContainerSpecs.md", "customcontainers+attributes|advanced"),
new Spec("Definition Lists", "DefinitionListSpecs.md", "definitionlists+attributes|advanced"),
new Spec("Emoji", "EmojiSpecs.md", "emojis|advanced+emojis"),
new Spec("Abbreviations", "AbbreviationSpecs.md", "abbreviations|advanced"),
new Spec("List Extras", "ListExtraSpecs.md", "listextras|advanced"),
new Spec("Math", "MathSpecs.md", "mathematics|advanced"),
new Spec("Bootstrap", "BootstrapSpecs.md", "bootstrap+pipetables+figures+attributes"),
new Spec("Media", "MediaSpecs.md", "medialinks|advanced+medialinks"),
new Spec("Smarty Pants", "SmartyPantsSpecs.md", "pipetables+smartypants|advanced+smartypants"),
new Spec("Auto Identifiers", "AutoIdentifierSpecs.md", "autoidentifiers|advanced"),
new Spec("Task Lists", "TaskListSpecs.md", "tasklists|advanced"),
new Spec("Diagrams", "DiagramsSpecs.md", "diagrams|advanced"),
new Spec("No Html", "NoHtmlSpecs.md", "nohtml"),
new Spec("Yaml", "YamlSpecs.md", "yaml"),
new Spec("Auto Links", "AutoLinks.md", "autolinks|advanced"),
new Spec("Jira Links", "JiraLinks.md", "jiralinks"),
new Spec("Globalization", "GlobalizationSpecs.md", "globalization+advanced+emojis"),
new Spec("Figures, Footers and Cites", "FigureFooterAndCiteSpecs.md", "figures+footers+citations|advanced"),
new NormalizeSpec("Headings", "Headings.md", ""),
new PlainTextSpec("Sample", "SamplePlainText.md", ""),
new RoundtripSpec("Roundtrip", "CommonMark.md", ""),
};
static void Main()
{
int totalTests = 0;
bool hasErrors = false;
foreach (var spec in Specs)
{
if (!File.Exists(spec.Path))
{
EmitError("Could not find the specification file at " + spec.Path);
hasErrors = true;
continue;
}
string source = ParseSpecification(spec, File.ReadAllText(spec.Path)).Replace("\r\n", "\n", StringComparison.Ordinal);
totalTests += spec.TestCount;
if (File.Exists(spec.OutputPath)) // If the source hasn't changed, don't bump the generated tag
{
string previousSource = File.ReadAllText(spec.OutputPath).Replace("\r\n", "\n", StringComparison.Ordinal);
if (previousSource == source && File.GetLastWriteTime(spec.OutputPath) > File.GetLastWriteTime(spec.Path))
{
continue;
}
Console.WriteLine($"Spec changed {spec.Path}. Need to regenerate");
}
Console.WriteLine($"Generating spec {spec.Name} to {spec.OutputPath}...");
File.WriteAllText(spec.OutputPath, source);
}
if (hasErrors)
{
Environment.Exit(1);
}
Console.WriteLine("There are {0} spec tests in total", totalTests);
}
static void EmitError(string error)
{
var prevColor = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine();
Console.WriteLine(new string('-', error.Length));
Console.WriteLine(error);
Console.WriteLine(new string('-', error.Length));
Console.WriteLine();
Console.ForegroundColor = prevColor;
}
static readonly StringBuilder StringBuilder = new StringBuilder(1 << 20); // 1 MB
static void Write(string text)
{
StringBuilder.Append(text);
}
static void Line(string text = null)
{
if (text != null) StringBuilder.Append(text);
StringBuilder.Append('\n');
}
static void Indent(int count = 1)
{
StringBuilder.Append(new string(' ', 4 * count));
}
static string ParseSpecification(Spec spec, string specSource)
{
Line();
Write("// "); Line(new string('-', 32));
Write("// "); Write(new string(' ', 16 - spec.Name.Length / 2)); Line(spec.Name);
Write("// "); Line(new string('-', 32));
Line();
Line("using System;");
Line("using NUnit.Framework;");
Line();
Write("namespace Markdig.Tests.Specs.");
if (spec.RendererType == RendererType.Normalize) Write("Normalize.");
else if (spec.RendererType == RendererType.PlainText) Write("PlainText.");
else if (spec.RendererType == RendererType.Roundtrip) Write("Roundtrip.");
Line(CompressedName(spec.Name).Replace('.', '_'));
Line("{");
var lines = specSource.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.None);
bool nameChanged = true;
string name = "";
string compressedName = "";
int number = 0;
int commentOffset = 0, commentEnd = 0, markdownOffset = 0, markdownEnd = 0, htmlOffset = 0, htmlEnd = 0;
bool first = true;
LinkedList<(string Heading, string Compressed, int Level)> headings = new LinkedList<(string, string, int)>();
StringBuilder nameBuilder = new StringBuilder(64);
int i = 0;
while (i < lines.Length)
{
commentOffset = commentEnd = i;
while (!lines[i].Equals("```````````````````````````````` example", StringComparison.Ordinal))
{
string line = lines[i];
if (line.Length > 2 && line[0] == '#')
{
int level = line.IndexOf(' ', StringComparison.Ordinal);
while (headings.Count != 0)
{
if (headings.Last.Value.Level < level) break;
headings.RemoveLast();
}
string heading = line.Substring(level + 1);
headings.AddLast((heading, CompressedName(heading), level));
foreach (var (Heading, _, _) in headings)
nameBuilder.Append(Heading + " / ");
nameBuilder.Length -= 3;
name = nameBuilder.ToString();
nameBuilder.Length = 0;
foreach (var (_, Compressed, _) in headings)
nameBuilder.Append(Compressed);
compressedName = nameBuilder.ToString();
nameBuilder.Length = 0;
nameChanged = true;
}
i++;
if (!IsEmpty(line))
commentEnd = i;
if (i == lines.Length)
{
if (commentOffset != commentEnd)
{
while (commentOffset < commentEnd && IsEmpty(lines[commentOffset])) commentOffset++;
for (i = commentOffset; i < commentEnd; i++)
{
Indent(2); Write("// "); Line(lines[i]);
}
}
goto End;
}
};
markdownOffset = ++i;
while (!(lines[i].Length == 1 && lines[i][0] == '.')) i++;
markdownEnd = i++;
htmlOffset = i;
while (!lines[i].Equals("````````````````````````````````", StringComparison.Ordinal)) i++;
htmlEnd = i++;
if (nameChanged)
{
if (!first)
{
Indent(); Line("}");
Line();
}
Indent(); Line("[TestFixture]");
Indent(); Line("public class Test" + compressedName);
Indent(); Line("{");
first = false;
nameChanged = false;
}
else Line();
WriteTest(name, compressedName, ++number, spec.Extensions, lines,
commentOffset, commentEnd,
markdownOffset, markdownEnd,
htmlOffset, htmlEnd,
spec.RendererType);
}
End:
if (!first)
{
Indent(); Line("}");
}
Line("}");
string source = StringBuilder.ToString();
StringBuilder.Length = 0;
spec.TestCount = number;
return source;
}
static void WriteTest(string name, string compressedName, int number, string extensions, string[] lines, int commentOffset, int commentEnd, int markdownOffset, int markdownEnd, int htmlOffset, int htmlEnd, RendererType rendererType)
{
if (commentOffset != commentEnd)
{
while (commentOffset < commentEnd && IsEmpty(lines[commentOffset])) commentOffset++;
for (int i = commentOffset; i < commentEnd; i++)
{
Indent(2); Write("// "); Line(lines[i]);
}
}
Indent(2); Line("[Test]");
Indent(2); Line("public void " + compressedName + "_Example" + number.ToString().PadLeft(3, '0') + "()");
Indent(2); Line("{");
Indent(3); Line("// Example " + number);
Indent(3); Line("// Section: " + name);
Indent(3); Line("//");
Indent(3); Line("// The following Markdown:");
for (int i = markdownOffset; i < markdownEnd; i++)
{
Indent(3); Write("// "); Indent(); Line(lines[i]);
}
Indent(3); Line("//");
Indent(3); Line("// Should be rendered as:");
for (int i = htmlOffset; i < htmlEnd; i++)
{
Indent(3); Write("// "); Indent(); Line(lines[i]);
}
if (htmlOffset >= htmlEnd)
{
Indent(3); Write("//");
}
Line();
Indent(3);
if (rendererType == RendererType.Html) Write("TestParser");
else if (rendererType == RendererType.Normalize) Write("TestNormalize");
else if (rendererType == RendererType.PlainText) Write("TestPlainText");
else if (rendererType == RendererType.Roundtrip) Write("TestRoundtrip");
Write(".TestSpec(\"");
for (int i = markdownOffset; i < markdownEnd; i++)
{
Write(Escape(lines[i]));
if (i != markdownEnd - 1) Write("\\n");
}
Write("\", \"");
for (int i = htmlOffset; i < htmlEnd; i++)
{
Write(Escape(lines[i]));
if (i != htmlEnd - 1) Write("\\n");
}
Write("\", \"");
Write(extensions);
Line($"\", context: \"Example {number}\\nSection {name}\\n\");");
Indent(2); Line("}");
}
static string Escape(string input)
{
return input
.Replace("→", "\t")
.Replace("\\", "\\\\")
.Replace("\"", "\\\"")
.Replace("\0", "\\0")
.Replace("\a", "\\a")
.Replace("\b", "\\b")
.Replace("\f", "\\f")
.Replace("\n", "\\n")
.Replace("\r", "\\r")
.Replace("\t", "\\t")
.Replace("\v", "\\v")
;
}
static string CompressedName(string name)
{
string compressedName = "";
foreach (var part in name.Replace(',', ' ').Split(' ', StringSplitOptions.RemoveEmptyEntries))
{
compressedName += char.IsLower(part[0])
? char.ToUpper(part[0]) + (part.Length > 1 ? part.Substring(1) : "")
: part;
}
return compressedName;
}
static bool IsEmpty(string str)
{
for (int i = 0; i < str.Length; i++)
{
if (str[i] != ' ') return false;
}
return true;
}
}