Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
65229f0
[no-new-version] Design of an equation editor for Avalonia
claude Nov 9, 2026
a1ef18d
[no-new-version] Equation editor for Avalonia: MathEditBox and MathView
claude Nov 9, 2026
743e607
[no-new-version] Editor: copy API, equation numbers, tools, demo, pac…
claude Nov 9, 2026
1b6d412
[no-new-version] Commands in the public API, completion, lines, table…
claude Nov 9, 2026
af7a315
[no-new-version] Automatic line breaks of long display equations
claude Nov 9, 2026
5fbb743
[no-new-version] Floating bar and error background in the dark theme
claude Nov 9, 2026
3f2d99b
[no-new-version] Paste the MathML of Word: the clipboard format as UT…
claude Nov 9, 2026
4513d53
Fix slnx demo name
simonebortolin Nov 9, 2026
10e953a
[no-new-version] The equation editor demo on WebAssembly
claude Nov 9, 2026
0ec4cda
[no-new-version] Demo: a change of font that shows on every system
claude Nov 9, 2026
3b68f59
[no-new-version] Test: the controls follow MathFontFamily, from the d…
claude Nov 9, 2026
1d736af
Merge main into claude/focused-clarke-f4k4a9
claude Nov 9, 2026
3e14733
[no-new-version] Editor: a LaTeX command waits for its arguments; the…
claude Nov 9, 2026
0eb2b83
Merge remote-tracking branch 'origin/claude/focused-clarke-f4k4a9' in…
claude Nov 9, 2026
38badf1
[no-new-version] Test: the font each family of the controls is drawn …
claude Nov 9, 2026
52d65fc
Merge remote-tracking branch 'origin/claude/focused-clarke-f4k4a9' in…
claude Nov 9, 2026
f644cb8
Merge claude/xslt-generator: the stylesheets compiled to C#, under th…
claude Nov 9, 2026
6484cf6
[no-new-version] Cambria Math on Windows: the face of cambria.ttc wit…
claude Nov 9, 2026
9a7fe40
[no-new-version] Web demo: compiled ahead of time, without the workar…
claude Nov 9, 2026
16dc228
[no-new-version] Web demo: deployed to GitHub Pages from any branch w…
claude Nov 9, 2026
9894ade
Fix to Browser demo csproj
simonebortolin Nov 10, 2026
1f3c9ea
[no-new-version] Editor: context menu
claude Nov 10, 2026
f0cdd1b
[no-new-version] Merge claude/editor-context-menu: the context menu o…
claude Nov 10, 2026
a247cf4
Merge branch 'claude/focused-clarke-f4k4a9' of https://github.com/sim…
claude Nov 10, 2026
492559f
[no-new-version] Editor: math autocorrect, as in Word
claude Nov 10, 2026
90e9934
[no-new-version] Merge claude/editor-autocorrect: Math AutoCorrect in…
claude Nov 10, 2026
9c6c7da
[no-new-version] Editor: a layout cache, and less layout and drawing …
claude Nov 11, 2026
849972e
Merge remote-tracking branch 'origin/claude/xslt-generator' into aval…
claude Nov 11, 2026
943843f
[no-new-version] Browser demo out of the solution build; one CtopStyl…
claude Nov 11, 2026
7fad45f
Merge branch 'claude/xslt-generator' into claude/focused-clarke-f4k4a9
claude Nov 11, 2026
bbf8d08
[no-new-version] Avalonia: a glyph of a font without outlines (color …
claude Nov 11, 2026
9c51239
[no-new-version] Merge claude/editor-perf: a layout cache for the editor
claude Nov 11, 2026
ce69404
Merge branch 'claude/focused-clarke-f4k4a9' of https://github.com/sim…
claude Nov 11, 2026
88f50c3
[no-new-version] Test: the sizes with and without the layout cache wh…
claude Nov 11, 2026
2e11345
[no-new-version] Avalonia: the fallback font of a code point is match…
claude Nov 11, 2026
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
36 changes: 30 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,14 @@ env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 'true'
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
SOLUTION: SharpEquations.slnx
LIBRARY_PROJECT: src/SharpEquations/SharpEquations.csproj
# the packages, all with the version of BuildVersion.props: the converter, and the equation editor on top of it
LIBRARY_PROJECTS: >-
src/SharpEquations/SharpEquations.csproj
src/SharpEquations.Editing/SharpEquations.Editing.csproj
src/SharpEquations.Rendering/SharpEquations.Rendering.csproj
src/SharpEquations.Avalonia/SharpEquations.Avalonia.csproj
TEST_PROJECT: tests/SharpEquations.Tests/SharpEquations.Tests.csproj
EDITOR_TEST_PROJECT: tests/SharpEquations.Avalonia.Tests/SharpEquations.Avalonia.Tests.csproj
FEED_URL: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json

# Stages: Resolve Version -> Test (Linux, Windows, macOS) -> Pack NuGet
Expand Down Expand Up @@ -103,18 +109,21 @@ jobs:
label: Linux x64
os: ubuntu-latest
tfms: net10.0 net8.0
editor_tfms: net10.0 net8.0
dotnet: |
8.0.x
10.0.x
- rid: win-x64
label: Windows x64
os: windows-latest
tfms: net10.0 net481
editor_tfms: net10.0
dotnet: 10.0.x
- rid: osx-arm64
label: macOS arm64
os: macos-latest
tfms: net10.0
editor_tfms: net10.0
dotnet: 10.0.x

steps:
Expand Down Expand Up @@ -150,6 +159,19 @@ jobs:
echo "::endgroup::"
done

# the equation editor (Avalonia 12 has no .NET Framework build: net10.0, and net8.0 where it is installed)
- name: Run editor tests
shell: bash
run: |
set -e
for TFM in ${{ matrix.editor_tfms }}; do
echo "::group::editor $TFM"
dotnet run --project "$EDITOR_TEST_PROJECT" -c Release -f "$TFM" --no-build -- \
--report-trx --report-trx-filename "${{ matrix.rid }}.editor.$TFM.trx" \
--results-directory "$GITHUB_WORKSPACE/TestResults"
echo "::endgroup::"
done

- name: Upload test results
if: always()
uses: actions/upload-artifact@v7
Expand Down Expand Up @@ -199,11 +221,13 @@ jobs:
# The .snupkg symbol package is written next to the .nupkg.
- name: Pack
run: |
dotnet pack "$LIBRARY_PROJECT" \
-c Release \
-o artifacts \
-p:VersionSuffix="$VERSION_SUFFIX" \
-p:ContinuousIntegrationBuild=true
for PROJECT in $LIBRARY_PROJECTS; do
dotnet pack "$PROJECT" \
-c Release \
-o artifacts \
-p:VersionSuffix="$VERSION_SUFFIX" \
-p:ContinuousIntegrationBuild=true
done
ls -lh artifacts

# nuget.org shows the README of the package: fail if it is missing or not declared.
Expand Down
69 changes: 69 additions & 0 deletions .github/workflows/demo.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Web demo

# The equation editor on WebAssembly (samples/SharpEquations.Avalonia.Browser.Demo): built on every pull request,
# published to GitHub Pages from main, or from any branch when the workflow is run by hand (Settings → Pages → Source:
# GitHub Actions; a branch other than main must be allowed in Settings → Environments → github-pages).
on:
push:
branches:
- main
pull_request:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: demo-${{ github.ref }}
cancel-in-progress: true

env:
DOTNET_NOLOGO: 'true'
DOTNET_CLI_TELEMETRY_OPTOUT: 'true'
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 'true'
PROJECT: samples/SharpEquations.Avalonia.Browser.Demo/SharpEquations.Avalonia.Browser.Demo.csproj

jobs:
build:
name: Build the web demo
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false

- name: Setup .NET
uses: actions/setup-dotnet@v6
with:
dotnet-version: '10.0.x'

- name: Install the WebAssembly workload
run: dotnet workload install wasm-tools

- name: Publish
run: dotnet publish "$PROJECT" -c Release -o publish

# GitHub Pages serves the site from a folder: no Jekyll, and the index for any path that is not a file
- name: Prepare the site
run: |
touch publish/wwwroot/.nojekyll
cp publish/wwwroot/index.html publish/wwwroot/404.html

- uses: actions/upload-pages-artifact@v4
with:
path: publish/wwwroot

deploy:
name: Deploy to GitHub Pages
if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: build
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v5
36 changes: 34 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,14 @@ 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# |
| `src/SharpEquations.Editing/` | The equation editor's model, no user interface: `MathNode` tree, caret, selection, undo, build up and build down |
| `src/SharpEquations.Rendering/` | The layout of MathML Core over the OpenType MATH table, no user interface: `MathLayoutEngine`, `MathLayout` |
| `src/SharpEquations.Avalonia/` | The controls for Avalonia: `MathEditBox` (the editor), `MathView`; embeds Libertinus Math |
| `samples/SharpEquations.Avalonia.Demo/` | A window with the editor (`DemoView`, shared with the web demo) |
| `samples/SharpEquations.Avalonia.Browser.Demo/` | The same demo on WebAssembly (needs the `wasm-tools` workload, so not in the solution; `.github/workflows/demo.yml` publishes it to GitHub Pages) |
| `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/SharpEquations.Avalonia.Tests/` | TUnit tests of the editor (Editing, Rendering, and the controls in a headless Avalonia session with Skia) |
| `tests/data/` | Test data and its provenance (`tests/data/README.md`) |

## Code Style
Expand Down Expand Up @@ -82,6 +88,27 @@ init-only record: `Display`, `Strict`, `PreserveFormatting`, `LaTeXFlavor` stand
UnicodeMath: a slash is a slash, not a fraction, and a bar without its pair is a bar, not an absolute value), and
written as standard LaTeX or as the LaTeX Word writes (`LaTeXSerializer`).

## The equation editor

`docs/EDITOR.md` is its design. The core does not depend on it: the editor is built on the public API of the core only,
and Avalonia is a dependency of `SharpEquations.Avalonia` alone (Editing and Rendering target netstandard2.0 and
net10.0 with no package, as the core).

- `Editing`: an equation being edited is a tree of `MathNode`s (the normalized MathML with a row, `mrow`, in every
slot) whose only text not built up is in runs. A run builds up with the nodes before it in its row: tokens are written
back as they were typed, structures as placeholders (Yi syllables, which the parsers read as identifiers) put back in
the result; this works because UnicodeMath round-trips exactly. Positions are paths of child indexes; offsets in runs
are code points.
- `Rendering`: `Layout/Engine.cs` lays out after MathML Core with the constants of the MATH table; the ink boxes of the
glyphs come from the outlines of the font (`OpenType/GlyphOutlines.cs`: `glyf`, or the CFF charstrings), never from
the rasterizer, and the tables from a copy of the font file (`OpenType/FontFile.cs`).
- `Avalonia`: `AvaloniaMathFont` reads the font file once with `TryGetStream` and asks Avalonia for its glyph typeface
only to draw, each time (`MathFonts.Register` registers the embedded font again with a new font manager: a headless
test session gives each test one, and a glyph typeface of the old one is released); the tests that use the headless
session are `[NotInParallel("headless")]`, since showing a window or typing runs the jobs of the dispatcher.
- The core's `EquationCommands` (public) lists the control words of UnicodeMath and the commands of LaTeX with their
symbols, for the completion of the editor; `PublicApi/EquationCommandsTests` checks that the parsers read them.

## Building and Testing

```bash
Expand All @@ -94,12 +121,17 @@ net481 (Windows, where the test project targets net10.0 and net481). The 738 exa
`tests/data/unicodemath/` test the UnicodeMath port, and `Property/RoundTripPropertyTests` checks, for all of them and
for the equations of Word documents (`tests/data/omml/`), that an equation written in its own format (UnicodeMath,
OMML) reads back to the same equation, and that one written in another format (standard LaTeX, Word's LaTeX, OMML)
loses only the first time what that format cannot hold. Keep these at 100%.
loses only the first time what that format cannot hold. Keep these at 100%. For the editor, an equation of the corpus
loaded in the editor, or built down to linear text and up again, is the same equation (100%); typed one character at a
time it is the equation of the whole text for 93% of the corpus (`Editing/CorpusTests`) and for 90% of the LaTeX of
latex2mathml (`Editing/RepositoryCorporaTests`), which a change must not lower. The public API of the editor's
packages is tracked in their `PublicAPI.*.txt`, as that of the core.

## Releases

`.github/workflows/ci.yml`, the same pipeline as SharpMediaInfo and Letra200bSharp: it tests on Linux (net10.0, net8.0),
Windows (net10.0, net481) and macOS (net10.0), packs, publishes a `pr-<N>-…` pre-release for each pull request (deleted on close by
Windows (net10.0, net481) and macOS (net10.0), packs the four packages (`SharpEquations` and the editor's `SharpEquations.Editing`, `.Rendering`, `.Avalonia`, with
one version), publishes a `pr-<N>-…` pre-release for each pull request (deleted on close by
`prerelease-cleanup.yml`) and, on `main`, tags `v<VersionPrefix>` of `BuildVersion.props`, creates the GitHub release
and pushes the package to GitHub Packages and nuget.org (Trusted Publishing, repository variable `NUGET_USER`). A change
that should release bumps `VersionPrefix`; one that should not says `[no-new-version]` in its commit message.
7 changes: 7 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia" Version="12.1.4" />
<PackageVersion Include="Avalonia.Browser" Version="12.1.4" />
<PackageVersion Include="Avalonia.Desktop" Version="12.1.4" />
<PackageVersion Include="Avalonia.Fonts.Inter" Version="12.1.4" />
<PackageVersion Include="Avalonia.Headless" Version="12.1.4" />
<PackageVersion Include="Avalonia.Skia" Version="12.1.4" />
<PackageVersion Include="Avalonia.Themes.Fluent" Version="12.1.4" />
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.401" PrivateAssets="All" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
Expand Down
9 changes: 9 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -300,3 +300,12 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

----------------------------------------------------------------------------
src/SharpEquations.Avalonia/Assets/Fonts/LibertinusMath-Regular.otf

Libertinus Math 7.040, from the Libertinus fonts, https://github.com/alerque/libertinus,
the math font that SharpEquations.Avalonia embeds for the systems without Cambria Math.
Copyright © 2012-2021 The Libertinus Project Authors, with Reserved Font Name
"Linux Libertine", "Biolinum", "STIX Fonts". Licensed under the SIL Open Font License,
Version 1.1: src/SharpEquations.Avalonia/Assets/Fonts/OFL.txt (LibertinusMath-OFL.txt in
the package).
42 changes: 41 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,46 @@ stream of its OLE object (`Equation.Parse(bytes, EquationFormat.Mtef)`); reading
Equations compare by their normalized MathML: an equation written in its own format (UnicodeMath, OMML) reads back to
the same equation, and one written in another format loses only what that format cannot hold.

## Equation editor for Avalonia

`SharpEquations.Avalonia` is a real-time equation editor in the manner of the `RichEditBox` of WinUI in math mode and of
the equations of Word: UnicodeMath or LaTeX typed at the caret builds up into the equation when a space or an operator
ends an argument (`a/b␣` becomes a fraction, `\alpha␣` α), the caret moves through the slots of the structures, and the
equation is read and written as MathML or as an `Equation`.

```xml
<sharp:MathEditBox xmlns:sharp="using:SharpEquations.Avalonia" FontSize="28" InputFormat="UnicodeMath" Equation="{Binding Equation}" />
```

```csharp
var editor = new MathEditBox { InputFormat = EquationFormat.LaTeX };
editor.EquationChanged += (_, e) => Console.WriteLine(e.Equation?.ToString(EquationFormat.MathML));
string mathML = editor.GetMath(); // as RichEditTextDocument.GetMath
editor.SetMath(mathML); // as RichEditTextDocument.SetMath
editor.ViewMode = MathViewMode.Linear; // the equation as one line of UnicodeMath (Ctrl+Shift+L)
editor.EquationNumber = "(1)"; // at the right of the line, as #(1) or \tag{1}
string latex = editor.GetSelectedText(EquationFormat.LaTeX)!;
editor.CopyTextFormat = EquationFormat.MathML; // Ctrl+C pastes as an equation into Word
editor.Editor.InsertLineBreak(); // Shift+Enter: lines aligned at &
editor.Editor.InsertTableRow(); // and columns, in the matrix of the caret
```

Typing `\` and the start of a command opens a floating bar at the caret with its completions
(`EquationCommands.Complete("al", EquationFormat.UnicodeMath)` gives them from the core); the bar also offers structures
for the selection, the commands of a table and quick symbols.

The packages are `SharpEquations.Avalonia` (the controls), `SharpEquations.Rendering` and `SharpEquations.Editing`, with
the version of `SharpEquations`.

`MathView` shows an equation without editing it. Both lay out MathML after MathML Core with the OpenType MATH table of
a math font: Cambria Math on Windows, and Libertinus Math (SIL Open Font License), embedded, elsewhere. The editor is
three projects, so that the core keeps no dependency: `SharpEquations.Editing` (the editing model, no user interface),
`SharpEquations.Rendering` (the layout, no user interface) and `SharpEquations.Avalonia` (the controls). The design is in
[docs/EDITOR.md](docs/EDITOR.md); `samples/SharpEquations.Avalonia.Demo` is a window with the editor and its MathML,
UnicodeMath and LaTeX, and `samples/SharpEquations.Avalonia.Browser.Demo` the same demo in the browser, on WebAssembly
(published to GitHub Pages from `main`; to run it locally: `dotnet workload install wasm-tools`, then
`dotnet run --project samples/SharpEquations.Avalonia.Browser.Demo`).

## Architecture

How the conversions work, format by format, and how round trips are kept exact: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
Expand All @@ -67,7 +107,7 @@ its README).
## Releases

CI (`.github/workflows/ci.yml`) tests every push and pull request on Linux (.NET 10 and .NET 8), Windows (.NET 10 and
.NET Framework 4.8.1) and macOS (.NET 10), and packs the NuGet package. The release version is `VersionPrefix` in `BuildVersion.props`: bump it, and merging to
.NET Framework 4.8.1) and macOS (.NET 10), and packs the NuGet packages (the converter and the three of the editor, with one version). The release version is `VersionPrefix` in `BuildVersion.props`: bump it, and merging to
`main` tags `vX.Y.Z`, creates the GitHub release with the package attached, and publishes it to GitHub Packages and to
nuget.org (with [Trusted Publishing](https://learn.microsoft.com/nuget/nuget-org/trusted-publishing): the repository
variable `NUGET_USER` names the nuget.org account; without it nuget.org is skipped). A push that keeps the version on
Expand Down
13 changes: 12 additions & 1 deletion SharpEquations.slnx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
<Solution>
<Folder Name="/demo/" Id="5d20aa90-6969-d8bd-9dcd-8634f4692fda">
<!-- built by demo.yml only: it needs the wasm-tools workload -->
<Project Path="samples/SharpEquations.Avalonia.Browser.Demo/SharpEquations.Avalonia.Browser.Demo.csproj">
<Build Project="false" />
</Project>
<Project Path="samples/SharpEquations.Avalonia.Demo/SharpEquations.Avalonia.Demo.csproj" />
</Folder>
<Folder Name="/Solution Items/">
<File Path="AGENTS.md" />
<File Path="BuildVersion.props" />
Expand All @@ -9,14 +16,18 @@
<File Path="README.md" />
</Folder>
<Folder Name="/src/">
<Project Path="src/SharpEquations/SharpEquations.csproj" />
<Project Path="src/SharpEquations.Avalonia/SharpEquations.Avalonia.csproj" />
<Project Path="src/SharpEquations.Editing/SharpEquations.Editing.csproj" />
<Project Path="src/SharpEquations.RegexPolyfill/SharpEquations.RegexPolyfill.csproj" />
<Project Path="src/SharpEquations.Rendering/SharpEquations.Rendering.csproj" />
<Project Path="src/SharpEquations.XsltGenerator/SharpEquations.XsltGenerator.csproj" />
<Project Path="src/SharpEquations/SharpEquations.csproj" />
</Folder>
<Folder Name="/benchmarks/">
<Project Path="benchmarks/SharpEquations.Benchmarks/SharpEquations.Benchmarks.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/SharpEquations.Avalonia.Tests/SharpEquations.Avalonia.Tests.csproj" />
<Project Path="tests/SharpEquations.Tests/SharpEquations.Tests.csproj" />
</Folder>
</Solution>
Loading
Loading