diff --git a/.gitignore b/.gitignore
index c31ed73..b9e9bc6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@
[Tt]est[Rr]esult*/
*.nupkg
*.snupkg
+BenchmarkDotNet.Artifacts/
diff --git a/AGENTS.md b/AGENTS.md
index 5825a69..4187489 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -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
diff --git a/benchmarks/SharpEquations.Benchmarks/Samples.cs b/benchmarks/SharpEquations.Benchmarks/Samples.cs
index 334c81b..12c6fcf 100644
--- a/benchmarks/SharpEquations.Benchmarks/Samples.cs
+++ b/benchmarks/SharpEquations.Benchmarks/Samples.cs
@@ -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();
}
diff --git a/benchmarks/SharpEquations.Benchmarks/SharpEquations.Benchmarks.csproj b/benchmarks/SharpEquations.Benchmarks/SharpEquations.Benchmarks.csproj
index 04931b3..f72051f 100644
--- a/benchmarks/SharpEquations.Benchmarks/SharpEquations.Benchmarks.csproj
+++ b/benchmarks/SharpEquations.Benchmarks/SharpEquations.Benchmarks.csproj
@@ -19,6 +19,9 @@
+
+
+
diff --git a/benchmarks/SharpEquations.Benchmarks/StylesheetBenchmarks.cs b/benchmarks/SharpEquations.Benchmarks/StylesheetBenchmarks.cs
new file mode 100644
index 0000000..8f712b7
--- /dev/null
+++ b/benchmarks/SharpEquations.Benchmarks/StylesheetBenchmarks.cs
@@ -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;
+
+ ///
+ /// 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.
+ ///
+ [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()
+ .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().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);
+ }
+}
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 0dab68c..f6604d8 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -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 `` rather than ``.
+ writes character references in pieces: they are read as they are, and a parser reads any other markup), and empty
+ text leaves `` rather than ``.
- 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`)
diff --git a/src/SharpEquations.XsltGenerator/CodeWriter.cs b/src/SharpEquations.XsltGenerator/CodeWriter.cs
index 537b48d..de3bdfe 100644
--- a/src/SharpEquations.XsltGenerator/CodeWriter.cs
+++ b/src/SharpEquations.XsltGenerator/CodeWriter.cs
@@ -38,6 +38,24 @@ public CodeWriter Append(CodeWriter other)
return this;
}
+ /// Text that is already indented.
+ public CodeWriter Raw(string code)
+ {
+ text.Append(code);
+ return this;
+ }
+
+ /// The lines of another writer, written without indentation, at the indentation of this one.
+ 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();
}
}
diff --git a/src/SharpEquations.XsltGenerator/StylesheetCompiler.cs b/src/SharpEquations.XsltGenerator/StylesheetCompiler.cs
index 605164e..4faf35e 100644
--- a/src/SharpEquations.XsltGenerator/StylesheetCompiler.cs
+++ b/src/SharpEquations.XsltGenerator/StylesheetCompiler.cs
@@ -7,10 +7,78 @@ namespace SharpEquations.XsltGenerator
using System.Xml.Linq;
///
- /// An XSLT 1.0 stylesheet compiled to a C# class: a method for each template, the dispatch of
- /// xsl:apply-templates for each mode (by the name of the element first), a lazy property for each
- /// global variable. What the stylesheets of SharpEquations do not use (keys, sorting, numbering, imports,
- /// extension functions) is reported as an error.
+ /// What a compilation of the stylesheet takes as known of its templates, parameters and global variables, and
+ /// learns of them: the stylesheet is compiled again, with what the compilation before learnt, 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, so the last compilation holds.
+ ///
+ internal sealed class Facts
+ {
+ /// The types of the values given to the parameters of the named templates.
+ public Dictionary ParameterTypes { get; } = new Dictionary();
+
+ /// The parameters of the named templates that a call leaves to their default.
+ public HashSet Omitted { get; } = new HashSet();
+
+ /// The named templates that read the context node, its position, its size.
+ public HashSet NodeUsed { get; } = new HashSet();
+
+ public HashSet PositionUsed { get; } = new HashSet();
+
+ public HashSet SizeUsed { get; } = new HashSet();
+
+ /// The modes with a template that reads the position of the node, the size.
+ public HashSet ModePosition { get; } = new HashSet();
+
+ public HashSet ModeSize { get; } = new HashSet();
+
+ /// The named templates that write more than text.
+ public HashSet NotText { get; } = new HashSet();
+
+ /// The named templates that read the input, or a global variable that depends on it.
+ public HashSet Impure { get; } = new HashSet();
+
+ /// The global variables that depend on the input, or are not strings.
+ public HashSet Dynamic { get; } = new HashSet();
+
+ private IEnumerable> Sets => new[] { Omitted, NodeUsed, PositionUsed, SizeUsed, ModePosition, ModeSize, NotText, Impure, Dynamic };
+
+ public static string Parameter(string template, string parameter) => template + "\n" + parameter;
+
+ public static XType Join(XType left, XType right)
+ {
+ if (left == XType.None)
+ return right;
+ if (right == XType.None || left == right)
+ return left;
+ // a node is a node-set too
+ return (left == XType.Node && right == XType.NodeSet) || (left == XType.NodeSet && right == XType.Node) ? XType.NodeSet : XType.Any;
+ }
+
+ public void Give(string parameter, XType type) =>
+ ParameterTypes[parameter] = Join(ParameterTypes.TryGetValue(parameter, out var known) ? known : XType.None, type);
+
+ /// Adds what another compilation learnt; whether it was anything new.
+ public bool Add(Facts other)
+ {
+ var before = Count;
+ foreach (var parameter in other.ParameterTypes)
+ Give(parameter.Key, parameter.Value);
+ foreach (var pair in Sets.Zip(other.Sets, (mine, theirs) => (mine, theirs)))
+ pair.mine.UnionWith(pair.theirs);
+ return Count != before;
+ }
+
+ // every fact moves one way, so the count tells whether one moved
+ private int Count => Sets.Sum(s => s.Count) + ParameterTypes.Values.Sum(t => t == XType.None ? 0 : t == XType.Any ? 3 : t == XType.NodeSet ? 2 : 1);
+ }
+
+ ///
+ /// An XSLT 1.0 stylesheet compiled to a C# class: a method for each template, with typed parameters for the named
+ /// ones; the dispatch of xsl:apply-templates for each mode (by the name of the element first); a lazy
+ /// property for each global variable, computed once for all when it is a string that does not depend on the
+ /// input; a method for each expression that walks the tree. What the stylesheets of SharpEquations do not use
+ /// (keys, sorting, numbering, imports, extension functions) is reported as an error.
///
internal sealed class StylesheetCompiler
{
@@ -19,33 +87,41 @@ internal sealed class StylesheetCompiler
private readonly XElement stylesheet;
private readonly string className;
private readonly string fileName;
+ private readonly Facts facts;
private readonly XPathCompiler xpath;
+ private readonly Names locals = new Names();
private readonly CodeWriter nameFields = new CodeWriter(2);
- private readonly CodeWriter testFields = new CodeWriter(2);
private readonly CodeWriter staticFields = new CodeWriter(2);
private readonly CodeWriter instanceFields = new CodeWriter(2);
private readonly CodeWriter members = new CodeWriter(2);
private readonly Dictionary names = new Dictionary();
- private readonly Dictionary tests = new Dictionary();
+ private readonly Dictionary helpers = new Dictionary();
+ private readonly Dictionary statics = new Dictionary();
private readonly Dictionary namedTemplates = new Dictionary();
private readonly Dictionary globals = new Dictionary();
private readonly Dictionary modes = new Dictionary { [""] = 0 };
private readonly List rules = new List();
- private int counter;
private sealed class NamedTemplate
{
- public NamedTemplate(string method, List parameters)
+ public NamedTemplate(string name, string method, List parameters, bool matches)
{
+ Name = name;
Method = method;
Parameters = parameters;
+ Matches = matches;
}
+ public string Name { get; }
+
public string Method { get; }
public List Parameters { get; }
+
+ /// Whether the template has a pattern too, and takes its parameters from xsl:apply-templates.
+ public bool Matches { get; }
}
private sealed class GlobalVariable
@@ -115,17 +191,24 @@ public Rule(int index, XElement template, string method, int mode, double priori
public List Steps { get; }
public Step? Last => Steps.Count == 0 ? null : Steps[Steps.Count - 1].Step;
+
+ /// Whether the rule matches every node it is tried on, those its last step tells by their kind or name.
+ public bool Always { get; set; }
}
- public StylesheetCompiler(XElement stylesheet, string className, string fileName)
+ public StylesheetCompiler(XElement stylesheet, string className, string fileName, Facts facts)
{
this.stylesheet = stylesheet;
this.className = className;
this.fileName = fileName;
+ this.facts = facts;
xpath = new XPathCompiler(this);
}
- public string NewName(string prefix) => prefix + (++counter);
+ /// What this compilation learnt.
+ public Facts Learnt { get; } = new Facts();
+
+ public string NewName(string prefix) => locals.New(prefix);
public static XsltException Error(XObject node, string message)
{
@@ -147,49 +230,41 @@ public string Name(XName name)
return field;
}
- public string NodeTest(NodeTestSyntax test, XElement scope)
+ /// A static field of the class with this value, the same for the same value.
+ public string StaticField(string type, string init)
{
- string key;
- string init;
- switch (test.Kind)
+ var key = type + " " + init;
+ if (!statics.TryGetValue(key, out var field))
{
- case TestKind.Name:
- {
- var name = XName.Get(test.Local!, Namespace(test.Prefix, scope));
- key = "name " + name;
- init = "NodeTest.Named(" + Name(name) + ")";
- break;
- }
- case TestKind.Namespace:
- {
- var ns = Namespace(test.Prefix, scope);
- key = "ns " + ns;
- init = "NodeTest.InNamespace(" + XPathCompiler.Literal(ns) + ")";
- break;
- }
- case TestKind.AnyName:
- return "NodeTest.AnyName";
- case TestKind.Node:
- return "NodeTest.Node";
- case TestKind.Text:
- return "NodeTest.Text";
- case TestKind.Comment:
- return "NodeTest.Comment";
- default:
- key = "pi " + test.Local;
- init = "NodeTest.ProcessingInstruction(" + (test.Local == null ? "null" : XPathCompiler.Literal(test.Local)) + ")";
- break;
+ field = "Static" + statics.Count;
+ statics.Add(key, field);
+ staticFields.Line("private static readonly " + type + " " + field + " = " + init + ";");
}
- if (!tests.TryGetValue(key, out var field))
+ return field;
+ }
+
+ /// A static field for a value kept from one use to the next.
+ public string CacheField(string type)
+ {
+ var field = NewName("Cache");
+ staticFields.Line("private static " + type + " " + field + ";");
+ return field;
+ }
+
+ /// A method of the class with this body, the same for the same body; its name.
+ public string HelperMethod(string returnType, string parameters, string body)
+ {
+ var key = returnType + " (" + parameters + ")\n" + body;
+ if (!helpers.TryGetValue(key, out var method))
{
- field = "Test" + tests.Count;
- tests.Add(key, field);
- testFields.Line("private static readonly NodeTest " + field + " = " + init + ";");
+ method = "Path" + helpers.Count;
+ helpers.Add(key, method);
+ members.Line().Open("private " + returnType + " " + method + "(" + parameters + ")").Raw(body).Close();
}
- return field;
+ return method;
}
- private static string Namespace(string? prefix, XElement scope)
+ public static string Namespace(string? prefix, XElement scope)
{
if (prefix == null)
return "";
@@ -234,7 +309,7 @@ public string Compile()
{
var name = Attr(templates[i], "name", false);
if (name != null)
- namedTemplates[name] = new NamedTemplate("Named" + i, TemplateParameters(templates[i]).Select(p => Attr(p, "name", true)!).ToList());
+ namedTemplates[name] = new NamedTemplate(name, "Named" + i, TemplateParameters(templates[i]).Select(p => Attr(p, "name", true)!).ToList(), Attr(templates[i], "match", false) != null);
}
for (var i = 0; i < templates.Count; i++)
@@ -283,7 +358,7 @@ private string Assemble()
.Line()
.Line("// the stylesheet " + fileName + ", compiled to C#")
.Open("internal sealed partial class " + className);
- file.Append(nameFields).Append(testFields).Append(staticFields);
+ file.Append(nameFields).Append(staticFields);
file.Line()
.Line("private readonly XsltOutput output = new XsltOutput();")
.Line("private readonly XObject root;")
@@ -295,7 +370,7 @@ private string Assemble()
.Open("public static partial XElement? Transform(XElement input)")
.Line("var document = new XDocument(input);")
.Line("var stylesheet = new " + className + "(document);")
- .Line("stylesheet.Apply0(Single(document), null);")
+ .Line("stylesheet.Dispatch0(document, 1, 1, null);")
.Line("return stylesheet.output.Result();")
.Close();
file.Append(members);
@@ -316,34 +391,58 @@ private void CompileTemplate(XElement template, int index)
if (name != null)
{
var named = namedTemplates[name];
- var signature = string.Concat(named.Parameters.Select((p, i) => ", object? a" + i));
+ var types = named.Parameters.Select(p => ParameterType(named, p)).ToList();
+ if (named.Matches)
+ {
+ foreach (var parameter in named.Parameters)
+ Learnt.Give(Facts.Parameter(name, parameter), XType.Any);
+ }
+ var signature = string.Concat(named.Parameters.Select((p, i) => ", " + XPathCompiler.TypeName(types[i]) + "? a" + i));
+ var focus = new Focus();
+ var purity = new Purity();
var w = new CodeWriter(2);
w.Line().Open("private void " + named.Method + "(XObject n0, int p0, int s0" + signature + ")");
- CompileBody(template, parameters, i => "a" + i, w);
+ CompileBody(template, parameters, i => "a" + i, i => types[i], named, focus, purity, w);
w.Close();
members.Append(w);
+ if (focus.NodeUsed || focus.CurrentUsed)
+ Learnt.NodeUsed.Add(name);
+ if (focus.PositionUsed)
+ Learnt.PositionUsed.Add(name);
+ if (focus.SizeUsed)
+ Learnt.SizeUsed.Add(name);
+ if (purity.Impure)
+ Learnt.Impure.Add(name);
+ if (!IsTextOnly(template.Nodes()))
+ Learnt.NotText.Add(name);
}
if (match == null)
return;
+ var mode = ModeIndex((string?)template.Attribute("mode"));
var method = "Template" + index;
var body = new CodeWriter(2);
body.Line().Open("private void " + method + "(XObject n0, int p0, int s0, TemplateArguments? args)");
+ var templateFocus = new Focus();
if (name != null)
{
var named = namedTemplates[name];
- body.Line(named.Method + "(n0, p0, s0" + string.Concat(named.Parameters.Select(p => ", args?.Get(" + XPathCompiler.Literal(p) + ")")) + ");");
+ var context = TemplateContext(template, templateFocus, new Purity(), new Scope(null));
+ body.Line(Call(named, context, named.Parameters.Select(p => "args?.Get(" + XPathCompiler.Literal(p) + ")")));
}
else
{
var names = parameters.Select(p => Attr(p, "name", true)!).ToList();
- CompileBody(template, parameters, i => "args?.Get(" + XPathCompiler.Literal(names[i]) + ")", body);
+ CompileBody(template, parameters, i => "args?.Get(" + XPathCompiler.Literal(names[i]) + ")", i => XType.Any, null, templateFocus, new Purity(), body);
}
body.Close();
members.Append(body);
+ if (templateFocus.PositionUsed)
+ Learnt.ModePosition.Add(ModeKey(mode));
+ if (templateFocus.SizeUsed)
+ Learnt.ModeSize.Add(ModeKey(mode));
- var mode = ModeIndex((string?)template.Attribute("mode"));
var priority = (string?)template.Attribute("priority");
foreach (var alternative in Alternatives(XPathParser.Parse(match), template))
{
@@ -372,6 +471,60 @@ private void CompileTemplate(XElement template, int index)
}
}
+ private static string ModeKey(int mode) => mode.ToString(System.Globalization.CultureInfo.InvariantCulture);
+
+ private Context TemplateContext(XElement element, Focus focus, Purity purity, Scope scope) =>
+ new Context("n0", NodeKind.Object, "p0", "s0", focus, "n0", NodeKind.Object, focus, scope, element, locals, purity);
+
+ /// The type of a parameter of a named template: that of every value it is given, if they have the same.
+ private XType ParameterType(NamedTemplate template, string parameter)
+ {
+ if (template.Matches)
+ return XType.Any;
+ return facts.ParameterTypes.TryGetValue(Facts.Parameter(template.Name, parameter), out var type) ? type : XType.None;
+ }
+
+ ///
+ /// The call of a named template, which is given the context node, its position and its size only if it reads them.
+ ///
+ private string Call(NamedTemplate template, Context context, IEnumerable arguments)
+ {
+ if (facts.Impure.Contains(template.Name))
+ context.Purity.Impure = true;
+ var node = facts.NodeUsed.Contains(template.Name) ? context.Node : "null";
+ var position = facts.PositionUsed.Contains(template.Name) ? context.Position : "0";
+ var size = facts.SizeUsed.Contains(template.Name) ? context.Size : "0";
+ return template.Method + "(" + node + ", " + position + ", " + size + string.Concat(arguments.Select(a => ", " + a)) + ");";
+ }
+
+ /// The value as a parameter of the type takes it.
+ private static string Convert(Compiled value, XType type)
+ {
+ switch (type)
+ {
+ case XType.Any:
+ case XType.None:
+ return XPathCompiler.Box(value);
+ case var _ when type == value.Type:
+ return value.Code;
+ case XType.NodeSet when value.Type == XType.Node:
+ return "ListOf(" + value.Code + ")";
+ case XType.Node:
+ return "null";
+ // no value of another type reaches a parameter whose type is known
+ case XType.Number:
+ return "0D";
+ case XType.Boolean:
+ return "false";
+ case XType.NodeSet:
+ return "Empty";
+ case XType.Fragment:
+ return "null!";
+ default:
+ return "\"\"";
+ }
+ }
+
private static IEnumerable Alternatives(Expr pattern, XElement template)
{
switch (pattern)
@@ -403,38 +556,192 @@ private static double DefaultPriority(PathExpr alternative, List st
}
}
- private void CompileBody(XElement template, List parameters, System.Func argument, CodeWriter w)
+ private void CompileBody(XElement template, List parameters, System.Func argument, System.Func type, NamedTemplate? named,
+ Focus focus, Purity purity, CodeWriter w)
{
var scope = new Scope(null);
- var context = new Context("n0", "p0", "s0", "n0", scope, template);
+ var context = TemplateContext(template, focus, purity, scope);
for (var i = 0; i < parameters.Count; i++)
{
var parameter = parameters[i];
+ var name = Attr(parameter, "name", true)!;
var id = NewName("v");
var paramContext = context.WithElement(parameter);
var select = Attr(parameter, "select", false);
+ var content = select == null && HasContent(parameter);
+ var valueType = type(i) == XType.Number || type(i) == XType.Boolean;
+
+ // the default is one of the values of the parameter if a call leaves the parameter to it
+ void Default(XType defaultType)
+ {
+ if (named != null && facts.Omitted.Contains(Facts.Parameter(named.Name, name)))
+ Learnt.Give(Facts.Parameter(named.Name, name), defaultType);
+ }
+
if (select != null)
{
- w.Line("var " + id + " = " + argument(i) + " ?? " + XPathCompiler.Box(xpath.Compile(select, paramContext)) + ";");
+ var value = xpath.Compile(select, paramContext).Bound();
+ Default(value.Type);
+ // a node that is null is an empty node-set, not a parameter left to its default
+ w.Line(type(i) == XType.Node
+ ? "var " + id + " = ReferenceEquals(" + argument(i) + ", Omitted) ? " + Convert(value, type(i)) + " : " + argument(i) + ";"
+ : "var " + id + " = " + argument(i) + " ?? " + Convert(value, type(i)) + ";");
}
- else if (HasContent(parameter))
+ else if (type(i) == XType.Node)
+ {
+ Default(content ? XType.Fragment : XType.String);
+ w.Line("var " + id + " = ReferenceEquals(" + argument(i) + ", Omitted) ? null : " + argument(i) + ";");
+ }
+ else if (content && !valueType)
{
w.Line("var " + id + " = " + argument(i) + ";");
w.Open("if (" + id + " == null)");
- w.Line("output.BeginCapture();");
- CompileSequence(parameter.Nodes(), paramContext, w);
- w.Line(id + " = output.EndFragment();");
+ var value = Capture(parameter, paramContext, w);
+ Default(value.Type);
+ w.Line(id + " = " + Convert(value, type(i)) + ";");
w.Close();
}
else
{
- w.Line("var " + id + " = " + argument(i) + " ?? \"\";");
+ Default(content ? XType.Fragment : XType.String);
+ w.Line("var " + id + " = " + argument(i) + " ?? " + Convert(new Compiled("\"\"", XType.String), type(i)) + ";");
}
- scope.Add(Attr(parameter, "name", true)!, new Variable(id, XType.Any));
+ scope.Add(name, new Variable(id, type(i), type(i) == XType.Node ? NodeOrder.Single : NodeOrder.Ordered));
}
CompileSequence(template.Nodes().Where(n => !(n is XElement e && parameters.Contains(e))), context, w);
}
+ ///
+ /// Whether the instructions write only text, so that what they write can be kept as a string: no element, no
+ /// text written as markup, no template applied, and only named templates that write text called.
+ ///
+ private bool IsTextOnly(IEnumerable nodes)
+ {
+ foreach (var element in nodes.OfType())
+ {
+ if (element.Name.Namespace != Xsl)
+ return false;
+ switch (element.Name.LocalName)
+ {
+ case "text":
+ case "value-of":
+ if (DisablesEscaping(element))
+ return false;
+ break;
+ case "variable":
+ case "param":
+ case "message":
+ case "fallback":
+ break;
+ case "if":
+ case "choose":
+ case "when":
+ case "otherwise":
+ case "for-each":
+ if (!IsTextOnly(element.Nodes()))
+ return false;
+ break;
+ case "call-template":
+ if (facts.NotText.Contains((string?)element.Attribute("name") ?? ""))
+ return false;
+ break;
+ default:
+ return false;
+ }
+ }
+ return true;
+ }
+
+ ///
+ /// What the instructions write as a C# expression of a string, if they are only text, values and choices
+ /// between them: null if they need statements.
+ ///
+ private string? TextExpression(IEnumerable nodes, Context context)
+ {
+ var parts = new List();
+ foreach (var node in nodes)
+ {
+ switch (node)
+ {
+ case XText text when text.Value.Trim().Length == 0:
+ break;
+ case XText text:
+ parts.Add(XPathCompiler.Literal(text.Value));
+ break;
+ case XElement element when element.Name == Xsl + "text" && !DisablesEscaping(element):
+ if (element.Value.Length > 0)
+ parts.Add(XPathCompiler.Literal(element.Value));
+ break;
+ case XElement element when element.Name == Xsl + "value-of" && !DisablesEscaping(element) && element.Attribute("select") != null:
+ parts.Add(XPathCompiler.Str(xpath.Compile(Attr(element, "select", true)!, context.WithElement(element))));
+ break;
+ case XElement element when element.Name == Xsl + "if" && element.Attribute("test") != null:
+ {
+ var then = TextExpression(element.Nodes(), context.WithElement(element));
+ if (then == null)
+ return null;
+ parts.Add("(" + XPathCompiler.Bool(xpath.Compile(Attr(element, "test", true)!, context.WithElement(element))) + " ? " + then + " : \"\")");
+ break;
+ }
+ case XElement element when element.Name == Xsl + "choose":
+ {
+ var choice = new StringBuilder();
+ var otherwise = "\"\"";
+ foreach (var branch in element.Elements())
+ {
+ var value = TextExpression(branch.Nodes(), context.WithElement(branch));
+ if (value == null)
+ return null;
+ if (branch.Name == Xsl + "when" && branch.Attribute("test") != null)
+ choice.Append(XPathCompiler.Bool(xpath.Compile(Attr(branch, "test", true)!, context.WithElement(branch)))).Append(" ? ").Append(value).Append(" : ");
+ else if (branch.Name == Xsl + "otherwise" && branch == element.Elements().Last())
+ otherwise = value;
+ else
+ return null;
+ }
+ parts.Add("(" + choice + otherwise + ")");
+ break;
+ }
+ case XElement _:
+ return null;
+ }
+ }
+ switch (parts.Count)
+ {
+ case 0:
+ return "\"\"";
+ case 1:
+ return parts[0];
+ default:
+ return "string.Concat(" + string.Join(", ", parts) + ")";
+ }
+ }
+
+ ///
+ /// The content of a variable or parameter as its value: a string when it only writes text, a result tree
+ /// fragment otherwise.
+ ///
+ private Compiled Capture(XElement element, Context context, CodeWriter w)
+ {
+ if (!IsTextOnly(element.Nodes()))
+ {
+ var fragment = NewName("f");
+ w.Line("output.BeginCapture();");
+ CompileSequence(element.Nodes(), context, w);
+ w.Line("var " + fragment + " = output.EndFragment();");
+ return new Compiled(fragment, XType.Fragment);
+ }
+
+ var expression = TextExpression(element.Nodes(), context);
+ if (expression != null)
+ return new Compiled(expression, XType.Text);
+ var text = NewName("t");
+ w.Line("output.BeginText();");
+ CompileSequence(element.Nodes(), context, w);
+ w.Line("var " + text + " = output.EndText();");
+ return new Compiled(text, XType.Text);
+ }
+
private static bool HasContent(XElement element) =>
element.Nodes().Any(n => n is XElement || (n is XText t && t.Value.Trim().Length > 0));
@@ -443,7 +750,6 @@ private static bool HasContent(XElement element) =>
private void CompileMatch(Rule rule)
{
var w = new CodeWriter(2);
- var context = new Context("node", "1", "1", "node", new Scope(null), rule.Template);
if (rule.IsRoot)
{
w.Line().Line("private static bool Match" + rule.Index + "(XObject node) => node is XDocument;");
@@ -454,19 +760,13 @@ private void CompileMatch(Rule rule)
for (var i = 0; i < rule.Steps.Count; i++)
{
var step = rule.Steps[i].Step;
- var attributeAxis = step.Axis == "attribute";
- var test = NodeTest(step.Test, rule.Template) + ".Matches(node, " + (attributeAxis ? "true" : "false") + ")";
- if (step.Predicates.Count > 0)
- {
- if (step.Predicates.Any(p => xpath.IsPositional(p, context)))
- test = "MatchesStep(node, Axis." + (attributeAxis ? "Attribute" : "Child") + ", " + NodeTest(step.Test, rule.Template) + ", "
- + string.Join(", ", step.Predicates.Select(p => xpath.Predicate(p, context))) + ")";
- else
- test += string.Concat(step.Predicates.Select(p => " && " + XPathCompiler.Bool(xpath.Compile(p, context))));
- }
+ var focus = new Focus();
+ var context = new Context("node", NodeKind.Object, null, null, focus, "node", NodeKind.Object, focus, new Scope(null), rule.Template, new Names(), new Purity());
+ var test = MatchStep(step, context);
w.Line().Open("private bool Match" + rule.Index + "_" + i + "(XObject node)");
- w.Line("if (!(" + test + "))").Line(" return false;");
+ if (test != null)
+ w.Line("if (!(" + test + "))").Line(" return false;");
if (i == 0)
{
w.Line(rule.Absolute ? "return Parent(node) is XDocument;" : "return true;");
@@ -486,6 +786,67 @@ private void CompileMatch(Rule rule)
}
w.Line().Line("private bool Match" + rule.Index + "(XObject node) => Match" + rule.Index + "_" + (rule.Steps.Count - 1) + "(node);");
members.Append(w);
+
+ var last = rule.Last!;
+ if (rule.Steps.Count == 1 && !rule.Absolute && last.Predicates.Count == 0)
+ {
+ switch (last.Test.Kind)
+ {
+ case TestKind.AnyName:
+ case TestKind.Node:
+ rule.Always = true;
+ break;
+ case TestKind.Namespace:
+ break;
+ case TestKind.ProcessingInstruction:
+ rule.Always = last.Test.Local == null;
+ break;
+ default:
+ rule.Always = last.Axis != "attribute";
+ break;
+ }
+ }
+ }
+
+ ///
+ /// Whether the node, node in the generated code, is one the step of a pattern selects from its parent: the
+ /// C# condition, or null if every node is.
+ ///
+ private string? MatchStep(Step step, Context context)
+ {
+ var attributeAxis = step.Axis == "attribute";
+ if (step.Predicates.Any(p => xpath.IsPositional(p, context)))
+ {
+ // the predicates count the node among those the step selects from its parent
+ var body = new CodeWriter(3);
+ body.Line("var parent = Parent(node);").Line("if (parent == null)").Line(" return false;");
+ xpath.EmitStep(new Cursor("parent", NodeKind.Object), step, context, body,
+ cursor => body.Line("if (ReferenceEquals(" + cursor.Var + ", node))").Line(" return true;"));
+ body.Line("return false;");
+ return HelperMethod("bool", "XObject node", body.ToString()) + "(node)";
+ }
+
+ XPathCompiler.Tested tested;
+ string? condition;
+ if (attributeAxis)
+ {
+ var attribute = context.Names.New("a_");
+ tested = xpath.Test(step.Test, true, new Cursor(attribute, NodeKind.Attribute), false, context);
+ condition = "node is XAttribute " + attribute + (tested.Condition == null ? "" : " && " + tested.Condition);
+ }
+ else
+ {
+ tested = xpath.Test(step.Test, false, new Cursor("node", NodeKind.Object), false, context);
+ condition = tested.Condition;
+ }
+ if (tested.Never)
+ return "false";
+ foreach (var predicate in step.Predicates)
+ {
+ var value = XPathCompiler.Bool(xpath.Compile(predicate, context.WithFocus(tested.Cursor.Var, tested.Cursor.Kind, null, null, new Focus())));
+ condition = condition == null ? value : condition + " && " + value;
+ }
+ return condition;
}
private static Bucket[] Buckets(Rule rule)
@@ -518,12 +879,30 @@ private void CompileDispatch(int mode)
// the rule that wins is the one of highest priority and, among those, the last in the stylesheet
var ordered = rules.Where(r => r.Mode == mode).OrderByDescending(r => r.Priority).ThenByDescending(r => r.Index).ToList();
List In(Bucket bucket) => ordered.Where(r => Buckets(r).Contains(bucket)).ToList();
+ var position = facts.ModePosition.Contains(ModeKey(mode));
+ var size = facts.ModeSize.Contains(ModeKey(mode));
var w = new CodeWriter(2);
w.Line().Open("private void Apply" + mode + "(List nodes, TemplateArguments? args)");
w.Line("for (var i = 0; i < nodes.Count; i++)").Line(" Dispatch" + mode + "(nodes[i], i + 1, nodes.Count, args);");
w.Close();
+ // the children of a node, without a list of them unless a template reads how many they are
+ w.Line().Open("private void ApplyChildren" + mode + "(XObject node, TemplateArguments? args)");
+ if (size)
+ {
+ w.Line("Apply" + mode + "(ChildNodes(node), args);");
+ }
+ else
+ {
+ if (position)
+ w.Line("var position = 0;");
+ w.Open("for (var child = FirstChild(node); child != null; child = child.NextNode)");
+ w.Line("if (IsNode(child))").Line(" Dispatch" + mode + "(child, " + (position ? "++position" : "0") + ", 0, args);");
+ w.Close();
+ }
+ w.Close();
+
w.Line().Open("private void Dispatch" + mode + "(XObject node, int position, int size, TemplateArguments? args)");
w.Open("switch (node)");
@@ -546,23 +925,23 @@ private void CompileDispatch(int mode)
w.Line("case " + i + ":").Open();
var candidates = ordered.Where(r => generic.Contains(r)
|| (named.Contains(r) && XName.Get(r.Last!.Test.Local!, Namespace(r.Last.Test.Prefix, r.Template)) == name));
- EmitCandidates(candidates, w);
+ EmitCandidates(candidates, w, true);
w.Line("break;").Close();
}
w.Line("default:").Open();
- EmitCandidates(generic, w);
+ EmitCandidates(generic, w, true);
w.Line("break;").Close();
w.Close();
}
else
{
- EmitCandidates(generic, w);
+ EmitCandidates(generic, w, true);
}
- w.Line("Apply" + mode + "(ChildNodes(node), null);").Line("return;").Close();
+ w.Line("ApplyChildren" + mode + "(node, null);").Line("return;").Close();
w.Line("case XDocument _:").Open();
EmitCandidates(In(Bucket.Root), w);
- w.Line("Apply" + mode + "(ChildNodes(node), null);").Line("return;").Close();
+ w.Line("ApplyChildren" + mode + "(node, null);").Line("return;").Close();
w.Line("case XAttribute _:").Open();
EmitCandidates(In(Bucket.Attribute), w);
@@ -584,22 +963,97 @@ private void CompileDispatch(int mode)
members.Append(w);
}
- private static void EmitCandidates(IEnumerable candidates, CodeWriter w)
+ ///
+ /// The child and the text a rule asks of an element, if its pattern starts its first predicate with
+ /// child = 'text': the rule can only match an element that has such a child.
+ ///
+ private static (XName Child, string Text)? KeyOf(Rule rule)
+ {
+ var last = rule.Last;
+ if (last == null || last.Axis != "child" || last.Predicates.Count == 0)
+ return null;
+ var condition = last.Predicates[0];
+ while (condition is BinaryExpr and && and.Op == "and")
+ condition = and.Left;
+ if (!(condition is BinaryExpr equal) || equal.Op != "=")
+ return null;
+ var path = equal.Left as PathExpr ?? equal.Right as PathExpr;
+ var literal = equal.Left as LiteralExpr ?? equal.Right as LiteralExpr;
+ if (path == null || literal == null || path.Start != null || path.Absolute || path.Steps.Count != 1)
+ return null;
+ var step = path.Steps[0];
+ if (step.Axis != "child" || step.Test.Kind != TestKind.Name || step.Predicates.Count != 0)
+ return null;
+ return (XName.Get(step.Test.Local!, Namespace(step.Test.Prefix, rule.Template)), literal.Value);
+ }
+
+ ///
+ /// The rules tried in order. Among those of an element, the many that ask for a child with a text (the
+ /// characters of MTEF, each with its code) are found by the text of that child, when the element has one.
+ ///
+ private void EmitCandidates(IEnumerable candidates, CodeWriter w, bool ofElement = false)
{
- foreach (var rule in candidates)
+ var rules = candidates.ToList();
+ var always = rules.FindIndex(r => r.Always);
+ if (always >= 0)
+ rules = rules.Take(always + 1).ToList();
+ var keyed = ofElement
+ ? rules.Where(r => KeyOf(r) != null).GroupBy(r => KeyOf(r)!.Value.Child).OrderByDescending(g => g.Count()).FirstOrDefault()
+ : null;
+ var child = keyed != null && keyed.Count() >= 8 ? keyed.Key : null;
+ bool Keyed(Rule rule) => child != null && KeyOf(rule)?.Child == child;
+
+ void Try(Rule rule)
{
+ if (rule.Always)
+ {
+ w.Line(rule.Method + "(node, position, size, args);").Line("return;");
+ return;
+ }
w.Open("if (Match" + rule.Index + "(node))");
w.Line(rule.Method + "(node, position, size, args);").Line("return;");
w.Close();
}
+
+ var key = NewName("key");
+ var count = NewName("keys");
+ if (child != null)
+ w.Line("var " + key + " = KeyValue(element, " + Name(child) + ", out var " + count + ");");
+ for (var i = 0; i < rules.Count;)
+ {
+ if (!Keyed(rules[i]))
+ {
+ Try(rules[i++]);
+ continue;
+ }
+ var run = rules.Skip(i).TakeWhile(Keyed).ToList();
+ i += run.Count;
+ w.Open("if (" + count + " == 1)");
+ w.Open("switch (" + key + ")");
+ foreach (var group in run.GroupBy(r => KeyOf(r)!.Value.Text))
+ {
+ w.Line("case " + XPathCompiler.Literal(group.Key) + ":").Open();
+ foreach (var rule in group)
+ Try(rule);
+ w.Line("break;").Close();
+ }
+ w.Close().Close();
+ // with more than one such child, any of them may have the text
+ w.Open("else if (" + count + " > 1)");
+ foreach (var rule in run)
+ Try(rule);
+ w.Close();
+ }
}
// global variables and parameters
- public Variable? Global(string name)
+ public Variable? Global(string name, Purity purity)
{
if (!globals.TryGetValue(name, out var global))
return null;
+ if (facts.Dynamic.Contains(name))
+ purity.Impure = true;
if (global.Variable != null)
return global.Variable;
if (global.Compiling)
@@ -608,49 +1062,62 @@ private static void EmitCandidates(IEnumerable candidates, CodeWriter w)
var id = "G" + NewName("_");
var element = global.Element;
- var context = new Context("n0", "p0", "s0", "n0", new Scope(null), element);
- var w = new CodeWriter(2);
+ var focus = new Focus();
+ var own = new Purity();
+ var context = TemplateContext(element, focus, own, new Scope(null));
+ var body = new CodeWriter(3);
var select = Attr(element, "select", false);
- XType type;
- w.Line();
+ Compiled value;
+ var order = NodeOrder.Ordered;
if (select != null)
{
- var value = xpath.Compile(select, context);
- type = value.Type;
- w.Open("private " + XPathCompiler.TypeName(type) + " Compute" + id + "()");
- w.Line("XObject n0 = root;").Line("const int p0 = 1, s0 = 1;");
- w.Line("return " + value.Code + ";");
+ var expr = XPathParser.Parse(select);
+ value = xpath.Compile(expr, context);
+ if (value.Type == XType.NodeSet)
+ order = xpath.OrderOf(expr, context);
}
else if (HasContent(element))
{
- type = XType.Fragment;
- w.Open("private ResultFragment Compute" + id + "()");
- w.Line("XObject n0 = root;").Line("const int p0 = 1, s0 = 1;");
- w.Line("output.BeginCapture();");
- CompileSequence(element.Nodes(), context, w);
- w.Line("return output.EndFragment();");
+ value = Capture(element, context, body);
}
else
{
- type = XType.String;
- w.Open("private string Compute" + id + "()");
- w.Line("return \"\";");
+ value = new Compiled("\"\"", XType.String);
}
+ var type = value.Type;
+ var typeName = XPathCompiler.TypeName(type);
+ body.Line("return " + value.Code + ";");
+
+ var w = new CodeWriter(2);
+ w.Line().Open("private " + typeName + " Compute" + id + "()");
+ w.Line("XObject n0 = root;").Line("const int p0 = 1, s0 = 1;");
+ w.Raw(body.ToString());
w.Close();
- var typeName = XPathCompiler.TypeName(type);
- instanceFields.Line("private bool " + id + "Done;").Line("private " + typeName + " " + id + "Value = default!;");
- w.Line().Open("private " + typeName + " " + id)
- .Open("get")
- .Open("if (!" + id + "Done)")
- .Line(id + "Value = Compute" + id + "();")
- .Line(id + "Done = true;")
- .Close()
- .Line("return " + id + "Value;")
- .Close()
- .Close();
+ // a string that does not depend on the input is computed once for all
+ var text = type == XType.String || type == XType.Text;
+ if (own.Impure || focus.NodeUsed || focus.CurrentUsed || !text)
+ Learnt.Dynamic.Add(name);
+ if (text && !facts.Dynamic.Contains(name))
+ {
+ staticFields.Line("private static string? " + id + "Value;");
+ w.Line().Line("private string " + id + " => " + id + "Value ??= Compute" + id + "();");
+ }
+ else
+ {
+ instanceFields.Line("private bool " + id + "Done;").Line("private " + typeName + " " + id + "Value = default!;");
+ w.Line().Open("private " + typeName + " " + id)
+ .Open("get")
+ .Open("if (!" + id + "Done)")
+ .Line(id + "Value = Compute" + id + "();")
+ .Line(id + "Done = true;")
+ .Close()
+ .Line("return " + id + "Value;")
+ .Close()
+ .Close();
+ }
members.Append(w);
- global.Variable = new Variable(id, type);
+ global.Variable = new Variable(id, type, order);
return global.Variable;
}
@@ -714,16 +1181,38 @@ private void CompileInstruction(XElement element, Context context, CodeWriter w)
case "apply-templates":
{
var select = Attr(element, "select", false);
- var nodes = select == null ? "ChildNodes(" + context.Node + ")" : XPathCompiler.Nodes(xpath.Compile(select, context));
+ var mode = ModeIndex((string?)element.Attribute("mode"));
+ context.Purity.Impure = true;
var args = "null";
var parameters = WithParameters(element, context, w);
if (parameters.Count > 0)
{
args = NewName("args");
w.Line("var " + args + " = new TemplateArguments(new[] { " + string.Join(", ", parameters.Keys.Select(XPathCompiler.Literal))
- + " }, new object[] { " + string.Join(", ", parameters.Values) + " });");
+ + " }, new object[] { " + string.Join(", ", parameters.Values.Select(XPathCompiler.Box)) + " });");
+ }
+ if (select == null)
+ {
+ w.Line("ApplyChildren" + mode + "(" + context.Node + ", " + args + ");");
+ break;
+ }
+
+ // the templates are given the position of each node, and a list of the nodes for their number,
+ // only if one of the mode reads them
+ var expr = XPathParser.Parse(select);
+ if (facts.ModeSize.Contains(ModeKey(mode)) || !XPathCompiler.IsStreamed(xpath.OrderOf(expr, context)))
+ {
+ w.Line("Apply" + mode + "(" + xpath.NodesCode(expr, context) + ", " + args + ");");
+ break;
+ }
+ var position = "0";
+ if (facts.ModePosition.Contains(ModeKey(mode)))
+ {
+ var p = NewName("p");
+ w.Line("var " + p + " = 0;");
+ position = "++" + p;
}
- w.Line("Apply" + ModeIndex((string?)element.Attribute("mode")) + "(" + nodes + ", " + args + ");");
+ xpath.EmitNodes(expr, context, w, cursor => w.Line("Dispatch" + mode + "(" + cursor.Var + ", " + position + ", 0, " + args + ");"));
break;
}
case "call-template":
@@ -732,8 +1221,22 @@ private void CompileInstruction(XElement element, Context context, CodeWriter w)
if (!namedTemplates.TryGetValue(name, out var template))
throw Error(element, "There is no template named " + name + ".");
var parameters = WithParameters(element, context, w);
- var args = string.Concat(template.Parameters.Select(p => ", " + (parameters.TryGetValue(p, out var value) ? value : "null")));
- w.Line(template.Method + "(" + context.Node + ", " + context.Position + ", " + context.Size + args + ");");
+ var args = new List();
+ foreach (var parameter in template.Parameters)
+ {
+ var key = Facts.Parameter(name, parameter);
+ if (parameters.TryGetValue(parameter, out var value))
+ {
+ Learnt.Give(key, value.Type);
+ args.Add(Convert(value, ParameterType(template, parameter)));
+ }
+ else
+ {
+ Learnt.Omitted.Add(key);
+ args.Add(ParameterType(template, parameter) == XType.Node ? "Omitted" : "null");
+ }
+ }
+ w.Line(Call(template, context, args));
break;
}
case "value-of":
@@ -781,15 +1284,39 @@ private void CompileInstruction(XElement element, Context context, CodeWriter w)
{
if (element.Elements(Xsl + "sort").Any())
throw Error(element, "xsl:sort is not supported.");
- var set = NewName("set");
- var i = NewName("i");
+ var expr = XPathParser.Parse(Attr(element, "select", true)!);
+ var kind = xpath.KindOf(expr, context);
var n = NewName("n");
var p = NewName("p");
var s = NewName("s");
- w.Line("var " + set + " = " + XPathCompiler.Nodes(xpath.Compile(Attr(element, "select", true)!, context)) + ";");
+
+ // the content first: whether it reads the position and the number of the nodes tells the loop it needs
+ var focus = new Focus();
+ var body = new CodeWriter();
+ CompileSequence(element.Nodes(), new Context(n, kind, p, s, focus, n, kind, focus, context.Scope, element, context.Names, context.Purity), body);
+ if (!focus.SizeUsed && XPathCompiler.IsStreamed(xpath.OrderOf(expr, context)))
+ {
+ if (focus.PositionUsed)
+ w.Line("var " + p + " = 0;");
+ xpath.EmitNodes(expr, context, w, cursor =>
+ {
+ w.Line("var " + n + " = " + cursor.Var + ";");
+ if (focus.PositionUsed)
+ w.Line(p + "++;");
+ w.Indented(body);
+ });
+ break;
+ }
+ var set = NewName("set");
+ var i = NewName("i");
+ w.Line("var " + set + " = " + xpath.NodesCode(expr, context) + ";");
w.Open("for (var " + i + " = 0; " + i + " < " + set + ".Count; " + i + "++)");
- w.Line("var " + n + " = " + set + "[" + i + "];").Line("var " + p + " = " + i + " + 1;").Line("var " + s + " = " + set + ".Count;");
- CompileSequence(element.Nodes(), new Context(n, p, s, n, context.Scope, element), w);
+ w.Line("var " + n + " = " + (kind == NodeKind.Object ? "" : "(" + XPathCompiler.TypeName(kind) + ")") + set + "[" + i + "];");
+ if (focus.PositionUsed)
+ w.Line("var " + p + " = " + i + " + 1;");
+ if (focus.SizeUsed)
+ w.Line("var " + s + " = " + set + ".Count;");
+ w.Indented(body);
w.Close();
break;
}
@@ -798,26 +1325,25 @@ private void CompileInstruction(XElement element, Context context, CodeWriter w)
var name = Attr(element, "name", true)!;
var id = NewName("v");
var select = Attr(element, "select", false);
- XType type;
+ Compiled value;
+ var order = NodeOrder.Ordered;
if (select != null)
{
- var value = xpath.Compile(select, context);
- type = value.Type;
- w.Line("var " + id + " = " + value.Code + ";");
+ var expr = XPathParser.Parse(select);
+ value = xpath.Compile(expr, context).Bound();
+ if (value.Type == XType.NodeSet)
+ order = xpath.OrderOf(expr, context);
}
else if (HasContent(element))
{
- type = XType.Fragment;
- w.Line("output.BeginCapture();");
- CompileSequence(element.Nodes(), context, w);
- w.Line("var " + id + " = output.EndFragment();");
+ value = Capture(element, context, w);
}
else
{
- type = XType.String;
- w.Line("var " + id + " = \"\";");
+ value = new Compiled("\"\"", XType.String);
}
- context.Scope.Add(name, new Variable(id, type));
+ w.Line("var " + id + " = " + value.Code + ";");
+ context.Scope.Add(name, new Variable(id, value.Type, order));
break;
}
case "attribute":
@@ -837,9 +1363,24 @@ private void CompileInstruction(XElement element, Context context, CodeWriter w)
}
case "copy-of":
{
- var value = xpath.Compile(Attr(element, "select", true)!, context);
- var code = value.Type == XType.Number || value.Type == XType.Boolean ? XPathCompiler.Str(value) : value.Code;
- w.Line("output.CopyOf(" + code + ");");
+ var expr = XPathParser.Parse(Attr(element, "select", true)!);
+ var value = xpath.Compile(expr, context);
+ switch (value.Type)
+ {
+ case XType.NodeSet when XPathCompiler.IsStreamed(xpath.OrderOf(expr, context)):
+ xpath.EmitNodes(expr, context, w, cursor => w.Line("output.Copy(" + cursor.Var + ");"));
+ break;
+ case XType.Text:
+ w.Line("output.CopyOfText(" + value.Code + ");");
+ break;
+ case XType.Number:
+ case XType.Boolean:
+ w.Line("output.CopyOf(" + XPathCompiler.Str(value) + ");");
+ break;
+ default:
+ w.Line("output.CopyOf(" + value.Code + ");");
+ break;
+ }
break;
}
case "copy":
@@ -876,10 +1417,10 @@ private void CompileInstruction(XElement element, Context context, CodeWriter w)
private static bool DisablesEscaping(XElement element) => (string?)element.Attribute("disable-output-escaping") == "yes";
- /// The parameters of xsl:apply-templates or xsl:call-template, by name, as C# expressions of type object.
- private Dictionary WithParameters(XElement element, Context context, CodeWriter w)
+ /// The parameters of xsl:apply-templates or xsl:call-template, by name.
+ private Dictionary WithParameters(XElement element, Context context, CodeWriter w)
{
- var parameters = new Dictionary();
+ var parameters = new Dictionary();
foreach (var child in element.Elements())
{
if (child.Name == Xsl + "sort")
@@ -891,19 +1432,16 @@ private Dictionary WithParameters(XElement element, Context cont
var paramContext = context.WithElement(child);
if (select != null)
{
- parameters[name] = XPathCompiler.Box(xpath.Compile(select, paramContext));
+ // compiled here, before the statements of the parameters after it
+ parameters[name] = xpath.Compile(select, paramContext).Bound();
}
else if (HasContent(child))
{
- var id = NewName("w");
- w.Line("output.BeginCapture();");
- CompileSequence(child.Nodes(), paramContext, w);
- w.Line("var " + id + " = output.EndFragment();");
- parameters[name] = id;
+ parameters[name] = Capture(child, paramContext, w);
}
else
{
- parameters[name] = "(object)\"\"";
+ parameters[name] = new Compiled("\"\"", XType.String);
}
}
return parameters;
@@ -924,6 +1462,8 @@ private string Content(XElement element, Context context, CodeWriter w)
case 1 when nodes[0] is XElement e && e.Name == Xsl + "value-of":
return XPathCompiler.Str(xpath.Compile(Attr(e, "select", true)!, context.WithElement(e)));
}
+ if (IsTextOnly(element.Nodes()))
+ return Capture(element, context, w).Code;
var id = NewName("text");
w.Line("output.BeginCapture();");
CompileSequence(element.Nodes(), context, w);
diff --git a/src/SharpEquations.XsltGenerator/XPathCompiler.cs b/src/SharpEquations.XsltGenerator/XPathCompiler.cs
index 847134b..731e0ba 100644
--- a/src/SharpEquations.XsltGenerator/XPathCompiler.cs
+++ b/src/SharpEquations.XsltGenerator/XPathCompiler.cs
@@ -6,44 +6,143 @@ namespace SharpEquations.XsltGenerator
using System.Text;
using System.Xml.Linq;
- /// The static type of an XPath value in the generated code.
+ ///
+ /// The static type of an XPath value in the generated code. Text is a result tree fragment that holds only
+ /// text, kept as a string; None is the type of a parameter no call has given a value yet, while the types
+ /// of the parameters are inferred.
+ ///
internal enum XType
{
+ None,
String,
Number,
Boolean,
+
+ /// A node-set of at most one node, kept as the node or null.
+ Node,
NodeSet,
+ Text,
Fragment,
Any,
}
+ /// What is known of the order of the nodes a path yields as the generated loops reach them.
+ internal enum NodeOrder
+ {
+ /// At most one node.
+ Single,
+
+ /// In document order, without duplicates, none an ancestor of another.
+ Disjoint,
+
+ /// In document order, without duplicates.
+ Ordered,
+
+ /// In reverse document order, without duplicates.
+ Reversed,
+
+ Unordered,
+ }
+
+ /// The C# type of the variable that holds a node.
+ internal enum NodeKind
+ {
+ Object,
+ Node,
+ Element,
+ Attribute,
+ Text,
+ }
+
+ /// A node-set the generated code holds as a list, in a path that starts from it.
+ internal sealed class ListExpr : Expr
+ {
+ public ListExpr(string code) => Code = code;
+
+ public string Code { get; }
+ }
+
+ /// A node the generated code holds in a variable.
+ internal readonly struct Cursor
+ {
+ public Cursor(string variable, NodeKind kind)
+ {
+ Var = variable;
+ Kind = kind;
+ }
+
+ public string Var { get; }
+
+ public NodeKind Kind { get; }
+ }
+
+ ///
+ /// An expression compiled to C#. A node-set is compiled when it is used, to the code its use needs: a test for a
+ /// node, the string value of the first one, a count, or the list of the nodes ().
+ ///
internal sealed class Compiled
{
+ private readonly XPathCompiler? owner;
+ private string? code;
+
public Compiled(string code, XType type)
{
- Code = code;
+ this.code = code;
Type = type;
}
- public string Code { get; }
+ public Compiled(XPathCompiler owner, Expr expr, Context context)
+ {
+ this.owner = owner;
+ Expr = expr;
+ Context = context;
+ Type = XType.NodeSet;
+ }
+
+ public string Code => code ??= owner!.NodesCode(Expr!, Context!);
public XType Type { get; }
+
+ public Expr? Expr { get; }
+
+ public Context? Context { get; }
+
+ public XPathCompiler Owner => owner!;
+
+ public bool IsNodeSet => Expr != null;
+
+ ///
+ /// The value as a variable or a parameter holds it: a node-set compiled here, to the node when it has at most
+ /// one, to the list of its nodes otherwise.
+ ///
+ public Compiled Bound()
+ {
+ if (!IsNodeSet)
+ return this;
+ return owner!.OrderOf(Expr!, Context!) == NodeOrder.Single
+ ? new Compiled(owner.FirstNodeCode(Expr!, Context!), XType.Node)
+ : new Compiled(Code, XType.NodeSet);
+ }
}
internal sealed class Variable
{
- public Variable(string id, XType type)
+ public Variable(string id, XType type, NodeOrder order = NodeOrder.Ordered)
{
Id = id;
Type = type;
+ Order = order;
}
public string Id { get; }
public XType Type { get; }
+
+ /// The order of the nodes of a node-set.
+ public NodeOrder Order { get; }
}
- internal sealed class Scope
+ internal class Scope
{
private readonly Dictionary variables = new Dictionary();
private readonly Scope? parent;
@@ -52,46 +151,146 @@ internal sealed class Scope
public void Add(string name, Variable variable) => variables[name] = variable;
- public Variable? Find(string name) =>
+ public virtual Variable? Find(string name) =>
variables.TryGetValue(name, out var variable) ? variable : parent?.Find(name);
}
+ /// The scope of a method generated for an expression: the variables it reads are its parameters.
+ internal sealed class CapturingScope : Scope
+ {
+ private readonly Scope outer;
+
+ public CapturingScope(Scope outer)
+ : base(null) => this.outer = outer;
+
+ public List Captured { get; } = new List();
+
+ public override Variable? Find(string name)
+ {
+ var variable = outer.Find(name);
+ if (variable != null && !Captured.Contains(variable))
+ Captured.Add(variable);
+ return variable;
+ }
+ }
+
+ /// What the code compiled in a context read of it.
+ internal sealed class Focus
+ {
+ public bool NodeUsed { get; set; }
+
+ public bool PositionUsed { get; set; }
+
+ public bool SizeUsed { get; set; }
+
+ public bool CurrentUsed { get; set; }
+ }
+
+ /// Whether what is compiled depends on more than its parameters: the input, or a variable that does.
+ internal sealed class Purity
+ {
+ public bool Impure { get; set; }
+ }
+
+ internal sealed class Names
+ {
+ private int counter;
+
+ public string New(string prefix) => prefix + (++counter).ToString(CultureInfo.InvariantCulture);
+ }
+
///
/// Where an expression is evaluated: the C# names of the context node, position and size, and of the current
/// node of XSLT; the variables in scope; the element whose namespace declarations resolve its prefixes.
///
internal sealed class Context
{
- public Context(string node, string position, string size, string current, Scope scope, XElement element)
+ private readonly string node;
+ private readonly string? position;
+ private readonly string? size;
+ private readonly string current;
+ private readonly Focus focus;
+ private readonly Focus currentFocus;
+
+ public Context(string node, NodeKind nodeKind, string? position, string? size, Focus focus, string current, NodeKind currentKind, Focus currentFocus,
+ Scope scope, XElement element, Names names, Purity purity)
{
- Node = node;
- Position = position;
- Size = size;
- Current = current;
+ this.node = node;
+ NodeKind = nodeKind;
+ this.position = position;
+ this.size = size;
+ this.focus = focus;
+ this.current = current;
+ CurrentKind = currentKind;
+ this.currentFocus = currentFocus;
Scope = scope;
Element = element;
+ Names = names;
+ Purity = purity;
+ }
+
+ public string Node
+ {
+ get
+ {
+ focus.NodeUsed = true;
+ return node;
+ }
}
- public string Node { get; }
+ public NodeKind NodeKind { get; }
- public string Position { get; }
+ public string Position
+ {
+ get
+ {
+ focus.PositionUsed = true;
+ return position ?? throw new XsltException("The position of the context is not known here.");
+ }
+ }
+
+ public string Size
+ {
+ get
+ {
+ focus.SizeUsed = true;
+ return size ?? throw new XsltException("The size of the context is not known here.");
+ }
+ }
- public string Size { get; }
+ public string Current
+ {
+ get
+ {
+ currentFocus.CurrentUsed = true;
+ return current;
+ }
+ }
- public string Current { get; }
+ public NodeKind CurrentKind { get; }
public Scope Scope { get; }
public XElement Element { get; }
- public Context WithFocus(string node, string position, string size) => new Context(node, position, size, Current, Scope, Element);
+ public Names Names { get; }
- public Context WithScope(Scope scope) => new Context(Node, Position, Size, Current, scope, Element);
+ public Purity Purity { get; }
- public Context WithElement(XElement element) => new Context(Node, Position, Size, Current, Scope, element);
+ public Context WithFocus(string node, NodeKind kind, string? position, string? size, Focus focus) =>
+ new Context(node, kind, position, size, focus, current, CurrentKind, currentFocus, Scope, Element, Names, Purity);
+
+ public Context WithScope(Scope scope) =>
+ new Context(node, NodeKind, position, size, focus, current, CurrentKind, currentFocus, scope, Element, Names, Purity);
+
+ public Context WithElement(XElement element) =>
+ new Context(node, NodeKind, position, size, focus, current, CurrentKind, currentFocus, Scope, element, Names, Purity);
}
- /// XPath 1.0 expressions compiled to C# expressions over XPathFunctions.
+ ///
+ /// XPath 1.0 expressions compiled to C#: the values as expressions over XPathFunctions, the location paths
+ /// as loops over the nodes, in the method that uses them or in a method of their own.
+ ///
internal sealed class XPathCompiler
{
private readonly StylesheetCompiler stylesheet;
@@ -110,27 +309,89 @@ public Compiled Compile(Expr expr, Context context)
return new Compiled(Number(number.Value), XType.Number);
case VariableExpr variable:
{
- var found = context.Scope.Find(variable.Name) ?? stylesheet.Global(variable.Name)
- ?? throw new XsltException("The variable $" + variable.Name + " is not declared.");
- return new Compiled(found.Id, found.Type);
+ var found = Find(variable, context);
+ return found.Type == XType.NodeSet || found.Type == XType.Node ? new Compiled(this, expr, context) : new Compiled(found.Id, found.Type);
}
case NegateExpr negate:
return new Compiled("(-" + Num(Compile(negate.Operand, context)) + ")", XType.Number);
+ case BinaryExpr union when union.Op == "|":
+ return new Compiled(this, expr, context);
case BinaryExpr binary:
return CompileBinary(binary, context);
+ case FunctionExpr function when function.Name == "current":
+ if (function.Arguments.Count != 0)
+ throw new XsltException("Wrong number of arguments to current().");
+ return new Compiled(this, expr, context);
case FunctionExpr function:
return CompileFunction(function, context);
- case FilterExpr filter:
+ case FilterExpr _:
+ case PathExpr _:
+ return new Compiled(this, expr, context);
+ default:
+ throw new XsltException("Unknown expression.");
+ }
+ }
+
+ private Variable Find(VariableExpr variable, Context context) =>
+ context.Scope.Find(variable.Name) ?? stylesheet.Global(variable.Name, context.Purity)
+ ?? throw new XsltException("The variable $" + variable.Name + " is not declared.");
+
+ /// The type of the value of an expression, without compiling it.
+ public XType TypeOf(Expr expr, Context context)
+ {
+ switch (expr)
+ {
+ case LiteralExpr _:
+ return XType.String;
+ case NumberExpr _:
+ case NegateExpr _:
+ return XType.Number;
+ case VariableExpr variable:
{
- var code = Nodes(Compile(filter.Primary, context));
- foreach (var predicate in filter.Predicates)
- code = "Filter(" + code + ", " + Predicate(predicate, context) + ")";
- return new Compiled(code, XType.NodeSet);
+ var type = Find(variable, context).Type;
+ return type == XType.Node ? XType.NodeSet : type;
}
- case PathExpr path:
- return CompilePath(path, context);
+ case BinaryExpr binary:
+ switch (binary.Op)
+ {
+ case "|":
+ return XType.NodeSet;
+ case "+":
+ case "-":
+ case "*":
+ case "div":
+ case "mod":
+ return XType.Number;
+ default:
+ return XType.Boolean;
+ }
+ case FunctionExpr function:
+ switch (function.Name)
+ {
+ case "last":
+ case "position":
+ case "count":
+ case "sum":
+ case "string-length":
+ case "number":
+ case "floor":
+ case "ceiling":
+ case "round":
+ return XType.Number;
+ case "starts-with":
+ case "contains":
+ case "boolean":
+ case "not":
+ case "true":
+ case "false":
+ return XType.Boolean;
+ case "current":
+ return XType.NodeSet;
+ default:
+ return XType.String;
+ }
default:
- throw new XsltException("Unknown expression.");
+ return XType.NodeSet;
}
}
@@ -152,19 +413,21 @@ private Compiled CompileBinary(BinaryExpr binary, Context context)
return new Compiled("(" + Num(left) + " / " + Num(right) + ")", XType.Number);
case "mod":
return new Compiled("(" + Num(left) + " % " + Num(right) + ")", XType.Number);
- case "|":
- return new Compiled("Union(" + Nodes(left) + ", " + Nodes(right) + ")", XType.NodeSet);
default:
return CompileComparison(binary.Op, left, right);
}
}
- private static bool IsNodes(XType type) => type == XType.NodeSet || type == XType.Fragment || type == XType.Any;
+ private static bool IsUnknown(XType type) => type == XType.Any || type == XType.None;
- private static Compiled CompileComparison(string op, Compiled left, Compiled right)
+ private Compiled CompileComparison(string op, Compiled left, Compiled right)
{
- if (IsNodes(left.Type) || IsNodes(right.Type))
- return new Compiled("Compare(" + Box(left) + ", " + Box(right) + ", Comparison." + ComparisonName(op) + ")", XType.Boolean);
+ if (left.IsNodeSet && !right.IsNodeSet && !IsUnknown(right.Type))
+ return new Compiled(CompareNodes(left, op, right, false), XType.Boolean);
+ if (right.IsNodeSet && !left.IsNodeSet && !IsUnknown(left.Type))
+ return new Compiled(CompareNodes(right, op, left, true), XType.Boolean);
+ if (left.IsNodeSet || right.IsNodeSet || IsUnknown(left.Type) || IsUnknown(right.Type))
+ return new Compiled("Compare(" + Operand(left) + ", " + Operand(right) + ", Comparison." + ComparisonName(op) + ")", XType.Boolean);
var csharp = op == "=" ? "==" : op;
string code;
if (op != "=" && op != "!=")
@@ -178,6 +441,40 @@ private static Compiled CompileComparison(string op, Compiled left, Compiled rig
return new Compiled(code, XType.Boolean);
}
+ ///
+ /// A node-set compared with a value that is not one: whether a node compares so, by its string value (with a
+ /// boolean, the node-set as a boolean).
+ ///
+ private string CompareNodes(Compiled nodes, string op, Compiled other, bool swapped)
+ {
+ var csharp = op == "=" ? "==" : op;
+ var equality = op == "=" || op == "!=";
+ if (other.Type == XType.Boolean)
+ {
+ var exists = Bool(nodes);
+ if (equality)
+ return "(" + exists + " " + csharp + " " + other.Code + ")";
+ var number = "(" + exists + " ? 1D : 0D)";
+ var otherNumber = "(" + other.Code + " ? 1D : 0D)";
+ return "(" + (swapped ? otherNumber + " " + csharp + " " + number : number + " " + csharp + " " + otherNumber) + ")";
+ }
+
+ var numeric = other.Type == XType.Number || !equality;
+ var argument = numeric ? Num(other) : Str(other);
+ return Helper("bool", nodes.Context!, (context, w) =>
+ {
+ EmitNodes(nodes.Expr!, context, w, cursor =>
+ {
+ var value = numeric ? "Num(" + StringValue(cursor) + ")" : StringValue(cursor);
+ w.Line("if (" + (swapped ? "o " + csharp + " " + value : value + " " + csharp + " o") + ")").Line(" return true;");
+ });
+ w.Line("return false;");
+ }, numeric ? "double" : "string", argument);
+ }
+
+ // a string or a number is compared with a value of unknown type as it is
+ private static string Operand(Compiled value) => value.Type == XType.String || value.Type == XType.Number ? value.Code : Box(value);
+
private static string ComparisonName(string op)
{
switch (op)
@@ -206,7 +503,7 @@ void Arity(int min, int max)
throw new XsltException("Wrong number of arguments to " + function.Name + "().");
}
- string StringArgument(int i) => args.Count > i ? Str(args[i]) : "StringValue(" + context.Node + ")";
+ string StringArgument(int i) => args.Count > i ? Str(args[i]) : StringValue(new Cursor(context.Node, context.NodeKind));
switch (function.Name)
{
@@ -218,7 +515,7 @@ void Arity(int min, int max)
return new Compiled("((double)" + context.Position + ")", XType.Number);
case "count":
Arity(1, 1);
- return new Compiled("Count(" + Nodes(args[0]) + ")", XType.Number);
+ return new Compiled(args[0].IsNodeSet ? "((double)" + CountCode(args[0].Expr!, args[0].Context!) + ")" : "Count(" + Nodes(args[0]) + ")", XType.Number);
case "sum":
Arity(1, 1);
return new Compiled("Sum(" + Nodes(args[0]) + ")", XType.Number);
@@ -228,7 +525,14 @@ void Arity(int min, int max)
{
Arity(0, 1);
var method = function.Name == "local-name" ? "LocalName" : function.Name == "name" ? "Name" : "NamespaceUri";
- return new Compiled(method + "(" + (args.Count == 0 ? context.Node : Nodes(args[0])) + ")", XType.String);
+ string node;
+ if (args.Count == 0)
+ node = context.Node;
+ else if (args[0].IsNodeSet)
+ node = FirstNodeCode(args[0].Expr!, args[0].Context!);
+ else
+ node = Nodes(args[0]);
+ return new Compiled(method + "(" + node + ")", XType.String);
}
case "string":
Arity(0, 1);
@@ -260,7 +564,9 @@ void Arity(int min, int max)
return new Compiled("NormalizeSpace(" + StringArgument(0) + ")", XType.String);
case "translate":
Arity(3, 3);
- return new Compiled("Translate(" + Str(args[0]) + ", " + Str(args[1]) + ", " + Str(args[2]) + ")", XType.String);
+ if (function.Arguments[1] is LiteralExpr from && function.Arguments[2] is LiteralExpr to)
+ return new Compiled(stylesheet.StaticField("TranslateMap", "new TranslateMap(" + Literal(from.Value) + ", " + Literal(to.Value) + ")") + ".Apply(" + Str(args[0]) + ")", XType.String);
+ return new Compiled("Translate(" + Str(args[0]) + ", " + Str(args[1]) + ", " + Str(args[2]) + ", ref " + stylesheet.CacheField("TranslateMap?") + ")", XType.String);
case "boolean":
Arity(1, 1);
return new Compiled(Bool(args[0]), XType.Boolean);
@@ -275,7 +581,7 @@ void Arity(int min, int max)
return new Compiled("false", XType.Boolean);
case "number":
Arity(0, 1);
- return new Compiled(args.Count == 0 ? "Num(StringValue(" + context.Node + "))" : Num(args[0]), XType.Number);
+ return new Compiled(args.Count == 0 ? "Num(" + StringArgument(0) + ")" : Num(args[0]), XType.Number);
case "floor":
Arity(1, 1);
return new Compiled("System.Math.Floor(" + Num(args[0]) + ")", XType.Number);
@@ -285,105 +591,801 @@ void Arity(int min, int max)
case "round":
Arity(1, 1);
return new Compiled("Round(" + Num(args[0]) + ")", XType.Number);
- case "current":
- Arity(0, 0);
- return new Compiled("Single(" + context.Current + ")", XType.NodeSet);
default:
throw new XsltException("The function " + function.Name + "() is not supported.");
}
}
- private Compiled CompilePath(PathExpr path, Context context)
+ // node-sets: what the use of one needs
+
+ /// The list of the nodes, in document order.
+ public string NodesCode(Expr expr, Context context)
{
- string code;
- if (path.Start != null)
- code = Nodes(Compile(path.Start, context));
- else if (path.Absolute)
- code = "Root(" + context.Node + ")";
- else
- code = "Single(" + context.Node + ")";
+ switch (expr)
+ {
+ case VariableExpr variable:
+ {
+ var found = Find(variable, context);
+ return found.Type == XType.NodeSet ? found.Id : Nodes(new Compiled(found.Id, found.Type));
+ }
+ case ListExpr list:
+ return list.Code;
+ case BinaryExpr union when union.Op == "|":
+ return "Union(" + NodesCode(union.Left, context) + ", " + NodesCode(union.Right, context) + ")";
+ case FilterExpr _:
+ case PathExpr _:
+ case FunctionExpr _ when IsCurrent(expr):
+ {
+ var order = OrderOf(expr, context);
+ return Helper("List", context, (inner, w) =>
+ {
+ // the nodes reached from one node are in order when those from more are not
+ if (order == NodeOrder.Unordered && expr is PathExpr path && path.Start != null
+ && IsStreamed(Steps(path, inner).Aggregate(NodeOrder.Single, (o, step) => After(o, step, inner))))
+ {
+ w.Line("var start = " + NodesCode(path.Start, inner) + ";");
+ w.Line("var list = new List();");
+ EmitNodes(new PathExpr(new ListExpr("start"), false, path.Steps), inner, w, cursor => w.Line("list.Add(" + cursor.Var + ");"));
+ w.Line("if (start.Count > 1)").Line(" SortUnique(list);");
+ w.Line("return list;");
+ return;
+ }
+ if (order == NodeOrder.Single)
+ {
+ EmitNodes(expr, inner, w, cursor => w.Line("return Single(" + cursor.Var + ");"));
+ w.Line("return Empty;");
+ return;
+ }
+ w.Line("var list = new List();");
+ EmitNodes(expr, inner, w, cursor => w.Line("list.Add(" + cursor.Var + ");"));
+ switch (order)
+ {
+ case NodeOrder.Reversed:
+ w.Line("list.Reverse();");
+ break;
+ case NodeOrder.Unordered:
+ w.Line("SortUnique(list);");
+ break;
+ }
+ w.Line("return list;");
+ });
+ }
+ default:
+ return Nodes(Compile(expr, context));
+ }
+ }
+
+ private static bool IsCurrent(Expr expr) => expr is FunctionExpr function && function.Name == "current";
+
+ public static bool IsStreamed(NodeOrder order) => order == NodeOrder.Single || order == NodeOrder.Disjoint || order == NodeOrder.Ordered;
+
+ /// Whether the node-set has a node.
+ private string ExistsCode(Expr expr, Context context)
+ {
+ switch (expr)
+ {
+ case VariableExpr variable when Find(variable, context).Type == XType.Node:
+ return "(" + Find(variable, context).Id + " != null)";
+ case VariableExpr _:
+ return "(" + NodesCode(expr, context) + ".Count > 0)";
+ case BinaryExpr union when union.Op == "|":
+ return "(" + ExistsCode(union.Left, context) + " || " + ExistsCode(union.Right, context) + ")";
+ default:
+ return Helper("bool", context, (inner, w) =>
+ {
+ EmitNodes(expr, inner, w, cursor => w.Line("return true;"));
+ w.Line("return false;");
+ });
+ }
+ }
+
+ /// The first node in document order, or null.
+ public string FirstNodeCode(Expr expr, Context context)
+ {
+ var order = OrderOf(expr, context);
+ if (IsNode(expr, context, out var node))
+ return node;
+ if (expr is PathExpr self && self.Start == null && !self.Absolute && Steps(self, context).Count == 0)
+ return context.Node;
+ if (expr is VariableExpr || expr is BinaryExpr || order == NodeOrder.Unordered)
+ return "First(" + NodesCode(expr, context) + ")";
+ return Helper("XObject?", context, (inner, w) =>
+ {
+ if (order == NodeOrder.Reversed)
+ {
+ w.Line("XObject? first = null;");
+ EmitNodes(expr, inner, w, cursor => w.Line("first = " + cursor.Var + ";"));
+ w.Line("return first;");
+ return;
+ }
+ EmitNodes(expr, inner, w, cursor => w.Line("return " + cursor.Var + ";"));
+ w.Line("return null;");
+ });
+ }
+
+ /// The string value of the first node in document order, or the empty string.
+ private string FirstStringCode(Expr expr, Context context)
+ {
+ var order = OrderOf(expr, context);
+ if (IsNode(expr, context, out var node))
+ return "StringValue(" + node + ")";
+ if (expr is VariableExpr || expr is BinaryExpr || order == NodeOrder.Unordered)
+ return "Str(" + NodesCode(expr, context) + ")";
+ return Helper("string", context, (inner, w) =>
+ {
+ if (order == NodeOrder.Reversed)
+ {
+ w.Line("XObject? first = null;");
+ EmitNodes(expr, inner, w, cursor => w.Line("first = " + cursor.Var + ";"));
+ w.Line("return first == null ? \"\" : StringValue(first);");
+ return;
+ }
+ EmitNodes(expr, inner, w, cursor => w.Line("return " + StringValue(cursor) + ";"));
+ w.Line("return \"\";");
+ });
+ }
+
+ // a variable that holds a node-set as its node
+ private bool IsNode(Expr expr, Context context, out string node)
+ {
+ node = "";
+ if (!(expr is VariableExpr variable))
+ return false;
+ var found = Find(variable, context);
+ node = found.Id;
+ return found.Type == XType.Node;
+ }
+
+ private string CountCode(Expr expr, Context context)
+ {
+ if (IsNode(expr, context, out var node))
+ return "(" + node + " == null ? 0 : 1)";
+ if (expr is VariableExpr || expr is BinaryExpr || OrderOf(expr, context) == NodeOrder.Unordered)
+ return NodesCode(expr, context) + ".Count";
+ return Helper("int", context, (inner, w) =>
+ {
+ w.Line("var count = 0;");
+ EmitNodes(expr, inner, w, cursor => w.Line("count++;"));
+ w.Line("return count;");
+ });
+ }
+
+ ///
+ /// A method for an expression, whose parameters are what the expression reads of its context (the node, the
+ /// position, the size, the current node) and the variables it reads; the code that calls it.
+ ///
+ private string Helper(string returnType, Context context, System.Action emit, string? extraType = null, string? extraArgument = null)
+ {
+ var scope = new CapturingScope(context.Scope);
+ var focus = new Focus();
+ var inner = new Context("n", context.NodeKind, "p", "s", focus, "c", context.CurrentKind, focus, scope, context.Element, new Names(), context.Purity);
+ var body = new CodeWriter(3);
+ emit(inner, body);
+
+ var parameters = new List();
+ var arguments = new List();
+ if (focus.NodeUsed)
+ {
+ parameters.Add(TypeName(context.NodeKind) + " n");
+ arguments.Add(context.Node);
+ }
+ if (focus.PositionUsed)
+ {
+ parameters.Add("int p");
+ arguments.Add(context.Position);
+ }
+ if (focus.SizeUsed)
+ {
+ parameters.Add("int s");
+ arguments.Add(context.Size);
+ }
+ if (focus.CurrentUsed)
+ {
+ parameters.Add(TypeName(context.CurrentKind) + " c");
+ arguments.Add(context.Current);
+ }
+ foreach (var variable in scope.Captured)
+ {
+ parameters.Add(TypeName(variable.Type) + " " + variable.Id);
+ arguments.Add(variable.Id);
+ }
+ if (extraType != null)
+ {
+ parameters.Add(extraType + " o");
+ arguments.Add(extraArgument!);
+ }
+ return stylesheet.HelperMethod(returnType, string.Join(", ", parameters), body.ToString()) + "(" + string.Join(", ", arguments) + ")";
+ }
+
+ // node-sets: the loops
+
+ /// The order in which reaches the nodes of the expression.
+ public NodeOrder OrderOf(Expr expr, Context context)
+ {
+ switch (expr)
+ {
+ case VariableExpr variable:
+ {
+ var found = Find(variable, context);
+ if (found.Type == XType.Node)
+ return NodeOrder.Single;
+ return found.Type == XType.NodeSet ? found.Order : NodeOrder.Ordered;
+ }
+ case FunctionExpr _ when IsCurrent(expr):
+ return NodeOrder.Single;
+ case FilterExpr filter:
+ return OrderOf(filter.Primary, context) == NodeOrder.Single || filter.Predicates.Any(p => SelectsOne(p, context)) ? NodeOrder.Single : NodeOrder.Ordered;
+ case PathExpr path:
+ {
+ var order = path.Start == null ? NodeOrder.Single : OrderOf(path.Start, context);
+ foreach (var step in Steps(path, context))
+ order = After(order, step, context);
+ return order;
+ }
+ default:
+ return NodeOrder.Ordered;
+ }
+ }
- var steps = path.Steps;
- for (var i = 0; i < steps.Count; i++)
+ private NodeOrder After(NodeOrder order, Step step, Context context)
+ {
+ if (step.Axis == "self")
+ return order;
+ var one = step.Predicates.Any(p => SelectsOne(p, context));
+ switch (order)
+ {
+ case NodeOrder.Single:
+ switch (step.Axis)
+ {
+ case "parent":
+ return NodeOrder.Single;
+ case "attribute" when step.Test.Kind == TestKind.Name:
+ return NodeOrder.Single;
+ case "child":
+ case "attribute":
+ case "following-sibling":
+ return one ? NodeOrder.Single : NodeOrder.Disjoint;
+ case "descendant":
+ case "descendant-or-self":
+ return one ? NodeOrder.Single : NodeOrder.Ordered;
+ case "preceding-sibling" when !IsReverse(step, context):
+ return NodeOrder.Disjoint;
+ default:
+ return one ? NodeOrder.Single : NodeOrder.Reversed;
+ }
+ case NodeOrder.Disjoint:
+ switch (step.Axis)
+ {
+ case "child":
+ case "attribute":
+ return NodeOrder.Disjoint;
+ case "descendant":
+ case "descendant-or-self":
+ return NodeOrder.Ordered;
+ default:
+ return NodeOrder.Unordered;
+ }
+ case NodeOrder.Ordered when step.Axis == "attribute":
+ return NodeOrder.Ordered;
+ default:
+ return NodeOrder.Unordered;
+ }
+ }
+
+ // a predicate that keeps at most one node of those of a context node: a position
+ private bool SelectsOne(Expr predicate, Context context) => IsLast(predicate) || TypeOf(predicate, context) == XType.Number;
+
+ private static bool IsLast(Expr predicate) => predicate is FunctionExpr function && function.Name == "last" && function.Arguments.Count == 0;
+
+ // the nodes before a node are reached from the nearest, as their positions count, only when a predicate counts them
+ private bool IsReverse(Step step, Context context)
+ {
+ switch (step.Axis)
{
- var step = steps[i];
+ case "ancestor":
+ case "ancestor-or-self":
+ return true;
+ case "preceding-sibling":
+ return step.Predicates.Any(p => IsPositional(p, context));
+ default:
+ return false;
+ }
+ }
+
+ /// The C# type of the variables that hold the nodes of the expression.
+ public NodeKind KindOf(Expr expr, Context context)
+ {
+ switch (expr)
+ {
+ case FunctionExpr _ when IsCurrent(expr):
+ return context.CurrentKind;
+ case PathExpr path:
+ {
+ NodeKind kind;
+ if (path.Start != null)
+ kind = KindOf(path.Start, context);
+ else
+ kind = path.Absolute ? NodeKind.Object : context.NodeKind;
+ foreach (var step in Steps(path, context))
+ kind = KindAfter(step, kind, context);
+ return kind;
+ }
+ default:
+ return NodeKind.Object;
+ }
+ }
+
+ private NodeKind KindAfter(Step step, NodeKind kind, Context context) =>
+ Test(step.Test, step.Axis == "attribute", new Cursor("node", AxisKind(step.Axis, kind)), false, context).Cursor.Kind;
+
+ private static NodeKind AxisKind(string axis, NodeKind kind)
+ {
+ switch (axis)
+ {
+ case "child":
+ case "following-sibling":
+ case "preceding-sibling":
+ return NodeKind.Node;
+ case "attribute":
+ return NodeKind.Attribute;
+ case "self":
+ return kind;
+ default:
+ return NodeKind.Object;
+ }
+ }
+
+ ///
+ /// The steps of a path as they are compiled: without the steps that do nothing, and with //name as
+ /// descendant::name when its predicates do not count positions among the children.
+ ///
+ private List Steps(PathExpr path, Context context)
+ {
+ var steps = new List();
+ for (var i = 0; i < path.Steps.Count; i++)
+ {
+ var step = path.Steps[i];
if (step.Axis == "self" && step.Test.Kind == TestKind.Node && step.Predicates.Count == 0)
continue;
-
- // //name: descendant::name, when the predicates do not count positions among the children
- if (step.IsDescendantOrSelfNode && i + 1 < steps.Count && steps[i + 1].Axis == "child"
- && steps[i + 1].Predicates.All(p => !IsPositional(p, context)))
+ if (step.IsDescendantOrSelfNode && i + 1 < path.Steps.Count && path.Steps[i + 1].Axis == "child"
+ && path.Steps[i + 1].Predicates.All(p => !IsPositional(p, context)))
{
- var next = steps[i + 1];
- code = StepCode(code, "Descendant", next.Test, next.Predicates, false, context);
+ var next = path.Steps[i + 1];
+ steps.Add(new Step("descendant", next.Test, next.Predicates));
i++;
continue;
}
- code = StepCode(code, AxisName(step.Axis), step.Test, step.Predicates, step.Axis == "attribute", context);
+ steps.Add(step);
+ }
+ return steps;
+ }
+
+ ///
+ /// Writes the loops over the nodes of the expression, and what writes where a node is
+ /// reached. The nodes are reached in the order tells, held as tells.
+ ///
+ public void EmitNodes(Expr expr, Context context, CodeWriter w, System.Action inner)
+ {
+ var names = context.Names;
+ switch (expr)
+ {
+ case PathExpr path:
+ {
+ var steps = Steps(path, context);
+ void Chain(int i, Cursor cursor)
+ {
+ if (i == steps.Count)
+ inner(cursor);
+ else
+ EmitStep(cursor, steps[i], context, w, next => Chain(i + 1, next));
+ }
+
+ if (path.Start != null)
+ {
+ EmitNodes(path.Start, context, w, cursor => Chain(0, cursor));
+ }
+ else if (path.Absolute)
+ {
+ var root = names.New("x_");
+ w.Line("var " + root + " = RootNode(" + context.Node + ");");
+ Chain(0, new Cursor(root, NodeKind.Object));
+ }
+ else
+ {
+ Chain(0, new Cursor(context.Node, context.NodeKind));
+ }
+ break;
+ }
+ case FilterExpr filter:
+ EmitFiltered(NodeKind.Object, true, source => EmitList(NodesCode(filter.Primary, context), context, w, source), filter.Predicates, context, w, inner);
+ break;
+ case FunctionExpr _ when IsCurrent(expr):
+ inner(new Cursor(context.Current, context.CurrentKind));
+ break;
+ case VariableExpr _ when IsNode(expr, context, out var node):
+ w.Open("if (" + node + " != null)");
+ inner(new Cursor(node, NodeKind.Object));
+ w.Close();
+ break;
+ default:
+ EmitList(NodesCode(expr, context), context, w, inner);
+ break;
+ }
+ }
+
+ private static void EmitList(string list, Context context, CodeWriter w, System.Action inner)
+ {
+ var l = context.Names.New("l_");
+ var i = context.Names.New("i_");
+ var x = context.Names.New("x_");
+ w.Line("var " + l + " = " + list + ";");
+ w.Open("for (var " + i + " = 0; " + i + " < " + l + ".Count; " + i + "++)");
+ w.Line("var " + x + " = " + l + "[" + i + "];");
+ inner(new Cursor(x, NodeKind.Object));
+ w.Close();
+ }
+
+ public void EmitStep(Cursor cursor, Step step, Context context, CodeWriter w, System.Action inner)
+ {
+ var reverse = IsReverse(step, context);
+ EmitFiltered(KindAfter(step, cursor.Kind, context), IsLoop(step), source => EmitAxis(cursor, step, reverse, context, w, source), step.Predicates, context, w, inner);
+ }
+
+ private static bool IsDeclaration(NodeTestSyntax test) => test.Prefix == "xmlns" || (test.Prefix == null && test.Local == "xmlns");
+
+ // whether the nodes of the step are reached in a loop, which a break ends
+ private static bool IsLoop(Step step)
+ {
+ switch (step.Axis)
+ {
+ case "self":
+ case "parent":
+ return false;
+ case "attribute":
+ return step.Test.Kind != TestKind.Name || IsDeclaration(step.Test);
+ default:
+ return true;
+ }
+ }
+
+ private sealed class Predicate
+ {
+ public Predicate(string position, string size)
+ {
+ Position = position;
+ Size = size;
}
- return new Compiled(code, XType.NodeSet);
+
+ public string Position { get; }
+
+ public string Size { get; }
+
+ public string Code { get; set; } = "";
+
+ public bool Positional { get; set; }
+
+ public bool IsLast { get; set; }
+
+ public bool NeedsSize { get; set; }
+
+ public bool CanBreak { get; set; }
}
- private string StepCode(string input, string axis, NodeTestSyntax test, List predicates, bool attributeAxis, Context context)
+ ///
+ /// The nodes of a source that pass the predicates. A predicate that counts positions counts them among the
+ /// nodes that passed the ones before it; one that reads the size has them counted first.
+ ///
+ public void EmitFiltered(NodeKind kind, bool isLoop, System.Action> source, List predicates, Context context, CodeWriter w, System.Action inner)
{
- var testField = stylesheet.NodeTest(test, context.Element);
if (predicates.Count == 0)
- return "Step(" + input + ", Axis." + axis + ", " + testField + ")";
- return "Step(" + input + ", Axis." + axis + ", " + testField + ", " + string.Join(", ", predicates.Select(p => Predicate(p, context))) + ")";
+ {
+ source(inner);
+ return;
+ }
+
+ var names = context.Names;
+ var node = names.New("m_");
+ var compiled = new List();
+ foreach (var expr in predicates)
+ {
+ var predicate = new Predicate(names.New("pos_"), names.New("size_")) { Positional = IsPositional(expr, context), IsLast = IsLast(expr) };
+ if (!predicate.IsLast)
+ {
+ var focus = new Focus();
+ var value = Compile(expr, context.WithFocus(node, kind, predicate.Position, predicate.Size, focus));
+ switch (value.Type)
+ {
+ case XType.Number:
+ predicate.Code = predicate.Position + " == " + value.Code;
+ predicate.CanBreak = !focus.NodeUsed && !focus.PositionUsed;
+ break;
+ case XType.Any:
+ case XType.None:
+ predicate.Code = "Predicate(" + value.Code + ", " + predicate.Position + ")";
+ break;
+ default:
+ predicate.Code = Bool(value);
+ break;
+ }
+ predicate.NeedsSize = focus.SizeUsed;
+ }
+ compiled.Add(predicate);
+ }
+
+ // whether the nodes that pass the first predicates are reached in the loop of the source
+ bool InLoop(int count) => isLoop && !compiled.Take(count).Any(p => p.Positional && p.IsLast);
+
+ void Passing(int count, System.Action body)
+ {
+ if (count == 0)
+ {
+ source(cursor =>
+ {
+ w.Line("var " + node + " = " + cursor.Var + ";");
+ body();
+ });
+ return;
+ }
+
+ var predicate = compiled[count - 1];
+ if (!predicate.Positional)
+ {
+ Passing(count - 1, () =>
+ {
+ w.Open("if (" + predicate.Code + ")");
+ body();
+ w.Close();
+ });
+ return;
+ }
+
+ if (predicate.IsLast)
+ {
+ var last = names.New("last_");
+ w.Line(TypeName(kind) + "? " + last + " = null;");
+ Passing(count - 1, () => w.Line(last + " = " + node + ";"));
+ w.Open("if (" + last + " != null)");
+ w.Line("var " + node + " = " + last + ";");
+ body();
+ w.Close();
+ return;
+ }
+
+ // the two passes declare the same variables, each in a block of its own
+ if (predicate.NeedsSize)
+ {
+ w.Line("var " + predicate.Size + " = 0;");
+ w.Open();
+ Passing(count - 1, () => w.Line(predicate.Size + "++;"));
+ w.Close();
+ w.Open();
+ }
+ w.Line("var " + predicate.Position + " = 0;");
+ Passing(count - 1, () =>
+ {
+ w.Line(predicate.Position + "++;");
+ w.Open("if (" + predicate.Code + ")");
+ body();
+ if (predicate.CanBreak && InLoop(count - 1))
+ w.Line("break;");
+ w.Close();
+ });
+ if (predicate.NeedsSize)
+ w.Close();
+ }
+
+ Passing(compiled.Count, () => inner(new Cursor(node, kind)));
}
- public static string AxisName(string axis)
+ /// The nodes of an axis from a node that pass the node test of the step.
+ private void EmitAxis(Cursor cursor, Step step, bool reverse, Context context, CodeWriter w, System.Action inner)
{
- switch (axis)
+ var names = context.Names;
+ var c = cursor.Var;
+ var leaf = cursor.Kind == NodeKind.Attribute || cursor.Kind == NodeKind.Text;
+
+ void Pass(Cursor candidate, bool attributeAxis, bool amongChildren, string? condition = null)
+ {
+ var tested = Test(step.Test, attributeAxis, candidate, amongChildren, context);
+ if (tested.Never)
+ return;
+ var all = condition == null ? tested.Condition : tested.Condition == null ? condition : condition + " && " + tested.Condition;
+ if (all == null)
+ {
+ inner(tested.Cursor);
+ return;
+ }
+ w.Open("if (" + all + ")");
+ inner(tested.Cursor);
+ w.Close();
+ }
+
+ void Loop(string type, string first, string next, bool amongChildren, NodeKind kind, string? until = null)
+ {
+ var x = names.New("x_");
+ w.Open("for (" + type + " " + x + " = " + first + "; " + x + " != null" + (until == null ? "" : " && !ReferenceEquals(" + x + ", " + until + ")") + "; " + x + " = " + string.Format(CultureInfo.InvariantCulture, next, x) + ")");
+ Pass(new Cursor(x, kind), false, amongChildren);
+ w.Close();
+ }
+
+ var typed = cursor.Kind == NodeKind.Element || cursor.Kind == NodeKind.Node || cursor.Kind == NodeKind.Text;
+ switch (step.Axis)
{
case "child":
- return "Child";
+ if (!leaf)
+ Loop("var", cursor.Kind == NodeKind.Element ? c + ".FirstNode" : "FirstChild(" + c + ")", "{0}.NextNode", true, NodeKind.Node);
+ break;
case "descendant":
- return "Descendant";
+ if (!leaf)
+ Loop("XObject?", "FirstChild(" + c + ")", "NextInTree({0}, " + c + ")", true, NodeKind.Object);
+ break;
case "descendant-or-self":
- return "DescendantOrSelf";
- case "parent":
- return "Parent";
- case "ancestor":
- return "Ancestor";
- case "ancestor-or-self":
- return "AncestorOrSelf";
+ Loop("XObject?", c, "NextInTree({0}, " + c + ")", true, NodeKind.Object);
+ break;
case "following-sibling":
- return "FollowingSibling";
+ if (cursor.Kind != NodeKind.Attribute)
+ Loop("var", typed ? c + ".NextNode" : "NextSibling(" + c + ")", "{0}.NextNode", true, NodeKind.Node);
+ break;
+ case "preceding-sibling" when cursor.Kind == NodeKind.Attribute:
+ break;
+ case "preceding-sibling" when reverse:
+ Loop("var", typed ? c + ".PreviousNode" : "PreviousSibling(" + c + ")", "{0}.PreviousNode", true, NodeKind.Node);
+ break;
case "preceding-sibling":
- return "PrecedingSibling";
+ Loop("var", "FirstSibling(" + c + ")", "{0}.NextNode", true, NodeKind.Node, c);
+ break;
+ case "ancestor":
+ Loop("var", "Parent(" + c + ")", "Parent({0})", false, NodeKind.Object);
+ break;
+ case "ancestor-or-self":
+ Loop("XObject?", c, "Parent({0})", false, NodeKind.Object);
+ break;
+ case "parent":
+ {
+ var parent = names.New("x_");
+ w.Line("var " + parent + " = Parent(" + c + ");");
+ Pass(new Cursor(parent, NodeKind.Object), false, false, parent + " != null");
+ break;
+ }
case "self":
- return "Self";
+ Pass(cursor, false, false);
+ break;
case "attribute":
- return "Attribute";
+ {
+ if (leaf)
+ break;
+ var element = c;
+ if (cursor.Kind != NodeKind.Element)
+ {
+ element = names.New("e_");
+ w.Open("if (" + c + " is XElement " + element + ")");
+ }
+ var attribute = names.New("a_");
+ if (IsLoop(step))
+ {
+ w.Open("for (var " + attribute + " = " + element + ".FirstAttribute; " + attribute + " != null; " + attribute + " = " + attribute + ".NextAttribute)");
+ Pass(new Cursor(attribute, NodeKind.Attribute), true, false, "!" + attribute + ".IsNamespaceDeclaration");
+ w.Close();
+ }
+ else
+ {
+ w.Line("var " + attribute + " = " + element + ".Attribute(" + stylesheet.Name(XName.Get(step.Test.Local!, StylesheetCompiler.Namespace(step.Test.Prefix, context.Element))) + ");");
+ w.Open("if (" + attribute + " != null)");
+ inner(new Cursor(attribute, NodeKind.Attribute));
+ w.Close();
+ }
+ if (cursor.Kind != NodeKind.Element)
+ w.Close();
+ break;
+ }
default:
- throw new XsltException("The axis " + axis + " is not supported.");
+ throw new XsltException("The axis " + step.Axis + " is not supported.");
}
}
- /// A lambda of XPathPredicate: the predicate evaluated with the node, its position and the size.
- public string Predicate(Expr predicate, Context context)
+ internal readonly struct Tested
{
- var n = stylesheet.NewName("n");
- var p = stylesheet.NewName("p");
- var s = stylesheet.NewName("s");
- var value = Compile(predicate, context.WithFocus(n, p, s));
- string body;
- switch (value.Type)
+ public Tested(string? condition, Cursor cursor, bool never = false)
{
- case XType.Number:
- body = p + " == " + value.Code;
- break;
- case XType.Any:
- body = "Predicate(" + value.Code + ", " + p + ")";
- break;
+ Condition = condition;
+ Cursor = cursor;
+ Never = never;
+ }
+
+ /// The C# condition of the test, or null when every node passes.
+ public string? Condition { get; }
+
+ /// The node, as the type the test tells it has.
+ public Cursor Cursor { get; }
+
+ /// Whether no node passes.
+ public bool Never { get; }
+ }
+
+ ///
+ /// A node test on a node: on the attribute axis, whose nodes are attributes, or on another axis, where a name
+ /// is the name of an element. Among the children of a node, those of a document that are no nodes of XPath
+ /// (the document type, the white space) do not pass.
+ ///
+ public Tested Test(NodeTestSyntax test, bool attributeAxis, Cursor cursor, bool amongChildren, Context context)
+ {
+ var c = cursor.Var;
+ if (attributeAxis)
+ {
+ switch (test.Kind)
+ {
+ case TestKind.Name:
+ return new Tested(c + ".Name == " + stylesheet.Name(XName.Get(test.Local!, StylesheetCompiler.Namespace(test.Prefix, context.Element))), cursor);
+ case TestKind.Namespace:
+ return new Tested(c + ".Name.Namespace == " + NamespaceField(test, context), cursor);
+ case TestKind.AnyName:
+ case TestKind.Node:
+ return new Tested(null, cursor);
+ default:
+ return new Tested(null, cursor, true);
+ }
+ }
+
+ var leaf = cursor.Kind == NodeKind.Attribute || cursor.Kind == NodeKind.Text;
+ switch (test.Kind)
+ {
+ case TestKind.Name:
+ case TestKind.Namespace:
+ case TestKind.AnyName:
+ {
+ if (leaf)
+ return new Tested(null, new Cursor(c, NodeKind.Element), true);
+ var element = c;
+ string? condition = null;
+ if (cursor.Kind != NodeKind.Element)
+ {
+ element = context.Names.New("e_");
+ condition = c + " is XElement " + element;
+ }
+ string? name = null;
+ switch (test.Kind)
+ {
+ case TestKind.Name:
+ name = element + ".Name == " + stylesheet.Name(XName.Get(test.Local!, StylesheetCompiler.Namespace(test.Prefix, context.Element)));
+ break;
+ case TestKind.Namespace:
+ name = element + ".Name.Namespace == " + NamespaceField(test, context);
+ break;
+ }
+ if (name != null)
+ condition = condition == null ? name : condition + " && " + name;
+ return new Tested(condition, new Cursor(element, NodeKind.Element));
+ }
+ case TestKind.Node:
+ return new Tested(amongChildren ? "IsNode(" + c + ")" : null, cursor);
+ case TestKind.Text:
+ {
+ switch (cursor.Kind)
+ {
+ case NodeKind.Text:
+ return new Tested(null, cursor);
+ case NodeKind.Element:
+ case NodeKind.Attribute:
+ return new Tested(null, new Cursor(c, NodeKind.Text), true);
+ }
+ var text = context.Names.New("t_");
+ return new Tested(c + " is XText " + text + (amongChildren ? " && IsNode(" + text + ")" : ""), new Cursor(text, NodeKind.Text));
+ }
+ case TestKind.Comment:
+ return new Tested(c + " is XComment", cursor, cursor.Kind == NodeKind.Element || leaf);
default:
- body = Bool(value);
- break;
+ {
+ if (test.Local == null)
+ return new Tested(c + " is XProcessingInstruction", cursor, cursor.Kind == NodeKind.Element || leaf);
+ var instruction = context.Names.New("i_");
+ return new Tested(c + " is XProcessingInstruction " + instruction + " && " + instruction + ".Target == " + Literal(test.Local), cursor, cursor.Kind == NodeKind.Element || leaf);
+ }
}
- return "(" + n + ", " + p + ", " + s + ") => " + body;
}
+ private string NamespaceField(NodeTestSyntax test, Context context) =>
+ stylesheet.StaticField("XNamespace", "XNamespace.Get(" + Literal(StylesheetCompiler.Namespace(test.Prefix, context.Element)) + ")");
+
///
/// Whether the value of a predicate depends on the position of the node among the others: a number (or a
/// value that may be one), or an expression that reads position() or last().
@@ -392,8 +1394,8 @@ public bool IsPositional(Expr predicate, Context context)
{
if (UsesFocus(predicate))
return true;
- var type = Compile(predicate, context.WithFocus("n", "p", "s")).Type;
- return type == XType.Number || type == XType.Any;
+ var type = TypeOf(predicate, context);
+ return type == XType.Number || IsUnknown(type);
}
// whether the expression reads the position or size of its context (not that of a nested predicate)
@@ -420,13 +1422,56 @@ private static bool UsesFocus(Expr expr)
// conversions
- public static string Str(Compiled value) => value.Type == XType.String ? value.Code : "Str(" + value.Code + ")";
+ public static string StringValue(Cursor cursor) =>
+ cursor.Kind == NodeKind.Object || cursor.Kind == NodeKind.Node ? "StringValue(" + cursor.Var + ")" : cursor.Var + ".Value";
- public static string Num(Compiled value) => value.Type == XType.Number ? value.Code : "Num(" + value.Code + ")";
+ public static string Str(Compiled value)
+ {
+ if (value.IsNodeSet)
+ return value.Owner.FirstStringCode(value.Expr!, value.Context!);
+ return value.Type == XType.String || value.Type == XType.Text ? value.Code : "Str(" + value.Code + ")";
+ }
- public static string Bool(Compiled value) => value.Type == XType.Boolean ? value.Code : "Bool(" + value.Code + ")";
+ public static string Num(Compiled value)
+ {
+ if (value.IsNodeSet)
+ return "Num(" + Str(value) + ")";
+ return value.Type == XType.Number ? value.Code : "Num(" + value.Code + ")";
+ }
- public static string Box(Compiled value) => "(object)" + value.Code;
+ public static string Bool(Compiled value)
+ {
+ if (value.IsNodeSet)
+ return value.Owner.ExistsCode(value.Expr!, value.Context!);
+ switch (value.Type)
+ {
+ case XType.Boolean:
+ return value.Code;
+ // a result tree fragment is a node-set of its root node
+ case XType.Text:
+ case XType.Fragment:
+ return "true";
+ default:
+ return "Bool(" + value.Code + ")";
+ }
+ }
+
+ /// The value as an object: one of unknown type, as the value of a parameter of unknown type.
+ public static string Box(Compiled value)
+ {
+ switch (value.Type)
+ {
+ case XType.Text:
+ return "new ResultFragment(" + value.Code + ")";
+ case XType.Node:
+ return "ListOf(" + value.Code + ")";
+ case XType.Any:
+ case XType.None:
+ return value.Code;
+ default:
+ return "(object)" + value.Code;
+ }
+ }
public static string Nodes(Compiled value)
{
@@ -434,8 +1479,12 @@ public static string Nodes(Compiled value)
{
case XType.NodeSet:
return value.Code;
+ case XType.Node:
+ return "ListOf(" + value.Code + ")";
case XType.Any:
+ case XType.None:
return "Nodes(" + value.Code + ")";
+ case XType.Text:
case XType.Fragment:
throw new XsltException("A result tree fragment cannot be used as a node-set in XSLT 1.0.");
default:
@@ -448,11 +1497,14 @@ public static string TypeName(XType type)
switch (type)
{
case XType.String:
+ case XType.Text:
return "string";
case XType.Number:
return "double";
case XType.Boolean:
return "bool";
+ case XType.Node:
+ return "XObject";
case XType.NodeSet:
return "List";
case XType.Fragment:
@@ -462,6 +1514,23 @@ public static string TypeName(XType type)
}
}
+ public static string TypeName(NodeKind kind)
+ {
+ switch (kind)
+ {
+ case NodeKind.Node:
+ return "XNode";
+ case NodeKind.Element:
+ return "XElement";
+ case NodeKind.Attribute:
+ return "XAttribute";
+ case NodeKind.Text:
+ return "XText";
+ default:
+ return "XObject";
+ }
+ }
+
public static string Literal(string value)
{
var result = new StringBuilder("\"");
diff --git a/src/SharpEquations.XsltGenerator/XsltGenerator.cs b/src/SharpEquations.XsltGenerator/XsltGenerator.cs
index 1363b85..c0353c0 100644
--- a/src/SharpEquations.XsltGenerator/XsltGenerator.cs
+++ b/src/SharpEquations.XsltGenerator/XsltGenerator.cs
@@ -51,7 +51,16 @@ private static void Generate(SourceProductionContext output, string path, string
using (var reader = XmlReader.Create(new StringReader(text), settings))
document = XDocument.Load(reader, LoadOptions.SetLineInfo | LoadOptions.PreserveWhitespace);
var className = ClassName(path);
- var source = new StylesheetCompiler(document.Root!, className, fileName).Compile();
+ // compiled again with what each compilation learns of the templates, until nothing is new
+ var facts = new Facts();
+ string source;
+ while (true)
+ {
+ var compiler = new StylesheetCompiler(document.Root!, className, fileName, facts);
+ source = compiler.Compile();
+ if (!facts.Add(compiler.Learnt))
+ break;
+ }
output.AddSource(className + ".g.cs", SourceText.From(source, Encoding.UTF8));
}
catch (XsltException e)
diff --git a/src/SharpEquations/Internal/UnicodeMath/UnicodeMathSerializer.cs b/src/SharpEquations/Internal/UnicodeMath/UnicodeMathSerializer.cs
index 42bcb67..c4b0d88 100644
--- a/src/SharpEquations/Internal/UnicodeMath/UnicodeMathSerializer.cs
+++ b/src/SharpEquations/Internal/UnicodeMath/UnicodeMathSerializer.cs
@@ -507,8 +507,9 @@ private string Binary(XElement node, string op)
if (retd.Length == 0)
retd = "〖〗";
- // a scripted base (𝑎_𝑏 under ^𝑐) is grouped, as 𝑎_𝑏^𝑐 is one subsup
- if ((op == "^" || op == "_" || IsPrime(Text(Last(node)))) && IsScripted(First(node)) && functionName == null)
+ // a scripted base (𝑎_𝑏 under ^𝑐) is grouped, as 𝑎_𝑏^𝑐 is one subsup, and so is a matrix in brackets (⒨(…)),
+ // which a script after it would not apply to
+ if ((op == "^" || op == "_" || IsPrime(Text(Last(node)))) && (IsScripted(First(node)) || IsBracketedMatrix(First(node))) && functionName == null)
ret = "〖" + ret.TrimEnd(' ') + "〗";
if (op == "^" && Name(Last(node)) == "msub" || op == "_" && Name(Last(node)) == "msup")
return ret + op + "(" + retd + ")";
@@ -522,6 +523,14 @@ private string Binary(XElement node, string op)
// a script that applies a function (log_(𝑏𝑎)): the space before the function name would end the script
if ((op == "^" || op == "_") && Name(Last(node)) == "mrow" && Role(Last(node)) == ":function")
retd = "(" + retd.Trim(' ') + ")";
+ // a denominator that applies a function (𝑎/sin𝛼): a space after the fraction bar would end the fraction
+ if (Name(node) == "mfrac" && Name(Last(node)) == "mrow" && Role(Last(node)) == ":function")
+ retd = retd.TrimStart(' ');
+ // a denominator that ends with a script, before an operand that follows the fraction (1/(4𝜋𝜀₀) 𝑞₁𝑞₂/𝑟²): the
+ // space after it would end the script, not the denominator (an operator after it ends both)
+ if (Name(node) == "mfrac" && op == "/" && EndsWithSpacedScript(Last(node)) && node.ElementsAfterSelf().FirstOrDefault() is { } next && Name(next) != "mo" &&
+ !(Ch(retd, 0) == "(" && RemoveOuterParens(retd) != retd))
+ retd = "(" + retd.Trim(' ') + ")";
if (IsMathMLObject(node, false) && ChildCount(node) > 0)
{
var attr = Role(Last(node));
@@ -757,10 +766,20 @@ private static bool EndsWithScript(XElement node)
return Name(node) is "msub" or "msup" or "msubsup" or "mover" or "munder" or "munderover";
}
+ private static bool IsBracketedMatrix(XElement node) => Name(node) == "mrow" && MatrixIntents.Any(m => m.Intent == Attr(node, "intent"));
+
+ // a script that a space after it ends, but not what is before it: all but the superscript digits (𝑥²)
+ private static bool EndsWithSpacedScript(XElement node)
+ {
+ while (Name(node) == "mrow" && ChildCount(node) > 0 && !HasAttr(node, "intent"))
+ node = Last(node);
+ return IsScripted(node) && !(Name(node) == "msup" && IsDigitArg(node));
+ }
+
private string ScriptBase(XElement node)
{
var ret = Dump(node);
- if (IsScripted(node) || Name(node) == "mrow" && ChildCount(node) > 1 && !HasAttr(node, "intent"))
+ if (IsScripted(node) || Name(node) == "mrow" && ChildCount(node) > 1 && !HasAttr(node, "intent") || IsBracketedMatrix(node))
ret = "〖" + ret.TrimEnd(' ') + "〗";
return ret;
}
diff --git a/src/SharpEquations/Internal/UnicodeMath/UnicodeMathTransform.cs b/src/SharpEquations/Internal/UnicodeMath/UnicodeMathTransform.cs
index 89391c8..4045c88 100644
--- a/src/SharpEquations/Internal/UnicodeMath/UnicodeMathTransform.cs
+++ b/src/SharpEquations/Internal/UnicodeMath/UnicodeMathTransform.cs
@@ -634,6 +634,13 @@ private static JsObject TransformSpaces(object value)
foreach (var space in spaceString)
spaces += "" + ((int)space).ToString("x", CultureInfo.InvariantCulture) + ";";
}
+ // a space of a width in eighteenths of an em (the ½ em of ", "), as the character of that width; none for
+ // the width 0 of an empty cell
+ else if (value is JsObject width && Get(width, "space") is { } eighteenths && System.Convert.ToInt32(eighteenths, CultureInfo.InvariantCulture) > 0)
+ {
+ foreach (var space in UnicodeMathSerializer.UniSpaces[System.Convert.ToInt32(eighteenths, CultureInfo.InvariantCulture)])
+ spaces += "" + ((int)space).ToString("x", CultureInfo.InvariantCulture) + ";";
+ }
return O("mo", NoAttr(spaces));
}
diff --git a/src/SharpEquations/Internal/Xslt/NodeTest.cs b/src/SharpEquations/Internal/Xslt/NodeTest.cs
deleted file mode 100644
index 0b96e75..0000000
--- a/src/SharpEquations/Internal/Xslt/NodeTest.cs
+++ /dev/null
@@ -1,75 +0,0 @@
-#nullable enable
-
-namespace SharpEquations.Internal.Xslt
-{
- using System.Xml.Linq;
-
- /// The node test of an XPath step: a name, a namespace wildcard, * or a node type.
- internal sealed class NodeTest
- {
- private enum Kind
- {
- Name,
- Namespace,
- AnyName,
- Node,
- Text,
- Comment,
- ProcessingInstruction,
- }
-
- private readonly Kind kind;
- private readonly XName? name;
- private readonly string? text;
-
- private NodeTest(Kind kind, XName? name = null, string? text = null)
- {
- this.kind = kind;
- this.name = name;
- this.text = text;
- }
-
- public static NodeTest Named(XName name) => new NodeTest(Kind.Name, name);
-
- public static NodeTest InNamespace(string namespaceName) => new NodeTest(Kind.Namespace, text: namespaceName);
-
- public static readonly NodeTest AnyName = new NodeTest(Kind.AnyName);
-
- public static readonly NodeTest Node = new NodeTest(Kind.Node);
-
- public static readonly NodeTest Text = new NodeTest(Kind.Text);
-
- public static readonly NodeTest Comment = new NodeTest(Kind.Comment);
-
- public static NodeTest ProcessingInstruction(string? target) => new NodeTest(Kind.ProcessingInstruction, text: target);
-
- ///
- /// Whether the node passes the test, on an axis whose principal node type is the attribute (the attribute
- /// axis) or the element (every other axis).
- ///
- public bool Matches(XObject node, bool attributeAxis)
- {
- switch (kind)
- {
- case Kind.Name:
- return attributeAxis
- ? node is XAttribute attribute && attribute.Name == name
- : node is XElement element && element.Name == name;
- case Kind.Namespace:
- return attributeAxis
- ? node is XAttribute attribute2 && attribute2.Name.NamespaceName == text
- : node is XElement element2 && element2.Name.NamespaceName == text;
- case Kind.AnyName:
- return attributeAxis ? node is XAttribute : node is XElement;
- case Kind.Node:
- return true;
- case Kind.Text:
- return node is XText;
- case Kind.Comment:
- return node is XComment;
- default:
- return node is XProcessingInstruction instruction && (text == null || instruction.Target == text);
- }
- }
- }
-}
diff --git a/src/SharpEquations/Internal/Xslt/ResultFragment.cs b/src/SharpEquations/Internal/Xslt/ResultFragment.cs
index 703040d..17d5479 100644
--- a/src/SharpEquations/Internal/Xslt/ResultFragment.cs
+++ b/src/SharpEquations/Internal/Xslt/ResultFragment.cs
@@ -7,12 +7,21 @@ namespace SharpEquations.Internal.Xslt
/// A result tree fragment: the value of a variable or parameter given by its content.
internal sealed class ResultFragment
{
- public ResultFragment(XElement root) => Root = root;
+ private readonly string? text;
+ private XElement? root;
+
+ public ResultFragment(XElement root) => this.root = root;
+
+ /// A fragment that is only text.
+ public ResultFragment(string text) => this.text = text;
+
+ /// The text of a fragment that is only text, or null.
+ public string? Text => text;
/// The nodes of the fragment, as the children of an element that is not part of it.
- public XElement Root { get; }
+ public XElement Root => root ??= new XElement(XsltOutput.Container, text!.Length > 0 ? new XText(text) : null);
- public string Value => Root.Value;
+ public string Value => text ?? root!.Value;
}
/// The parameters passed by xsl:apply-templates.
@@ -37,7 +46,4 @@ public TemplateArguments(string[] names, object[] values)
return null;
}
}
-
- /// An XPath predicate, or any expression evaluated with a context node, position and size.
- internal delegate bool XPathPredicate(XObject node, int position, int size);
}
diff --git a/src/SharpEquations/Internal/Xslt/TranslateMap.cs b/src/SharpEquations/Internal/Xslt/TranslateMap.cs
new file mode 100644
index 0000000..bdb858d
--- /dev/null
+++ b/src/SharpEquations/Internal/Xslt/TranslateMap.cs
@@ -0,0 +1,95 @@
+#nullable enable
+
+namespace SharpEquations.Internal.Xslt
+{
+ using System.Collections.Generic;
+ using System.Text;
+
+ /// The map of an XPath translate(), by code points: the characters replaced and those removed.
+ internal sealed class TranslateMap
+ {
+ private const int Keep = -1;
+ private const int Remove = -2;
+
+ private readonly string from;
+ private readonly string to;
+ private readonly int[] ascii = new int[128];
+ private readonly Dictionary? others;
+
+ public TranslateMap(string from, string to)
+ {
+ this.from = from;
+ this.to = to;
+ for (var i = 0; i < ascii.Length; i++)
+ ascii[i] = Keep;
+ var target = new List();
+ foreach (var rune in to.EnumerateRunes())
+ target.Add(rune.Value);
+ var index = 0;
+ foreach (var rune in from.EnumerateRunes())
+ {
+ var replacement = index < target.Count ? target[index] : Remove;
+ index++;
+ // the first occurrence of a character in the map is the one that counts
+ if (rune.Value < ascii.Length)
+ {
+ if (ascii[rune.Value] == Keep)
+ ascii[rune.Value] = replacement;
+ }
+ else
+ {
+ others ??= new Dictionary();
+ if (!others.TryGetValue(rune.Value, out _))
+ others[rune.Value] = replacement;
+ }
+ }
+ }
+
+ public bool Maps(string from, string to) => this.from == from && this.to == to;
+
+ private int Find(int codePoint)
+ {
+ if (codePoint < ascii.Length)
+ return ascii[codePoint];
+ return others != null && others.TryGetValue(codePoint, out var replacement) ? replacement : Keep;
+ }
+
+ /// The text translated: the text itself when nothing in it is replaced or removed.
+ public string Apply(string s)
+ {
+ StringBuilder? result = null;
+ var index = 0;
+ foreach (var rune in s.EnumerateRunes())
+ {
+ var replacement = Find(rune.Value);
+ // a lone surrogate, which reads as U+FFFD, is written as U+FFFD
+ if (replacement == Keep && rune.Value == 0xFFFD && s[index] != '�')
+ replacement = 0xFFFD;
+ if (replacement != Keep && result == null)
+ result = new StringBuilder(s, 0, index, s.Length);
+ if (result != null)
+ {
+ switch (replacement)
+ {
+ case Keep when rune.IsBmp:
+ result.Append((char)rune.Value);
+ break;
+ case Keep:
+ result.Append(rune.ToString());
+ break;
+ case Remove:
+ break;
+ case <= 0xFFFF:
+ result.Append((char)replacement);
+ break;
+ default:
+ result.Append(char.ConvertFromUtf32(replacement));
+ break;
+ }
+ }
+ index += rune.IsBmp ? 1 : 2;
+ }
+ return result == null ? s : result.ToString();
+ }
+ }
+}
diff --git a/src/SharpEquations/Internal/Xslt/XPathFunctions.cs b/src/SharpEquations/Internal/Xslt/XPathFunctions.cs
index e3576a0..5257b99 100644
--- a/src/SharpEquations/Internal/Xslt/XPathFunctions.cs
+++ b/src/SharpEquations/Internal/Xslt/XPathFunctions.cs
@@ -10,20 +10,6 @@ namespace SharpEquations.Internal.Xslt
using System.Xml.Linq;
using SharpEquations.Internal.Text;
- internal enum Axis
- {
- Child,
- Descendant,
- DescendantOrSelf,
- Parent,
- Ancestor,
- AncestorOrSelf,
- FollowingSibling,
- PrecedingSibling,
- Self,
- Attribute,
- }
-
internal enum Comparison
{
Equal,
@@ -110,30 +96,42 @@ public static string Str(object value)
public static double Num(string value)
{
- var s = value.Trim(' ', '\t', '\r', '\n');
- var i = 0;
- if (i < s.Length && s[i] == '-')
+ var start = 0;
+ var end = value.Length;
+ while (start < end && IsSpace(value[start]))
+ start++;
+ while (end > start && IsSpace(value[end - 1]))
+ end--;
+ var i = start;
+ if (i < end && value[i] == '-')
i++;
var digits = 0;
- while (i < s.Length && s[i] >= '0' && s[i] <= '9')
+ var whole = 0L;
+ while (i < end && value[i] >= '0' && value[i] <= '9')
{
+ whole = whole * 10 + (value[i] - '0');
i++;
digits++;
}
- if (i < s.Length && s[i] == '.')
+ // a short whole number, as most are, is exact as it is
+ if (i == end && digits > 0 && digits <= 15)
+ return value[start] == '-' ? -(double)whole : whole;
+ if (i < end && value[i] == '.')
{
i++;
- while (i < s.Length && s[i] >= '0' && s[i] <= '9')
+ while (i < end && value[i] >= '0' && value[i] <= '9')
{
i++;
digits++;
}
}
- if (digits == 0 || i != s.Length)
+ if (digits == 0 || i != end)
return double.NaN;
- return double.Parse(s, NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture);
+ return double.Parse(value.Substring(start, end - start), NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture);
}
+ private static bool IsSpace(char c) => c == ' ' || c == '\t' || c == '\r' || c == '\n';
+
public static double Num(double value) => value;
public static double Num(bool value) => value ? 1 : 0;
@@ -218,9 +216,22 @@ public static string StringValue(XObject node)
public static bool Compare(object left, object right, Comparison comparison)
{
if (IsNodes(left))
- return IsNodes(right) ? CompareNodes(left, right, comparison) : CompareNodes(left, right, comparison, false);
+ {
+ if (!IsNodes(right))
+ return CompareNodes(left, right, comparison);
+ for (int i = 0, leftCount = ValueCount(left); i < leftCount; i++)
+ {
+ var value = ValueAt(left, i);
+ for (int j = 0, rightCount = ValueCount(right); j < rightCount; j++)
+ {
+ if (CompareStrings(value, ValueAt(right, j), comparison))
+ return true;
+ }
+ }
+ return false;
+ }
if (IsNodes(right))
- return CompareNodes(right, left, comparison, true);
+ return CompareNodes(right, left, Swap(comparison));
if (comparison == Comparison.Equal || comparison == Comparison.NotEqual)
{
bool equal;
@@ -235,6 +246,43 @@ public static bool Compare(object left, object right, Comparison comparison)
return Compare(Num(left), Num(right), comparison);
}
+ public static bool Compare(object left, string right, Comparison comparison)
+ {
+ if (!IsNodes(left))
+ return Compare(left, (object)right, comparison);
+ for (int i = 0, count = ValueCount(left); i < count; i++)
+ {
+ if (CompareStrings(ValueAt(left, i), right, comparison))
+ return true;
+ }
+ return false;
+ }
+
+ public static bool Compare(string left, object right, Comparison comparison) => Compare(right, left, Swap(comparison));
+
+ public static bool Compare(object left, double right, Comparison comparison)
+ {
+ switch (left)
+ {
+ case double number:
+ return Compare(number, right, comparison);
+ case string text when comparison != Comparison.Equal && comparison != Comparison.NotEqual:
+ return Compare(Num(text), right, comparison);
+ case string text:
+ return (Num(text) == right) == (comparison == Comparison.Equal);
+ case bool _:
+ return Compare(left, (object)right, comparison);
+ }
+ for (int i = 0, count = ValueCount(left); i < count; i++)
+ {
+ if (Compare(Num(ValueAt(left, i)), right, comparison))
+ return true;
+ }
+ return false;
+ }
+
+ public static bool Compare(double left, object right, Comparison comparison) => Compare(right, left, Swap(comparison));
+
public static bool Compare(double left, double right, Comparison comparison)
{
switch (comparison)
@@ -256,27 +304,14 @@ public static bool Compare(double left, double right, Comparison comparison)
private static bool IsNodes(object value) => value is List || value is ResultFragment;
- private static IEnumerable StringValues(object value) =>
- value is ResultFragment fragment ? new[] { fragment.Value } : ((List)value).Select(StringValue);
+ // the string values of a node-set: those of its nodes, or that of a result tree fragment
+ private static int ValueCount(object nodes) => nodes is List list ? list.Count : 1;
- private static bool CompareNodes(object left, object right, Comparison comparison)
- {
- var rightValues = StringValues(right).ToList();
- foreach (var l in StringValues(left))
- {
- foreach (var r in rightValues)
- {
- if (CompareStrings(l, r, comparison))
- return true;
- }
- }
- return false;
- }
+ private static string ValueAt(object nodes, int index) => nodes is List list ? StringValue(list[index]) : ((ResultFragment)nodes).Value;
- private static bool CompareNodes(object nodes, object other, Comparison comparison, bool swapped)
+ // a node-set compared with a value that is not one
+ private static bool CompareNodes(object nodes, object other, Comparison comparison)
{
- if (swapped)
- comparison = Swap(comparison);
if (other is bool b)
{
var value = Bool(nodes);
@@ -284,10 +319,23 @@ private static bool CompareNodes(object nodes, object other, Comparison comparis
? (value == b) == (comparison == Comparison.Equal)
: Compare(value ? 1 : 0, b ? 1 : 0, comparison);
}
+ var count = ValueCount(nodes);
if (other is double d)
- return StringValues(nodes).Any(s => Compare(Num(s), d, comparison));
+ {
+ for (var i = 0; i < count; i++)
+ {
+ if (Compare(Num(ValueAt(nodes, i)), d, comparison))
+ return true;
+ }
+ return false;
+ }
var text = Str(other);
- return StringValues(nodes).Any(s => CompareStrings(s, text, comparison));
+ for (var i = 0; i < count; i++)
+ {
+ if (CompareStrings(ValueAt(nodes, i), text, comparison))
+ return true;
+ }
+ return false;
}
private static bool CompareStrings(string left, string right, Comparison comparison)
@@ -324,14 +372,6 @@ private static Comparison Swap(Comparison comparison)
public static List Single(XObject node) => new List(1) { node };
- public static List Root(XObject node)
- {
- var root = node;
- for (var parent = Parent(root); parent != null; parent = Parent(parent))
- root = parent;
- return Single(root);
- }
-
public static XObject? Parent(XObject node)
{
switch (node)
@@ -360,129 +400,79 @@ public static List Union(List left, List right)
return result;
}
- public static List Step(List input, Axis axis, NodeTest test)
- {
- var result = new List();
- var attributeAxis = axis == Axis.Attribute;
- foreach (var context in input)
- {
- foreach (var node in Along(context, axis))
- {
- if (test.Matches(node, attributeAxis))
- result.Add(node);
- }
- }
- Order(result, input.Count, axis);
- return result;
- }
+ /// What a template is given for a parameter that is a node, when the call leaves it to its default.
+ public static readonly XObject Omitted = new XComment("");
- public static List Step(List input, Axis axis, NodeTest test, params XPathPredicate[] predicates)
- {
- var result = new List();
- var attributeAxis = axis == Axis.Attribute;
- var candidates = new List();
- foreach (var context in input)
- {
- candidates.Clear();
- foreach (var node in Along(context, axis))
- {
- if (test.Matches(node, attributeAxis))
- candidates.Add(node);
- }
- result.AddRange(Apply(candidates, predicates));
- }
- Order(result, input.Count, axis);
- return result;
- }
+ public static List ListOf(XObject? node) => node == null ? Empty : Single(node);
- public static List Filter(List nodes, XPathPredicate predicate)
+ /// The string value of the first child of the element with the name, and whether it has none, one or more.
+ public static string? KeyValue(XElement element, XName name, out int count)
{
- var result = new List();
- for (var i = 0; i < nodes.Count; i++)
+ string? value = null;
+ count = 0;
+ for (var node = element.FirstNode; node != null; node = node.NextNode)
{
- if (predicate(nodes[i], i + 1, nodes.Count))
- result.Add(nodes[i]);
+ if (!(node is XElement child) || child.Name != name)
+ continue;
+ if (++count > 1)
+ break;
+ value = child.Value;
}
- return result;
+ return value;
}
- private static List Apply(List candidates, XPathPredicate[] predicates)
+ public static XObject? First(List nodes) => nodes.Count == 0 ? null : nodes[0];
+
+ public static XObject RootNode(XObject node)
{
- var nodes = candidates;
- foreach (var predicate in predicates)
- nodes = Filter(nodes, predicate);
- return ReferenceEquals(nodes, candidates) ? new List(candidates) : nodes;
+ var root = node;
+ for (var parent = Parent(root); parent != null; parent = Parent(parent))
+ root = parent;
+ return root;
}
- private static bool IsReverse(Axis axis) =>
- axis == Axis.Ancestor || axis == Axis.AncestorOrSelf || axis == Axis.PrecedingSibling;
+ public static XNode? FirstChild(XObject node) => node is XContainer container ? container.FirstNode : null;
- private static void Order(List nodes, int contexts, Axis axis)
- {
- if (contexts > 1)
- SortUnique(nodes);
- else if (IsReverse(axis))
- nodes.Reverse();
- }
+ public static XNode? NextSibling(XObject node) => node is XNode sibling ? sibling.NextNode : null;
+
+ public static XNode? PreviousSibling(XObject node) => node is XNode sibling ? sibling.PreviousNode : null;
+
+ /// The first of the nodes that have the parent of the node, if it is one of them (an attribute is not).
+ public static XNode? FirstSibling(XObject node) =>
+ node is XNode && !(node is XDocument) && Parent(node) is XContainer parent ? parent.FirstNode : null;
- private static IEnumerable Along(XObject node, Axis axis)
+ /// The node after a node of a tree in document order, within the tree.
+ public static XObject? NextInTree(XObject node, XObject root)
{
- switch (axis)
+ if (node is XContainer container && container.FirstNode is XNode first)
+ return first;
+ for (var current = node; !ReferenceEquals(current, root); )
{
- case Axis.Child:
- return Children(node);
- case Axis.Descendant:
- return Descendants(node);
- case Axis.DescendantOrSelf:
- return new[] { node }.Concat(Descendants(node));
- case Axis.Parent:
- var parent = Parent(node);
- return parent == null ? Enumerable.Empty() : new[] { parent };
- case Axis.Ancestor:
- return Ancestors(node);
- case Axis.AncestorOrSelf:
- return new[] { node }.Concat(Ancestors(node));
- case Axis.FollowingSibling:
- return node is XNode following && !(node is XDocument) ? following.NodesAfterSelf().Where(IsInModel) : Enumerable.Empty();
- case Axis.PrecedingSibling:
- return node is XNode preceding && !(node is XDocument) ? preceding.NodesBeforeSelf().Reverse().Where(IsInModel) : Enumerable.Empty();
- case Axis.Self:
- return new[] { node };
- default:
- return node is XElement element ? element.Attributes().Where(a => !a.IsNamespaceDeclaration) : Enumerable.Empty();
+ if (current is XNode sibling && sibling.NextNode is XNode next)
+ return next;
+ var parent = Parent(current);
+ if (parent == null)
+ return null;
+ current = parent;
}
+ return null;
}
public static List ChildNodes(XObject node) => new List(Children(node));
- ///
- /// Whether a node matches a step of a pattern whose predicates count positions: whether the step, from the
- /// parent of the node, selects it.
- ///
- public static bool MatchesStep(XObject node, Axis axis, NodeTest test, params XPathPredicate[] predicates)
- {
- var parent = Parent(node);
- return parent != null && test.Matches(node, axis == Axis.Attribute) && Step(Single(parent), axis, test, predicates).Contains(node);
- }
-
public static IEnumerable Children(XObject node) =>
node is XContainer container ? container.Nodes().Where(IsInModel) : Enumerable.Empty();
- private static IEnumerable Descendants(XObject node) =>
- node is XContainer container ? container.DescendantNodes().Where(IsInModel) : Enumerable.Empty();
-
- private static IEnumerable Ancestors(XObject node)
- {
- for (var parent = Parent(node); parent != null; parent = Parent(parent))
- yield return parent;
- }
-
// the document type and the white space around the document element are not nodes of XPath
private static bool IsInModel(XNode node) =>
!(node is XDocumentType) && !(node is XText && node.Parent == null);
- private static void SortUnique(List nodes)
+ public static bool IsNode(XObject node) => !(node is XNode other) || IsInModel(other);
+
+ public static void SortUnique(List nodes)
{
+ if (nodes.Count < 2)
+ return;
nodes.Sort(DocumentOrder);
var write = 0;
for (var read = 0; read < nodes.Count; read++)
@@ -605,9 +595,39 @@ public static string Substring(string s, double start, double length)
{
var first = Round(start);
var end = first + Round(length);
+ var position = 0;
+ var index = 0;
+ var from = -1;
+ var to = 0;
+ foreach (var rune in s.EnumerateRunes())
+ {
+ position++;
+ var next = index + (rune.IsBmp ? 1 : 2);
+ if (position >= first && position < end)
+ {
+ // a lone surrogate, which reads as U+FFFD, is written as U+FFFD
+ if (rune.Value == 0xFFFD && s[index] != '\uFFFD')
+ return SubstringOfRunes(s, first, end);
+ if (from < 0)
+ from = index;
+ to = next;
+ }
+ else if (from >= 0)
+ {
+ break;
+ }
+ index = next;
+ }
+ if (from < 0)
+ return "";
+ return from == 0 && to == s.Length ? s : s.Substring(from, to - from);
+ }
+
+ private static string SubstringOfRunes(string s, double first, double end)
+ {
var result = new StringBuilder();
var position = 0;
- foreach (var rune in CodePoints.Of(s))
+ foreach (var rune in s.EnumerateRunes())
{
position++;
if (position >= first && position < end)
@@ -616,7 +636,13 @@ public static string Substring(string s, double start, double length)
return result.ToString();
}
- public static double StringLength(string s) => CodePoints.Of(s).Count();
+ public static double StringLength(string s)
+ {
+ var length = 0;
+ foreach (var rune in s.EnumerateRunes())
+ length++;
+ return length;
+ }
public static string NormalizeSpace(string s)
{
@@ -637,20 +663,14 @@ public static string NormalizeSpace(string s)
return result.ToString();
}
- public static string Translate(string s, string from, string to)
+ public static string Translate(string s, string from, string to) => new TranslateMap(from, to).Apply(s);
+
+ /// translate() with the map of the last call from the same place, which mostly has the same arguments.
+ public static string Translate(string s, string from, string to, ref TranslateMap? map)
{
- var source = CodePoints.Of(from).ToList();
- var target = CodePoints.Of(to).ToList();
- var result = new StringBuilder(s.Length);
- foreach (var rune in CodePoints.Of(s))
- {
- var index = source.IndexOf(rune);
- if (index < 0)
- result.Append(rune.ToString());
- else if (index < target.Count)
- result.Append(target[index].ToString());
- }
- return result.ToString();
+ if (map == null || !map.Maps(from, to))
+ map = new TranslateMap(from, to);
+ return map.Apply(s);
}
public static double Round(double value)
diff --git a/src/SharpEquations/Internal/Xslt/XsltOutput.cs b/src/SharpEquations/Internal/Xslt/XsltOutput.cs
index 0d820e9..7f72df8 100644
--- a/src/SharpEquations/Internal/Xslt/XsltOutput.cs
+++ b/src/SharpEquations/Internal/Xslt/XsltOutput.cs
@@ -19,54 +19,88 @@ namespace SharpEquations.Internal.Xslt
///
internal sealed class XsltOutput
{
- private static readonly XName Container = XName.Get("fragment");
+ public static readonly XName Container = XName.Get("fragment");
- private readonly Stack open = new Stack();
- private readonly Stack roots = new Stack();
private readonly XElement result = new XElement(Container);
- // the text written since the last node, as it is and as markup (escaped, with the unescaped text as it is)
- private readonly StringBuilder text = new StringBuilder();
- private readonly StringBuilder markup = new StringBuilder();
+ // the element being written, the root of what is written (the result, or the fragment being captured), and
+ // those of the captures around it
+ private XContainer top;
+ private XElement root;
+ private Stack? outer;
+
+ // the text written since the last node: as it is (one piece, or more in the builder) and, once some is written
+ // with disable-output-escaping, as markup (escaped, with the unescaped text as it is)
+ private string? piece;
+ private StringBuilder? text;
+ private StringBuilder? markup;
private bool hasText;
private bool hasMarkup;
+ // the text captured as strings, one capture after the other, and where the one in progress starts (-1: none)
+ private StringBuilder? captured;
+ private Stack? captures;
+ private int capture = -1;
+
public XsltOutput()
{
- open.Push(result);
- roots.Push(result);
+ top = result;
+ root = result;
}
- private XContainer Top => open.Peek();
-
public void StartElement(XName name)
{
Flush();
var element = new XElement(name);
- Top.Add(element);
- open.Push(element);
+ top.Add(element);
+ top = element;
}
public void EndElement()
{
Flush();
- open.Pop();
+ top = top.Parent!;
}
public void Attribute(XName name, string value)
{
Flush();
- if (!(Top is XElement element) || ReferenceEquals(element, roots.Peek()))
+ if (!(top is XElement element) || ReferenceEquals(element, root))
throw new InvalidOperationException("An attribute can be added only to an element.");
- if (element.Nodes().Any(n => !(n is XText t) || t.Value.Length > 0))
- throw new InvalidOperationException("An attribute cannot be added to an element after its children.");
+ for (var node = element.FirstNode; node != null; node = node.NextNode)
+ {
+ if (!(node is XText t) || t.Value.Length > 0)
+ throw new InvalidOperationException("An attribute cannot be added to an element after its children.");
+ }
element.SetAttributeValue(name, value);
}
public void Text(string value)
{
- hasText = true;
- text.Append(value);
+ if (capture >= 0)
+ {
+ captured!.Append(value);
+ return;
+ }
+ if (hasMarkup)
+ Escape(markup!, value);
+ if (!hasText)
+ {
+ hasText = true;
+ piece = value;
+ return;
+ }
+ if (piece != null)
+ {
+ text ??= new StringBuilder();
+ text.Append(piece);
+ piece = null;
+ }
+ text!.Append(value);
+ }
+
+ private static void Escape(StringBuilder markup, string value)
+ {
foreach (var c in value)
{
switch (c)
@@ -87,46 +121,168 @@ public void Text(string value)
}
}
+ private string Pending => piece ?? text!.ToString();
+
/// Text written with disable-output-escaping.
public void RawText(string value)
{
- hasText = true;
- hasMarkup = true;
- text.Append(value);
- markup.Append(value);
+ if (capture >= 0)
+ throw new InvalidOperationException("Text cannot be written as markup here.");
+ if (!hasMarkup)
+ {
+ markup ??= new StringBuilder();
+ markup.Clear();
+ if (hasText)
+ Escape(markup, Pending);
+ hasMarkup = true;
+ }
+ markup!.Append(value);
+ if (!hasText)
+ {
+ hasText = true;
+ piece = value;
+ return;
+ }
+ if (piece != null)
+ {
+ text ??= new StringBuilder();
+ text.Append(piece);
+ piece = null;
+ }
+ text!.Append(value);
}
private void Flush()
{
if (!hasText)
return;
- var top = Top;
- if (hasMarkup)
+ if (hasMarkup && Decode(markup!) is string decoded)
+ {
+ if (decoded.Length > 0)
+ top.Add(new XText(decoded));
+ markup!.Clear();
+ }
+ else if (hasMarkup)
{
var settings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null };
using (var reader = XmlReader.Create(new StringReader("" + markup + ""), settings))
top.Add(XElement.Load(reader, LoadOptions.PreserveWhitespace).Nodes().ToList());
+ markup!.Clear();
}
- else if (text.Length > 0 || (top is XElement element && element.IsEmpty && !ReferenceEquals(element, roots.Peek())))
+ else
{
- top.Add(text.ToString());
+ var pending = Pending;
+ if (pending.Length > 0 || (top is XElement element && element.IsEmpty && !ReferenceEquals(element, root)))
+ top.Add(pending);
}
- text.Clear();
- markup.Clear();
+ piece = null;
+ text?.Clear();
hasText = false;
hasMarkup = false;
}
+ ///
+ /// The text of markup that is only text and references to characters, as most is (a character reference
+ /// written as text): what a parser reads it as. Null for any other markup, which a parser reads.
+ ///
+ private static string? Decode(StringBuilder markup)
+ {
+ var result = new StringBuilder(markup.Length);
+ for (var i = 0; i < markup.Length; i++)
+ {
+ var c = markup[i];
+ switch (c)
+ {
+ case '&':
+ {
+ var end = i + 1;
+ while (end < markup.Length && end - i <= 10 && markup[end] != ';')
+ end++;
+ if (end >= markup.Length || markup[end] != ';')
+ return null;
+ var reference = Reference(markup.ToString(i + 1, end - i - 1));
+ if (reference == null)
+ return null;
+ result.Append(reference);
+ i = end;
+ break;
+ }
+ // markup, the end of a CDATA section, the ends of lines a parser normalizes, the characters XML
+ // does not allow and the surrogates, whose pairs a parser checks
+ case '<':
+ case '>':
+ case '\r':
+ case var _ when c < 0x20 && c != '\t' && c != '\n':
+ case var _ when c >= 0xD800 && c <= 0xDFFF:
+ case var _ when c >= 0xFFFE:
+ return null;
+ default:
+ result.Append(c);
+ break;
+ }
+ }
+ return result.ToString();
+ }
+
+ // the character of a reference to a character or to one of the entities of XML, or null if it is not one
+ private static string? Reference(string name)
+ {
+ switch (name)
+ {
+ case "amp":
+ return "&";
+ case "lt":
+ return "<";
+ case "gt":
+ return ">";
+ case "quot":
+ return "\"";
+ case "apos":
+ return "'";
+ }
+ if (name.Length < 2 || name[0] != '#')
+ return null;
+ var hexadecimal = name[1] == 'x';
+ var value = 0;
+ for (var i = hexadecimal ? 2 : 1; i < name.Length; i++)
+ {
+ var c = name[i];
+ int digit;
+ switch (c)
+ {
+ case >= '0' and <= '9':
+ digit = c - '0';
+ break;
+ case >= 'a' and <= 'f' when hexadecimal:
+ digit = c - 'a' + 10;
+ break;
+ case >= 'A' and <= 'F' when hexadecimal:
+ digit = c - 'A' + 10;
+ break;
+ default:
+ return null;
+ }
+ value = value * (hexadecimal ? 16 : 10) + digit;
+ if (value > 0x10FFFF)
+ return null;
+ }
+ if (name.Length == (hexadecimal ? 2 : 1))
+ return null;
+ // the characters of XML but the ends of lines
+ var allowed = value == 0x9 || (value >= 0x20 && value <= 0xD7FF) || (value >= 0xE000 && value <= 0xFFFD) || value >= 0x10000;
+ return allowed ? char.ConvertFromUtf32(value) : null;
+ }
+
public void Comment(string value)
{
Flush();
- Top.Add(new XComment(value));
+ top.Add(new XComment(value));
}
public void ProcessingInstruction(string target, string data)
{
Flush();
- Top.Add(new XProcessingInstruction(target, data));
+ top.Add(new XProcessingInstruction(target, data));
}
public void CopyOf(List nodes)
@@ -137,12 +293,24 @@ public void CopyOf(List nodes)
public void CopyOf(ResultFragment fragment)
{
+ if (fragment.Text != null)
+ {
+ CopyOfText(fragment.Text);
+ return;
+ }
foreach (var node in fragment.Root.Nodes())
Copy(node);
}
public void CopyOf(string value) => Text(value);
+ /// The copy of a result tree fragment that is only text: its text node, if it has one.
+ public void CopyOfText(string value)
+ {
+ if (value.Length > 0)
+ Text(value);
+ }
+
public void CopyOf(object value)
{
switch (value)
@@ -159,7 +327,7 @@ public void CopyOf(object value)
}
}
- private void Copy(XObject node)
+ public void Copy(XObject node)
{
switch (node)
{
@@ -169,7 +337,7 @@ private void Copy(XObject node)
break;
case XElement element:
Flush();
- Top.Add(new XElement(element));
+ top.Add(new XElement(element));
break;
case XAttribute attribute:
Attribute(attribute.Name, attribute.Value);
@@ -202,28 +370,53 @@ public bool StartCopy(XObject node)
}
}
+ /// Starts keeping the text written as a string: only text is written until .
+ public void BeginText()
+ {
+ captured ??= new StringBuilder();
+ captures ??= new Stack();
+ captures.Push(capture);
+ capture = captured.Length;
+ }
+
+ public string EndText()
+ {
+ var value = captured!.ToString(capture, captured.Length - capture);
+ captured.Length = capture;
+ capture = captures!.Pop();
+ return value;
+ }
+
public void BeginCapture()
{
Flush();
- var root = new XElement(Container);
- open.Push(root);
- roots.Push(root);
+ captures ??= new Stack();
+ captures.Push(capture);
+ capture = -1;
+ outer ??= new Stack();
+ outer.Push(top);
+ outer.Push(root);
+ root = new XElement(Container);
+ top = root;
}
- public ResultFragment EndFragment()
+ private XElement EndCapture()
{
Flush();
- open.Pop();
- return new ResultFragment(roots.Pop());
+ var fragment = root;
+ root = (XElement)outer!.Pop();
+ top = outer.Pop();
+ capture = captures!.Pop();
+ return fragment;
}
+ public ResultFragment EndFragment() => new ResultFragment(EndCapture());
+
/// The text captured, for an attribute or a comment, where disable-output-escaping does not apply.
public string EndString()
{
hasMarkup = false;
- Flush();
- open.Pop();
- return roots.Pop().Value;
+ return EndCapture().Value;
}
/// The document element of the result, or null if the result is only white space.
diff --git a/src/SharpEquations/SharpEquations.csproj b/src/SharpEquations/SharpEquations.csproj
index 7c43b0b..6454104 100644
--- a/src/SharpEquations/SharpEquations.csproj
+++ b/src/SharpEquations/SharpEquations.csproj
@@ -18,6 +18,7 @@
+
diff --git a/tests/SharpEquations.Tests/Regression/WriterRoundTripTests.cs b/tests/SharpEquations.Tests/Regression/WriterRoundTripTests.cs
index 458b869..c71470a 100644
--- a/tests/SharpEquations.Tests/Regression/WriterRoundTripTests.cs
+++ b/tests/SharpEquations.Tests/Regression/WriterRoundTripTests.cs
@@ -35,6 +35,36 @@ public async Task UnicodeMathWritesWhatItReads(string mathML, string unicodeMath
await Assert.That(RoundTrip(back, EquationFormat.UnicodeMath)).IsEqualTo(back);
}
+ ///
+ /// UnicodeMath that reads to an equation the writer wrote as text that read back otherwise: a script of a matrix
+ /// in brackets (⒨(…)²), a function in a denominator (𝑎/ sin𝛼), a denominator that ends with a script before
+ /// what follows the fraction (1/4𝜋𝜀₀ 𝑞₁𝑞₂/𝑟²), and the space after a comma that two spaces type (𝑎, 𝑏).
+ ///
+ [Test]
+ [Arguments("(■(a&b@c&d))^2")]
+ [Arguments("(■(a&b@c&d))^(-1)=1/(ad-bc) (■(d&-b@-c&a))")]
+ [Arguments("[■(a&b@c&d)]_ij")]
+ [Arguments("|■(a&b@c&d)|^2")]
+ [Arguments("a/sinα")]
+ [Arguments("a/sinα=b/sinβ=c/sinγ")]
+ [Arguments("(sinα)/a")]
+ [Arguments("1/(4πε_0) (q_1 q_2)/r^2")]
+ [Arguments("1/(x_1) y")]
+ [Arguments("1/(ab^2) c/d")]
+ [Arguments("A=√s, s=2")]
+ [Arguments("f(x, y)")]
+ public async Task UnicodeMathReadsBackWhatItWrites(string unicodeMath)
+ {
+ var equation = Equation.Parse(unicodeMath, EquationFormat.UnicodeMath);
+ await Assert.That(RoundTrip(equation, EquationFormat.UnicodeMath)).IsEqualTo(equation).Because(equation.ToString(EquationFormat.UnicodeMath));
+ }
+
+ [Test]
+ public async Task UnicodeMathReadsTwoSpacesAfterACommaAsASpace()
+ {
+ await Assert.That(Equation.Parse("a, b", EquationFormat.UnicodeMath)).IsEqualTo(Equation.Parse("a,\u2002b", EquationFormat.UnicodeMath));
+ }
+
[Test]
[Arguments("[a|b]")]
[Arguments("[a‖b]")]
diff --git a/tests/SharpEquations.Tests/SharpEquations.Tests.csproj b/tests/SharpEquations.Tests/SharpEquations.Tests.csproj
index 53abb9f..1e4d407 100644
--- a/tests/SharpEquations.Tests/SharpEquations.Tests.csproj
+++ b/tests/SharpEquations.Tests/SharpEquations.Tests.csproj
@@ -24,4 +24,10 @@
SetTargetFramework="TargetFramework=netstandard2.0" />
+
+
+
+
+
+
diff --git a/tests/SharpEquations.Tests/Xslt/CompiledStylesheetTests.cs b/tests/SharpEquations.Tests/Xslt/CompiledStylesheetTests.cs
index 4d6d4be..658fe6f 100644
--- a/tests/SharpEquations.Tests/Xslt/CompiledStylesheetTests.cs
+++ b/tests/SharpEquations.Tests/Xslt/CompiledStylesheetTests.cs
@@ -1,5 +1,16 @@
#nullable enable
+namespace SharpEquations.Internal.Xslt
+{
+ using System.Xml.Linq;
+
+ /// Xslt/conformance.xsl, compiled by SharpEquations.XsltGenerator.
+ internal sealed partial class ConformanceStylesheet
+ {
+ public static partial XElement? Transform(XElement input);
+ }
+}
+
namespace SharpEquations.Tests.Xslt
{
using System;
@@ -32,7 +43,9 @@ public class CompiledStylesheetTests
{
var stylesheet = Stylesheets.GetOrAdd(name, n =>
{
- using var stream = File.OpenRead(Path.Combine(TestData.Path(), "..", "..", "src", "SharpEquations", "Resources", n));
+ using var stream = File.OpenRead(n == "conformance.xsl"
+ ? Path.Combine(TestData.Path(), "..", "SharpEquations.Tests", "Xslt", n)
+ : Path.Combine(TestData.Path(), "..", "..", "src", "SharpEquations", "Resources", n));
using var reader = XmlReader.Create(stream, new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null });
var transform = new XslCompiledTransform();
transform.Load(reader, XsltSettings.Default, null);
@@ -114,7 +127,7 @@ private static string Around(string text, string other)
private static async Task AssertNoDifferences((List Differences, int Astral) result, int inputs)
{
- await Assert.That(inputs).IsGreaterThan(100);
+ await Assert.That(inputs).IsGreaterThan(30);
Console.WriteLine(result.Astral + " of " + inputs + " differ, with characters outside the basic plane");
await Assert.That(result.Differences.Count).IsEqualTo(0).Because(string.Join("\n", result.Differences.Take(10)));
}
@@ -133,6 +146,44 @@ public async Task MathML2Omml()
await AssertNoDifferences(Differences("mml2omml.xsl", Mml2ommlStylesheet.Transform, inputs), inputs.Count);
}
+ private const string ConformanceInput = ""
+ + "- alpha310
"
+ + "- beta45
text"
+ + "gamma-1.5"
+ + "- delta78
"
+ + "c01mc01tc01"
+ + "c02tc02c03mc03"
+ + "c04c05c05c01tc06"
+ + "c05c09mc10 c07"
+ + "";
+
+ ///
+ /// What the generator compiles, beyond what the stylesheets of the library use of it on the corpora: the axes,
+ /// the predicates that count, the node-sets as variables and parameters, the comparisons, the rules found by the
+ /// text of a child (Xslt/conformance.xsl).
+ ///
+ [Test]
+ public async Task GeneratorConformance()
+ {
+ var input = XElement.Parse(ConformanceInput);
+ var expected = Canonical(Reference("conformance.xsl", new XElement(input)));
+ var actual = Canonical(ConformanceStylesheet.Transform(new XElement(input)));
+ await Assert.That(XElement.Parse(expected).Elements().Count()).IsGreaterThan(30);
+ foreach (var (e, a) in XElement.Parse(expected).Elements().Zip(XElement.Parse(actual).Elements(), (e, a) => (e, a)))
+ await Assert.That(a.ToString(SaveOptions.DisableFormatting)).IsEqualTo(e.ToString(SaveOptions.DisableFormatting));
+ await Assert.That(actual).IsEqualTo(expected);
+ }
+
+ [Test]
+ public async Task ContentMathML2MathML()
+ {
+ var inputs = Directory.GetFiles(TestData.Path("contentmathml"), "*.mml")
+ .OrderBy(f => f, StringComparer.Ordinal)
+ .SelectMany(f => XElement.Load(f, LoadOptions.PreserveWhitespace).DescendantsAndSelf().Where(e => e.Name.LocalName == "math").Select((e, i) => (Path.GetFileName(f) + "#" + i, e)))
+ .ToList();
+ await AssertNoDifferences(Differences("ctop.xsl", CtopStylesheet.Transform, inputs), inputs.Count);
+ }
+
[Test]
public async Task Mtef2MathML()
{
diff --git a/tests/SharpEquations.Tests/Xslt/conformance.xsl b/tests/SharpEquations.Tests/Xslt/conformance.xsl
new file mode 100644
index 0000000..6ca7068
--- /dev/null
+++ b/tests/SharpEquations.Tests/Xslt/conformance.xsl
@@ -0,0 +1,215 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 12
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ is-x
+ gt-2
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+
+ !
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ one
+ other
+
+
+ x
+
+
+
+
+
+
+
+ ,,
+ ,
+ |||
+ |||
+ |||
+ ||
+ |||
+ |||
+ ||||
+ ||||||
+ |||||||
+ |||||||
+ ||||||
+ ||||||
+ ||||
+ ||||
+ |||||||||||
+ |||||||||
+ |||
+ |||
+
+
+
+
+ /:;
+
+ =;
+
+
+
+
+ =
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ inside
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ∑<r a="1">x</r>
+
+
+ a
+
+
+ c
+ data
+
+
+
+
+
+
+
+
+
+
+
+
+ [second ]
+ [last ]
+ (sub )
+ (deep)
+ (top )
+ (attribute )
+ (comment )
+ (pi )
+
+
+
+
+
+
+ [other ]
+ [m ]
+ A
+ a
+ B
+ C
+ D
+ E
+ [plain ]
+ F
+ G
+ H
+ b
+ I
+