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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ generated parser, on netstandard2.0): keep it so, and keep containers and other
|---|---|
| `src/SharpEquations/` | The library: `Public/` (the public API), `Internal/` (one folder per format), `Resources/` (XSLT stylesheets) |
| `src/SharpEquations.RegexPolyfill/` | Build time only: the `[GeneratedRegex]` source generator of the netstandard2.0 build |
| `src/SharpEquations.XsltGenerator/` | Build time only: the source generator that compiles the XSLT stylesheets of `Resources/` to C# |
| `benchmarks/SharpEquations.Benchmarks/` | BenchmarkDotNet benchmarks of reading, writing and converting the corpora |
| `tests/SharpEquations.Tests/` | TUnit tests, in folders by subject (PublicApi, MathML, Omml, Mtef, UnicodeMath, LaTeX, Property, Regression) |
| `tests/data/` | Test data and its provenance (`tests/data/README.md`) |

Expand Down Expand Up @@ -58,8 +60,14 @@ init-only record: `Display`, `Strict`, `PreserveFormatting`, `LaTeXFlavor` stand
UnicodeMath writers. Content MathML of the source (`apply`, `ci`, `cn`..., outside the annotations) is first rewritten
as presentation MathML by `ctop.xsl` (David Carlisle's, see `NOTICE`), keeping the presentation MathML around it.
- `Internal/Omml/`: OMML ↔ MathML with the XSLT 1.0 stylesheets of `Resources/` (from the TEI Stylesheets, see
`NOTICE`), run with `XslCompiledTransform`. OMML is written as Word writes it (plain letters with `m:sty`/`m:scr`,
`NOTICE`). OMML is written as Word writes it (plain letters with `m:sty`/`m:scr`,
underbars as `m:bar`), and an upright word of OMML (`max`) is read as one identifier.
- `Internal/Xslt/`: the run time of the stylesheets, which `SharpEquations.XsltGenerator` compiles to C# at build time
(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.
- `Internal/Mtef/`: MTEF of Equation Editor 3.0 and MathType, read only; a C# port of the jure/mathtype gem, converted
with `mtef2mml.xsl`. It reads the "Equation Native" stream of the OLE object; opening the OLE object is the caller's.
- `Internal/UnicodeMath/`: a port of UnicodeMathML. Its PEG grammar is `UnicodeMathGrammar.parlot.cs`, compiled at
Expand Down
2 changes: 1 addition & 1 deletion BuildVersion.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<PropertyGroup>
<!-- The release version: bump this, and merging to main tags and publishes vX.Y.Z.
CI passes a pre-release VersionSuffix for pull request builds (pr.12.34 -> 1.0.0-pr.12.34). -->
<VersionPrefix>1.0.7</VersionPrefix>
<VersionPrefix>1.1.0</VersionPrefix>
</PropertyGroup>

<PropertyGroup>
Expand Down
1 change: 1 addition & 0 deletions SharpEquations.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<Folder Name="/src/">
<Project Path="src/SharpEquations/SharpEquations.csproj" />
<Project Path="src/SharpEquations.RegexPolyfill/SharpEquations.RegexPolyfill.csproj" />
<Project Path="src/SharpEquations.XsltGenerator/SharpEquations.XsltGenerator.csproj" />
</Folder>
<Folder Name="/benchmarks/">
<Project Path="benchmarks/SharpEquations.Benchmarks/SharpEquations.Benchmarks.csproj" />
Expand Down
36 changes: 31 additions & 5 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,32 @@ The XSLT stylesheets recurse over the tree, and a stack overflow ends the proces
caught, so `EquationStylesheets` rejects input nested more than 512 levels deep (`MaxDepth`). The MTEF reader stops at
256 nested record lists for the same reason.

## 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
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).
- `XsltOutput` builds the result as LINQ to XML, as `XslCompiledTransform` gave it when its output was written as text
and parsed again: text written with `disable-output-escaping` is markup, with the text next to it (`mtef2mml.xsl`
writes character references in pieces), and empty text leaves `<t></t>` rather than `<t />`.
- 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.

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

`Normalize` is what makes equality and round trips possible. The same mathematics read from different formats, or
Expand Down Expand Up @@ -146,15 +172,15 @@ an `annotation-xml` is left alone, and a `semantics` whose first child is Conten
### OMML (`Internal/Omml/`)

OMML ↔ MathML uses the XSLT 1.0 stylesheets of the TEI Consortium (`Resources/omml2mml.xsl`, `Resources/mml2omml.xsl`,
BSD-2-Clause), the descendants of the ones Microsoft ships with Office. `EquationStylesheets` compiles them once
with `XslCompiledTransform`. C# code before and after each stylesheet fixes what the stylesheets get wrong or what
Word writes differently.
BSD-2-Clause), the descendants of the ones Microsoft ships with Office, compiled to C# at build time (see "The
stylesheets compiled to C#" below). C# code before and after each stylesheet fixes what the stylesheets get wrong or
what Word writes differently.

**Reading (`OmmlParser`):**
- `m:oMathPara` is a display equation, `m:oMath` an inline one.
- `ST_OnOff` values are normalized, because the stylesheet only knows `on`/`off`.
- Characters outside the basic plane go through the stylesheet as private use characters, because it splits text into
UTF-16 units.
- Characters outside the basic plane go through the stylesheet as private use characters, because
`XslCompiledTransform`, which ran it before it was compiled to C#, split text into UTF-16 units.
- Lines aligned with `m:aln` become a table marked as aligned lines.
- Numbers split across runs are joined.
- Word's equation numbers (`#(1)`) become `mlabeledtr`.
Expand Down
2 changes: 2 additions & 0 deletions src/SharpEquations.XsltGenerator/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
; Shipped analyzer releases
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
; Unshipped analyzer release
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
XSLT001 | SharpEquations.Xslt | Error | XsltGenerator, a stylesheet that cannot be compiled
43 changes: 43 additions & 0 deletions src/SharpEquations.XsltGenerator/CodeWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
namespace SharpEquations.XsltGenerator
{
using System.Text;

internal sealed class CodeWriter
{
private readonly StringBuilder text = new StringBuilder();
private int indent;

public CodeWriter(int indent = 0) => this.indent = indent;

public CodeWriter Line(string line = "")
{
if (line.Length > 0)
text.Append(' ', indent * 4).Append(line);
text.Append('\n');
return this;
}

public CodeWriter Open(string? line = null)
{
if (line != null)
Line(line);
Line("{");
indent++;
return this;
}

public CodeWriter Close(string suffix = "")
{
indent--;
return Line("}" + suffix);
}

public CodeWriter Append(CodeWriter other)
{
text.Append(other.text);
return this;
}

public override string ToString() => text.ToString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<!-- Build time only: the XSLT 1.0 stylesheets of SharpEquations compiled to C#, which runs without generating code
at run time (XslCompiledTransform does, which Native AOT cannot). It is referenced as an analyzer and is neither
shipped nor a dependency of the package. -->
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Nullable>enable</Nullable>
<IsRoslynComponent>true</IsRoslynComponent>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
<AdditionalFiles Include="AnalyzerReleases.Shipped.md" />
<AdditionalFiles Include="AnalyzerReleases.Unshipped.md" />
</ItemGroup>

</Project>
Loading
Loading