Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
[Tt]est[Rr]esult*/
*.nupkg
*.snupkg
BenchmarkDotNet.Artifacts/
8 changes: 6 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,12 @@ init-only record: `Display`, `Strict`, `PreserveFormatting`, `LaTeXFlavor` stand
(no code generated at run time, so the library runs under Native AOT, `IsAotCompatible` on net10.0): the XPath data
model over LINQ to XML (`XPathFunctions`), the result tree (`XsltOutput`), and the partial classes the generator
implements (`Stylesheets.cs`; the generated code goes to `obj/`, and is declared there so that Parlot, which compiles
the project without the other generators, sees it). `Xslt/CompiledStylesheetTests` compares them with
`XslCompiledTransform` on the corpora.
the project without the other generators, sees it). The generated code is static: a location path is loops over the
nodes, in a method whose parameters are what it reads, and builds a list only where one is needed; the parameters of
the named templates are typed by the values the calls give them, which the generator learns by compiling the
stylesheet until nothing new is learnt (`Facts`). `Xslt/CompiledStylesheetTests` compares the stylesheets with
`XslCompiledTransform` on the corpora, and the generator on `Xslt/conformance.xsl`; `StylesheetBenchmarks` measures
them against it.
- `Internal/Mtef/`: MTEF of Equation Editor 3.0 and MathType, read only; a C# port of the jure/mathtype gem, converted
with `mtef2mml.xsl`. It reads the "Equation Native" stream of the OLE object; opening the OLE object is the caller's.
- `Internal/UnicodeMath/`: a port of UnicodeMathML. Its PEG grammar is `UnicodeMathGrammar.parlot.cs`, compiled at
Expand Down
9 changes: 9 additions & 0 deletions benchmarks/SharpEquations.Benchmarks/Samples.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ public static string[] UnicodeMathCorpus()
return json.RootElement.EnumerateArray().Select(example => example.GetProperty("input").GetString()!).ToArray();
}

public static byte[][] MtefCorpus() =>
Directory.GetFiles(DataPath("mtef"), "*.*", SearchOption.AllDirectories).OrderBy(path => path, StringComparer.Ordinal).Select(File.ReadAllBytes).ToArray();

public static XElement[] ContentMathMLCorpus() =>
Directory.GetFiles(DataPath("contentmathml"), "*.mml").OrderBy(path => path, StringComparer.Ordinal)
.SelectMany(path => XElement.Load(path, LoadOptions.PreserveWhitespace).DescendantsAndSelf().Where(e => e.Name.LocalName == "math")).ToArray();

public static string Stylesheet(string name) => DataPath("xsl", name);

public static XElement[] OmmlCorpus() =>
Directory.GetFiles(DataPath("omml"), "*.omml").OrderBy(path => path, StringComparer.Ordinal).Select(path => XElement.Load(path)).ToArray();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@

<ItemGroup>
<None Include="..\..\tests\data\omml\*.omml" Link="data\omml\%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
<None Include="..\..\tests\data\mtef\**\*" Link="data\mtef\%(RecursiveDir)%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
<None Include="..\..\src\SharpEquations\Resources\*.xsl" Link="data\xsl\%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
<None Include="..\..\tests\data\contentmathml\*.mml" Link="data\contentmathml\%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
<None Include="..\..\tests\data\unicodemath\unicodemathml-examples.json" Link="data\unicodemath\%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

Expand Down
159 changes: 159 additions & 0 deletions benchmarks/SharpEquations.Benchmarks/StylesheetBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
namespace SharpEquations.Benchmarks
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Xsl;
using BenchmarkDotNet.Attributes;
using SharpEquations.Internal.Mtef;
using SharpEquations.Internal.Xslt;

/// <summary>
/// The stylesheets alone, over the corpora of the tests: compiled to C# by SharpEquations.XsltGenerator, as the
/// library runs them, and run by XslCompiledTransform, as it ran them before (the output written as text and read
/// again). The times are per corpus.
/// </summary>
[MemoryDiagnoser]
public class StylesheetBenchmarks
{
private XElement[] omml = [];
private XElement[] mathML = [];
private XElement[] mtef = [];
private XElement[] contentMathML = [];
private XslCompiledTransform omml2mml = null!;
private XslCompiledTransform mml2omml = null!;
private XslCompiledTransform mtef2mml = null!;
private XslCompiledTransform ctop = null!;

[GlobalSetup]
public void Setup()
{
var equations = Samples.UnicodeMathCorpus()
.Select(source => Equation.TryParse(source, EquationFormat.UnicodeMath, out var equation) ? equation : null)
.OfType<Equation>()
.Concat(Samples.OmmlCorpus().Select(source => Equation.Parse(source, EquationFormat.Omml)))
.ToArray();
mathML = equations.Select(equation => equation.ToMathML()).ToArray();
omml = Samples.OmmlCorpus().SelectMany(source => source.DescendantsAndSelf(MathMLNamespaces.Omml + "oMath"))
.Concat(equations.Select(equation => equation.ToOmml()))
.ToArray();
mtef = Samples.MtefCorpus().Select(Records).OfType<XElement>().ToArray();
omml2mml = Load("omml2mml.xsl");
omml = omml.Where(oMath => Converts(omml2mml, oMath)).ToArray();
mml2omml = Load("mml2omml.xsl");
mtef2mml = Load("mtef2mml.xsl");
contentMathML = Samples.ContentMathMLCorpus();
ctop = Load("ctop.xsl");
}

// an equation written in the text of another is two document elements for the stylesheet, and
// XslCompiledTransform splits the characters outside the basic plane
private static bool Converts(XslCompiledTransform stylesheet, XElement oMath)
{
try
{
return Omml2mmlStylesheet.Transform(oMath) != null && Run(stylesheet, [oMath]) == 1;
}
catch (Exception e) when (e is XmlException or XsltException or ArgumentException)
{
return false;
}
}

private static XElement? Records(byte[] bytes)
{
var data = bytes.AsSpan();
if (MtefParser.HasEquationNativeHeader(data))
data = data.Slice(data[0] | data[1] << 8);
try
{
var xml = MtefReader.Read(data);
MtefPreprocessor.Prepare(xml);
Mtef2mmlStylesheet.Transform(xml);
return xml;
}
catch (Exception e) when (e is not OutOfMemoryException)
{
return null;
}
}

private static XslCompiledTransform Load(string name)
{
using var reader = XmlReader.Create(Samples.Stylesheet(name), new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null });
var transform = new XslCompiledTransform();
transform.Load(reader, XsltSettings.Default, null);
return transform;
}

private static int Count(XElement? element) => element == null ? 0 : 1;

private static int Run(XslCompiledTransform stylesheet, XElement[] inputs)
{
var count = 0;
foreach (var input in inputs)
{
var output = new StringWriter();
using (var reader = new XDocument(input).CreateReader())
stylesheet.Transform(reader, null, output);
var text = output.ToString();
if (text.Trim().Length == 0)
continue;
using var result = XmlReader.Create(new StringReader(text), new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null });
count += Count(XElement.Load(result, LoadOptions.PreserveWhitespace));
}
return count;
}

[Benchmark]
public int Omml2MathML()
{
var count = 0;
foreach (var input in omml)
count += Count(Omml2mmlStylesheet.Transform(input));
return count;
}

[Benchmark]
public int Omml2MathMLXslCompiledTransform() => Run(omml2mml, omml);

[Benchmark]
public int MathML2Omml()
{
var count = 0;
foreach (var input in mathML)
count += Count(Mml2ommlStylesheet.Transform(input));
return count;
}

[Benchmark]
public int MathML2OmmlXslCompiledTransform() => Run(mml2omml, mathML);

[Benchmark]
public int Mtef2MathML()
{
var count = 0;
foreach (var input in mtef)
count += Count(Mtef2mmlStylesheet.Transform(input));
return count;
}

[Benchmark]
public int Mtef2MathMLXslCompiledTransform() => Run(mtef2mml, mtef);

[Benchmark]
public int ContentMathML2MathML()
{
var count = 0;
foreach (var input in contentMathML)
count += Count(CtopStylesheet.Transform(input));
return count;
}

[Benchmark]
public int ContentMathML2MathMLXslCompiledTransform() => Run(ctop, contentMathML);
}
}
38 changes: 29 additions & 9 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,29 +80,49 @@ caught, so `EquationStylesheets` rejects input nested more than 512 levels deep

## The stylesheets compiled to C# (`src/SharpEquations.XsltGenerator/`, `Internal/Xslt/`)

The three XSLT 1.0 stylesheets of `Resources/` (`omml2mml.xsl`, `mml2omml.xsl`, `mtef2mml.xsl`) are compiled to C# at
build time by `SharpEquations.XsltGenerator`, a source generator that reads them as additional files. Nothing is
The XSLT 1.0 stylesheets of `Resources/` (`omml2mml.xsl`, `mml2omml.xsl`, `mtef2mml.xsl`, `ctop.xsl`) are compiled to
C# at build time by `SharpEquations.XsltGenerator`, a source generator that reads them as additional files. Nothing is
generated at run time, unlike `XslCompiledTransform`, which emits IL: so the library runs under Native AOT, and the
stylesheets are not embedded in it.

- Each stylesheet becomes a class named after its file (`Omml2mmlStylesheet`), declared partial in
`Internal/Xslt/Stylesheets.cs`, with a method for each template, a lazy property for each global variable and, for
each mode, the dispatch of `xsl:apply-templates`: by the name of the element first, then the rules in the order of
XSLT (highest priority, then the last in the stylesheet), then the built-in templates.
- Each XPath expression becomes a C# expression over `XPathFunctions` (the data model of XPath over LINQ to XML, its
functions and conversions). Its type is known when it can be (a string, a number, a boolean, a node-set, a result
tree fragment), and is `object` otherwise (a parameter).
XSLT (highest priority, then the last in the stylesheet), then the built-in templates. The many rules of an element
whose pattern asks for a child with a text (`char[mt_code_value='0x2211' and ...]` of `mtef2mml.xsl`) are found by a
switch on the text of that child.
- A location path becomes loops over the nodes (first child and next sibling, the node test inlined, the names
compared as static `XName` fields), written where the path is used (`xsl:for-each`, `xsl:apply-templates`) or in a
method of its own, whose parameters are what the expression reads: the context node, the variables. What uses the
path tells the loop: a test stops at the first node, a string is the value of the first node, a comparison stops at
the node that compares so, and a list of the nodes is built only for a variable of more than one node, or to sort
the nodes when the loops do not reach them in document order. `position()` and `last()` are counted only for the
predicates, the contents and the templates that read them.
- The type of an expression is known when it can be: a string, a number, a boolean, a node-set (the node, or null,
when it has at most one; a list otherwise), a result tree fragment (a string, when its content only writes text).
The parameters of a named template have the type of the values the calls give them, and are `object` only when the
calls differ. For this, and to know which templates read the context node, its position or the number of nodes,
which only write text and which do not depend on the input, the generator compiles the stylesheet again with what
the compilation before learnt (`Facts`), until nothing new is learnt: every fact starts as the one that gives the
best code and only moves to the one that is always right.
- A global variable that is a string and does not depend on the input is computed once, not for each equation; the
map of a `translate()` is built once.
- `XsltOutput` builds the result as LINQ to XML, as `XslCompiledTransform` gave it when its output was written as text
and parsed again: text written with `disable-output-escaping` is markup, with the text next to it (`mtef2mml.xsl`
writes character references in pieces), and empty text leaves `<t></t>` rather than `<t />`.
writes character references in pieces: they are read as they are, and a parser reads any other markup), and empty
text leaves `<t></t>` rather than `<t />`.
- Strings are counted, cut and translated by code points, as XPath defines them (`XslCompiledTransform` counted UTF-16
units).
- What the stylesheets do not use (keys, `xsl:sort`, `xsl:number`, imports, extension functions, the `following` and
`preceding` axes) is an error of the build (XSLT001).

`Xslt/CompiledStylesheetTests` checks that the compiled stylesheets give what `XslCompiledTransform` gives for the
corpora: the OMML of Word documents and of the UnicodeMathML examples, their MathML, and the MTEF of the test data. It
leaves out the inputs with characters outside the basic plane, where the two count differently.
corpora: the OMML of Word documents and of the UnicodeMathML examples, their MathML, the MTEF of the test data and the
Content MathML of the tests of ctop. It leaves out the inputs with characters outside the basic plane, where the two
count differently. `Xslt/conformance.xsl`, compiled into the tests, does the same for what the generator compiles
beyond what the stylesheets of the library use of it on the corpora (the axes, the predicates that count, the
parameters of mixed types). `StylesheetBenchmarks` measures the stylesheets alone, compiled and run by
`XslCompiledTransform`.

## The normalized MathML (`Internal/MathML/MathMLParser.cs`)

Expand Down
18 changes: 18 additions & 0 deletions src/SharpEquations.XsltGenerator/CodeWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,24 @@ public CodeWriter Append(CodeWriter other)
return this;
}

/// <summary>Text that is already indented.</summary>
public CodeWriter Raw(string code)
{
text.Append(code);
return this;
}

/// <summary>The lines of another writer, written without indentation, at the indentation of this one.</summary>
public CodeWriter Indented(CodeWriter other)
{
foreach (var line in other.text.ToString().Split('\n'))
{
if (line.Length > 0)
Line(line);
}
return this;
}

public override string ToString() => text.ToString();
}
}
Loading
Loading