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
49 changes: 49 additions & 0 deletions .github/workflows/Blazor.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
permissions:
contents: read
name: Blazor example
on:
push:
branches: [master]
pull_request:
paths:
- CSharpMath.Blazor.Example/**
- CSharpMath.Blazor.Example.Tests/**
- CSharpMath/**
- CSharpMath.SkiaSharp/**
- CSharpMath.Rendering/**
- .github/workflows/Blazor.yml
- CSharpMath.slnx
- Directory.Build.props
- Directory.Build.targets
- global.json
- Typography/**
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
with: { submodules: recursive }
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
with: { dotnet-version: 10.0.x }
- run: dotnet workload install wasm-tools --skip-manifest-update
- run: dotnet test CSharpMath.Blazor.Example.Tests/CSharpMath.Blazor.Example.Tests.csproj --configuration Release
- run: dotnet publish CSharpMath.Blazor.Example/CSharpMath.Blazor.Example.csproj --configuration Release --output "$RUNNER_TEMP/csharpmath-blazor"
- name: Install pinned browser smoke dependencies
working-directory: browser-smoke
run: npm ci --ignore-scripts
- name: Install Chromium
working-directory: browser-smoke
run: npx playwright install --with-deps chromium
- name: Serve and run browser smoke
shell: bash
run: |
python3 -m http.server 4173 --directory "$RUNNER_TEMP/csharpmath-blazor/wwwroot" >"$RUNNER_TEMP/blazor-server.log" 2>&1 &
server=$!
trap "kill $server" EXIT
npm --prefix browser-smoke run smoke
- name: Verify browser assets
shell: pwsh
run: |
$root = Join-Path "$env:RUNNER_TEMP/csharpmath-blazor" "wwwroot"
$required = @('_framework/blazor.webassembly.js', '_framework/dotnet.native*.wasm', '_framework/*SkiaSharp*.wasm', '_content/SkiaSharp.Views.Blazor/SKHtmlCanvas.js')
foreach ($pattern in $required) { if (-not (Get-ChildItem (Join-Path $root $pattern) -ErrorAction SilentlyContinue)) { throw "Missing published asset: $pattern" } }
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup><TargetFramework>net10.0</TargetFramework><OutputType>Exe</OutputType><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup>
<ItemGroup><Compile Include="..\CSharpMath.Blazor.Example\FormulaEditorState.cs" Link="FormulaEditorState.cs" /></ItemGroup>
</Project>
23 changes: 23 additions & 0 deletions CSharpMath.Blazor.Example.Tests/FormulaEditorStateTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using CSharpMath.Blazor.Example;
using Xunit;

public class FormulaEditorStateTests {
[Fact]
public void Default_is_multiline_and_reset_restores_it() {
var state = new FormulaEditorState();
Assert.Contains("\\\\", state.Latex);
var initialRevision = state.Revision;
state.SetLatex("x");
state.Reset();
Assert.Equal(FormulaEditorState.DefaultLatex, state.Latex);
Assert.Equal(initialRevision + 2, state.Revision);
}

[Fact]
public void SetLatex_preserves_invalid_input_for_renderer_error_display() {
var state = new FormulaEditorState();
state.SetLatex(@"\\notacommand{");
Assert.Equal(@"\\notacommand{", state.Latex);
Assert.Equal(1, state.Revision);
}
}
10 changes: 10 additions & 0 deletions CSharpMath.Blazor.Example/App.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Router AppAssembly="typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="typeof(Layout.MainLayout)"><p>Sorry, there is nothing at this address.</p></LayoutView>
</NotFound>
</Router>
19 changes: 19 additions & 0 deletions CSharpMath.Blazor.Example/CSharpMath.Blazor.Example.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<!-- Standalone Blazor WebAssembly uses net10.0 with the browser-wasm runtime. -->
<TargetFramework>net10.0</TargetFramework>
<RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
<RootNamespace>CSharpMath.Blazor.Example</RootNamespace>
<AssemblyName>CSharpMath.Blazor.Example</AssemblyName>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization>
<!-- Link SkiaSharp native WebAssembly assets for the interpreter build. -->
<WasmBuildNative>true</WasmBuildNative>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\CSharpMath.SkiaSharp\CSharpMath.SkiaSharp.csproj" />
<PackageReference Include="SkiaSharp.Views.Blazor" Version="3.119.4" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0" />
</ItemGroup>
</Project>
11 changes: 11 additions & 0 deletions CSharpMath.Blazor.Example/FormulaEditorState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace CSharpMath.Blazor.Example;

/// Small, renderer-independent state holder used by the sample and smoke tests.
public sealed class FormulaEditorState {
public const string DefaultLatex = @"f(x) = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}\\
\int_0^\infty e^{-x^2}\,dx = \frac{\sqrt{\pi}}{2}";
public string Latex { get; private set; } = DefaultLatex;
public int Revision { get; private set; }
public void SetLatex(string value) { Latex = value; Revision++; }
public void Reset() { SetLatex(DefaultLatex); }
}
4 changes: 4 additions & 0 deletions CSharpMath.Blazor.Example/Layout/MainLayout.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@inherits LayoutComponentBase
<div class="page">
<main><article class="content px-4">@Body</article></main>
</div>
2 changes: 2 additions & 0 deletions CSharpMath.Blazor.Example/Layout/MainLayout.razor.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.page { min-height: 100vh; }
main { max-width: 1100px; margin: 0 auto; }
36 changes: 36 additions & 0 deletions CSharpMath.Blazor.Example/Pages/Home.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
@page "/"
@using CSharpMath.SkiaSharp
@using global::SkiaSharp.Views.Blazor
<PageTitle>CSharpMath Blazor example</PageTitle>

<h1>CSharpMath in Blazor</h1>
<p>Edit the LaTeX below. The canvas resizes with its container and redraws after every change.</p>
<div class="editor-grid">
<label for="latex">LaTeX</label>
<textarea id="latex" @bind="latex" @bind:event="oninput" @bind:after="Invalidate" rows="6" aria-describedby="formula-error"></textarea>
<button @onclick="Reset">Reset example</button>
<div class="math-canvas" role="img" aria-label="Rendered LaTeX formula" aria-describedby="formula-text">
<SKCanvasView class="formula-canvas" @ref="canvas" OnPaintSurface="PaintSurface" IgnorePixelScaling="true" />
</div>
<span id="formula-text" class="sr-only">LaTeX formula: @latex</span>
</div>
@if (!string.IsNullOrWhiteSpace(error)) { <p id="formula-error" class="error" role="alert" aria-live="assertive">@error</p> }

@code {
private readonly FormulaEditorState state = new();
private string latex { get => state.Latex; set => state.SetLatex(value); }
private string? error;
private SKCanvasView? canvas;
private MathPainter painter = new() { FontSize = 24, LineStyle = CSharpMath.Atom.LineStyle.Display };

private void PaintSurface(global::SkiaSharp.Views.Blazor.SKPaintSurfaceEventArgs args) {
var surface = args.Surface;
surface.Canvas.Clear(global::SkiaSharp.SKColors.White);
painter.LaTeX = latex;
error = painter.ErrorMessage;
painter.Draw(surface.Canvas, CSharpMath.Rendering.FrontEnd.TextAlignment.Center);
}

private void Reset() { state.Reset(); latex = state.Latex; error = null; canvas?.Invalidate(); }
private void Invalidate() { painter.LaTeX = latex; error = painter.ErrorMessage; canvas?.Invalidate(); }
}
6 changes: 6 additions & 0 deletions CSharpMath.Blazor.Example/Pages/Home.razor.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
h1 { color: #512bd4; }
.editor-grid { display: grid; gap: .75rem; }
textarea { font: 1rem ui-monospace, monospace; width: 100%; box-sizing: border-box; }
.math-canvas { min-height: 180px; width: 100%; border: 1px solid #d5d5d5; background: white; }
.error { color: #b00020; white-space: pre-wrap; }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
8 changes: 8 additions & 0 deletions CSharpMath.Blazor.Example/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using CSharpMath.Blazor.Example;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
await builder.Build().RunAsync();
9 changes: 9 additions & 0 deletions CSharpMath.Blazor.Example/_Imports.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using CSharpMath.Blazor.Example
@using CSharpMath.Blazor.Example.Layout
4 changes: 4 additions & 0 deletions CSharpMath.Blazor.Example/wwwroot/css/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
html, body { font-family: system-ui, sans-serif; margin: 0; color: #202124; }
/* This must be global: CSS isolation cannot target the canvas rendered inside SKCanvasView. */
.formula-canvas { display: block; width: 100%; height: 180px; }
#blazor-error-ui { display: none; position: fixed; bottom: 0; width: 100%; padding: .75rem; background: #fee; }
1 change: 1 addition & 0 deletions CSharpMath.Blazor.Example/wwwroot/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions CSharpMath.Blazor.Example/wwwroot/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>CSharpMath Blazor</title><base href="/" /><link href="css/app.css" rel="stylesheet" /><link rel="icon" href="favicon.svg" /></head>
<body><div id="app"><p>Loading…</p></div><div id="blazor-error-ui">An unexpected error occurred.<a href="" class="reload">Reload</a></div><script src="_framework/blazor.webassembly.js"></script></body>
</html>
6 changes: 6 additions & 0 deletions CSharpMath.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@
<Project Path="CSharpMath.SkiaSharp.Example/CSharpMath.SkiaSharp.Example.csproj">
<BuildType Solution="Not.Uno.Example|*" Project="Release" />
</Project>
<Project Path="CSharpMath.Blazor.Example/CSharpMath.Blazor.Example.csproj">
<BuildType Solution="Not.Uno.Example|*" Project="Release" />
</Project>
<Project Path="CSharpMath.Blazor.Example.Tests/CSharpMath.Blazor.Example.Tests.csproj">
<BuildType Solution="Not.Uno.Example|*" Project="Release" />
</Project>
<Project Path="CSharpMath.Uno/CSharpMath.Uno.csproj">
<BuildType Solution="Not.Uno.Example|*" Project="Release" />
</Project>
Expand Down
15 changes: 15 additions & 0 deletions ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,3 +390,18 @@ Shhh... Don't tell anybody!
0.4.0: Math evaluation??
0.5.0: Handwritten math recognition???
-->

## Blazor WebAssembly

`CSharpMath.Blazor.Example` is a standalone browser-WASM sample. It owns the canvas lifecycle while `MathPainter` owns parsing, measuring, and drawing:

```xml
<PackageReference Include="CSharpMath.SkiaSharp" Version="3.119.4" />
<PackageReference Include="SkiaSharp.Views.Blazor" Version="3.119.4" />
```

Use `Microsoft.NET.Sdk.BlazorWebAssembly`, target `net10.0`, and set `RuntimeIdentifier` to `browser-wasm`. The sample uses the package's `SKCanvasView` size/DPI watchers and invalidates it after editor changes. Invalid LaTeX remains visible as an error message, and the default demonstrates a multiline formula.

The normal build uses the interpreter, avoiding the download and build cost of AOT. `dotnet publish -p:RunAOTCompilation=true` can be used when faster steady-state execution is worth a substantially larger download and longer publish; AOT is a publish-time choice, not required for development. The generated `wwwroot` can be served by any static host with SPA fallback to `index.html` (including correct MIME types for `.wasm`).

This sample is specifically standalone Blazor WebAssembly. Blazor Server is not tested or packaged here: it may be possible to host the component in a Server app, but browser-WASM Skia assets, canvas initialization, latency, and server-side rendering constraints require a separate integration test before claiming support.
75 changes: 75 additions & 0 deletions browser-smoke/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions browser-smoke/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"private": true,
"scripts": { "smoke": "node smoke.mjs" },
"devDependencies": { "@playwright/test": "1.52.0" }
}
Loading
Loading