diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index fe4dd39..a355cb3 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -12,16 +12,19 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v5 with: - dotnet-version: 10.0.x + dotnet-version: | + 8.0.x + 9.0.x + 10.0.x - name: Install Tools run: dotnet tool restore - working-directory: ./fsharp-view-engine + working-directory: ./lib - name: Install Packages run: dotnet paket install - working-directory: ./fsharp-view-engine + working-directory: ./lib - name: Test run: ./fake.sh Test - working-directory: ./fsharp-view-engine + working-directory: ./lib preview: name: Preview runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8a5a16e..1d0478e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,16 +14,19 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v5 with: - dotnet-version: 10.0.x + dotnet-version: | + 8.0.x + 9.0.x + 10.0.x - name: Restore Tools run: dotnet tool restore - working-directory: ./fsharp-view-engine + working-directory: ./lib - name: Install Packages run: dotnet paket install - working-directory: ./fsharp-view-engine + working-directory: ./lib - name: Publish - run: ./fake.sh Publish - working-directory: ./fsharp-view-engine + run: ./fake.sh PushNugets + working-directory: ./lib env: NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} deploy: diff --git a/.gitignore b/.gitignore index 5eec986..4e52848 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ +.idea +.vscode .claude diff --git a/fsharp-view-engine/AGENTS.md b/AGENTS.md similarity index 100% rename from fsharp-view-engine/AGENTS.md rename to AGENTS.md diff --git a/CLAUDE.md b/CLAUDE.md index 3dd6247..c89bd6c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,21 +3,24 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview -FSharp.ViewEngine is a view engine for F# web applications. It provides a functional approach to generating HTML with type-safe F# code, including integrated support for HTMX, Alpine.js, Tailwind CSS, and SVG elements. +FSharp.ViewEngine is a view engine for F# web applications. It provides a functional approach to generating HTML with type-safe F# code, including integrated support for HTMX, Alpine.js, Datastar, Tailwind CSS, and SVG elements. ## Repository Structure -- `fsharp-view-engine/` — F# library, app, tests, and build system -- `pulumi/` — Pulumi infrastructure (TypeScript) for deploying the demo app -- `.github/` — CI workflows (at repo root, with `working-directory: fsharp-view-engine`) +- `lib/` — Core F# library, tests, benchmarks, and build system +- `docs/` — Documentation Giraffe web app with Markdown pages and build system +- `etc/` — Assets (logos) +- `pulumi/` — Pulumi infrastructure (TypeScript) for deploying the docs app +- `.github/` — CI workflows - `.claude/` — Claude Code settings ## Common Development Commands **Important:** When running in a bash shell (including Claude Code), always use `./fake.sh` instead of `fake.cmd`. +### Library (`lib/`) + ```bash -# All dotnet/fake commands run from fsharp-view-engine/ -cd fsharp-view-engine +cd lib # Restore tools and packages dotnet tool restore @@ -30,13 +33,28 @@ dotnet run --project src/Tests/Tests.fsproj # Direct # Run a single test by name dotnet run --project src/Tests/Tests.fsproj -- --filter "Should render html document" -# Run demo app with Tailwind watch -./fake.sh Watch - # Create NuGet package (needs GITHUB_REF_NAME env var) ./fake.sh Pack +``` + +### Docs app (`docs/`) + +```bash +cd docs -# Pulumi deployment +# Run docs app with Tailwind watch (hot reload) +./fake.sh Watch + +# Build CSS for production +./fake.sh BuildCss + +# Publish docs app +./fake.sh Publish +``` + +### Pulumi deployment + +```bash cd pulumi npm install pulumi up -s prod @@ -44,17 +62,18 @@ pulumi up -s prod ## Architecture -### Core Type System (`fsharp-view-engine/src/FSharp.ViewEngine/Core.fs`) +### Core Type System (`lib/src/FSharp.ViewEngine/Core.fs`) Two discriminated unions form the foundation: - **Element**: `Text | Tag | Void | Fragment | Raw | Noop` — represents DOM nodes - **Attribute**: `KeyValue | Boolean | Children | Noop` — represents HTML attributes Rendering uses `StringBuilder` with recursive pattern matching. `Text` is HTML-encoded; `Raw` is not. `Void` elements (e.g., `br`, `img`) are self-closing and reject children. -### Module Organization +### Module Organization (`lib/src/FSharp.ViewEngine/`) - `Html.fs` — Standard HTML elements and attributes as static members on `Html` type - `Htmx.fs` — HTMX attributes (`_hxGet`, `_hxPost`, etc.) on `Htmx` type - `Alpine.fs` — Alpine.js directives (`_xData`, `_xShow`, etc.) on `Alpine` type +- `Datastar.fs` — Datastar attributes (`_dsSignals`, `_dsOn`, etc.) on `Datastar` type - `Tailwind.fs` — Tailwind UI custom elements on `Tailwind` type - `Svg.fs` — SVG elements and attributes on `Svg` type @@ -64,31 +83,38 @@ open FSharp.ViewEngine open type Html open type Htmx -div [ _class "container"; _hxGet "/api"; _children [ h1 "Hello" ] ] -|> Element.render +div { + _class "container" + _hxGet "/api" + h1 { "Hello" } +} +|> Render.toHtmlDocString ``` ### Project Structure -- `fsharp-view-engine/src/FSharp.ViewEngine/` — Core library (NuGet package) -- `fsharp-view-engine/src/Tests/` — xUnit tests -- `fsharp-view-engine/src/Build/` — FAKE build system (`Program.fs` defines targets) -- `fsharp-view-engine/src/App/` — Demo Giraffe web app with Markdown docs +- `lib/src/FSharp.ViewEngine/` — Core library (NuGet package) +- `lib/src/Tests/` — Expecto tests +- `lib/src/Benchmarks/` — BenchmarkDotNet benchmarks +- `lib/src/Build/` — FAKE build system (Test, Pack, PushNugets targets) +- `docs/src/Docs/` — Documentation Giraffe web app with Markdown pages +- `docs/src/Build/` — FAKE build system (Watch, BuildCss, Publish targets) - `pulumi/` — Infrastructure as code (Pulumi + TypeScript) ## Development Patterns - **New HTML elements** in `Html.fs`: use `Tag` for normal elements, `Void` for self-closing. Add convenience overloads (e.g., `p (text:string)`). -- **Framework attributes**: HTMX → `Htmx.fs` with `_hx` prefix; Alpine → `Alpine.fs` with `_x` prefix. +- **Framework attributes**: HTMX → `Htmx.fs` with `_hx` prefix; Alpine → `Alpine.fs` with `_x` prefix; Datastar → `Datastar.fs` with `_ds` prefix. +- **New doc pages**: Add markdown in `docs/src/Docs/docs/`, handler in `Handlers.fs`, route in `Program.fs`, nav link in `Views.fs`. - **Tests** compare rendered HTML strings using `String.clean` for whitespace normalization. Use `// language=HTML` comment for IDE syntax highlighting in expected strings. ## Build System -- FAKE build scripts invoked via `fake.cmd`/`fake.sh` -- Paket for package management (`paket.dependencies` at root of `fsharp-view-engine/`) +- FAKE build scripts invoked via `fake.cmd`/`fake.sh` (separate in `lib/` and `docs/`) +- Paket for package management (`paket.dependencies` in `lib/` and `docs/`) - .NET 10.0 SDK (`global.json`) - CI: GitHub Actions — tests on PRs, NuGet publish on release tags (`v*.*.*`) ## Infrastructure (Pulumi) - TypeScript Pulumi project in `pulumi/` -- Deploys demo app to Kubernetes via AWS ECR + Cloudflare Tunnel +- Deploys docs app to Kubernetes via AWS ECR - Domain: `fsharpviewengine.meiermade.com` - Stack references: `identity`, `infrastructure`, `fsharp-view-engine-identity` diff --git a/fsharp-view-engine/LICENSE b/LICENSE similarity index 100% rename from fsharp-view-engine/LICENSE rename to LICENSE diff --git a/README.md b/README.md index 2214ce8..312700e 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,15 @@ [![Release](https://github.com/meiermade/FSharp.ViewEngine/actions/workflows/release.yml/badge.svg)](https://github.com/meiermade/FSharp.ViewEngine/actions/workflows/release.yml) +

+ FSharp.ViewEngine +

+ # FSharp.ViewEngine -View engine for F#. Inspired by [Giraffe.ViewEngine](https://github.com/giraffe-fsharp/Giraffe.ViewEngine) and -[Feliz.ViewEngine](https://github.com/dbrattli/Feliz.ViewEngine). +View engine for F#. Inspired by [Giraffe.ViewEngine](https://github.com/giraffe-fsharp/Giraffe.ViewEngine), +[Feliz.ViewEngine](https://github.com/dbrattli/Feliz.ViewEngine), and +[Oxpecker.ViewEngine](https://github.com/Lanayx/Oxpecker). Documentation site built using FSharp.ViewEngine available at [https://fsharpviewengine.meiermade.com](https://fsharpviewengine.meiermade.com). -> See [App](./src/App) for the source code. +> See [docs/src/Docs](./docs/src/Docs) for the source code. ## Installation Add the core view engine package. @@ -18,47 +23,38 @@ open FSharp.ViewEngine open type Html open type Htmx open type Alpine +open type Datastar open type Tailwind -html [ +html { _lang "en" - _children [ - head [ - title "Test" - meta [ _charset "utf-8" ] - link [ _href "/css/compiled.css"; _rel "stylesheet" ] - ] - body [ - _xData "{showContent: false}" - _class "bg-gray-50" - _children [ - div [ - _id "page" - _class [ "flex"; "flex-col" ] - _children [ - h1 [ _hxGet "/hello"; _hxTarget "#page"; _children "Hello" ] - h1 [ _hxGet "/world"; _hxTarget "#page"; _children "World" ] - ] - ] - br - div [ - _xShow "showContent" - _children [ - h2 [ _children "Content" ] - p [ _children "Some content" ] - ul [ - _children [ - li [ _children "One" ] - li [ _children "Two" ] - ] - ] - ] - ] - ] - ] - ] -] -|> Element.render + head { + title "Test" + meta { _charset "utf-8" } + link { _href "/css/compiled.css"; _rel "stylesheet" } + } + body { + _xData "{showContent: false}" + _class "bg-gray-50" + div { + _id "page" + _class [ "flex"; "flex-col" ] + h1 { _hxGet "/hello"; _hxTarget "#page"; "Hello" } + h1 { _hxGet "/world"; _hxTarget "#page"; "World" } + } + br + div { + _xShow "showContent" + h2 { "Content" } + p { "Some content" } + ul { + li { "One" } + li { "Two" } + } + } + } +} +|> Render.toHtmlDocString ``` ```html @@ -85,3 +81,40 @@ html [ ``` + +## Benchmarks +Ran on February 6, 2026 with BenchmarkDotNet MediumRun only. + +Environment: +- Windows 11 (10.0.26200.7623) +- 12th Gen Intel Core i9-12900HK +- .NET SDK 10.0.102, .NET Runtime 10.0.2 (X64 RyuJIT AVX2) + +Command: +``` +cd lib && dotnet run -c Release --project src/Benchmarks/Benchmarks.fsproj +``` + +BuildAndRender (mean, lower is better): +| Method | Mean | Allocated | +|-------------- |----------:|----------:| +| ViewEngineApi | 5.763 μs | 11.4 KB | +| OxpeckerApi | 7.562 μs | 12.88 KB | +| GiraffeApi | 7.925 μs | 23.95 KB | +| FelizApi | 11.053 μs | 25.87 KB | + +RenderOnly: +| Method | Mean | Allocated | +|-------------- |---------:|----------:| +| ViewEngineApi | 2.464 μs | 2.94 KB | +| OxpeckerApi | 2.796 μs | 2.94 KB | +| GiraffeApi | 3.176 μs | 12.77 KB | +| FelizApi | 6.151 μs | 14.2 KB | + +BuildOnly: +| Method | Mean | Allocated | +|-------------- |---------:|----------:| +| ViewEngineApi | 2.153 μs | 8.46 KB | +| OxpeckerApi | 5.275 μs | 9.95 KB | +| GiraffeApi | 7.323 μs | 11.17 KB | +| FelizApi | 7.707 μs | 11.66 KB | diff --git a/fsharp-view-engine/.config/dotnet-tools.json b/docs/.config/dotnet-tools.json similarity index 100% rename from fsharp-view-engine/.config/dotnet-tools.json rename to docs/.config/dotnet-tools.json diff --git a/fsharp-view-engine/.dockerignore b/docs/.dockerignore similarity index 100% rename from fsharp-view-engine/.dockerignore rename to docs/.dockerignore diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..7e44b7a --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,5 @@ +.paket +obj +bin +paket-files +*DotSettings.user diff --git a/fsharp-view-engine/Dockerfile b/docs/Dockerfile similarity index 87% rename from fsharp-view-engine/Dockerfile rename to docs/Dockerfile index 1f72440..744574c 100644 --- a/fsharp-view-engine/Dockerfile +++ b/docs/Dockerfile @@ -20,12 +20,12 @@ RUN dotnet paket install && dotnet paket restore COPY src src COPY fake.sh ./ RUN chmod +x fake.sh -RUN ./fake.sh PublishApp +RUN ./fake.sh Publish FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled WORKDIR /app -COPY --from=build /app/src/App/out . +COPY --from=build /app/src/Docs/out . -ENTRYPOINT ["dotnet", "App.dll"] +ENTRYPOINT ["dotnet", "Docs.dll"] diff --git a/fsharp-view-engine/fake.cmd b/docs/fake.cmd similarity index 100% rename from fsharp-view-engine/fake.cmd rename to docs/fake.cmd diff --git a/fsharp-view-engine/fake.sh b/docs/fake.sh similarity index 100% rename from fsharp-view-engine/fake.sh rename to docs/fake.sh diff --git a/fsharp-view-engine/paket.dependencies b/docs/paket.dependencies similarity index 72% rename from fsharp-view-engine/paket.dependencies rename to docs/paket.dependencies index 17c8c0d..4c28c1e 100644 --- a/fsharp-view-engine/paket.dependencies +++ b/docs/paket.dependencies @@ -2,7 +2,6 @@ source https://api.nuget.org/v3/index.json storage: none nuget Fake.Core.Target +nuget FSharp.Core nuget Giraffe -nuget JetBrains.Annotations nuget Markdig -nuget Expecto diff --git a/fsharp-view-engine/paket.lock b/docs/paket.lock similarity index 76% rename from fsharp-view-engine/paket.lock rename to docs/paket.lock index 6047145..f1dffa1 100644 --- a/fsharp-view-engine/paket.lock +++ b/docs/paket.lock @@ -1,9 +1,6 @@ STORAGE: NONE NUGET remote: https://api.nuget.org/v3/index.json - Expecto (10.2.3) - FSharp.Core (>= 7.0.200) - restriction: >= net6.0 - Mono.Cecil (>= 0.11.4 < 1.0) - restriction: >= net6.0 Fake.Core.CommandLineParsing (6.1.4) - restriction: >= netstandard2.0 FParsec (>= 1.1.1) - restriction: >= netstandard2.0 FSharp.Core (>= 8.0.400) - restriction: >= netstandard2.0 @@ -48,7 +45,7 @@ NUGET FSharp.Control.Reactive (6.1.2) - restriction: >= netstandard2.0 FSharp.Core (>= 6.0.7) - restriction: >= netstandard2.0 System.Reactive (>= 6.0.1) - restriction: >= netstandard2.0 - FSharp.Core (10.0.102) - restriction: >= netstandard2.0 + FSharp.Core (10.0.102) FSharp.SystemTextJson (1.4.36) - restriction: >= net6.0 FSharp.Core (>= 4.7) - restriction: >= netstandard2.0 System.Text.Json (>= 6.0.10) - restriction: >= netstandard2.0 @@ -60,15 +57,10 @@ NUGET System.Text.Json (>= 8.0.6) - restriction: >= net6.0 Giraffe.ViewEngine (1.4) - restriction: >= net6.0 FSharp.Core (>= 5.0) - restriction: >= netstandard2.0 - JetBrains.Annotations (2025.2.4) - System.Runtime (>= 4.1) - restriction: && (< net20) (>= netstandard1.0) (< netstandard2.0) Markdig (0.44) System.Memory (>= 4.6.3) - restriction: || (>= net462) (&& (>= netstandard2.0) (< netstandard2.1)) Microsoft.Bcl.AsyncInterfaces (10.0.2) - restriction: || (&& (>= net462) (>= net6.0)) (&& (>= net6.0) (< net8.0)) Microsoft.IO.RecyclableMemoryStream (3.0.1) - restriction: >= net6.0 - Microsoft.NETCore.Platforms (7.0.4) - restriction: || (&& (< monoandroid) (< net20) (>= netstandard1.0) (< netstandard1.2) (< win8) (< wp8)) (&& (< monoandroid) (< net20) (>= netstandard1.2) (< netstandard1.3) (< win8) (< wpa81)) (&& (< monoandroid) (< net20) (>= netstandard1.3) (< netstandard1.5) (< win8) (< wpa81)) (&& (< monotouch) (< net20) (>= netstandard1.5) (< netstandard2.0) (< win8) (< wpa81) (< xamarintvos) (< xamarinwatchos)) - Microsoft.NETCore.Targets (5.0) - restriction: || (&& (< monoandroid) (< net20) (>= netstandard1.0) (< netstandard1.2) (< win8) (< wp8)) (&& (< monoandroid) (< net20) (>= netstandard1.2) (< netstandard1.3) (< win8) (< wpa81)) (&& (< monoandroid) (< net20) (>= netstandard1.3) (< netstandard1.5) (< win8) (< wpa81)) (&& (< monotouch) (< net20) (>= netstandard1.5) (< netstandard2.0) (< win8) (< wpa81) (< xamarintvos) (< xamarinwatchos)) - Mono.Cecil (0.11.6) - restriction: >= net6.0 System.Buffers (4.6.1) - restriction: || (>= net462) (&& (>= net6.0) (< net8.0)) (&& (< netcoreapp2.1) (>= netstandard2.0) (< netstandard2.1)) System.Collections.Immutable (10.0.2) - restriction: >= netstandard2.0 System.Memory (>= 4.6.3) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0)) @@ -84,9 +76,6 @@ NUGET System.Numerics.Vectors (4.6.1) - restriction: || (>= net462) (&& (< netcoreapp2.1) (>= netstandard2.0) (< netstandard2.1)) System.Reactive (6.1) - restriction: >= netstandard2.0 System.Threading.Tasks.Extensions (>= 4.5.4) - restriction: || (>= net472) (&& (< net6.0) (>= netstandard2.0)) (>= uap10.1) - System.Runtime (4.3.1) - restriction: && (< net20) (>= netstandard1.0) (< netstandard2.0) - Microsoft.NETCore.Platforms (>= 1.1.1) - restriction: || (&& (< monoandroid) (< net45) (>= netstandard1.0) (< netstandard1.2) (< win8) (< wp8)) (&& (< monoandroid) (< net45) (>= netstandard1.2) (< netstandard1.3) (< win8) (< wpa81)) (&& (< monoandroid) (< net45) (>= netstandard1.3) (< netstandard1.5) (< win8) (< wpa81)) (&& (< monotouch) (< net45) (>= netstandard1.5) (< win8) (< wpa81) (< xamarinios) (< xamarinmac) (< xamarintvos) (< xamarinwatchos)) - Microsoft.NETCore.Targets (>= 1.1.3) - restriction: || (&& (< monoandroid) (< net45) (>= netstandard1.0) (< netstandard1.2) (< win8) (< wp8)) (&& (< monoandroid) (< net45) (>= netstandard1.2) (< netstandard1.3) (< win8) (< wpa81)) (&& (< monoandroid) (< net45) (>= netstandard1.3) (< netstandard1.5) (< win8) (< wpa81)) (&& (< monotouch) (< net45) (>= netstandard1.5) (< win8) (< wpa81) (< xamarinios) (< xamarinmac) (< xamarintvos) (< xamarinwatchos)) System.Runtime.CompilerServices.Unsafe (6.1.2) - restriction: || (>= net462) (&& (>= net6.0) (< net8.0)) (&& (< net8.0) (>= netstandard2.0)) (&& (< netcoreapp2.1) (>= netstandard2.0) (< netstandard2.1)) System.Text.Encodings.Web (10.0.2) - restriction: || (&& (< net10.0) (>= net9.0)) (&& (>= net462) (>= net6.0)) (&& (>= net6.0) (< net8.0)) (&& (>= net8.0) (< net9.0)) System.Buffers (>= 4.6.1) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0)) diff --git a/fsharp-view-engine/src/Build/Build.fsproj b/docs/src/Build/Build.fsproj similarity index 100% rename from fsharp-view-engine/src/Build/Build.fsproj rename to docs/src/Build/Build.fsproj diff --git a/docs/src/Build/Program.fs b/docs/src/Build/Program.fs new file mode 100644 index 0000000..a748dc4 --- /dev/null +++ b/docs/src/Build/Program.fs @@ -0,0 +1,65 @@ +open Fake.Core +open Fake.Core.TargetOperators +open Fake.IO +open Fake.IO.FileSystemOperators +open Fake.IO.Globbing.Operators +open System.Text.RegularExpressions + +System.Environment.GetCommandLineArgs() +|> Array.tail +|> Array.toList +|> Context.FakeExecutionContext.Create false "build.fsx" +|> Context.RuntimeContext.Fake +|> Context.setExecutionContext + +let inline (==>!) x y = x ==> y |> ignore + +let srcDir = Path.getDirectory __SOURCE_DIRECTORY__ +let rootDir = Path.getDirectory srcDir +let docsDir = srcDir "Docs" + +let exec workDir cmd args = + CreateProcess.fromRawCommand cmd args + |> CreateProcess.withWorkingDirectory workDir + |> CreateProcess.ensureExitCode + |> Proc.start + |> Async.AwaitTask + |> Async.Ignore + +let execEnv env workDir cmd args = + CreateProcess.fromRawCommand cmd args + |> CreateProcess.withEnvironmentMap env + |> CreateProcess.withWorkingDirectory workDir + |> CreateProcess.ensureExitCode + |> Proc.start + |> Async.AwaitTask + |> Async.Ignore + +let dotnet workdir args = exec workdir "dotnet" args +let tailwindcss args = exec docsDir "tailwindcss" args + +Target.create "Watch" (fun _ -> + let watchApp = dotnet docsDir ["watch"; "run"; "--no-restore"] + let watchCss = tailwindcss ["--input"; "input.css"; "--output"; "wwwroot/css/output.css"; "--watch"] + Async.Parallel [| watchApp; watchCss |] + |> Async.RunSynchronously + |> ignore +) + +Target.create "BuildCss" <| fun _ -> + tailwindcss [ "--input"; "input.css"; "--output"; "wwwroot/css/output.css"; "--minify" ] + |> Async.RunSynchronously + +Target.create "Publish" <| fun _ -> + dotnet docsDir [ + "publish" + "--output"; "./out" + "--self-contained"; "false" + ] + |> Async.RunSynchronously + +Target.create "Default" (fun _ -> Target.listAvailable()) + +"BuildCss" ==>! "Publish" + +Target.runOrDefaultWithArguments "Default" diff --git a/fsharp-view-engine/src/Build/paket.references b/docs/src/Build/paket.references similarity index 100% rename from fsharp-view-engine/src/Build/paket.references rename to docs/src/Build/paket.references diff --git a/fsharp-view-engine/src/App/Config.fs b/docs/src/Docs/Config.fs similarity index 94% rename from fsharp-view-engine/src/App/Config.fs rename to docs/src/Docs/Config.fs index eda2226..828c661 100644 --- a/fsharp-view-engine/src/App/Config.fs +++ b/docs/src/Docs/Config.fs @@ -1,4 +1,4 @@ -namespace App +namespace Docs open System diff --git a/fsharp-view-engine/src/App/App.fsproj b/docs/src/Docs/Docs.fsproj similarity index 75% rename from fsharp-view-engine/src/App/App.fsproj rename to docs/src/Docs/Docs.fsproj index 7548c50..b50cd20 100644 --- a/fsharp-view-engine/src/App/App.fsproj +++ b/docs/src/Docs/Docs.fsproj @@ -5,6 +5,7 @@ $(NoWarn);NU1510 + @@ -21,13 +22,7 @@ - <_ContentIncludedByDefault Remove="Properties\launchSettings.json" /> - - - - - - + - \ No newline at end of file + diff --git a/docs/src/Docs/Handlers.fs b/docs/src/Docs/Handlers.fs new file mode 100644 index 0000000..91032d7 --- /dev/null +++ b/docs/src/Docs/Handlers.fs @@ -0,0 +1,66 @@ +namespace Docs + +open System +open System.IO +open System.Text.RegularExpressions +open Giraffe +open Microsoft.AspNetCore.Http +open Markdig +open FSharp.ViewEngine + +module Handlers = + + let private markdownPipeline = + MarkdownPipelineBuilder() + .UseAdvancedExtensions() + .Build() + + let private readMarkdownFile (fileName: string) = + let filePath = Path.Combine(AppContext.BaseDirectory, "docs", fileName + ".md") + if File.Exists(filePath) then + let content = File.ReadAllText(filePath) + Markdown.ToHtml(content, markdownPipeline) + else + "

Page Not Found

The requested page could not be found.

" + + let private extractHeadings (html: string) = + let pattern = """]*>([^<]+)""" + Regex.Matches(html, pattern) + |> Seq.cast + |> Seq.map (fun m -> (m.Groups.[2].Value.Trim(), m.Groups.[1].Value)) + |> Seq.toList + + let private renderPage (title: string) (fileName: string) : HttpHandler = + fun next ctx -> task { + let currentPath = ctx.Request.Path.Value + let markdownContent = readMarkdownFile fileName + let headings = extractHeadings markdownContent + let content = Views.layout title currentPath headings markdownContent + let html = Render.toHtmlDocString content + return! htmlString html next ctx + } + + // Route handlers + let homeHandler : HttpHandler = + renderPage "FSharp.ViewEngine Documentation" "home" + + let installationHandler : HttpHandler = + renderPage "Installation - FSharp.ViewEngine" "installation" + + let quickstartHandler : HttpHandler = + renderPage "Quickstart - FSharp.ViewEngine" "quickstart" + + let alpineHandler : HttpHandler = + renderPage "Alpine.js - FSharp.ViewEngine" "alpine" + + let datastarHandler : HttpHandler = + renderPage "Datastar - FSharp.ViewEngine" "datastar" + + let htmxHandler : HttpHandler = + renderPage "HTMX - FSharp.ViewEngine" "htmx" + + let svgHandler : HttpHandler = + renderPage "SVG - FSharp.ViewEngine" "svg" + + let tailwindHandler : HttpHandler = + renderPage "Tailwind - FSharp.ViewEngine" "tailwind" diff --git a/fsharp-view-engine/src/App/Program.fs b/docs/src/Docs/Program.fs similarity index 72% rename from fsharp-view-engine/src/App/Program.fs rename to docs/src/Docs/Program.fs index 306730c..4ef07bc 100644 --- a/fsharp-view-engine/src/App/Program.fs +++ b/docs/src/Docs/Program.fs @@ -2,7 +2,7 @@ open Microsoft.AspNetCore.Builder open Microsoft.Extensions.Hosting open Microsoft.Extensions.DependencyInjection open Giraffe -open App.Handlers +open Docs.Handlers let webApp = choose [ @@ -10,6 +10,11 @@ let webApp = route "/" >=> homeHandler route "/installation" >=> installationHandler route "/quickstart" >=> quickstartHandler + route "/extensions/alpine" >=> alpineHandler + route "/extensions/datastar" >=> datastarHandler + route "/extensions/htmx" >=> htmxHandler + route "/extensions/svg" >=> svgHandler + route "/extensions/tailwind" >=> tailwindHandler ] ] @@ -32,7 +37,7 @@ let main args = configureApp app - let config = App.Config.load() + let config = Docs.Config.load() app.Run(config.serverUrl) 0 // Exit code diff --git a/docs/src/Docs/Views.fs b/docs/src/Docs/Views.fs new file mode 100644 index 0000000..76e3a26 --- /dev/null +++ b/docs/src/Docs/Views.fs @@ -0,0 +1,291 @@ +module Docs.Views + +open FSharp.ViewEngine +open type Html +open type Alpine +open type Tailwind + +type Page = + { title:string } + +let magnifyingGlassIcon = raw """ + + + + """ +let menuIcon = raw """ + + + + """ +let githubIcon = raw """""" +let sunIcon = raw """""" +let moonIcon = raw """""" +let sunIconSmall = raw """""" +let moonIconSmall = raw """""" +let monitorIcon = raw """""" + +let private pageHeader = + header { + _class [ + "sticky top-0 z-50 flex flex-none flex-wrap items-center justify-between" + "bg-white px-4 py-5 shadow-md shadow-slate-900/5 transition duration-500" + "sm:px-6 lg:px-8 dark:shadow-none dark:bg-slate-900/75 dark:backdrop-blur" + ] + // Left section: hamburger + logo + div { + _class "flex items-center gap-4" + // Mobile menu button (hidden on desktop) + div { + _class "flex lg:hidden" + button { + _type "button" + _class "relative text-slate-500 hover:text-slate-600 dark:text-slate-400 dark:hover:text-slate-300" + menuIcon + } + } + // Logo + a { + _href "/" + _class "flex items-center gap-2 text-sm font-semibold tracking-wider text-slate-700 dark:text-white" + img { _src "/logo.png"; _alt "FSharp.ViewEngine"; _class "h-6 w-6" } + "FSharp.ViewEngine" + } + } + // Right section with theme toggle and GitHub + div { + _class "relative flex basis-0 justify-end gap-6 sm:gap-8 md:grow" + // Theme toggle dropdown + div { + _class "relative z-10" + _xData "{ open: false, theme: localStorage.getItem('theme') || 'system' }" + _xInit """ + $watch('theme', (val) => { + localStorage.setItem('theme', val); + if (val === 'dark' || (val === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + }); + if (theme === 'dark' || (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) { + document.documentElement.classList.add('dark'); + } + """ + button { + _type "button" + _class [ + "flex h-6 w-6 items-center justify-center rounded-lg shadow-md ring-1" + "shadow-black/5 ring-black/5 dark:bg-slate-700 dark:ring-white/5" + "dark:ring-inset" + ] + _xOn ("click", "open = !open") + // Light mode icon (sun) + sunIcon + moonIcon + } + // Dropdown menu + div { + _xShow "open" + _xOn ("click.away", "open = false") + _xTransition () + _class [ + "absolute right-0 top-full mt-3 w-36 overflow-hidden rounded-lg" + "bg-white py-1 text-sm font-semibold text-slate-700 shadow-lg ring-1" + "ring-slate-900/10 dark:bg-slate-800 dark:text-slate-300 dark:ring-0" + "dark:highlight-white/5" + ] + // Light option + button { + _type "button" + _class "flex w-full items-center gap-2 px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-700/50" + _xOn ("click", "theme = 'light'; open = false") + _xBind ("class", "theme === 'light' ? 'text-sky-500' : ''") + sunIconSmall + text "Light" + } + // Dark option + button { + _type "button" + _class "flex w-full items-center gap-2 px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-700/50" + _xOn ("click", "theme = 'dark'; open = false") + _xBind ("class", "theme === 'dark' ? 'text-sky-500' : ''") + moonIconSmall + text "Dark" + } + // System option + button { + _type "button" + _class "flex w-full items-center gap-2 px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-700/50" + _xOn ("click", "theme = 'system'; open = false") + _xBind ("class", "theme === 'system' ? 'text-sky-500' : ''") + monitorIcon + text "System" + } + } + } + // GitHub link + a { + _href "https://github.com/meiermade/FSharp.ViewEngine" + _class "group" + githubIcon + } + } + } + +let private navLink (currentPath: string) (href': string) (label': string) = + let isActive = currentPath = href' + li { + _class "relative" + a { + _class [ + "block w-full pl-3.5 before:pointer-events-none before:absolute" + "before:-left-1 before:top-1/2 before:h-1.5 before:w-1.5" + "before:-translate-y-1/2 before:rounded-full" + if isActive then + "font-semibold text-sky-500 before:bg-sky-500" + else + "text-slate-500 before:hidden before:bg-slate-300 hover:text-slate-600" + + " hover:before:block dark:text-slate-400 dark:before:bg-slate-700 dark:hover:text-slate-300" + ] + _href href' + label' + } + } + +let private sidebarNavigation (currentPath: string) = + nav { + _class "text-base lg:text-sm" + ul { + _role "list" + _class "space-y-9" + li { + h2 { + _class "font-display font-medium text-slate-900 dark:text-white" + "Getting started" + } + ul { + _role "list" + _class [ + "mt-2 space-y-2 border-l-2 border-slate-100" + "lg:mt-4 lg:space-y-4 lg:border-slate-200 dark:border-slate-800" + ] + navLink currentPath "/" "Introduction" + navLink currentPath "/installation" "Installation" + navLink currentPath "/quickstart" "Quickstart" + } + } + li { + h2 { + _class "font-display font-medium text-slate-900 dark:text-white" + "Extensions" + } + ul { + _role "list" + _class [ + "mt-2 space-y-2 border-l-2 border-slate-100" + "lg:mt-4 lg:space-y-4 lg:border-slate-200 dark:border-slate-800" + ] + navLink currentPath "/extensions/alpine" "Alpine" + navLink currentPath "/extensions/datastar" "Datastar" + navLink currentPath "/extensions/htmx" "HTMX" + navLink currentPath "/extensions/svg" "SVG" + navLink currentPath "/extensions/tailwind" "Tailwind" + } + } + } + } + +let private sidebar (currentPath: string) = + div { + _class "hidden lg:relative lg:block lg:flex-none" + div { + _class [ + "sticky top-[4.75rem] -ml-0.5 h-[calc(100vh-4.75rem)] w-64" + "overflow-y-auto overflow-x-hidden py-16 pl-0.5 pr-8 xl:w-72 xl:pr-16" + ] + sidebarNavigation currentPath + } + } + +let private tableOfContents (headings: (string * string) list) = + if List.isEmpty headings then + empty + else + nav { + _class "sticky top-[4.75rem] -mr-6 w-56 flex-none overflow-y-auto py-16 pr-6" + h2 { + _class "font-display text-sm font-medium text-zinc-900 dark:text-white" + "On this page" + } + ul { + _role "list" + _class "mt-4 space-y-3 text-sm" + for (title', anchor) in headings do + li { + a { + _href $"#{anchor}" + _class "text-zinc-500 hover:text-zinc-600 dark:text-zinc-400 dark:hover:text-zinc-300" + title' + } + } + } + } + +let layout (pageTitle: string) (currentPath: string) (headings: (string * string) list) (content: string) = + html { + _lang "en" + _class "h-full antialiased" + head { + meta { _charset "utf-8" } + meta { _name "viewport"; _content "width=device-width, initial-scale=1" } + title pageTitle + script { js "let t=localStorage.getItem('theme');if(t==='dark'||(!t||t==='system')&&window.matchMedia('(prefers-color-scheme: dark)').matches){document.documentElement.classList.add('dark')}" } + link { _rel "stylesheet"; _href "/css/output.css" } + script { _src "https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"; _defer true } + script { _src "https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js" } + link { _rel "stylesheet"; _href "https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" } + script { _src "https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-fsharp.min.js" } + } + body { + _class "min-h-full bg-white dark:bg-slate-900" + pageHeader + div { + _class [ + "relative mx-auto flex max-w-8xl justify-center" + "sm:px-2 lg:px-8 xl:px-12" + ] + sidebar currentPath + div { + _class [ + "min-w-0 max-w-3xl flex-auto px-4 py-16" + "lg:max-w-none lg:pl-8 lg:pr-0 xl:px-16" + ] + article { + div { + _class "mb-8" + p { + _class "font-display text-sm font-medium text-sky-500" + if currentPath.StartsWith("/extensions") then + "Extensions" + else + "Getting started" + } + } + div { + _class "prose prose-slate dark:prose-invert max-w-none" + raw content + } + } + } + div { + _class [ + "hidden xl:sticky xl:top-[4.75rem] xl:-mr-6 xl:block" + "xl:h-[calc(100vh-4.75rem)] xl:flex-none xl:overflow-y-auto" + "xl:py-16 xl:pr-6" + ] + tableOfContents headings + } + } + } + } diff --git a/docs/src/Docs/docs/alpine.md b/docs/src/Docs/docs/alpine.md new file mode 100644 index 0000000..ca06fda --- /dev/null +++ b/docs/src/Docs/docs/alpine.md @@ -0,0 +1,185 @@ +# Alpine.js + +FSharp.ViewEngine provides type-safe Alpine.js directives through the `Alpine` type. + +## Setup + +Open the `Alpine` type to access Alpine.js directives: + +```fsharp +open FSharp.ViewEngine +open type Html +open type Alpine +``` + +## Directives + +### x-data + +Initialize a component's reactive data with `_xData`: + +```fsharp +div { + _xData "{ open: false, count: 0 }" + button { + _xOn ("click", "count++") + "Increment" + } + span { _xText "count" } +} +``` + +### x-init + +Run an expression when a component is initialized: + +```fsharp +div { + _xData "{ users: [] }" + _xInit "users = await (await fetch('/api/users')).json()" +} +``` + +### x-show + +Toggle the visibility of an element: + +```fsharp +div { + _xData "{ open: false }" + button { _xOn ("click", "open = !open"); "Toggle" } + div { _xShow "open"; "Content here" } +} +``` + +### x-if + +Conditionally add or remove an element from the DOM: + +```fsharp +template { _xIf "open"; div { "Shown when open is true" } } +``` + +### x-for + +Loop over a list and render elements: + +```fsharp +template { + _xFor "item in items" + li { _xText "item" } +} +``` + +### x-bind + +Dynamically set HTML attributes: + +```fsharp +div { + _xBind ("class", "open ? 'block' : 'hidden'") +} +``` + +### x-on + +Listen for browser events: + +```fsharp +button { + _xOn ("click", "handleClick()") + _xOn ("mouseenter", "hovering = true") + "Click me" +} +``` + +### x-text + +Set the text content of an element: + +```fsharp +span { _xText "message" } +``` + +### x-model + +Two-way data binding for form elements: + +```fsharp +div { + _xData "{ search: '' }" + input { _type "text"; _xModel "search" } + p { _xText "search" } +} +``` + +You can also use modifiers: + +```fsharp +input { _xModel ("value", ".debounce.500ms") } +``` + +### x-ref + +Reference elements directly: + +```fsharp +div { + input { _xRef "nameInput" } + button { _xOn ("click", "$refs.nameInput.focus()"); "Focus" } +} +``` + +## Additional Directives + +### x-effect + +Execute a script each time one of its dependencies change: + +```fsharp +div { _xEffect "console.log(count)" } +``` + +### x-transition + +Apply transition classes during enter/leave: + +```fsharp +div { + _xShow "open" + _xTransition () + "Animated content" +} +``` + +### x-cloak + +Hide an element until Alpine is initialized: + +```fsharp +div { _xCloak; "Hidden until Alpine loads" } +``` + +### x-teleport + +Move an element to another location in the DOM: + +```fsharp +div { _xTeleport "body"; "Teleported to body" } +``` + +### x-trap + +Trap focus within an element: + +```fsharp +div { _xTrap "open"; "Focus is trapped here" } +``` + +### x-id + +Scope generated IDs to the component: + +```fsharp +div { _xId "['dropdown']" } +``` diff --git a/docs/src/Docs/docs/datastar.md b/docs/src/Docs/docs/datastar.md new file mode 100644 index 0000000..018d86c --- /dev/null +++ b/docs/src/Docs/docs/datastar.md @@ -0,0 +1,313 @@ +# Datastar + +FSharp.ViewEngine provides type-safe Datastar attributes through the `Datastar` type. + +## Setup + +Open the `Datastar` type to access Datastar attributes: + +```fsharp +open FSharp.ViewEngine +open type Html +open type Datastar +``` + +## Generic Attribute + +### data-* + +Use `_ds` for any Datastar `data-*` attribute: + +```fsharp +div { + _ds ("star", "true") + _ds "loading" +} +``` + +## Core Attributes + +### data-signals + +Define reactive signals on an element: + +```fsharp +div { + _dsSignals ("count", "0") + _dsSignals ("name", "'World'") +} +``` + +### data-on + +Listen for events and run expressions: + +```fsharp +button { + _dsOn ("click", "$count++") + "Increment" +} +``` + +### data-bind + +Two-way bind a signal to an input element: + +```fsharp +input { _type "text"; _dsBind "name" } +input { _type "text"; _dsBind ("name", "value") } +``` + +### data-show + +Conditionally show or hide an element: + +```fsharp +div { _dsShow "$count > 0"; "Count is positive" } +``` + +### data-text + +Set the text content of an element reactively: + +```fsharp +span { _dsText "$count" } +``` + +### data-effect + +Run an expression whenever its dependencies change: + +```fsharp +div { _dsEffect "console.log($count)" } +``` + +### data-init + +Run an expression when the element is initialized: + +```fsharp +div { _dsInit "console.log('initialized')" } +``` + +### data-attr + +Dynamically set an HTML attribute: + +```fsharp +div { _dsAttr ("disabled", "$count === 0") } +``` + +### data-class + +Toggle a CSS class based on an expression: + +```fsharp +div { _dsClass ("active", "$isActive") } +``` + +### data-computed + +Define a computed signal derived from other signals: + +```fsharp +div { _dsComputed ("double", "$count * 2") } +``` + +### data-style + +Dynamically set a CSS style property: + +```fsharp +div { _dsStyle ("color", "$isError ? 'red' : 'green'") } +``` + +### data-ref + +Reference an element by name: + +```fsharp +input { _dsRef "myInput" } +input { _dsRef ("myInput", "value") } +``` + +### data-indicator + +Bind a loading indicator signal: + +```fsharp +button { _dsIndicator "loading" } +button { _dsIndicator ("loading", "true") } +``` + +### data-json-signals + +Merge JSON signals into the signal store: + +```fsharp +div { _dsJsonSignals """{"count": 0}""" } +div { _dsJsonSignals () } +``` + +### data-ignore + +Prevent Datastar from processing an element: + +```fsharp +div { _dsIgnore } +``` + +### data-ignore-morph + +Prevent morphing of an element during updates: + +```fsharp +div { _dsIgnoreMorph } +``` + +### data-on-intersect + +Run an expression when an element enters the viewport: + +```fsharp +div { _dsOnIntersect "$count++" } +``` + +### data-on-interval + +Run an expression on a timed interval: + +```fsharp +div { _dsOnInterval "$count++" } +``` + +### data-on-signal-patch + +Run an expression when signals are patched: + +```fsharp +div { _dsOnSignalPatch "console.log('patched')" } +``` + +### data-on-signal-patch-filter + +Filter which signal patches trigger the expression: + +```fsharp +div { _dsOnSignalPatchFilter "count" } +``` + +### data-preserve-attr + +Preserve specified attributes during morphing: + +```fsharp +div { _dsPreserveAttr "class" } +``` + +## Pro Attributes + +### data-animate + +Apply animations to an element: + +```fsharp +div { _dsAnimate "fadeIn 0.5s" } +``` + +### data-custom-validity + +Set custom validation messages: + +```fsharp +input { _dsCustomValidity "$name === '' ? 'Name is required' : ''" } +``` + +### data-on-raf + +Run an expression on every animation frame: + +```fsharp +canvas { _dsOnRaf "draw()" } +``` + +### data-on-resize + +Run an expression when the element is resized: + +```fsharp +div { _dsOnResize "console.log('resized')" } +``` + +### data-persist + +Persist signals to local storage: + +```fsharp +div { _dsPersist "count" } +div { _dsPersist ("count", "session") } +``` + +### data-query-string + +Sync signals with URL query parameters: + +```fsharp +div { _dsQueryString "count" } +div { _dsQueryString () } +``` + +### data-replace-url + +Replace the current URL: + +```fsharp +div { _dsReplaceUrl "/new-path" } +``` + +### data-rocket + +Prefetch pages for instant navigation: + +```fsharp +a { _dsRocket "true"; _href "/next-page"; "Next" } +``` + +### data-scroll-into-view + +Scroll the element into view: + +```fsharp +div { _dsScrollIntoView } +``` + +### data-view-transition + +Apply view transitions: + +```fsharp +div { _dsViewTransition "fade" } +``` + +## Complete Example + +Here's a complete example combining multiple Datastar attributes: + +```fsharp +div { + _dsSignals ("count", "0") + _dsSignals ("name", "'World'") + _dsComputed ("greeting", "'Hello, ' + $name + '!'") + + input { _type "text"; _dsBind "name" } + span { _dsText "$greeting" } + + button { + _dsOn ("click", "$count++") + _dsClass ("active", "$count > 0") + "Clicked " + } + span { _dsText "$count" } + span { _dsShow "$count > 0"; " times" } +} +``` diff --git a/docs/src/Docs/docs/home.md b/docs/src/Docs/docs/home.md new file mode 100644 index 0000000..f119e58 --- /dev/null +++ b/docs/src/Docs/docs/home.md @@ -0,0 +1,56 @@ +# FSharp.ViewEngine Documentation + +A powerful F# view engine for building HTML with type safety and composability. Inspired by Giraffe.ViewEngine, Feliz.ViewEngine, and Oxpecker.ViewEngine. + +## Key Features + +- **Type-safe HTML generation** with F# +- **Built-in support for HTMX attributes** +- **Tailwind CSS integration** +- **Alpine.js directives support** +- **Composable and functional approach** +- **No runtime dependencies** + +## Quick Example + +```fsharp +open FSharp.ViewEngine +open type Html +open type Htmx +open type Tailwind + +let myPage = + html { + _lang "en" + head { + title "My App" + meta { _charset "utf-8" } + link { _href "/css/tailwind.css"; _rel "stylesheet" } + } + body { + _class "bg-gray-100" + div { + _class [ "container"; "mx-auto"; "p-4" ] + h1 { + _class [ "text-3xl"; "font-bold"; "text-blue-600"; "mb-4" ] + "Welcome!" + } + button { + _class [ "bg-blue-500"; "text-white"; "px-4"; "py-2"; "rounded" ] + _hxGet "/api/data" + _hxTarget "#content" + "Load Content" + } + div { + _id "content" + _class [ "mt-4" ] + } + } + } + } + |> Render.toHtmlDocString +``` + +## Getting Started + +To get started with FSharp.ViewEngine, check out the [Installation](installation) guide and then follow the [Quickstart](quickstart) tutorial. diff --git a/docs/src/Docs/docs/htmx.md b/docs/src/Docs/docs/htmx.md new file mode 100644 index 0000000..8b284aa --- /dev/null +++ b/docs/src/Docs/docs/htmx.md @@ -0,0 +1,206 @@ +# HTMX + +FSharp.ViewEngine provides type-safe HTMX attributes through the `Htmx` type. + +## Setup + +Open the `Htmx` type to access HTMX attributes: + +```fsharp +open FSharp.ViewEngine +open type Html +open type Htmx +``` + +## Request Attributes + +### hx-get + +Issue a GET request to a URL: + +```fsharp +button { + _hxGet "/api/data" + "Load Data" +} +``` + +### hx-post + +Issue a POST request to a URL: + +```fsharp +form { + _hxPost "/api/submit" + input { _type "text"; _name "email" } + button { "Submit" } +} +``` + +### hx-delete + +Issue a DELETE request to a URL: + +```fsharp +button { + _hxDelete "/api/items/1" + "Delete" +} +``` + +### Generic hx attribute + +Use `_hx` for any HTMX attribute not covered by a dedicated helper: + +```fsharp +div { + _hx ("put", "/api/items/1") + _hx ("patch", "/api/items/1") +} +``` + +## Targeting and Swapping + +### hx-target + +Specify the target element for the response: + +```fsharp +button { + _hxGet "/api/data" + _hxTarget "#result" + "Load into #result" +} +div { _id "result" } +``` + +### hx-swap + +Control how the response content is swapped in: + +```fsharp +button { + _hxGet "/api/items" + _hxSwap "beforeend" + "Append Items" +} +``` + +Common swap values: `innerHTML`, `outerHTML`, `beforebegin`, `afterbegin`, `beforeend`, `afterend`, `delete`, `none`. + +### hx-swap-oob + +Swap content out-of-band (outside the target): + +```fsharp +div { + _hxSwapOOB "true" + _id "notifications" + "Updated notification content" +} +``` + +## Triggering and Events + +### hx-trigger + +Specify the event that triggers the request: + +```fsharp +input { + _type "text" + _hxGet "/api/search" + _hxTrigger "keyup changed delay:500ms" + _hxTarget "#results" +} +``` + +### hx-on + +Listen for HTMX events: + +```fsharp +form { + _hxPost "/api/submit" + _hxOn ("htmx:beforeRequest", "showSpinner()") + _hxOn ("htmx:afterRequest", "hideSpinner()") +} +``` + +## Other Attributes + +### hx-indicator + +Show a loading indicator during requests: + +```fsharp +button { + _hxGet "/api/slow" + _hxIndicator "#spinner" + "Load" +} +div { _id "spinner"; _class "htmx-indicator"; "Loading..." } +``` + +### hx-include + +Include additional element values in the request: + +```fsharp +button { + _hxPost "/api/submit" + _hxInclude "[name='email']" + "Submit" +} +``` + +### hx-encoding + +Set the encoding type for requests: + +```fsharp +form { + _hxPost "/api/upload" + _hxEncoding "multipart/form-data" +} +``` + +### hx-vals + +Add additional values to the request: + +```fsharp +button { + _hxPost "/api/action" + _hxVals """{"key": "value"}""" + "Submit" +} +``` + +### hx-history + +Control the history behavior: + +```fsharp +div { _hxHistory "false" } +``` + +## Complete Example + +Here's a complete example combining multiple HTMX attributes: + +```fsharp +div { + _class "search-container" + input { + _type "text" + _name "q" + _hxGet "/api/search" + _hxTrigger "keyup changed delay:300ms" + _hxTarget "#search-results" + _hxIndicator "#search-spinner" + } + span { _id "search-spinner"; _class "htmx-indicator"; "Searching..." } + div { _id "search-results" } +} +``` diff --git a/fsharp-view-engine/src/App/docs/installation.md b/docs/src/Docs/docs/installation.md similarity index 100% rename from fsharp-view-engine/src/App/docs/installation.md rename to docs/src/Docs/docs/installation.md diff --git a/docs/src/Docs/docs/quickstart.md b/docs/src/Docs/docs/quickstart.md new file mode 100644 index 0000000..b576d97 --- /dev/null +++ b/docs/src/Docs/docs/quickstart.md @@ -0,0 +1,88 @@ +# Quickstart + +Get started with FSharp.ViewEngine in just a few steps. + +## Basic Usage + +Import the core modules and start building HTML: + +```fsharp +open FSharp.ViewEngine +open type Html + +let myView = + div { + _class "container" + h1 { "Hello, World!" } + p { "Welcome to FSharp.ViewEngine" } + } + +// Render to string +let htmlString = Render.toHtmlDocString myView +``` + +This will produce the following HTML: + +```html +
+

Hello, World!

+

Welcome to FSharp.ViewEngine

+
+``` + +## With HTMX and Tailwind CSS + +FSharp.ViewEngine includes built-in support for HTMX and Tailwind CSS: + +```fsharp +open FSharp.ViewEngine +open type Html +open type Htmx +open type Tailwind + +let interactiveView = + div { + _class [ "bg-blue-500"; "text-white"; "p-4"; "rounded" ] + button { + _class [ "bg-green-500"; "hover:bg-green-700"; "px-4"; "py-2"; "rounded" ] + _hxGet "/api/data" + _hxTarget "#result" + "Load Data" + } + div { + _id "result" + _class [ "mt-4" ] + } + } +``` + +## Complete HTML Document + +Here's how to create a complete HTML document: + +```fsharp +let completePage = + html { + _lang "en" + head { + title "My Page" + meta { _charset "utf-8" } + meta { _name "viewport"; _content "width=device-width, initial-scale=1" } + link { _rel "stylesheet"; _href "https://cdn.tailwindcss.com" } + } + body { + _class [ "bg-gray-100"; "font-sans" ] + div { + _class [ "container"; "mx-auto"; "px-4"; "py-8" ] + h1 { + _class [ "text-3xl"; "font-bold"; "text-gray-900"; "mb-4" ] + "Welcome to my site!" + } + p { + _class [ "text-lg"; "text-gray-600" ] + "This is built with FSharp.ViewEngine." + } + } + } + } +``` diff --git a/docs/src/Docs/docs/svg.md b/docs/src/Docs/docs/svg.md new file mode 100644 index 0000000..2713b26 --- /dev/null +++ b/docs/src/Docs/docs/svg.md @@ -0,0 +1,155 @@ +# SVG + +FSharp.ViewEngine provides type-safe SVG elements and attributes through the `Svg` type. + +## Setup + +Open the `Svg` type to access SVG elements and attributes: + +```fsharp +open FSharp.ViewEngine +open type Html +open type Svg +``` + +## Elements + +### svg + +The root SVG container element: + +```fsharp +svg { + _viewBox "0 0 24 24" + _width 24 + _height 24 + // child elements here +} +``` + +### path + +Define a shape using a path data string: + +```fsharp +svg { + _viewBox "0 0 24 24" + _fill "none" + path { + _d "M12 2L2 22h20L12 2z" + _stroke "currentColor" + _strokeWidth 2 + } +} +``` + +### circle + +Draw a circle: + +```fsharp +svg { + _viewBox "0 0 100 100" + circle { + _cx 50 + _cy 50 + _r 40 + _fill "blue" + _stroke "black" + _strokeWidth 2 + } +} +``` + +## Attributes + +### Dimensions + +Set the width and height of the SVG: + +```fsharp +svg { _width 48; _height 48 } +``` + +### viewBox + +Define the coordinate system and aspect ratio: + +```fsharp +svg { _viewBox "0 0 100 100" } +``` + +### Fill and Stroke + +Control the fill color and stroke styling: + +```fsharp +path { + _d "M0 0 L10 10" + _fill "none" + _stroke "red" + _strokeWidth 2 + _strokeLinecap "round" + _strokeLinejoin "round" +} +``` + +### Fill Rule and Clip Rule + +Control how overlapping paths are filled or clipped: + +```fsharp +path { + _d "M0 0 L10 10 L20 0 Z" + _fillRule "evenodd" + _clipRule "evenodd" +} +``` + +## Complete Example + +Here's a complete icon example: + +```fsharp +let checkIcon = + svg { + _viewBox "0 0 24 24" + _width 24 + _height 24 + _fill "none" + path { + _d "M5 13l4 4L19 7" + _stroke "currentColor" + _strokeWidth 2 + _strokeLinecap "round" + _strokeLinejoin "round" + } + } +``` + +And a more complex example with multiple elements: + +```fsharp +let statusIcon = + svg { + _viewBox "0 0 100 100" + _width 100 + _height 100 + circle { + _cx 50 + _cy 50 + _r 45 + _fill "green" + _stroke "darkgreen" + _strokeWidth 2 + } + path { + _d "M30 50 L45 65 L70 35" + _fill "none" + _stroke "white" + _strokeWidth 6 + _strokeLinecap "round" + _strokeLinejoin "round" + } + } +``` diff --git a/docs/src/Docs/docs/tailwind.md b/docs/src/Docs/docs/tailwind.md new file mode 100644 index 0000000..196647b --- /dev/null +++ b/docs/src/Docs/docs/tailwind.md @@ -0,0 +1,201 @@ +# Tailwind + +FSharp.ViewEngine provides custom elements for Tailwind UI components through the `Tailwind` type. + +## Setup + +Open the `Tailwind` type to access Tailwind custom elements: + +```fsharp +open FSharp.ViewEngine +open type Html +open type Tailwind +``` + +## Form Elements + +### Autocomplete + +Build an autocomplete input with filtered options: + +```fsharp +elAutocomplete { + _class "w-64" + input { _type "text"; _placeholder "Search..." } + elOptions { + elOption { "Option 1" } + elOption { "Option 2" } + elOption { "Option 3" } + } +} +``` + +### Select + +Create a custom select dropdown: + +```fsharp +elSelect { + _class "w-48" + elSelectedContent { "Choose an option" } + elOptions { + elOption { "Small" } + elOption { "Medium" } + elOption { "Large" } + } +} +``` + +## Overlay Elements + +### Dialog + +Build a modal dialog with a backdrop: + +```fsharp +elDialog { + elDialogBackdrop { _class "fixed inset-0 bg-black/30" } + elDialogPanel { + _class "mx-auto max-w-sm rounded bg-white p-6" + h2 { "Dialog Title" } + p { "Dialog content goes here." } + button { "Close" } + } +} +``` + +### Dropdown + +Create a dropdown menu: + +```fsharp +elDropdown { + button { "Options" } + elMenu { + _class "absolute mt-2 w-48 rounded bg-white shadow-lg" + a { _href "#"; "Edit" } + a { _href "#"; "Delete" } + } +} +``` + +## Navigation Elements + +### Tab Group + +Build a tabbed interface: + +```fsharp +elTabGroup { + elTabList { + _class "flex space-x-1 rounded-xl bg-blue-900/20 p-1" + button { "Tab 1" } + button { "Tab 2" } + button { "Tab 3" } + } + elTabPanels { + div { "Content for Tab 1" } + div { "Content for Tab 2" } + div { "Content for Tab 3" } + } +} +``` + +## Command Palette + +### Command Palette + +Build a command palette for search and navigation: + +```fsharp +elCommandPalette { + input { _type "text"; _placeholder "Search commands..." } + elCommandList { + elCommandGroup { + _class "p-2" + div { "Open File" } + div { "Run Command" } + } + elCommandPreview { + _class "p-4" + "Preview content here" + } + } + elNoResults { "No results found." } +} +``` + +## Attributes + +### popover + +Mark an element as a popover: + +```fsharp +div { _popover; "Popover content" } +``` + +### anchor + +Position an element relative to a reference using anchor positioning: + +```fsharp +div { + _anchor "bottom-start" + "Anchored content" +} +``` + +## Defaults + +Use `elDefaults` to set default values for child components: + +```fsharp +elDefaults { + _class "text-sm" + elSelect { + elOptions { + elOption { "A" } + elOption { "B" } + } + } +} +``` + +## Complete Example + +Here's a complete example combining several Tailwind UI elements: + +```fsharp +div { + _class "p-8" + elTabGroup { + elTabList { + _class "flex space-x-1 rounded-xl bg-blue-900/20 p-1" + button { _class "rounded-lg px-3 py-2"; "Search" } + button { _class "rounded-lg px-3 py-2"; "Browse" } + } + elTabPanels { + div { + elAutocomplete { + _class "mt-4 w-full" + input { _type "text"; _placeholder "Search items..." } + elOptions { + elOption { "Item 1" } + elOption { "Item 2" } + } + } + } + div { + elSelect { + _class "mt-4 w-full" + elOptions { + elOption { "Category A" } + elOption { "Category B" } + } + } + } + } + } +} +``` diff --git a/fsharp-view-engine/src/App/input.css b/docs/src/Docs/input.css similarity index 95% rename from fsharp-view-engine/src/App/input.css rename to docs/src/Docs/input.css index 6300d01..5fc0506 100644 --- a/fsharp-view-engine/src/App/input.css +++ b/docs/src/Docs/input.css @@ -54,3 +54,8 @@ font-size: 0.875rem !important; font-weight: 500 !important; } + +.prose code::before, +.prose code::after { + content: none !important; +} diff --git a/fsharp-view-engine/src/App/paket.references b/docs/src/Docs/paket.references similarity index 100% rename from fsharp-view-engine/src/App/paket.references rename to docs/src/Docs/paket.references diff --git a/docs/src/Docs/wwwroot/android-chrome-192x192.png b/docs/src/Docs/wwwroot/android-chrome-192x192.png new file mode 100644 index 0000000..fdc6c41 Binary files /dev/null and b/docs/src/Docs/wwwroot/android-chrome-192x192.png differ diff --git a/docs/src/Docs/wwwroot/android-chrome-512x512.png b/docs/src/Docs/wwwroot/android-chrome-512x512.png new file mode 100644 index 0000000..a656cd0 Binary files /dev/null and b/docs/src/Docs/wwwroot/android-chrome-512x512.png differ diff --git a/docs/src/Docs/wwwroot/apple-touch-icon.png b/docs/src/Docs/wwwroot/apple-touch-icon.png new file mode 100644 index 0000000..583cd52 Binary files /dev/null and b/docs/src/Docs/wwwroot/apple-touch-icon.png differ diff --git a/docs/src/Docs/wwwroot/css/output.css b/docs/src/Docs/wwwroot/css/output.css new file mode 100644 index 0000000..54ba522 --- /dev/null +++ b/docs/src/Docs/wwwroot/css/output.css @@ -0,0 +1,1760 @@ +/*! tailwindcss v4.1.11 | MIT License | https://tailwindcss.com */ +@layer properties; +@layer theme, base, components, utilities; +@layer theme { + :root, :host { + --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', + 'Noto Color Emoji'; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', + monospace; + --color-green-500: oklch(72.3% 0.219 149.579); + --color-green-700: oklch(52.7% 0.154 150.069); + --color-sky-500: oklch(68.5% 0.169 237.323); + --color-blue-500: oklch(62.3% 0.214 259.815); + --color-blue-600: oklch(54.6% 0.245 262.881); + --color-blue-900: oklch(37.9% 0.146 265.522); + --color-slate-100: oklch(96.8% 0.007 247.896); + --color-slate-200: oklch(92.9% 0.013 255.508); + --color-slate-300: oklch(86.9% 0.022 252.894); + --color-slate-400: oklch(70.4% 0.04 256.788); + --color-slate-500: oklch(55.4% 0.046 257.417); + --color-slate-600: oklch(44.6% 0.043 257.281); + --color-slate-700: oklch(37.2% 0.044 257.287); + --color-slate-800: oklch(27.9% 0.041 260.031); + --color-slate-900: oklch(20.8% 0.042 265.755); + --color-gray-100: oklch(96.7% 0.003 264.542); + --color-gray-600: oklch(44.6% 0.03 256.802); + --color-gray-900: oklch(21% 0.034 264.665); + --color-zinc-300: oklch(87.1% 0.006 286.286); + --color-zinc-400: oklch(70.5% 0.015 286.067); + --color-zinc-500: oklch(55.2% 0.016 285.938); + --color-zinc-600: oklch(44.2% 0.017 285.786); + --color-zinc-900: oklch(21% 0.006 285.885); + --color-black: #000; + --color-white: #fff; + --spacing: 0.25rem; + --container-sm: 24rem; + --container-3xl: 48rem; + --text-sm: 0.875rem; + --text-sm--line-height: calc(1.25 / 0.875); + --text-base: 1rem; + --text-base--line-height: calc(1.5 / 1); + --text-lg: 1.125rem; + --text-lg--line-height: calc(1.75 / 1.125); + --text-3xl: 1.875rem; + --text-3xl--line-height: calc(2.25 / 1.875); + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + --tracking-wider: 0.05em; + --radius-lg: 0.5rem; + --radius-xl: 0.75rem; + --default-transition-duration: 150ms; + --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + --default-font-family: var(--font-sans); + --default-mono-font-family: var(--font-mono); + } +} +@layer base { + *, ::after, ::before, ::backdrop, ::file-selector-button { + box-sizing: border-box; + margin: 0; + padding: 0; + border: 0 solid; + } + html, :host { + line-height: 1.5; + -webkit-text-size-adjust: 100%; + tab-size: 4; + font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'); + font-feature-settings: var(--default-font-feature-settings, normal); + font-variation-settings: var(--default-font-variation-settings, normal); + -webkit-tap-highlight-color: transparent; + } + hr { + height: 0; + color: inherit; + border-top-width: 1px; + } + abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + } + h1, h2, h3, h4, h5, h6 { + font-size: inherit; + font-weight: inherit; + } + a { + color: inherit; + -webkit-text-decoration: inherit; + text-decoration: inherit; + } + b, strong { + font-weight: bolder; + } + code, kbd, samp, pre { + font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace); + font-feature-settings: var(--default-mono-font-feature-settings, normal); + font-variation-settings: var(--default-mono-font-variation-settings, normal); + font-size: 1em; + } + small { + font-size: 80%; + } + sub, sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + sub { + bottom: -0.25em; + } + sup { + top: -0.5em; + } + table { + text-indent: 0; + border-color: inherit; + border-collapse: collapse; + } + :-moz-focusring { + outline: auto; + } + progress { + vertical-align: baseline; + } + summary { + display: list-item; + } + ol, ul, menu { + list-style: none; + } + img, svg, video, canvas, audio, iframe, embed, object { + display: block; + vertical-align: middle; + } + img, video { + max-width: 100%; + height: auto; + } + button, input, select, optgroup, textarea, ::file-selector-button { + font: inherit; + font-feature-settings: inherit; + font-variation-settings: inherit; + letter-spacing: inherit; + color: inherit; + border-radius: 0; + background-color: transparent; + opacity: 1; + } + :where(select:is([multiple], [size])) optgroup { + font-weight: bolder; + } + :where(select:is([multiple], [size])) optgroup option { + padding-inline-start: 20px; + } + ::file-selector-button { + margin-inline-end: 4px; + } + ::placeholder { + opacity: 1; + } + @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { + ::placeholder { + color: currentcolor; + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, currentcolor 50%, transparent); + } + } + } + textarea { + resize: vertical; + } + ::-webkit-search-decoration { + -webkit-appearance: none; + } + ::-webkit-date-and-time-value { + min-height: 1lh; + text-align: inherit; + } + ::-webkit-datetime-edit { + display: inline-flex; + } + ::-webkit-datetime-edit-fields-wrapper { + padding: 0; + } + ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field { + padding-block: 0; + } + :-moz-ui-invalid { + box-shadow: none; + } + button, input:where([type='button'], [type='reset'], [type='submit']), ::file-selector-button { + appearance: button; + } + ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { + height: auto; + } + [hidden]:where(:not([hidden='until-found'])) { + display: none !important; + } +} +@layer utilities { + .collapse { + visibility: collapse; + } + .visible { + visibility: visible; + } + .absolute { + position: absolute; + } + .fixed { + position: fixed; + } + .relative { + position: relative; + } + .sticky { + position: sticky; + } + .inset-0 { + inset: calc(var(--spacing) * 0); + } + .top-0 { + top: calc(var(--spacing) * 0); + } + .top-\[4\.75rem\] { + top: 4.75rem; + } + .top-full { + top: 100%; + } + .right-0 { + right: calc(var(--spacing) * 0); + } + .z-10 { + z-index: 10; + } + .z-50 { + z-index: 50; + } + .container { + width: 100%; + @media (width >= 40rem) { + max-width: 40rem; + } + @media (width >= 48rem) { + max-width: 48rem; + } + @media (width >= 64rem) { + max-width: 64rem; + } + @media (width >= 80rem) { + max-width: 80rem; + } + @media (width >= 96rem) { + max-width: 96rem; + } + } + .mx-auto { + margin-inline: auto; + } + .prose { + color: var(--tw-prose-body); + max-width: 65ch; + :where(p):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 1.25em; + margin-bottom: 1.25em; + } + :where([class~="lead"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-lead); + font-size: 1.25em; + line-height: 1.6; + margin-top: 1.2em; + margin-bottom: 1.2em; + } + :where(a):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-links); + text-decoration: underline; + font-weight: 500; + } + :where(strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-bold); + font-weight: 600; + } + :where(a strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; + } + :where(blockquote strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; + } + :where(thead th strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; + } + :where(ol):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: decimal; + margin-top: 1.25em; + margin-bottom: 1.25em; + padding-inline-start: 1.625em; + } + :where(ol[type="A"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: upper-alpha; + } + :where(ol[type="a"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: lower-alpha; + } + :where(ol[type="A" s]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: upper-alpha; + } + :where(ol[type="a" s]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: lower-alpha; + } + :where(ol[type="I"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: upper-roman; + } + :where(ol[type="i"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: lower-roman; + } + :where(ol[type="I" s]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: upper-roman; + } + :where(ol[type="i" s]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: lower-roman; + } + :where(ol[type="1"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: decimal; + } + :where(ul):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + list-style-type: disc; + margin-top: 1.25em; + margin-bottom: 1.25em; + padding-inline-start: 1.625em; + } + :where(ol > li):not(:where([class~="not-prose"],[class~="not-prose"] *))::marker { + font-weight: 400; + color: var(--tw-prose-counters); + } + :where(ul > li):not(:where([class~="not-prose"],[class~="not-prose"] *))::marker { + color: var(--tw-prose-bullets); + } + :where(dt):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-headings); + font-weight: 600; + margin-top: 1.25em; + } + :where(hr):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + border-color: var(--tw-prose-hr); + border-top-width: 1; + margin-top: 3em; + margin-bottom: 3em; + } + :where(blockquote):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + font-weight: 500; + font-style: italic; + color: var(--tw-prose-quotes); + border-inline-start-width: 0.25rem; + border-inline-start-color: var(--tw-prose-quote-borders); + quotes: "\201C""\201D""\2018""\2019"; + margin-top: 1.6em; + margin-bottom: 1.6em; + padding-inline-start: 1em; + } + :where(blockquote p:first-of-type):not(:where([class~="not-prose"],[class~="not-prose"] *))::before { + content: open-quote; + } + :where(blockquote p:last-of-type):not(:where([class~="not-prose"],[class~="not-prose"] *))::after { + content: close-quote; + } + :where(h1):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-headings); + font-weight: 800; + font-size: 2.25em; + margin-top: 0; + margin-bottom: 0.8888889em; + line-height: 1.1111111; + } + :where(h1 strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + font-weight: 900; + color: inherit; + } + :where(h2):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-headings); + font-weight: 700; + font-size: 1.5em; + margin-top: 2em; + margin-bottom: 1em; + line-height: 1.3333333; + } + :where(h2 strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + font-weight: 800; + color: inherit; + } + :where(h3):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-headings); + font-weight: 600; + font-size: 1.25em; + margin-top: 1.6em; + margin-bottom: 0.6em; + line-height: 1.6; + } + :where(h3 strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + font-weight: 700; + color: inherit; + } + :where(h4):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-headings); + font-weight: 600; + margin-top: 1.5em; + margin-bottom: 0.5em; + line-height: 1.5; + } + :where(h4 strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + font-weight: 700; + color: inherit; + } + :where(img):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 2em; + margin-bottom: 2em; + } + :where(picture):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + display: block; + margin-top: 2em; + margin-bottom: 2em; + } + :where(video):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 2em; + margin-bottom: 2em; + } + :where(kbd):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + font-weight: 500; + font-family: inherit; + color: var(--tw-prose-kbd); + box-shadow: 0 0 0 1px rgb(var(--tw-prose-kbd-shadows) / 10%), 0 3px 0 rgb(var(--tw-prose-kbd-shadows) / 10%); + font-size: 0.875em; + border-radius: 0.3125rem; + padding-top: 0.1875em; + padding-inline-end: 0.375em; + padding-bottom: 0.1875em; + padding-inline-start: 0.375em; + } + :where(code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-code); + font-weight: 600; + font-size: 0.875em; + } + :where(code):not(:where([class~="not-prose"],[class~="not-prose"] *))::before { + content: "`"; + } + :where(code):not(:where([class~="not-prose"],[class~="not-prose"] *))::after { + content: "`"; + } + :where(a code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; + } + :where(h1 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; + } + :where(h2 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; + font-size: 0.875em; + } + :where(h3 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; + font-size: 0.9em; + } + :where(h4 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; + } + :where(blockquote code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; + } + :where(thead th code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: inherit; + } + :where(pre):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-pre-code); + background-color: var(--tw-prose-pre-bg); + overflow-x: auto; + font-weight: 400; + font-size: 0.875em; + line-height: 1.7142857; + margin-top: 1.7142857em; + margin-bottom: 1.7142857em; + border-radius: 0.375rem; + padding-top: 0.8571429em; + padding-inline-end: 1.1428571em; + padding-bottom: 0.8571429em; + padding-inline-start: 1.1428571em; + } + :where(pre code):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + background-color: transparent; + border-width: 0; + border-radius: 0; + padding: 0; + font-weight: inherit; + color: inherit; + font-size: inherit; + font-family: inherit; + line-height: inherit; + } + :where(pre code):not(:where([class~="not-prose"],[class~="not-prose"] *))::before { + content: none; + } + :where(pre code):not(:where([class~="not-prose"],[class~="not-prose"] *))::after { + content: none; + } + :where(table):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + width: 100%; + table-layout: auto; + margin-top: 2em; + margin-bottom: 2em; + font-size: 0.875em; + line-height: 1.7142857; + } + :where(thead):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + border-bottom-width: 1px; + border-bottom-color: var(--tw-prose-th-borders); + } + :where(thead th):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-headings); + font-weight: 600; + vertical-align: bottom; + padding-inline-end: 0.5714286em; + padding-bottom: 0.5714286em; + padding-inline-start: 0.5714286em; + } + :where(tbody tr):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + border-bottom-width: 1px; + border-bottom-color: var(--tw-prose-td-borders); + } + :where(tbody tr:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + border-bottom-width: 0; + } + :where(tbody td):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + vertical-align: baseline; + } + :where(tfoot):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + border-top-width: 1px; + border-top-color: var(--tw-prose-th-borders); + } + :where(tfoot td):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + vertical-align: top; + } + :where(th, td):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + text-align: start; + } + :where(figure > *):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0; + margin-bottom: 0; + } + :where(figcaption):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + color: var(--tw-prose-captions); + font-size: 0.875em; + line-height: 1.4285714; + margin-top: 0.8571429em; + } + --tw-prose-body: oklch(37.3% 0.034 259.733); + --tw-prose-headings: oklch(21% 0.034 264.665); + --tw-prose-lead: oklch(44.6% 0.03 256.802); + --tw-prose-links: oklch(21% 0.034 264.665); + --tw-prose-bold: oklch(21% 0.034 264.665); + --tw-prose-counters: oklch(55.1% 0.027 264.364); + --tw-prose-bullets: oklch(87.2% 0.01 258.338); + --tw-prose-hr: oklch(92.8% 0.006 264.531); + --tw-prose-quotes: oklch(21% 0.034 264.665); + --tw-prose-quote-borders: oklch(92.8% 0.006 264.531); + --tw-prose-captions: oklch(55.1% 0.027 264.364); + --tw-prose-kbd: oklch(21% 0.034 264.665); + --tw-prose-kbd-shadows: NaN NaN NaN; + --tw-prose-code: oklch(21% 0.034 264.665); + --tw-prose-pre-code: oklch(92.8% 0.006 264.531); + --tw-prose-pre-bg: oklch(27.8% 0.033 256.848); + --tw-prose-th-borders: oklch(87.2% 0.01 258.338); + --tw-prose-td-borders: oklch(92.8% 0.006 264.531); + --tw-prose-invert-body: oklch(87.2% 0.01 258.338); + --tw-prose-invert-headings: #fff; + --tw-prose-invert-lead: oklch(70.7% 0.022 261.325); + --tw-prose-invert-links: #fff; + --tw-prose-invert-bold: #fff; + --tw-prose-invert-counters: oklch(70.7% 0.022 261.325); + --tw-prose-invert-bullets: oklch(44.6% 0.03 256.802); + --tw-prose-invert-hr: oklch(37.3% 0.034 259.733); + --tw-prose-invert-quotes: oklch(96.7% 0.003 264.542); + --tw-prose-invert-quote-borders: oklch(37.3% 0.034 259.733); + --tw-prose-invert-captions: oklch(70.7% 0.022 261.325); + --tw-prose-invert-kbd: #fff; + --tw-prose-invert-kbd-shadows: 255 255 255; + --tw-prose-invert-code: #fff; + --tw-prose-invert-pre-code: oklch(87.2% 0.01 258.338); + --tw-prose-invert-pre-bg: rgb(0 0 0 / 50%); + --tw-prose-invert-th-borders: oklch(44.6% 0.03 256.802); + --tw-prose-invert-td-borders: oklch(37.3% 0.034 259.733); + font-size: 1rem; + line-height: 1.75; + :where(picture > img):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0; + margin-bottom: 0; + } + :where(li):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0.5em; + margin-bottom: 0.5em; + } + :where(ol > li):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + padding-inline-start: 0.375em; + } + :where(ul > li):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + padding-inline-start: 0.375em; + } + :where(.prose > ul > li p):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0.75em; + margin-bottom: 0.75em; + } + :where(.prose > ul > li > p:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 1.25em; + } + :where(.prose > ul > li > p:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-bottom: 1.25em; + } + :where(.prose > ol > li > p:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 1.25em; + } + :where(.prose > ol > li > p:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-bottom: 1.25em; + } + :where(ul ul, ul ol, ol ul, ol ol):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0.75em; + margin-bottom: 0.75em; + } + :where(dl):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 1.25em; + margin-bottom: 1.25em; + } + :where(dd):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0.5em; + padding-inline-start: 1.625em; + } + :where(hr + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0; + } + :where(h2 + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0; + } + :where(h3 + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0; + } + :where(h4 + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0; + } + :where(thead th:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + padding-inline-start: 0; + } + :where(thead th:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + padding-inline-end: 0; + } + :where(tbody td, tfoot td):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + padding-top: 0.5714286em; + padding-inline-end: 0.5714286em; + padding-bottom: 0.5714286em; + padding-inline-start: 0.5714286em; + } + :where(tbody td:first-child, tfoot td:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + padding-inline-start: 0; + } + :where(tbody td:last-child, tfoot td:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + padding-inline-end: 0; + } + :where(figure):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 2em; + margin-bottom: 2em; + } + :where(.prose > :first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-top: 0; + } + :where(.prose > :last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) { + margin-bottom: 0; + } + } + .mt-2 { + margin-top: calc(var(--spacing) * 2); + } + .mt-3 { + margin-top: calc(var(--spacing) * 3); + } + .mt-4 { + margin-top: calc(var(--spacing) * 4); + } + .-mr-6 { + margin-right: calc(var(--spacing) * -6); + } + .mb-4 { + margin-bottom: calc(var(--spacing) * 4); + } + .mb-8 { + margin-bottom: calc(var(--spacing) * 8); + } + .-ml-0\.5 { + margin-left: calc(var(--spacing) * -0.5); + } + .block { + display: block; + } + .flex { + display: flex; + } + .hidden { + display: none; + } + .inline { + display: inline; + } + .size-4 { + width: calc(var(--spacing) * 4); + height: calc(var(--spacing) * 4); + } + .h-5 { + height: calc(var(--spacing) * 5); + } + .h-6 { + height: calc(var(--spacing) * 6); + } + .h-\[calc\(100vh-4\.75rem\)\] { + height: calc(100vh - 4.75rem); + } + .h-full { + height: 100%; + } + .min-h-full { + min-height: 100%; + } + .w-5 { + width: calc(var(--spacing) * 5); + } + .w-6 { + width: calc(var(--spacing) * 6); + } + .w-36 { + width: calc(var(--spacing) * 36); + } + .w-48 { + width: calc(var(--spacing) * 48); + } + .w-56 { + width: calc(var(--spacing) * 56); + } + .w-64 { + width: calc(var(--spacing) * 64); + } + .w-full { + width: 100%; + } + .max-w-3xl { + max-width: var(--container-3xl); + } + .max-w-none { + max-width: none; + } + .max-w-sm { + max-width: var(--container-sm); + } + .min-w-0 { + min-width: calc(var(--spacing) * 0); + } + .flex-auto { + flex: auto; + } + .flex-none { + flex: none; + } + .basis-0 { + flex-basis: calc(var(--spacing) * 0); + } + .transform { + transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); + } + .flex-wrap { + flex-wrap: wrap; + } + .items-center { + align-items: center; + } + .justify-between { + justify-content: space-between; + } + .justify-center { + justify-content: center; + } + .justify-end { + justify-content: flex-end; + } + .gap-2 { + gap: calc(var(--spacing) * 2); + } + .gap-4 { + gap: calc(var(--spacing) * 4); + } + .gap-6 { + gap: calc(var(--spacing) * 6); + } + .space-y-2 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse))); + } + } + .space-y-3 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse))); + } + } + .space-y-9 { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 9) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 9) * calc(1 - var(--tw-space-y-reverse))); + } + } + .space-x-1 { + :where(& > :not(:last-child)) { + --tw-space-x-reverse: 0; + margin-inline-start: calc(calc(var(--spacing) * 1) * var(--tw-space-x-reverse)); + margin-inline-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-x-reverse))); + } + } + .overflow-hidden { + overflow: hidden; + } + .overflow-x-hidden { + overflow-x: hidden; + } + .overflow-y-auto { + overflow-y: auto; + } + .rounded { + border-radius: 0.25rem; + } + .rounded-lg { + border-radius: var(--radius-lg); + } + .rounded-xl { + border-radius: var(--radius-xl); + } + .border-l-2 { + border-left-style: var(--tw-border-style); + border-left-width: 2px; + } + .border-slate-100 { + border-color: var(--color-slate-100); + } + .bg-black\/30 { + background-color: color-mix(in srgb, #000 30%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-black) 30%, transparent); + } + } + .bg-blue-500 { + background-color: var(--color-blue-500); + } + .bg-blue-900\/20 { + background-color: color-mix(in srgb, oklch(37.9% 0.146 265.522) 20%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-blue-900) 20%, transparent); + } + } + .bg-gray-100 { + background-color: var(--color-gray-100); + } + .bg-green-500 { + background-color: var(--color-green-500); + } + .bg-white { + background-color: var(--color-white); + } + .fill-slate-400 { + fill: var(--color-slate-400); + } + .stroke-sky-500 { + stroke: var(--color-sky-500); + } + .p-1 { + padding: calc(var(--spacing) * 1); + } + .p-2 { + padding: calc(var(--spacing) * 2); + } + .p-4 { + padding: calc(var(--spacing) * 4); + } + .p-6 { + padding: calc(var(--spacing) * 6); + } + .p-8 { + padding: calc(var(--spacing) * 8); + } + .px-3 { + padding-inline: calc(var(--spacing) * 3); + } + .px-4 { + padding-inline: calc(var(--spacing) * 4); + } + .py-1 { + padding-block: calc(var(--spacing) * 1); + } + .py-2 { + padding-block: calc(var(--spacing) * 2); + } + .py-5 { + padding-block: calc(var(--spacing) * 5); + } + .py-8 { + padding-block: calc(var(--spacing) * 8); + } + .py-16 { + padding-block: calc(var(--spacing) * 16); + } + .pr-6 { + padding-right: calc(var(--spacing) * 6); + } + .pr-8 { + padding-right: calc(var(--spacing) * 8); + } + .pl-0\.5 { + padding-left: calc(var(--spacing) * 0.5); + } + .pl-3\.5 { + padding-left: calc(var(--spacing) * 3.5); + } + .font-sans { + font-family: var(--font-sans); + } + .text-3xl { + font-size: var(--text-3xl); + line-height: var(--tw-leading, var(--text-3xl--line-height)); + } + .text-base { + font-size: var(--text-base); + line-height: var(--tw-leading, var(--text-base--line-height)); + } + .text-lg { + font-size: var(--text-lg); + line-height: var(--tw-leading, var(--text-lg--line-height)); + } + .text-sm { + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + } + .font-bold { + --tw-font-weight: var(--font-weight-bold); + font-weight: var(--font-weight-bold); + } + .font-medium { + --tw-font-weight: var(--font-weight-medium); + font-weight: var(--font-weight-medium); + } + .font-semibold { + --tw-font-weight: var(--font-weight-semibold); + font-weight: var(--font-weight-semibold); + } + .tracking-wider { + --tw-tracking: var(--tracking-wider); + letter-spacing: var(--tracking-wider); + } + .text-blue-600 { + color: var(--color-blue-600); + } + .text-gray-600 { + color: var(--color-gray-600); + } + .text-gray-900 { + color: var(--color-gray-900); + } + .text-sky-500 { + color: var(--color-sky-500); + } + .text-slate-500 { + color: var(--color-slate-500); + } + .text-slate-700 { + color: var(--color-slate-700); + } + .text-slate-900 { + color: var(--color-slate-900); + } + .text-white { + color: var(--color-white); + } + .text-zinc-500 { + color: var(--color-zinc-500); + } + .text-zinc-900 { + color: var(--color-zinc-900); + } + .antialiased { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + .shadow-lg { + --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-md { + --tw-shadow: 0 4px 6px -1px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 2px 4px -2px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .ring-1 { + --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + .shadow-black\/5 { + --tw-shadow-color: color-mix(in srgb, #000 5%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-black) 5%, transparent) var(--tw-shadow-alpha), transparent); + } + } + .shadow-slate-900\/5 { + --tw-shadow-color: color-mix(in srgb, oklch(20.8% 0.042 265.755) 5%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-shadow-color: color-mix(in oklab, color-mix(in oklab, var(--color-slate-900) 5%, transparent) var(--tw-shadow-alpha), transparent); + } + } + .ring-black\/5 { + --tw-ring-color: color-mix(in srgb, #000 5%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-ring-color: color-mix(in oklab, var(--color-black) 5%, transparent); + } + } + .ring-slate-900\/10 { + --tw-ring-color: color-mix(in srgb, oklch(20.8% 0.042 265.755) 10%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-ring-color: color-mix(in oklab, var(--color-slate-900) 10%, transparent); + } + } + .transition { + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, visibility, content-visibility, overlay, pointer-events; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .duration-500 { + --tw-duration: 500ms; + transition-duration: 500ms; + } + .prose-slate { + --tw-prose-body: oklch(37.2% 0.044 257.287); + --tw-prose-headings: oklch(20.8% 0.042 265.755); + --tw-prose-lead: oklch(44.6% 0.043 257.281); + --tw-prose-links: oklch(20.8% 0.042 265.755); + --tw-prose-bold: oklch(20.8% 0.042 265.755); + --tw-prose-counters: oklch(55.4% 0.046 257.417); + --tw-prose-bullets: oklch(86.9% 0.022 252.894); + --tw-prose-hr: oklch(92.9% 0.013 255.508); + --tw-prose-quotes: oklch(20.8% 0.042 265.755); + --tw-prose-quote-borders: oklch(92.9% 0.013 255.508); + --tw-prose-captions: oklch(55.4% 0.046 257.417); + --tw-prose-kbd: oklch(20.8% 0.042 265.755); + --tw-prose-kbd-shadows: NaN NaN NaN; + --tw-prose-code: oklch(20.8% 0.042 265.755); + --tw-prose-pre-code: oklch(92.9% 0.013 255.508); + --tw-prose-pre-bg: oklch(27.9% 0.041 260.031); + --tw-prose-th-borders: oklch(86.9% 0.022 252.894); + --tw-prose-td-borders: oklch(92.9% 0.013 255.508); + --tw-prose-invert-body: oklch(86.9% 0.022 252.894); + --tw-prose-invert-headings: #fff; + --tw-prose-invert-lead: oklch(70.4% 0.04 256.788); + --tw-prose-invert-links: #fff; + --tw-prose-invert-bold: #fff; + --tw-prose-invert-counters: oklch(70.4% 0.04 256.788); + --tw-prose-invert-bullets: oklch(44.6% 0.043 257.281); + --tw-prose-invert-hr: oklch(37.2% 0.044 257.287); + --tw-prose-invert-quotes: oklch(96.8% 0.007 247.896); + --tw-prose-invert-quote-borders: oklch(37.2% 0.044 257.287); + --tw-prose-invert-captions: oklch(70.4% 0.04 256.788); + --tw-prose-invert-kbd: #fff; + --tw-prose-invert-kbd-shadows: 255 255 255; + --tw-prose-invert-code: #fff; + --tw-prose-invert-pre-code: oklch(86.9% 0.022 252.894); + --tw-prose-invert-pre-bg: rgb(0 0 0 / 50%); + --tw-prose-invert-th-borders: oklch(44.6% 0.043 257.281); + --tw-prose-invert-td-borders: oklch(37.2% 0.044 257.287); + } + .group-hover\:fill-slate-500 { + &:is(:where(.group):hover *) { + @media (hover: hover) { + fill: var(--color-slate-500); + } + } + } + .before\:pointer-events-none { + &::before { + content: var(--tw-content); + pointer-events: none; + } + } + .before\:absolute { + &::before { + content: var(--tw-content); + position: absolute; + } + } + .before\:top-1\/2 { + &::before { + content: var(--tw-content); + top: calc(1/2 * 100%); + } + } + .before\:-left-1 { + &::before { + content: var(--tw-content); + left: calc(var(--spacing) * -1); + } + } + .before\:hidden { + &::before { + content: var(--tw-content); + display: none; + } + } + .before\:h-1\.5 { + &::before { + content: var(--tw-content); + height: calc(var(--spacing) * 1.5); + } + } + .before\:w-1\.5 { + &::before { + content: var(--tw-content); + width: calc(var(--spacing) * 1.5); + } + } + .before\:-translate-y-1\/2 { + &::before { + content: var(--tw-content); + --tw-translate-y: calc(calc(1/2 * 100%) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + } + .before\:rounded-full { + &::before { + content: var(--tw-content); + border-radius: calc(infinity * 1px); + } + } + .before\:bg-sky-500 { + &::before { + content: var(--tw-content); + background-color: var(--color-sky-500); + } + } + .before\:bg-slate-300 { + &::before { + content: var(--tw-content); + background-color: var(--color-slate-300); + } + } + .hover\:bg-green-700 { + &:hover { + @media (hover: hover) { + background-color: var(--color-green-700); + } + } + } + .hover\:bg-slate-100 { + &:hover { + @media (hover: hover) { + background-color: var(--color-slate-100); + } + } + } + .hover\:text-slate-600 { + &:hover { + @media (hover: hover) { + color: var(--color-slate-600); + } + } + } + .hover\:text-zinc-600 { + &:hover { + @media (hover: hover) { + color: var(--color-zinc-600); + } + } + } + .hover\:before\:block { + &:hover { + @media (hover: hover) { + &::before { + content: var(--tw-content); + display: block; + } + } + } + } + .sm\:gap-8 { + @media (width >= 40rem) { + gap: calc(var(--spacing) * 8); + } + } + .sm\:px-2 { + @media (width >= 40rem) { + padding-inline: calc(var(--spacing) * 2); + } + } + .sm\:px-6 { + @media (width >= 40rem) { + padding-inline: calc(var(--spacing) * 6); + } + } + .md\:grow { + @media (width >= 48rem) { + flex-grow: 1; + } + } + .lg\:relative { + @media (width >= 64rem) { + position: relative; + } + } + .lg\:mt-4 { + @media (width >= 64rem) { + margin-top: calc(var(--spacing) * 4); + } + } + .lg\:block { + @media (width >= 64rem) { + display: block; + } + } + .lg\:hidden { + @media (width >= 64rem) { + display: none; + } + } + .lg\:max-w-none { + @media (width >= 64rem) { + max-width: none; + } + } + .lg\:flex-none { + @media (width >= 64rem) { + flex: none; + } + } + .lg\:space-y-4 { + @media (width >= 64rem) { + :where(& > :not(:last-child)) { + --tw-space-y-reverse: 0; + margin-block-start: calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse)); + margin-block-end: calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse))); + } + } + } + .lg\:border-slate-200 { + @media (width >= 64rem) { + border-color: var(--color-slate-200); + } + } + .lg\:px-8 { + @media (width >= 64rem) { + padding-inline: calc(var(--spacing) * 8); + } + } + .lg\:pr-0 { + @media (width >= 64rem) { + padding-right: calc(var(--spacing) * 0); + } + } + .lg\:pl-8 { + @media (width >= 64rem) { + padding-left: calc(var(--spacing) * 8); + } + } + .lg\:text-sm { + @media (width >= 64rem) { + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + } + } + .xl\:sticky { + @media (width >= 80rem) { + position: sticky; + } + } + .xl\:top-\[4\.75rem\] { + @media (width >= 80rem) { + top: 4.75rem; + } + } + .xl\:-mr-6 { + @media (width >= 80rem) { + margin-right: calc(var(--spacing) * -6); + } + } + .xl\:block { + @media (width >= 80rem) { + display: block; + } + } + .xl\:h-\[calc\(100vh-4\.75rem\)\] { + @media (width >= 80rem) { + height: calc(100vh - 4.75rem); + } + } + .xl\:w-72 { + @media (width >= 80rem) { + width: calc(var(--spacing) * 72); + } + } + .xl\:flex-none { + @media (width >= 80rem) { + flex: none; + } + } + .xl\:overflow-y-auto { + @media (width >= 80rem) { + overflow-y: auto; + } + } + .xl\:px-12 { + @media (width >= 80rem) { + padding-inline: calc(var(--spacing) * 12); + } + } + .xl\:px-16 { + @media (width >= 80rem) { + padding-inline: calc(var(--spacing) * 16); + } + } + .xl\:py-16 { + @media (width >= 80rem) { + padding-block: calc(var(--spacing) * 16); + } + } + .xl\:pr-6 { + @media (width >= 80rem) { + padding-right: calc(var(--spacing) * 6); + } + } + .xl\:pr-16 { + @media (width >= 80rem) { + padding-right: calc(var(--spacing) * 16); + } + } + .dark\:block { + &:where(.dark, .dark *) { + display: block; + } + } + .dark\:hidden { + &:where(.dark, .dark *) { + display: none; + } + } + .dark\:border-slate-800 { + &:where(.dark, .dark *) { + border-color: var(--color-slate-800); + } + } + .dark\:bg-slate-700 { + &:where(.dark, .dark *) { + background-color: var(--color-slate-700); + } + } + .dark\:bg-slate-800 { + &:where(.dark, .dark *) { + background-color: var(--color-slate-800); + } + } + .dark\:bg-slate-900 { + &:where(.dark, .dark *) { + background-color: var(--color-slate-900); + } + } + .dark\:bg-slate-900\/75 { + &:where(.dark, .dark *) { + background-color: color-mix(in srgb, oklch(20.8% 0.042 265.755) 75%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-slate-900) 75%, transparent); + } + } + } + .dark\:text-slate-300 { + &:where(.dark, .dark *) { + color: var(--color-slate-300); + } + } + .dark\:text-slate-400 { + &:where(.dark, .dark *) { + color: var(--color-slate-400); + } + } + .dark\:text-white { + &:where(.dark, .dark *) { + color: var(--color-white); + } + } + .dark\:text-zinc-400 { + &:where(.dark, .dark *) { + color: var(--color-zinc-400); + } + } + .dark\:shadow-none { + &:where(.dark, .dark *) { + --tw-shadow: 0 0 #0000; + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + } + .dark\:ring-0 { + &:where(.dark, .dark *) { + --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); + } + } + .dark\:ring-white\/5 { + &:where(.dark, .dark *) { + --tw-ring-color: color-mix(in srgb, #fff 5%, transparent); + @supports (color: color-mix(in lab, red, red)) { + --tw-ring-color: color-mix(in oklab, var(--color-white) 5%, transparent); + } + } + } + .dark\:backdrop-blur { + &:where(.dark, .dark *) { + --tw-backdrop-blur: blur(8px); + -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + } + } + .dark\:prose-invert { + &:where(.dark, .dark *) { + --tw-prose-body: var(--tw-prose-invert-body); + --tw-prose-headings: var(--tw-prose-invert-headings); + --tw-prose-lead: var(--tw-prose-invert-lead); + --tw-prose-links: var(--tw-prose-invert-links); + --tw-prose-bold: var(--tw-prose-invert-bold); + --tw-prose-counters: var(--tw-prose-invert-counters); + --tw-prose-bullets: var(--tw-prose-invert-bullets); + --tw-prose-hr: var(--tw-prose-invert-hr); + --tw-prose-quotes: var(--tw-prose-invert-quotes); + --tw-prose-quote-borders: var(--tw-prose-invert-quote-borders); + --tw-prose-captions: var(--tw-prose-invert-captions); + --tw-prose-kbd: var(--tw-prose-invert-kbd); + --tw-prose-kbd-shadows: var(--tw-prose-invert-kbd-shadows); + --tw-prose-code: var(--tw-prose-invert-code); + --tw-prose-pre-code: var(--tw-prose-invert-pre-code); + --tw-prose-pre-bg: var(--tw-prose-invert-pre-bg); + --tw-prose-th-borders: var(--tw-prose-invert-th-borders); + --tw-prose-td-borders: var(--tw-prose-invert-td-borders); + } + } + .dark\:ring-inset { + &:where(.dark, .dark *) { + --tw-ring-inset: inset; + } + } + .dark\:group-hover\:fill-slate-300 { + &:where(.dark, .dark *) { + &:is(:where(.group):hover *) { + @media (hover: hover) { + fill: var(--color-slate-300); + } + } + } + } + .dark\:before\:bg-slate-700 { + &:where(.dark, .dark *) { + &::before { + content: var(--tw-content); + background-color: var(--color-slate-700); + } + } + } + .dark\:hover\:bg-slate-700\/50 { + &:where(.dark, .dark *) { + &:hover { + @media (hover: hover) { + background-color: color-mix(in srgb, oklch(37.2% 0.044 257.287) 50%, transparent); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-slate-700) 50%, transparent); + } + } + } + } + } + .dark\:hover\:text-slate-300 { + &:where(.dark, .dark *) { + &:hover { + @media (hover: hover) { + color: var(--color-slate-300); + } + } + } + } + .dark\:hover\:text-zinc-300 { + &:where(.dark, .dark *) { + &:hover { + @media (hover: hover) { + color: var(--color-zinc-300); + } + } + } + } +} +.prose pre { + position: relative; + border-radius: 0.75rem; + background-color: rgb(15 23 42); + border: 1px solid rgb(30 41 59); + box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); + overflow-x: auto; +} +.prose pre code { + background-color: transparent !important; + border-radius: 0; + padding: 0; + color: rgb(226 232 240); + font-size: 0.875rem; + line-height: 1.7; + font-family: 'Fira Code', 'JetBrains Mono', 'Monaco', 'Cascadia Code', 'Segoe UI Mono', 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace; +} +.prose pre code .hljs-keyword { + color: rgb(168 85 247); +} +.prose pre code .hljs-string { + color: rgb(34 197 94); +} +.prose pre code .hljs-comment { + color: rgb(148 163 184); + font-style: italic; +} +.prose pre code .hljs-function { + color: rgb(59 130 246); +} +.prose pre code .hljs-number { + color: rgb(251 146 60); +} +.prose :not(pre) > code { + background-color: rgb(51 65 85) !important; + color: rgb(226 232 240) !important; + padding: 0.25rem 0.5rem !important; + border-radius: 0.375rem !important; + font-size: 0.875rem !important; + font-weight: 500 !important; +} +.prose code::before, .prose code::after { + content: none !important; +} +@property --tw-rotate-x { + syntax: "*"; + inherits: false; +} +@property --tw-rotate-y { + syntax: "*"; + inherits: false; +} +@property --tw-rotate-z { + syntax: "*"; + inherits: false; +} +@property --tw-skew-x { + syntax: "*"; + inherits: false; +} +@property --tw-skew-y { + syntax: "*"; + inherits: false; +} +@property --tw-space-y-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-space-x-reverse { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-border-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} +@property --tw-font-weight { + syntax: "*"; + inherits: false; +} +@property --tw-tracking { + syntax: "*"; + inherits: false; +} +@property --tw-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-inset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-inset-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-inset-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-ring-color { + syntax: "*"; + inherits: false; +} +@property --tw-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-inset-ring-color { + syntax: "*"; + inherits: false; +} +@property --tw-inset-ring-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-ring-inset { + syntax: "*"; + inherits: false; +} +@property --tw-ring-offset-width { + syntax: ""; + inherits: false; + initial-value: 0px; +} +@property --tw-ring-offset-color { + syntax: "*"; + inherits: false; + initial-value: #fff; +} +@property --tw-ring-offset-shadow { + syntax: "*"; + inherits: false; + initial-value: 0 0 #0000; +} +@property --tw-duration { + syntax: "*"; + inherits: false; +} +@property --tw-content { + syntax: "*"; + initial-value: ""; + inherits: false; +} +@property --tw-translate-x { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-translate-y { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-translate-z { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-backdrop-blur { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-brightness { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-contrast { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-grayscale { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-hue-rotate { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-invert { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-opacity { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-saturate { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-sepia { + syntax: "*"; + inherits: false; +} +@layer properties { + @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { + *, ::before, ::after, ::backdrop { + --tw-rotate-x: initial; + --tw-rotate-y: initial; + --tw-rotate-z: initial; + --tw-skew-x: initial; + --tw-skew-y: initial; + --tw-space-y-reverse: 0; + --tw-space-x-reverse: 0; + --tw-border-style: solid; + --tw-font-weight: initial; + --tw-tracking: initial; + --tw-shadow: 0 0 #0000; + --tw-shadow-color: initial; + --tw-shadow-alpha: 100%; + --tw-inset-shadow: 0 0 #0000; + --tw-inset-shadow-color: initial; + --tw-inset-shadow-alpha: 100%; + --tw-ring-color: initial; + --tw-ring-shadow: 0 0 #0000; + --tw-inset-ring-color: initial; + --tw-inset-ring-shadow: 0 0 #0000; + --tw-ring-inset: initial; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-offset-shadow: 0 0 #0000; + --tw-duration: initial; + --tw-content: ""; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-translate-z: 0; + --tw-backdrop-blur: initial; + --tw-backdrop-brightness: initial; + --tw-backdrop-contrast: initial; + --tw-backdrop-grayscale: initial; + --tw-backdrop-hue-rotate: initial; + --tw-backdrop-invert: initial; + --tw-backdrop-opacity: initial; + --tw-backdrop-saturate: initial; + --tw-backdrop-sepia: initial; + } + } +} diff --git a/docs/src/Docs/wwwroot/favicon-16x16.png b/docs/src/Docs/wwwroot/favicon-16x16.png new file mode 100644 index 0000000..18875a4 Binary files /dev/null and b/docs/src/Docs/wwwroot/favicon-16x16.png differ diff --git a/docs/src/Docs/wwwroot/favicon-32x32.png b/docs/src/Docs/wwwroot/favicon-32x32.png new file mode 100644 index 0000000..2e3065f Binary files /dev/null and b/docs/src/Docs/wwwroot/favicon-32x32.png differ diff --git a/docs/src/Docs/wwwroot/favicon.ico b/docs/src/Docs/wwwroot/favicon.ico new file mode 100644 index 0000000..719dd27 Binary files /dev/null and b/docs/src/Docs/wwwroot/favicon.ico differ diff --git a/docs/src/Docs/wwwroot/logo.png b/docs/src/Docs/wwwroot/logo.png new file mode 100644 index 0000000..d75e5fd Binary files /dev/null and b/docs/src/Docs/wwwroot/logo.png differ diff --git a/fsharp-view-engine/src/App/wwwroot/scripts/alpinejs.3.15.0.min.js b/docs/src/Docs/wwwroot/scripts/alpinejs.3.15.0.min.js similarity index 100% rename from fsharp-view-engine/src/App/wwwroot/scripts/alpinejs.3.15.0.min.js rename to docs/src/Docs/wwwroot/scripts/alpinejs.3.15.0.min.js diff --git a/docs/src/Docs/wwwroot/site.webmanifest b/docs/src/Docs/wwwroot/site.webmanifest new file mode 100644 index 0000000..45dc8a2 --- /dev/null +++ b/docs/src/Docs/wwwroot/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file diff --git a/etc/logo.png b/etc/logo.png new file mode 100644 index 0000000..d75e5fd Binary files /dev/null and b/etc/logo.png differ diff --git a/etc/logo.svg b/etc/logo.svg new file mode 100644 index 0000000..244f1e8 --- /dev/null +++ b/etc/logo.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + diff --git a/fsharp-view-engine/FSharp.ViewEngine.sln b/fsharp-view-engine/FSharp.ViewEngine.sln deleted file mode 100644 index 01a0d46..0000000 --- a/fsharp-view-engine/FSharp.ViewEngine.sln +++ /dev/null @@ -1,48 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30114.105 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{EA093A44-5365-45ED-B8FB-C506C95405A6}" -EndProject -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharp.ViewEngine", "src\FSharp.ViewEngine\FSharp.ViewEngine.fsproj", "{4093E9E9-D923-4945-A458-C1F861C3FD1D}" -EndProject -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Build", "src\Build\Build.fsproj", "{2562C7E1-2366-4DB0-9A86-278B8AA5117B}" -EndProject -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Tests", "src\Tests\Tests.fsproj", "{65C2946A-1B1E-45AB-8196-DFE772FA9240}" -EndProject -Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "App", "src\App\App.fsproj", "{ADA5276C-21C3-4633-B83F-F50A93D5FF36}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {4093E9E9-D923-4945-A458-C1F861C3FD1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4093E9E9-D923-4945-A458-C1F861C3FD1D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4093E9E9-D923-4945-A458-C1F861C3FD1D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4093E9E9-D923-4945-A458-C1F861C3FD1D}.Release|Any CPU.Build.0 = Release|Any CPU - {2562C7E1-2366-4DB0-9A86-278B8AA5117B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2562C7E1-2366-4DB0-9A86-278B8AA5117B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2562C7E1-2366-4DB0-9A86-278B8AA5117B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2562C7E1-2366-4DB0-9A86-278B8AA5117B}.Release|Any CPU.Build.0 = Release|Any CPU - {65C2946A-1B1E-45AB-8196-DFE772FA9240}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {65C2946A-1B1E-45AB-8196-DFE772FA9240}.Debug|Any CPU.Build.0 = Debug|Any CPU - {65C2946A-1B1E-45AB-8196-DFE772FA9240}.Release|Any CPU.ActiveCfg = Release|Any CPU - {65C2946A-1B1E-45AB-8196-DFE772FA9240}.Release|Any CPU.Build.0 = Release|Any CPU - {ADA5276C-21C3-4633-B83F-F50A93D5FF36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {ADA5276C-21C3-4633-B83F-F50A93D5FF36}.Debug|Any CPU.Build.0 = Debug|Any CPU - {ADA5276C-21C3-4633-B83F-F50A93D5FF36}.Release|Any CPU.ActiveCfg = Release|Any CPU - {ADA5276C-21C3-4633-B83F-F50A93D5FF36}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {4093E9E9-D923-4945-A458-C1F861C3FD1D} = {EA093A44-5365-45ED-B8FB-C506C95405A6} - {2562C7E1-2366-4DB0-9A86-278B8AA5117B} = {EA093A44-5365-45ED-B8FB-C506C95405A6} - {65C2946A-1B1E-45AB-8196-DFE772FA9240} = {EA093A44-5365-45ED-B8FB-C506C95405A6} - {ADA5276C-21C3-4633-B83F-F50A93D5FF36} = {EA093A44-5365-45ED-B8FB-C506C95405A6} - EndGlobalSection -EndGlobal diff --git a/fsharp-view-engine/src/App/Handlers.fs b/fsharp-view-engine/src/App/Handlers.fs deleted file mode 100644 index d2a1ef4..0000000 --- a/fsharp-view-engine/src/App/Handlers.fs +++ /dev/null @@ -1,39 +0,0 @@ -namespace App - -open System -open System.IO -open Giraffe -open Microsoft.AspNetCore.Http -open Markdig -open FSharp.ViewEngine - -module Handlers = - - let private markdownPipeline = MarkdownPipelineBuilder().UseAdvancedExtensions().Build() - - let private readMarkdownFile (fileName: string) = - let filePath = Path.Combine(AppContext.BaseDirectory, "docs", fileName + ".md") - if File.Exists(filePath) then - let content = File.ReadAllText(filePath) - Markdown.ToHtml(content, markdownPipeline) - else - "

Page Not Found

The requested page could not be found.

" - - let private renderPage (title: string) (fileName: string) : HttpHandler = - fun next ctx -> task { - let currentPath = ctx.Request.Path.Value - let markdownContent = readMarkdownFile fileName - let content = Views.layout title currentPath markdownContent - let html = Element.render content - return! htmlString html next ctx - } - - // Route handlers - let homeHandler : HttpHandler = - renderPage "FSharp.ViewEngine Documentation" "home" - - let installationHandler : HttpHandler = - renderPage "Installation - FSharp.ViewEngine" "installation" - - let quickstartHandler : HttpHandler = - renderPage "Quickstart - FSharp.ViewEngine" "quickstart" diff --git a/fsharp-view-engine/src/App/Views.fs b/fsharp-view-engine/src/App/Views.fs deleted file mode 100644 index 5a335a4..0000000 --- a/fsharp-view-engine/src/App/Views.fs +++ /dev/null @@ -1,324 +0,0 @@ -module App.Views - -open FSharp.ViewEngine -open type Html -open type Alpine - -type Page = - { title:string } - -let magnifyingGlassIcon = raw """ - - - - """ -let menuIcon = raw """ - - - - """ -let githubIcon = raw """""" -let sunIcon = raw """""" -let moonIcon = raw """""" -let sunIconSmall = raw """""" -let moonIconSmall = raw """""" -let monitorIcon = raw """""" - -let private pageHeader = - header [ - _class [ - "sticky top-0 z-50 flex flex-none flex-wrap items-center justify-between" - "bg-white px-4 py-5 shadow-md shadow-slate-900/5 transition duration-500" - "sm:px-6 lg:px-8 dark:shadow-none dark:bg-slate-900/75 dark:backdrop-blur" - ] - _children [ - // Left section: hamburger + logo - div [ - _class "flex items-center gap-4" - _children [ - // Mobile menu button (hidden on desktop) - div [ - _class "flex lg:hidden" - _children [ - button [ - _type "button" - _class "relative text-slate-500 hover:text-slate-600 dark:text-slate-400 dark:hover:text-slate-300" - _children menuIcon - ] - ] - ] - // Logo - a [ - _href "/" - _class "text-sm font-semibold tracking-wider text-slate-700 dark:text-white" - _children "FSharp.ViewEngine" - ] - ] - ] - // Right section with theme toggle and GitHub - div [ - _class "relative flex basis-0 justify-end gap-6 sm:gap-8 md:grow" - _children [ - // Theme toggle dropdown - div [ - _class "relative z-10" - _xData "{ open: false, theme: localStorage.getItem('theme') || 'system' }" - _xInit """ - $watch('theme', (val) => { - localStorage.setItem('theme', val); - if (val === 'dark' || (val === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) { - document.documentElement.classList.add('dark'); - } else { - document.documentElement.classList.remove('dark'); - } - }); - if (theme === 'dark' || (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) { - document.documentElement.classList.add('dark'); - } - """ - _children [ - button [ - _type "button" - _class [ - "flex h-6 w-6 items-center justify-center rounded-lg shadow-md ring-1" - "shadow-black/5 ring-black/5 dark:bg-slate-700 dark:ring-white/5" - "dark:ring-inset" - ] - _xOn ("click", "open = !open") - _children [ - // Light mode icon (sun) - sunIcon - moonIcon - ] - ] - // Dropdown menu - div [ - _xShow "open" - _xOn ("click.away", "open = false") - _xTransition () - _class [ - "absolute right-0 top-full mt-3 w-36 overflow-hidden rounded-lg" - "bg-white py-1 text-sm font-semibold text-slate-700 shadow-lg ring-1" - "ring-slate-900/10 dark:bg-slate-800 dark:text-slate-300 dark:ring-0" - "dark:highlight-white/5" - ] - _children [ - // Light option - button [ - _type "button" - _class "flex w-full items-center gap-2 px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-700/50" - _xOn ("click", "theme = 'light'; open = false") - _xBind ("class", "theme === 'light' ? 'text-sky-500' : ''") - _children [ - sunIconSmall - text "Light" - ] - ] - // Dark option - button [ - _type "button" - _class "flex w-full items-center gap-2 px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-700/50" - _xOn ("click", "theme = 'dark'; open = false") - _xBind ("class", "theme === 'dark' ? 'text-sky-500' : ''") - _children [ - moonIconSmall - text "Dark" - ] - ] - // System option - button [ - _type "button" - _class "flex w-full items-center gap-2 px-3 py-2 hover:bg-slate-100 dark:hover:bg-slate-700/50" - _xOn ("click", "theme = 'system'; open = false") - _xBind ("class", "theme === 'system' ? 'text-sky-500' : ''") - _children [ - monitorIcon - text "System" - ] - ] - ] - ] - ] - ] - // GitHub link - a [ - _href "https://github.com/meiermade/FSharp.ViewEngine" - _class "group" - _children [ - githubIcon - ] - ] - ] - ] - ] - ] - -let private navLink (currentPath: string) (href: string) (label: string) = - let isActive = currentPath = href - li [ - _class "relative" - _children [ - a [ - _class [ - "block w-full pl-3.5 before:pointer-events-none before:absolute" - "before:-left-1 before:top-1/2 before:h-1.5 before:w-1.5" - "before:-translate-y-1/2 before:rounded-full" - if isActive then - "font-semibold text-sky-500 before:bg-sky-500" - else - "text-slate-500 before:hidden before:bg-slate-300 hover:text-slate-600" - + " hover:before:block dark:text-slate-400 dark:before:bg-slate-700 dark:hover:text-slate-300" - ] - _href href - _children label - ] - ] - ] - -let private sidebarNavigation (currentPath: string) = - nav [ - _class "text-base lg:text-sm" - _children [ - ul [ - _role "list" - _class "space-y-9" - _children [ - li [ - _children [ - h2 [ - _class "font-display font-medium text-slate-900 dark:text-white" - _children "Getting started" - ] - ul [ - _role "list" - _class [ - "mt-2 space-y-2 border-l-2 border-slate-100" - "lg:mt-4 lg:space-y-4 lg:border-slate-200 dark:border-slate-800" - ] - _children [ - navLink currentPath "/" "Introduction" - navLink currentPath "/installation" "Installation" - navLink currentPath "/quickstart" "Quickstart" - ] - ] - ] - ] - ] - ] - ] - ] - -let private sidebar (currentPath: string) = - div [ - _class "hidden lg:relative lg:block lg:flex-none" - _children [ - div [ - _class [ - "sticky top-[4.75rem] -ml-0.5 h-[calc(100vh-4.75rem)] w-64" - "overflow-y-auto overflow-x-hidden py-16 pl-0.5 pr-8 xl:w-72 xl:pr-16" - ] - _children [ - sidebarNavigation currentPath - ] - ] - ] - ] - -let private tableOfContents (headings: (string * string) list) = - if List.isEmpty headings then - empty - else - nav [ - _class "sticky top-[4.75rem] -mr-6 w-56 flex-none overflow-y-auto py-16 pr-6" - _children [ - h2 [ - _class "font-display text-sm font-medium text-zinc-900 dark:text-white" - _children "On this page" - ] - ul [ - _role "list" - _class "mt-4 space-y-3 text-sm" - _children [ - for (title, anchor) in headings do - yield li [ - _children [ - a [ - _href $"#{anchor}" - _class "text-zinc-500 hover:text-zinc-600 dark:text-zinc-400 dark:hover:text-zinc-300" - _children title - ] - ] - ] - ] - ] - ] - ] - -let layout (pageTitle: string) (currentPath: string) (content: string) = - html [ - _lang "en" - _class "h-full antialiased" - _children [ - head [ - meta [ _charset "utf-8" ] - meta [ _name "viewport"; _content "width=device-width, initial-scale=1" ] - title pageTitle - script "let t=localStorage.getItem('theme');if(t==='dark'||(!t||t==='system')&&window.matchMedia('(prefers-color-scheme: dark)').matches){document.documentElement.classList.add('dark')}" - link [ _rel "stylesheet"; _href "/css/output.css" ] - script [ _src "https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"; KeyValue ("defer", "true") ] - script [ _src "https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js" ] - link [ _rel "stylesheet"; _href "https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" ] - script [ _src "https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-fsharp.min.js" ] - ] - body [ - _class "min-h-full bg-white dark:bg-slate-900" - _children [ - pageHeader - div [ - _class [ - "relative mx-auto flex max-w-8xl justify-center" - "sm:px-2 lg:px-8 xl:px-12" - ] - _children [ - sidebar currentPath - div [ - _class [ - "min-w-0 max-w-3xl flex-auto px-4 py-16" - "lg:max-w-none lg:pl-8 lg:pr-0 xl:px-16" - ] - _children [ - article [ - _children [ - div [ - _class "mb-8" - _children [ - p [ - _class "font-display text-sm font-medium text-sky-500" - _children "Getting started" - ] - ] - ] - div [ - _class "prose prose-slate dark:prose-invert max-w-none" - _children [ raw content ] - ] - ] - ] - ] - ] - div [ - _class [ - "hidden xl:sticky xl:top-[4.75rem] xl:-mr-6 xl:block" - "xl:h-[calc(100vh-4.75rem)] xl:flex-none xl:overflow-y-auto" - "xl:py-16 xl:pr-6" - ] - _children [ - tableOfContents [] - ] - ] - ] - ] - ] - ] - ] - ] diff --git a/fsharp-view-engine/src/App/docs/home.md b/fsharp-view-engine/src/App/docs/home.md deleted file mode 100644 index 9ef741d..0000000 --- a/fsharp-view-engine/src/App/docs/home.md +++ /dev/null @@ -1,62 +0,0 @@ -# FSharp.ViewEngine Documentation - -A powerful F# view engine for building HTML with type safety and composability. Inspired by Giraffe.ViewEngine and Feliz.ViewEngine. - -## Key Features - -- **Type-safe HTML generation** with F# -- **Built-in support for HTMX attributes** -- **Tailwind CSS integration** -- **Alpine.js directives support** -- **Composable and functional approach** -- **No runtime dependencies** - -## Quick Example - -```fsharp -open FSharp.ViewEngine -open type Html -open type Htmx -open type Tailwind - -let myPage = - html [ - _lang "en" - _children [ - head [ - title "My App" - meta [ _charset "utf-8" ] - link [ _href "/css/tailwind.css"; _rel "stylesheet" ] - ] - body [ - _class "bg-gray-100" - _children [ - div [ - _class [ "container"; "mx-auto"; "p-4" ] - _children [ - h1 [ - _class [ "text-3xl"; "font-bold"; "text-blue-600"; "mb-4" ] - _children "Welcome!" - ] - button [ - _class [ "bg-blue-500"; "text-white"; "px-4"; "py-2"; "rounded" ] - _hxGet "/api/data" - _hxTarget "#content" - _children "Load Content" - ] - div [ - _id "content" - _class [ "mt-4" ] - ] - ] - ] - ] - ] - ] - ] - |> Element.render -``` - -## Getting Started - -To get started with FSharp.ViewEngine, check out the [Installation](installation) guide and then follow the [Quickstart](quickstart) tutorial. \ No newline at end of file diff --git a/fsharp-view-engine/src/App/docs/quickstart.md b/fsharp-view-engine/src/App/docs/quickstart.md deleted file mode 100644 index 76ab000..0000000 --- a/fsharp-view-engine/src/App/docs/quickstart.md +++ /dev/null @@ -1,98 +0,0 @@ -# Quickstart - -Get started with FSharp.ViewEngine in just a few steps. - -## Basic Usage - -Import the core modules and start building HTML: - -```fsharp -open FSharp.ViewEngine -open type Html - -let myView = - div [ - _class "container" - _children [ - h1 [ _children "Hello, World!" ] - p [ _children "Welcome to FSharp.ViewEngine" ] - ] - ] - -// Render to string -let htmlString = Element.render myView -``` - -This will produce the following HTML: - -```html -
-

Hello, World!

-

Welcome to FSharp.ViewEngine

-
-``` - -## With HTMX and Tailwind CSS - -FSharp.ViewEngine includes built-in support for HTMX and Tailwind CSS: - -```fsharp -open FSharp.ViewEngine -open type Html -open type Htmx -open type Tailwind - -let interactiveView = - div [ - _class [ "bg-blue-500"; "text-white"; "p-4"; "rounded" ] - _children [ - button [ - _class [ "bg-green-500"; "hover:bg-green-700"; "px-4"; "py-2"; "rounded" ] - _hxGet "/api/data" - _hxTarget "#result" - _children "Load Data" - ] - div [ - _id "result" - _class [ "mt-4" ] - ] - ] - ] -``` - -## Complete HTML Document - -Here's how to create a complete HTML document: - -```fsharp -let completePage = - html [ - _lang "en" - _children [ - head [ - title [ _children "My Page" ] - meta [ _charset "utf-8" ] - meta [ _name "viewport"; _content "width=device-width, initial-scale=1" ] - link [ _rel "stylesheet"; _href "https://cdn.tailwindcss.com" ] - ] - body [ - _class [ "bg-gray-100"; "font-sans" ] - _children [ - div [ - _class [ "container"; "mx-auto"; "px-4"; "py-8" ] - _children [ - h1 [ - _class [ "text-3xl"; "font-bold"; "text-gray-900"; "mb-4" ] - _children "Welcome to my site!" - ] - p [ - _class [ "text-lg"; "text-gray-600" ] - _children "This is built with FSharp.ViewEngine." - ] - ] - ] - ] - ] - ] - ] -``` \ No newline at end of file diff --git a/fsharp-view-engine/src/FSharp.ViewEngine/Alpine.fs b/fsharp-view-engine/src/FSharp.ViewEngine/Alpine.fs deleted file mode 100644 index 57050e1..0000000 --- a/fsharp-view-engine/src/FSharp.ViewEngine/Alpine.fs +++ /dev/null @@ -1,38 +0,0 @@ -namespace FSharp.ViewEngine - -type Alpine = - static member _by(value:string) = KeyValue("by", value) - static member _x (key:string, ?value:string) = match value with Some v -> KeyValue ($"x-{key}", v) | None -> Boolean $"x-{key}" - static member _xOn (event:string, v:string) = KeyValue ($"x-on:{event}", v) - static member _xOn (event:string) = Boolean $"x-on:{event}" - static member _xInit (value:string) = KeyValue ("x-init", value) - static member _xData (value:string) = KeyValue ("x-data", value) - static member _xRef (value:string) = KeyValue ("x-ref", value) - static member _xText (value:string) = KeyValue ("x-text", value) - static member _xBind (attr:string, value:string) = KeyValue ($"x-bind:{attr}", value) - static member _xShow (value:string) = KeyValue ("x-show", value) - static member _xIf (value:string) = KeyValue ("x-if", value) - static member _xFor (value:string) = KeyValue ("x-for", value) - static member _xModel (value:string, ?modifier:string) = - match modifier with - | Some modifier -> KeyValue ($"x-model{modifier}", value) - | None -> KeyValue("x-model", value) - static member _xModelable (value:string) = KeyValue ("x-modelable", value) - static member _xId (value:string) = KeyValue ("x-id", value) - static member _xEffect (value:string) = KeyValue ("x-effect", value) - static member _xTransition (?value:string, ?modifier:string) = - match value, modifier with - | Some value, Some modifier -> KeyValue ($"x-transition{modifier}", value) - | Some value, None -> KeyValue ("x-transition", value) - | None, Some modifier -> Boolean $"x-transition{modifier}" - | None, None -> Boolean "x-transition" - static member _xTrap (value:string, ?modifier:string) = - match modifier with - | Some modifier -> KeyValue ($"x-trap{modifier}", value) - | None -> KeyValue ("x-trap", value) - static member _xCloak = Boolean "x-cloak" - static member _xAnchor (value:string, ?modifier:string) = - match modifier with - | Some m -> KeyValue ($"x-anchor{m}", value) - | None -> KeyValue ("x-anchor", value) - static member _xTeleport(value:string) = KeyValue("x-teleport", value) diff --git a/fsharp-view-engine/src/FSharp.ViewEngine/Core.fs b/fsharp-view-engine/src/FSharp.ViewEngine/Core.fs deleted file mode 100644 index 927e33a..0000000 --- a/fsharp-view-engine/src/FSharp.ViewEngine/Core.fs +++ /dev/null @@ -1,62 +0,0 @@ -namespace FSharp.ViewEngine - -type Attribute = - | KeyValue of string * string // e.g., div [ _class "text-xl" ] ->
- | Boolean of string // e.g., button [ _disabled ] -> - | Children of Element seq // e.g., div [ p "Hello" ] ->

Hello

- | Noop // No op - -and Element = - | Raw of string // Raw content - | Text of string // Text content (html encoded) - | Tag of string * Attribute seq // e.g.,

Hello

- | Void of string * Attribute seq // e.g.,
- | Fragment of Element seq // Directly render children - | Noop // No op - -module private ViewBuilder = - open System.Text - open System.Web - - let inline (+=) (sb:StringBuilder) (s:string) = sb.Append(s) - let inline (+!) (sb:StringBuilder) (s:string) = sb.Append(s) |> ignore - - let encode (s:string) = HttpUtility.HtmlEncode(s) - - let rec buildElement (el:Element) (sb:StringBuilder) = - match el with - | Raw text -> sb +! text - | Text text -> sb +! encode text - | Tag (tag, attributes) -> - sb += "<" +! tag - let children = ResizeArray() - for attr in attributes do - match attr with - | KeyValue (key, value) -> sb += " " += key += "=\"" += value +! "\"" - | Boolean key -> sb += " " +! key - | Children elements -> children.AddRange(elements) - | Attribute.Noop -> () - sb +! ">" - for child in children do buildElement child sb - sb += "" - | Void (tag, attributes) -> - sb += "<" +! tag - for attr in attributes do - match attr with - | KeyValue (key, value) -> sb += " " += key += "=\"" += value +! "\"" - | Boolean key -> sb += " " +! key - | Children _ -> failwith "void elements cannot have children" - | Attribute.Noop -> () - sb +! ">" - | Fragment children -> for child in children do buildElement child sb - | Element.Noop -> () - -[] -module Element = - open System.Text - open ViewBuilder - - let render (element:Element) = - let sb = StringBuilder() - buildElement element sb - sb.ToString() diff --git a/fsharp-view-engine/src/FSharp.ViewEngine/Html.fs b/fsharp-view-engine/src/FSharp.ViewEngine/Html.fs deleted file mode 100644 index 3fde1c8..0000000 --- a/fsharp-view-engine/src/FSharp.ViewEngine/Html.fs +++ /dev/null @@ -1,122 +0,0 @@ -namespace FSharp.ViewEngine -open JetBrains.Annotations - -type Html = - static member empty = Element.Noop - static member fragment (children:Element seq) = Fragment children - static member raw ([]v:string) = Raw v - static member text (v:string) = Text v - static member html (attrs:Attribute list) = Fragment [ Void ("!DOCTYPE", [ Boolean "html" ]); Tag ("html", attrs) ] - static member head (children:Element list) = Tag ("head", [ Children children ]) - static member title (value:string) = Tag ("title", [ Children [ Text value ] ]) - static member meta (attrs:Attribute seq) = Void ("meta", attrs) - static member link (attrs:Attribute seq) = Void ("link", attrs) - static member script (attrs:Attribute seq) = Tag ("script", attrs) - static member script (value:string) = Tag ("script", [ Children [ Raw value ]]) - static member body (attrs:Attribute seq) = Tag ("body", attrs) - static member main (attrs:Attribute seq) = Tag ("main", attrs) - static member header (attrs:Attribute seq) = Tag ("header", attrs) - static member footer (attrs:Attribute seq) = Tag ("footer", attrs) - static member nav (attrs:Attribute seq) = Tag ("nav", attrs) - static member h1 (attrs:Attribute seq) = Tag ("h1", attrs) - static member h2 (attrs:Attribute seq) = Tag ("h2", attrs) - static member h3 (attrs:Attribute seq) = Tag ("h3", attrs) - static member h4 (attrs:Attribute seq) = Tag ("h4", attrs) - static member div (attrs:Attribute seq) = Tag ("div", attrs) - static member div (children:Element seq) = Tag ("div", [ Children children ]) - static member p (attrs:Attribute seq) = Tag ("p", attrs) - static member p (text:string) = Tag ("p", [ Children [ Text text ] ]) - static member span (attrs:Attribute seq) = Tag ("span", attrs) - static member a (attrs:Attribute seq) = Tag ("a", attrs) - static member button (attrs:Attribute seq) = Tag ("button", attrs) - static member img (attrs:Attribute seq) = Tag ("img", attrs) - static member code (attrs:Attribute seq) = Tag ("code", attrs) - static member pre (attrs:Attribute seq) = Tag ("pre", attrs) - static member br = Void ("br", []) - static member ul (attrs:Attribute seq) = Tag ("ul", attrs) - static member ol (attrs:Attribute seq) = Tag ("ol", attrs) - static member li (attrs:Attribute seq) = Tag ("li", attrs) - static member blockquote (attrs:Attribute seq) = Tag ("blockquote", attrs) - static member article (attrs:Attribute seq) = Tag("article", attrs) - static member dialog (attrs:Attribute seq) = Tag("dialog", attrs) - static member time (attrs:Attribute seq) = Tag("time", attrs) - static member form (attrs:Attribute seq) = Tag("form", attrs) - static member input (attrs:Attribute seq) = Tag("input", attrs) - static member label (attrs:Attribute seq) = Tag("label", attrs) - static member textarea (attrs:Attribute seq) = Tag("textarea", attrs) - static member select (attrs:Attribute seq) = Tag("select", attrs) - static member option (attrs:Attribute seq) = Tag("option", attrs) - static member table (attrs:Attribute seq) = Tag("table", attrs) - static member thead (attrs:Attribute seq) = Tag("thead", attrs) - static member thead (children:Element seq) = Tag("thead", [ Children children ]) - static member tr (attrs:Attribute seq) = Tag("tr", attrs) - static member tr (children:Element seq) = Tag("tr", [ Children children ]) - static member th (attrs:Attribute seq) = Tag("th", attrs) - static member tbody (attrs:Attribute seq) = Tag("tbody", attrs) - static member td (attrs:Attribute seq) = Tag("td", attrs) - static member dl (attrs:Attribute seq) = Tag("dl", attrs) - static member dt (attrs:Attribute seq) = Tag("dt", attrs) - static member dd (attrs:Attribute seq) = Tag("dd", attrs) - static member template (attrs:Attribute seq) = Tag("template", attrs) - static member iframe (attrs:Attribute seq) = Tag ("iframe", attrs) - - static member _empty = Attribute.Noop - static member _id (v:string) = KeyValue ("id", v) - static member _class (v:string) = KeyValue ("class", v) - static member _class (v:string seq) = KeyValue ("class", v |> String.concat " ") - static member _style (v:string) = KeyValue ("style", v) - static member _children (v:Element seq) = Children v - static member _children (v:Element) = Children [ v ] - static member _children (v:string) = Children [ Text v ] - static member _lang (v:string) = KeyValue ("lang", v) - static member _charset (v:string) = KeyValue ("charset", v) - static member _name (v:string) = KeyValue ("name", v) - static member _content (v:string) = KeyValue ("content", v) - static member _href (v:string) = KeyValue ("href", v) - static member _rel (v:string) = KeyValue ("rel", v) - static member _src (v:string) = KeyValue ("src", v) - static member _async (v:bool) = if v then Boolean "async" else Attribute.Noop - static member _defer (v:bool) = if v then Boolean "defer" else Attribute.Noop - static member _action (v:string) = KeyValue("action", v) - static member _method (v:string) = KeyValue("method", v) - static member _formmethod (v:string) = KeyValue("formmethod", v) - static member _type (v:string) = KeyValue("type", v) - static member _for (v:string) = KeyValue("for", v) - static member _rows (v:int) = KeyValue("rows", string v) - static member _cols (v:int) = KeyValue("cols", string v) - static member _data (attr:string, ?v:string) = let key = $"data-{attr}" in match v with Some v -> KeyValue (key, v) | None -> Boolean(key) - static member _datetime (v:string) = KeyValue("datetime", v) - static member _width (v:string) = KeyValue("width", v) - static member _height (v:string) = KeyValue("height", v) - static member _value (v:string) = KeyValue("value", v) - static member _hidden (v:bool) = if v then Boolean("hidden") else Attribute.Noop - static member _required (v:bool) = if v then Boolean("required") else Attribute.Noop - static member _disabled (v:bool) = if v then Boolean("disabled") else Attribute.Noop - static member _readonly (v:bool) = if v then Boolean("readonly") else Attribute.Noop - static member _multiple (v:bool) = if v then Boolean("multiple") else Attribute.Noop - static member _selected (v:bool) = if v then Boolean("selected") else Attribute.Noop - static member _min (v:string) = KeyValue("min", v) - static member _min (v:float) = KeyValue("min", string v) - static member _minlength (v:string) = KeyValue("minlength", v) - static member _minlength (v:int) = KeyValue("minlength", string v) - static member _max (v:string) = KeyValue("max", v) - static member _max (v:float) = KeyValue("max", string v) - static member _maxlength (v:string) = KeyValue("maxlength", v) - static member _maxlength (v:int) = KeyValue("maxlength", string v) - static member _step (v:string) = KeyValue("step", v) - static member _step (v:float) = KeyValue("step", string v) - static member _checked (v:bool) = if v then Boolean("checked") else Attribute.Noop - static member _role (v:string) = KeyValue("role", v) - static member _ariaLabelledby (v:string) = KeyValue("aria-labelledby", v) - static member _ariaDescribedby (v:string) = KeyValue("aria-describedby", v) - static member _ariaModal (v:string) = KeyValue("aria-modal", v) - static member _placeholder value = KeyValue ("placeholder", value) - static member _autocomplete value = KeyValue ("autocomplete", value) - static member _pattern value = KeyValue ("pattern", value) - static member _accept value = KeyValue ("accept", value) - static member _title value = KeyValue ("title", value) - static member _wrap value = KeyValue ("wrap", value) - static member _size (value:int) = KeyValue ("size", string value) - static member _colspan (value:int) = KeyValue ("colspan", string value) - static member _onload(value:string) = KeyValue("onload", value) - static member _crossorigin = Boolean("crossorigin") diff --git a/fsharp-view-engine/src/FSharp.ViewEngine/Htmx.fs b/fsharp-view-engine/src/FSharp.ViewEngine/Htmx.fs deleted file mode 100644 index c8939b3..0000000 --- a/fsharp-view-engine/src/FSharp.ViewEngine/Htmx.fs +++ /dev/null @@ -1,17 +0,0 @@ -namespace FSharp.ViewEngine - -type Htmx = - static member _hx (key:string, value:string) = KeyValue ($"hx-{key}", value) - static member _hxGet (v:string) = KeyValue ("hx-get", v) - static member _hxPost (v:string) = KeyValue("hx-post", v) - static member _hxDelete (v:string) = KeyValue("hx-delete", v) - static member _hxTrigger (v:string) = KeyValue ("hx-trigger", v) - static member _hxTarget (v:string) = KeyValue ("hx-target", v) - static member _hxIndicator (v:string) = KeyValue ("hx-indicator", v) - static member _hxInclude (v:string) = KeyValue ("hx-include", v) - static member _hxSwap (v:string) = KeyValue ("hx-swap", v) - static member _hxSwapOOB (v:string) = KeyValue ("hx-swap-oob", v) - static member _hxEncoding (value:string) = KeyValue("hx-encoding", value) - static member _hxOn (event:string, value:string) = KeyValue($"hx-on:{event}", value) - static member _hxHistory (value:string) = KeyValue("hx-history", value) - static member _hxVals(value:string) = KeyValue ("hx-vals", value) diff --git a/fsharp-view-engine/src/FSharp.ViewEngine/Svg.fs b/fsharp-view-engine/src/FSharp.ViewEngine/Svg.fs deleted file mode 100644 index 0f0e914..0000000 --- a/fsharp-view-engine/src/FSharp.ViewEngine/Svg.fs +++ /dev/null @@ -1,22 +0,0 @@ -namespace FSharp.ViewEngine - -type Svg = - static member svg (attrs:Attribute seq) = Tag ("svg", attrs) - static member path (attrs:Attribute seq) = Tag ("path", attrs) - static member circle (attrs:Attribute seq) = Tag ("circle", attrs) - - static member _viewBox (v:string) = KeyValue ("viewBox", v) - static member _width (v:int) = KeyValue ("width", string v) - static member _height (v:int) = KeyValue ("height", string v) - static member _fill (v:string) = KeyValue ("fill", v) - static member _stroke (v:string) = KeyValue ("stroke", v) - static member _strokeWidth (v:int) = KeyValue ("stroke-width", string v) - static member _strokeLinecap (v:string) = KeyValue ("stroke-linecap", v) - static member _strokeLinejoin (v:string) = KeyValue ("stroke-linejoin", v) - static member _fillRule (v:string) = KeyValue ("fill-rule", v) - static member _clipRule (v:string) = KeyValue ("clip-rule", v) - static member _d (v:string) = KeyValue ("d", v) - static member _cx (v:int) = KeyValue ("cx", string v) - static member _cy (v:int) = KeyValue ("cy", string v) - static member _r (v:int) = KeyValue ("r", string v) - diff --git a/fsharp-view-engine/src/FSharp.ViewEngine/Tailwind.fs b/fsharp-view-engine/src/FSharp.ViewEngine/Tailwind.fs deleted file mode 100644 index c9b9d5d..0000000 --- a/fsharp-view-engine/src/FSharp.ViewEngine/Tailwind.fs +++ /dev/null @@ -1,24 +0,0 @@ -namespace FSharp.ViewEngine - -type Tailwind = - static member _popover = Boolean "popover" - static member _anchor (position:string) = KeyValue ("anchor", position) - static member elAutocomplete (attrs:Attribute seq) = Tag ("el-autocomplete", attrs) - static member elOptions (attrs:Attribute seq) = Tag ("el-options", attrs) - static member elOption (attrs:Attribute seq) = Tag ("el-option", attrs) - static member elSelect (attrs:Attribute seq) = Tag ("el-select", attrs) - static member elSelectedContent (attrs:Attribute seq) = Tag ("el-selectedcontent", attrs) - static member elDropdown (attrs:Attribute seq) = Tag ("el-dropdown", attrs) - static member elMenu (attrs:Attribute seq) = Tag ("el-menu", attrs) - static member elDialog (attrs:Attribute seq) = Tag ("el-dialog", attrs) - static member elDialogBackdrop (attrs:Attribute seq) = Tag ("el-dialog-backdrop", attrs) - static member elDialogPanel (attrs:Attribute seq) = Tag ("el-dialog-panel", attrs) - static member elCommandPalette (attrs:Attribute seq) = Tag ("el-command-palette", attrs) - static member elCommandList (attrs:Attribute seq) = Tag ("el-command-list", attrs) - static member elCommandGroup (attrs:Attribute seq) = Tag ("el-command-group", attrs) - static member elCommandPreview (attrs:Attribute seq) = Tag ("el-command-preview", attrs) - static member elDefaults (attrs:Attribute seq) = Tag ("el-defaults", attrs) - static member elNoResults (attrs:Attribute seq) = Tag ("el-no-results", attrs) - static member elTabGroup (attrs:Attribute seq) = Tag ("el-tab-group", attrs) - static member elTabList (attrs:Attribute seq) = Tag ("el-tab-list", attrs) - static member elTabPanels (attrs:Attribute seq) = Tag ("el-tab-panels", attrs) diff --git a/fsharp-view-engine/src/Tests/Tests.fs b/fsharp-view-engine/src/Tests/Tests.fs deleted file mode 100644 index 3de527b..0000000 --- a/fsharp-view-engine/src/Tests/Tests.fs +++ /dev/null @@ -1,165 +0,0 @@ -module Tests - -open FSharp.ViewEngine -open System.Text.Json -open System.Text.RegularExpressions -open Expecto -open type Html -open type Htmx -open type Alpine -open type Svg -open type Tailwind - -module String = - let replace (oldValue:string) (newValue:string) (s:string) = s.Replace(oldValue, newValue) - let clean (s:string) = Regex.Replace(s, @"\s{2,}|\r|\n|\r\n", "") - -[] -let tests = - testList "ViewEngine Tests" [ - test "Should render html document" { - let actual = - let xData = - {| showContent = false; x = [ 1; 2; 3 ]; y = [ "a"; "b"; "c" ] |} - |> JsonSerializer.Serialize - |> String.replace "\"" "'" - html [ - _lang "en" - _children [ - head [ - title "Test" - meta [ _charset "utf-8" ] - link [ _href "/css/compiled.css"; _rel "stylesheet" ] - ] - body [ - _xData xData - _class "bg-gray-50" - _children [ - div [ - _id "page" - _class [ "flex"; "flex-col" ] - _children [ - h1 [ _hxGet "/hello"; _hxTarget "#page"; _children "Hello" ] - h1 [ _hxGet "/world"; _hxTarget "#page"; _children "World" ] - ] - ] - br - div [ - _xShow "showContent" - _children [ - h2 [ _children "Content" ] - p [ _children "Some content" ] - raw "

Some more content

" - pre [ - _class "language-html" - _children [ - code [ - _class "language-html" - _children [ - text "

Even more content

" - ] - ] - ] - ] - ul [ - _children [ - li [ _children "One" ] - li [ _children "Two" ] - ] - ] - a [ - _href "https://github.com/meiermade/FSharp.ViewEngine" - _class "rounded-lg text-gray-800 font-semibold flex items-center gap-3 p-1" - _children [ - svg [ - _viewBox "0 0 24 24" - _class "h-6 w-6 fill-current" - _children [ - path [ - _fillRule "evenodd" - _clipRule "evenodd" - _d "M12 2C6.477 2 2 6.463 2 11.97c0 4.404 2.865 8.14 6.839 9.458.5.092.682-.216.682-.48 0-.236-.008-.864-.013-1.695-2.782.602-3.369-1.337-3.369-1.337-.454-1.151-1.11-1.458-1.11-1.458-.908-.618.069-.606.069-.606 1.003.07 1.531 1.027 1.531 1.027.892 1.524 2.341 1.084 2.91.828.092-.643.35-1.083.636-1.332-2.22-.251-4.555-1.107-4.555-4.927 0-1.088.39-1.979 1.029-2.675-.103-.252-.446-1.266.098-2.638 0 0 .84-.268 2.75 1.022A9.607 9.607 0 0 1 12 6.82c.85.004 1.705.114 2.504.336 1.909-1.29 2.747-1.022 2.747-1.022.546 1.372.202 2.386.1 2.638.64.696 1.028 1.587 1.028 2.675 0 3.83-2.339 4.673-4.566 4.92.359.307.678.915.678 1.846 0 1.332-.012 2.407-.012 2.734 0 .267.18.577.688.48 3.97-1.32 6.833-5.054 6.833-9.458C22 6.463 17.522 2 12 2Z" - ] - ] - ] - raw "Documentation" - ] - ] - elSelect [ - _name "status" - _value "active" - _children [ - button [ - _type "button" - _children [ - elSelectedContent [ _children "Active" ] - ] - ] - elOptions [ - _popover - _children [ - elOption [ _value "active"; _children "Active" ] - elOption [ _value "inactive"; _children "Inactive" ] - elOption [ _value "archived"; _children "Archived" ] - ] - ] - ] - ] - ] - ] - ] - ] - ] - ] - |> Element.render - // language=HTML - let expected = """ - - - - Test - - - - -
-

Hello

-

World

-
-
-
-

Content

-

Some content

-

Some more content

-
-                    
-                        <p>Even more content</p>
-                    
-                
-
    -
  • One
  • -
  • Two
  • -
- - - - - Documentation - - - - - Active - Inactive - Archived - - -
- - - """ - Expect.equal (String.clean actual) (String.clean expected) "Rendered HTML should match expected" - } - ] diff --git a/lib/.config/dotnet-tools.json b/lib/.config/dotnet-tools.json new file mode 100644 index 0000000..4321530 --- /dev/null +++ b/lib/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "paket": { + "version": "10.3.1", + "commands": [ + "paket" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/fsharp-view-engine/.gitignore b/lib/.gitignore similarity index 100% rename from fsharp-view-engine/.gitignore rename to lib/.gitignore diff --git a/lib/fake.cmd b/lib/fake.cmd new file mode 100644 index 0000000..df4b954 --- /dev/null +++ b/lib/fake.cmd @@ -0,0 +1 @@ +dotnet run --project ./src/Build/Build.fsproj -- --target %1 diff --git a/lib/fake.sh b/lib/fake.sh new file mode 100755 index 0000000..d25bbc3 --- /dev/null +++ b/lib/fake.sh @@ -0,0 +1 @@ +dotnet run --project ./src/Build/Build.fsproj -- --target $1 diff --git a/fsharp-view-engine/global.json b/lib/global.json similarity index 100% rename from fsharp-view-engine/global.json rename to lib/global.json diff --git a/lib/paket.dependencies b/lib/paket.dependencies new file mode 100644 index 0000000..7fe6d97 --- /dev/null +++ b/lib/paket.dependencies @@ -0,0 +1,13 @@ +source https://api.nuget.org/v3/index.json +storage: none + +nuget Fake.Core.Target +nuget Giraffe +nuget JetBrains.Annotations +nuget Markdig +nuget Expecto +nuget BenchmarkDotNet 0.14.0 +nuget FSharp.Core +nuget Giraffe.ViewEngine +nuget Feliz.ViewEngine +nuget Oxpecker.ViewEngine diff --git a/lib/paket.lock b/lib/paket.lock new file mode 100644 index 0000000..d8bbd0e --- /dev/null +++ b/lib/paket.lock @@ -0,0 +1,209 @@ +STORAGE: NONE +NUGET + remote: https://api.nuget.org/v3/index.json + BenchmarkDotNet (0.14) + BenchmarkDotNet.Annotations (>= 0.14) - restriction: >= netstandard2.0 + CommandLineParser (>= 2.9.1) - restriction: >= netstandard2.0 + Gee.External.Capstone (>= 2.3) - restriction: >= netstandard2.0 + Iced (>= 1.17) - restriction: >= netstandard2.0 + Microsoft.CodeAnalysis.CSharp (>= 4.1) - restriction: >= netstandard2.0 + Microsoft.Diagnostics.Runtime (>= 2.2.332302) - restriction: >= netstandard2.0 + Microsoft.Diagnostics.Tracing.TraceEvent (>= 3.1.8) - restriction: >= netstandard2.0 + Microsoft.DotNet.PlatformAbstractions (>= 3.1.6) - restriction: >= netstandard2.0 + Microsoft.Win32.Registry (>= 5.0) - restriction: && (< net6.0) (>= netstandard2.0) + Perfolizer (0.3.17) - restriction: >= netstandard2.0 + System.Management (>= 5.0) - restriction: >= netstandard2.0 + System.Numerics.Vectors (>= 4.5) - restriction: && (< net6.0) (>= netstandard2.0) + System.Reflection.Emit (>= 4.7) - restriction: && (< net6.0) (>= netstandard2.0) + System.Reflection.Emit.Lightweight (>= 4.7) - restriction: && (< net6.0) (>= netstandard2.0) + System.Threading.Tasks.Extensions (>= 4.5.4) - restriction: && (< net6.0) (>= netstandard2.0) + BenchmarkDotNet.Annotations (0.15.8) - restriction: >= netstandard2.0 + CommandLineParser (2.9.1) - restriction: >= netstandard2.0 + Expecto (10.2.3) + FSharp.Core (>= 7.0.200) - restriction: >= net6.0 + Mono.Cecil (>= 0.11.4 < 1.0) - restriction: >= net6.0 + Fake.Core.CommandLineParsing (6.1.4) - restriction: >= netstandard2.0 + FParsec (>= 1.1.1) - restriction: >= netstandard2.0 + FSharp.Core (>= 8.0.400) - restriction: >= netstandard2.0 + Fake.Core.Context (6.1.4) - restriction: >= netstandard2.0 + FSharp.Core (>= 8.0.400) - restriction: >= netstandard2.0 + Fake.Core.Environment (6.1.4) - restriction: >= netstandard2.0 + FSharp.Core (>= 8.0.400) - restriction: >= netstandard2.0 + Fake.Core.FakeVar (6.1.4) - restriction: >= netstandard2.0 + Fake.Core.Context (>= 6.1.4) - restriction: >= netstandard2.0 + FSharp.Core (>= 8.0.400) - restriction: >= netstandard2.0 + Fake.Core.Process (6.1.4) - restriction: >= netstandard2.0 + Fake.Core.Environment (>= 6.1.4) - restriction: >= netstandard2.0 + Fake.Core.FakeVar (>= 6.1.4) - restriction: >= netstandard2.0 + Fake.Core.String (>= 6.1.4) - restriction: >= netstandard2.0 + Fake.Core.Trace (>= 6.1.4) - restriction: >= netstandard2.0 + Fake.IO.FileSystem (>= 6.1.4) - restriction: >= netstandard2.0 + FSharp.Core (>= 8.0.400) - restriction: >= netstandard2.0 + System.Collections.Immutable (>= 8.0) - restriction: >= netstandard2.0 + Fake.Core.String (6.1.4) - restriction: >= netstandard2.0 + FSharp.Core (>= 8.0.400) - restriction: >= netstandard2.0 + Fake.Core.Target (6.1.4) + Fake.Core.CommandLineParsing (>= 6.1.4) - restriction: >= netstandard2.0 + Fake.Core.Context (>= 6.1.4) - restriction: >= netstandard2.0 + Fake.Core.Environment (>= 6.1.4) - restriction: >= netstandard2.0 + Fake.Core.FakeVar (>= 6.1.4) - restriction: >= netstandard2.0 + Fake.Core.Process (>= 6.1.4) - restriction: >= netstandard2.0 + Fake.Core.String (>= 6.1.4) - restriction: >= netstandard2.0 + Fake.Core.Trace (>= 6.1.4) - restriction: >= netstandard2.0 + FSharp.Control.Reactive (>= 5.0.2) - restriction: >= netstandard2.0 + FSharp.Core (>= 8.0.400) - restriction: >= netstandard2.0 + Fake.Core.Trace (6.1.4) - restriction: >= netstandard2.0 + Fake.Core.Environment (>= 6.1.4) - restriction: >= netstandard2.0 + Fake.Core.FakeVar (>= 6.1.4) - restriction: >= netstandard2.0 + FSharp.Core (>= 8.0.400) - restriction: >= netstandard2.0 + Fake.IO.FileSystem (6.1.4) - restriction: >= netstandard2.0 + Fake.Core.String (>= 6.1.4) - restriction: >= netstandard2.0 + Fake.Core.Trace (>= 6.1.4) - restriction: >= netstandard2.0 + FSharp.Core (>= 8.0.400) - restriction: >= netstandard2.0 + Feliz.ViewEngine (1.0.3) + FSharp.Core (>= 4.7) - restriction: >= netstandard2.0 + FParsec (1.1.1) - restriction: >= netstandard2.0 + FSharp.Core (>= 4.3.4) - restriction: || (>= net45) (>= netstandard2.0) + System.ValueTuple (>= 4.4) - restriction: >= net45 + FSharp.Control.Reactive (6.1.2) - restriction: >= netstandard2.0 + FSharp.Core (>= 6.0.7) - restriction: >= netstandard2.0 + System.Reactive (>= 6.0.1) - restriction: >= netstandard2.0 + FSharp.Core (10.0.102) + FSharp.SystemTextJson (1.4.36) - restriction: >= net6.0 + FSharp.Core (>= 4.7) - restriction: >= netstandard2.0 + System.Text.Json (>= 6.0.10) - restriction: >= netstandard2.0 + Gee.External.Capstone (2.3) - restriction: >= netstandard2.0 + Giraffe (8.2) + FSharp.Core (>= 6.0) - restriction: >= net6.0 + FSharp.SystemTextJson (>= 1.3.13) - restriction: >= net6.0 + Giraffe.ViewEngine (>= 1.4) - restriction: >= net6.0 + Microsoft.IO.RecyclableMemoryStream (>= 3.0.1) - restriction: >= net6.0 + System.Text.Json (>= 8.0.6) - restriction: >= net6.0 + Giraffe.ViewEngine (1.4) + FSharp.Core (>= 5.0) - restriction: >= netstandard2.0 + Iced (1.21) - restriction: >= netstandard2.0 + JetBrains.Annotations (2025.2.4) + System.Runtime (>= 4.1) - restriction: && (< net20) (>= netstandard1.0) (< netstandard2.0) + Markdig (0.44) + System.Memory (>= 4.6.3) - restriction: || (>= net462) (&& (>= netstandard2.0) (< netstandard2.1)) + Microsoft.Bcl.AsyncInterfaces (10.0.2) - restriction: || (&& (>= net462) (>= net6.0)) (&& (>= net462) (>= netstandard2.0)) (&& (>= net6.0) (< net8.0)) (&& (< net8.0) (>= netstandard2.0)) (&& (>= netstandard2.0) (< netstandard2.1)) + Microsoft.CodeAnalysis.Analyzers (3.11) - restriction: >= netstandard2.0 + Microsoft.CodeAnalysis.Common (5.0) - restriction: >= netstandard2.0 + Microsoft.CodeAnalysis.Analyzers (>= 3.11) - restriction: >= netstandard2.0 + System.Buffers (>= 4.6) - restriction: && (< net8.0) (>= netstandard2.0) + System.Collections.Immutable (>= 9.0) - restriction: || (&& (>= net8.0) (< net9.0)) (&& (< net8.0) (>= netstandard2.0)) + System.Memory (>= 4.6) - restriction: && (< net8.0) (>= netstandard2.0) + System.Numerics.Vectors (>= 4.6) - restriction: && (< net8.0) (>= netstandard2.0) + System.Reflection.Metadata (>= 9.0) - restriction: || (&& (>= net8.0) (< net9.0)) (&& (< net8.0) (>= netstandard2.0)) + System.Runtime.CompilerServices.Unsafe (>= 6.1) - restriction: && (< net8.0) (>= netstandard2.0) + System.Text.Encoding.CodePages (>= 8.0) - restriction: && (< net8.0) (>= netstandard2.0) + System.Threading.Tasks.Extensions (>= 4.6) - restriction: && (< net8.0) (>= netstandard2.0) + Microsoft.CodeAnalysis.CSharp (5.0) - restriction: >= netstandard2.0 + Microsoft.CodeAnalysis.Analyzers (>= 3.11) - restriction: >= netstandard2.0 + Microsoft.CodeAnalysis.Common (5.0) - restriction: >= netstandard2.0 + System.Buffers (>= 4.6) - restriction: && (< net8.0) (>= netstandard2.0) + System.Collections.Immutable (>= 9.0) - restriction: || (&& (>= net8.0) (< net9.0)) (&& (< net8.0) (>= netstandard2.0)) + System.Memory (>= 4.6) - restriction: && (< net8.0) (>= netstandard2.0) + System.Numerics.Vectors (>= 4.6) - restriction: && (< net8.0) (>= netstandard2.0) + System.Reflection.Metadata (>= 9.0) - restriction: || (&& (>= net8.0) (< net9.0)) (&& (< net8.0) (>= netstandard2.0)) + System.Runtime.CompilerServices.Unsafe (>= 6.1) - restriction: && (< net8.0) (>= netstandard2.0) + System.Text.Encoding.CodePages (>= 8.0) - restriction: && (< net8.0) (>= netstandard2.0) + System.Threading.Tasks.Extensions (>= 4.6) - restriction: && (< net8.0) (>= netstandard2.0) + Microsoft.Diagnostics.NETCore.Client (0.2.661903) - restriction: >= netstandard2.0 + Microsoft.Bcl.AsyncInterfaces (>= 9.0.8) - restriction: && (< net8.0) (>= netstandard2.0) + Microsoft.Extensions.Logging.Abstractions (>= 8.0.3) - restriction: >= netstandard2.0 + System.Buffers (>= 4.5.1) - restriction: && (< net8.0) (>= netstandard2.0) + Microsoft.Diagnostics.Runtime (3.1.512801) - restriction: >= netstandard2.0 + Microsoft.Diagnostics.NETCore.Client (>= 0.2.410101) - restriction: >= netstandard2.0 + System.Collections.Immutable (>= 6.0) - restriction: && (< net6.0) (>= netstandard2.0) + System.Runtime.CompilerServices.Unsafe (>= 6.0) - restriction: && (< net6.0) (>= netstandard2.0) + Microsoft.Diagnostics.Tracing.TraceEvent (3.1.29) - restriction: >= netstandard2.0 + Microsoft.Diagnostics.NETCore.Client (>= 0.2.510501) - restriction: >= netstandard2.0 + Microsoft.Win32.Registry (>= 5.0) - restriction: >= netstandard2.0 + System.Collections.Immutable (>= 9.0.8) - restriction: >= netstandard2.0 + System.Reflection.Metadata (>= 9.0.8) - restriction: >= netstandard2.0 + System.Reflection.TypeExtensions (>= 4.7) - restriction: >= netstandard2.0 + System.Runtime.CompilerServices.Unsafe (>= 6.1.2) - restriction: >= netstandard2.0 + System.Text.Json (>= 9.0.8) - restriction: >= netstandard2.0 + Microsoft.DotNet.PlatformAbstractions (3.1.6) - restriction: >= netstandard2.0 + System.Runtime.InteropServices.RuntimeInformation (>= 4.0) - restriction: || (>= net45) (&& (>= netstandard1.3) (< netstandard2.0)) + Microsoft.Extensions.DependencyInjection.Abstractions (10.0.2) - restriction: >= netstandard2.0 + Microsoft.Bcl.AsyncInterfaces (>= 10.0.2) - restriction: || (>= net462) (&& (>= netstandard2.0) (< netstandard2.1)) + System.Threading.Tasks.Extensions (>= 4.6.3) - restriction: || (>= net462) (&& (>= netstandard2.0) (< netstandard2.1)) + Microsoft.Extensions.Logging.Abstractions (10.0.2) - restriction: >= netstandard2.0 + Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.2) - restriction: || (>= net462) (>= netstandard2.0) + System.Buffers (>= 4.6.1) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0)) + System.Diagnostics.DiagnosticSource (>= 10.0.2) - restriction: || (&& (< net10.0) (>= net9.0)) (>= net462) (&& (>= net8.0) (< net9.0)) (&& (< net8.0) (>= netstandard2.0)) + System.Memory (>= 4.6.3) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0)) + Microsoft.Extensions.ObjectPool (10.0.2) - restriction: >= net10.0 + Microsoft.IO.RecyclableMemoryStream (3.0.1) - restriction: >= net6.0 + Microsoft.NETCore.Platforms (7.0.4) - restriction: || (&& (>= monoandroid) (>= netcoreapp2.0) (< netstandard1.3)) (&& (>= monoandroid) (>= netcoreapp2.1) (< netstandard1.3)) (&& (< monoandroid) (< net20) (>= netstandard1.0) (< netstandard1.2) (< win8) (< wp8)) (&& (< monoandroid) (< net20) (>= netstandard1.2) (< netstandard1.3) (< win8) (< wpa81)) (&& (< monoandroid) (< net20) (>= netstandard1.3) (< netstandard1.5) (< win8) (< wpa81)) (&& (< monoandroid) (>= netcoreapp2.0) (< netcoreapp2.1)) (&& (>= monotouch) (>= netcoreapp2.0)) (&& (>= monotouch) (>= netcoreapp2.1)) (&& (< monotouch) (< net20) (>= netstandard1.5) (< netstandard2.0) (< win8) (< wpa81) (< xamarintvos) (< xamarinwatchos)) (&& (>= net461) (>= netcoreapp2.0)) (&& (>= net461) (>= netcoreapp2.1)) (&& (>= netcoreapp2.0) (< netcoreapp2.1) (>= xamarinios)) (&& (>= netcoreapp2.0) (< netcoreapp2.1) (>= xamarinmac)) (&& (>= netcoreapp2.0) (< netcoreapp2.1) (>= xamarintvos)) (&& (>= netcoreapp2.0) (< netcoreapp2.1) (>= xamarinwatchos)) (&& (>= netcoreapp2.0) (>= uap10.1)) (&& (< netcoreapp2.0) (>= netcoreapp2.1)) (&& (>= netcoreapp2.1) (< netcoreapp3.0)) (&& (>= netcoreapp2.1) (>= uap10.1)) + Microsoft.NETCore.Targets (5.0) - restriction: || (&& (< monoandroid) (< net20) (>= netstandard1.0) (< netstandard1.2) (< win8) (< wp8)) (&& (< monoandroid) (< net20) (>= netstandard1.2) (< netstandard1.3) (< win8) (< wpa81)) (&& (< monoandroid) (< net20) (>= netstandard1.3) (< netstandard1.5) (< win8) (< wpa81)) (&& (< monotouch) (< net20) (>= netstandard1.5) (< netstandard2.0) (< win8) (< wpa81) (< xamarintvos) (< xamarinwatchos)) + Microsoft.Win32.Registry (5.0) - restriction: >= netstandard2.0 + System.Buffers (>= 4.5.1) - restriction: || (&& (>= monoandroid) (< netstandard1.3)) (>= monotouch) (&& (< net46) (< netcoreapp2.0) (>= netstandard2.0)) (>= xamarinios) (>= xamarinmac) (>= xamarintvos) (>= xamarinwatchos) + System.Memory (>= 4.5.4) - restriction: || (&& (< monoandroid) (>= netcoreapp2.0) (< netcoreapp2.1) (< xamarinios) (< xamarinmac) (< xamarintvos) (< xamarinwatchos)) (&& (< net46) (< netcoreapp2.0) (>= netstandard2.0) (< xamarinios) (< xamarinmac) (< xamarintvos) (< xamarinwatchos)) (>= uap10.1) + System.Security.AccessControl (>= 5.0) - restriction: || (&& (>= monoandroid) (< netstandard1.3)) (&& (< monoandroid) (>= netcoreapp2.0)) (>= monotouch) (&& (< net46) (< netcoreapp2.0) (>= netstandard2.0)) (>= net461) (>= netcoreapp2.1) (>= uap10.1) (>= xamarinios) (>= xamarinmac) (>= xamarintvos) (>= xamarinwatchos) + System.Security.Principal.Windows (>= 5.0) - restriction: || (&& (>= monoandroid) (< netstandard1.3)) (&& (< monoandroid) (>= netcoreapp2.0)) (>= monotouch) (&& (< net46) (< netcoreapp2.0) (>= netstandard2.0)) (>= net461) (>= netcoreapp2.1) (>= uap10.1) (>= xamarinios) (>= xamarinmac) (>= xamarintvos) (>= xamarinwatchos) + Mono.Cecil (0.11.6) - restriction: >= net6.0 + Oxpecker.ViewEngine (2.0) + FSharp.Core (>= 10.0.100) - restriction: >= net10.0 + Microsoft.Extensions.ObjectPool (>= 10.0) - restriction: >= net10.0 + Perfolizer (0.3.17) - restriction: >= netstandard2.0 + System.Memory (>= 4.5.5) - restriction: && (>= netstandard2.0) (< netstandard2.1) + System.Buffers (4.6.1) - restriction: || (&& (>= monoandroid) (< netstandard1.3) (>= netstandard2.0)) (&& (>= monotouch) (>= netstandard2.0)) (&& (< net46) (< netcoreapp2.0) (>= netstandard2.0)) (>= net462) (&& (>= net6.0) (< net8.0)) (&& (< net6.0) (>= netstandard2.0) (>= xamarintvos)) (&& (< net6.0) (>= netstandard2.0) (>= xamarinwatchos)) (&& (< net6.0) (>= xamarinios)) (&& (< net6.0) (>= xamarinmac)) (&& (< net8.0) (>= netstandard2.0)) (&& (< netcoreapp2.1) (>= netstandard2.0) (< netstandard2.1)) + System.CodeDom (10.0.2) - restriction: >= netstandard2.0 + System.Collections.Immutable (10.0.2) - restriction: || (&& (< net10.0) (>= net9.0)) (&& (>= net8.0) (< net9.0)) (>= netstandard2.0) + System.Memory (>= 4.6.3) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0)) + System.Runtime.CompilerServices.Unsafe (>= 6.1.2) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0)) + System.Diagnostics.DiagnosticSource (10.0.2) - restriction: || (&& (< net10.0) (>= net9.0)) (&& (>= net462) (>= netstandard2.0)) (&& (>= net8.0) (< net9.0)) (&& (< net8.0) (>= netstandard2.0)) + System.Memory (>= 4.6.3) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0)) + System.Runtime.CompilerServices.Unsafe (>= 6.1.2) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0)) + System.IO.Pipelines (10.0.2) - restriction: || (&& (< net10.0) (>= net9.0)) (&& (>= net462) (>= net6.0)) (&& (>= net6.0) (< net8.0)) (&& (>= net8.0) (< net9.0)) + System.Buffers (>= 4.6.1) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0)) + System.Memory (>= 4.6.3) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0)) + System.Threading.Tasks.Extensions (>= 4.6.3) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0)) + System.Management (10.0.2) - restriction: >= netstandard2.0 + System.CodeDom (>= 10.0.2) - restriction: >= netstandard2.0 + System.Memory (4.6.3) - restriction: || (&& (< monoandroid) (>= netcoreapp2.0) (< netcoreapp2.1) (< xamarinios) (< xamarinmac) (< xamarintvos) (< xamarinwatchos)) (&& (< net46) (< netcoreapp2.0) (>= netstandard2.0) (< xamarinios) (< xamarinmac) (< xamarintvos) (< xamarinwatchos)) (>= net462) (&& (>= net6.0) (< net8.0)) (&& (< net8.0) (>= netstandard2.0)) (&& (>= netstandard2.0) (< netstandard2.1)) (&& (>= netstandard2.0) (>= uap10.1)) + System.Buffers (>= 4.6.1) - restriction: || (>= net462) (&& (< netcoreapp2.1) (>= netstandard2.0) (< netstandard2.1)) + System.Numerics.Vectors (>= 4.6.1) - restriction: || (>= net462) (&& (< netcoreapp2.1) (>= netstandard2.0) (< netstandard2.1)) + System.Runtime.CompilerServices.Unsafe (>= 6.1.2) - restriction: || (>= net462) (&& (< netcoreapp2.1) (>= netstandard2.0) (< netstandard2.1)) + System.Numerics.Vectors (4.6.1) - restriction: || (>= net462) (&& (< net6.0) (>= netstandard2.0)) (&& (< net8.0) (>= netstandard2.0)) (&& (< netcoreapp2.1) (>= netstandard2.0) (< netstandard2.1)) + System.Reactive (6.1) - restriction: >= netstandard2.0 + System.Threading.Tasks.Extensions (>= 4.5.4) - restriction: || (>= net472) (&& (< net6.0) (>= netstandard2.0)) (>= uap10.1) + System.Reflection.Emit (4.7) - restriction: && (< net6.0) (>= netstandard2.0) + System.Reflection.Emit.ILGeneration (>= 4.7) - restriction: || (&& (< monoandroid) (< monotouch) (< net45) (>= netstandard1.1) (< netstandard2.0) (< win8) (< wpa81) (< xamarintvos) (< xamarinwatchos)) (&& (< monoandroid) (< net45) (< netcoreapp2.0) (>= netstandard2.0) (< netstandard2.1) (< xamarintvos) (< xamarinwatchos)) (&& (< monoandroid) (< netstandard1.1) (>= portable-net45+win8+wpa81) (< win8)) (&& (< netstandard1.1) (>= win8)) (&& (< netstandard2.0) (>= wpa81)) (>= uap10.1) + System.Reflection.Emit.ILGeneration (4.7) - restriction: || (&& (< monoandroid) (< net45) (< netcoreapp2.0) (>= netstandard2.0) (< netstandard2.1) (< xamarintvos) (< xamarinwatchos)) (&& (< monoandroid) (< netstandard1.1) (>= netstandard2.0) (< win8)) (&& (< netstandard1.1) (>= netstandard2.0) (>= win8)) (&& (>= netstandard2.0) (>= uap10.1)) + System.Reflection.Emit.Lightweight (4.7) - restriction: && (< net6.0) (>= netstandard2.0) + System.Reflection.Emit.ILGeneration (>= 4.7) - restriction: || (&& (< monoandroid) (< monotouch) (< net45) (>= netstandard1.0) (< netstandard2.0) (< win8) (< wp8) (< wpa81) (< xamarintvos) (< xamarinwatchos)) (&& (< monoandroid) (< net45) (< netcoreapp2.0) (>= netstandard2.0) (< netstandard2.1) (< xamarintvos) (< xamarinwatchos)) (&& (< netstandard2.0) (>= wpa81)) (&& (>= portable-net45+win8+wp8+wpa81) (< portable-net45+wp8) (< win8)) (&& (< portable-net45+wp8) (>= win8)) (>= uap10.1) + System.Reflection.Metadata (10.0.2) - restriction: || (&& (>= net8.0) (< net9.0)) (>= netstandard2.0) + System.Collections.Immutable (>= 10.0.2) - restriction: || (&& (< net10.0) (>= net9.0)) (>= net462) (&& (>= net8.0) (< net9.0)) (&& (< net8.0) (>= netstandard2.0)) + System.Reflection.TypeExtensions (4.7) - restriction: >= netstandard2.0 + System.Runtime (4.3.1) - restriction: && (< net20) (>= netstandard1.0) (< netstandard2.0) + Microsoft.NETCore.Platforms (>= 1.1.1) - restriction: || (&& (< monoandroid) (< net45) (>= netstandard1.0) (< netstandard1.2) (< win8) (< wp8)) (&& (< monoandroid) (< net45) (>= netstandard1.2) (< netstandard1.3) (< win8) (< wpa81)) (&& (< monoandroid) (< net45) (>= netstandard1.3) (< netstandard1.5) (< win8) (< wpa81)) (&& (< monotouch) (< net45) (>= netstandard1.5) (< win8) (< wpa81) (< xamarinios) (< xamarinmac) (< xamarintvos) (< xamarinwatchos)) + Microsoft.NETCore.Targets (>= 1.1.3) - restriction: || (&& (< monoandroid) (< net45) (>= netstandard1.0) (< netstandard1.2) (< win8) (< wp8)) (&& (< monoandroid) (< net45) (>= netstandard1.2) (< netstandard1.3) (< win8) (< wpa81)) (&& (< monoandroid) (< net45) (>= netstandard1.3) (< netstandard1.5) (< win8) (< wpa81)) (&& (< monotouch) (< net45) (>= netstandard1.5) (< win8) (< wpa81) (< xamarinios) (< xamarinmac) (< xamarintvos) (< xamarinwatchos)) + System.Runtime.CompilerServices.Unsafe (6.1.2) - restriction: || (>= net462) (&& (>= net6.0) (< net8.0)) (>= netstandard2.0) + System.Runtime.InteropServices.RuntimeInformation (4.3) - restriction: && (>= net45) (>= netstandard2.0) + System.Security.AccessControl (6.0.1) - restriction: || (&& (>= monoandroid) (< netstandard1.3) (>= netstandard2.0)) (&& (< monoandroid) (< net6.0) (>= netcoreapp2.0)) (&& (>= monotouch) (>= netstandard2.0)) (&& (< net46) (< netcoreapp2.0) (>= netstandard2.0)) (&& (>= net461) (>= netstandard2.0)) (&& (< net6.0) (>= netcoreapp2.1)) (&& (< net6.0) (>= netstandard2.0) (>= xamarintvos)) (&& (< net6.0) (>= netstandard2.0) (>= xamarinwatchos)) (&& (< net6.0) (>= xamarinios)) (&& (< net6.0) (>= xamarinmac)) (&& (>= netstandard2.0) (>= uap10.1)) + System.Security.Principal.Windows (>= 5.0) - restriction: || (>= net461) (&& (< net6.0) (>= netstandard2.0)) + System.Security.Principal.Windows (5.0) - restriction: || (&& (>= monoandroid) (< netstandard1.3) (>= netstandard2.0)) (&& (< monoandroid) (< net6.0) (>= netcoreapp2.0)) (&& (>= monotouch) (>= netstandard2.0)) (&& (< net46) (< netcoreapp2.0) (>= netstandard2.0)) (&& (>= net461) (>= netstandard2.0)) (&& (< net6.0) (>= netcoreapp2.1)) (&& (< net6.0) (>= netstandard2.0) (>= xamarintvos)) (&& (< net6.0) (>= netstandard2.0) (>= xamarinwatchos)) (&& (< net6.0) (>= xamarinios)) (&& (< net6.0) (>= xamarinmac)) (&& (>= netstandard2.0) (>= uap10.1)) + Microsoft.NETCore.Platforms (>= 5.0) - restriction: || (&& (>= netcoreapp2.0) (< netcoreapp2.1)) (&& (>= netcoreapp2.1) (< netcoreapp3.0)) + System.Text.Encoding.CodePages (10.0.2) - restriction: && (< net8.0) (>= netstandard2.0) + System.Memory (>= 4.6.3) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0)) + System.Runtime.CompilerServices.Unsafe (>= 6.1.2) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0)) + System.ValueTuple (>= 4.6.1) - restriction: >= net462 + System.Text.Encodings.Web (10.0.2) - restriction: || (&& (< net10.0) (>= net9.0)) (&& (>= net462) (>= net6.0)) (&& (>= net6.0) (< net8.0)) (&& (>= net8.0) (< net9.0)) + System.Buffers (>= 4.6.1) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0)) + System.Memory (>= 4.6.3) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0)) + System.Runtime.CompilerServices.Unsafe (>= 6.1.2) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0)) + System.Text.Json (10.0.2) - restriction: >= netstandard2.0 + Microsoft.Bcl.AsyncInterfaces (>= 10.0.2) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0)) + System.Buffers (>= 4.6.1) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0)) + System.IO.Pipelines (>= 10.0.2) - restriction: || (&& (< net10.0) (>= net9.0)) (>= net462) (&& (>= net8.0) (< net9.0)) (&& (< net8.0) (>= netstandard2.0)) + System.Memory (>= 4.6.3) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0)) + System.Runtime.CompilerServices.Unsafe (>= 6.1.2) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0)) + System.Text.Encodings.Web (>= 10.0.2) - restriction: || (&& (< net10.0) (>= net9.0)) (>= net462) (&& (>= net8.0) (< net9.0)) (&& (< net8.0) (>= netstandard2.0)) + System.Threading.Tasks.Extensions (>= 4.6.3) - restriction: || (>= net462) (&& (< net8.0) (>= netstandard2.0)) + System.Threading.Tasks.Extensions (4.6.3) - restriction: || (&& (>= net462) (>= net6.0)) (&& (>= net462) (>= netstandard2.0)) (&& (>= net6.0) (< net8.0)) (&& (< net6.0) (>= netstandard2.0)) (&& (< net8.0) (>= netstandard2.0)) (&& (>= netstandard2.0) (< netstandard2.1)) + System.Runtime.CompilerServices.Unsafe (>= 6.1.2) - restriction: || (>= net462) (&& (< netcoreapp2.1) (>= netstandard2.0) (< netstandard2.1)) + System.ValueTuple (4.6.1) - restriction: && (>= net462) (>= netstandard2.0) diff --git a/lib/src/Benchmarks/Benchmarks.fs b/lib/src/Benchmarks/Benchmarks.fs new file mode 100644 index 0000000..09c8f03 --- /dev/null +++ b/lib/src/Benchmarks/Benchmarks.fs @@ -0,0 +1,472 @@ +module Benchmarks +open BenchmarkDotNet.Attributes +open BenchmarkDotNet.Configs +open BenchmarkDotNet.Jobs +open BenchmarkDotNet.Running +open BenchmarkDotNet.Toolchains.InProcess.NoEmit + +module ViewEngineApi = + open FSharp.ViewEngine + open type Html + + let buildDocument () = + html { + _lang "en" + head { + meta { _charset "utf-8" } + meta { _name "viewport"; _content "width=device-width, initial-scale=1" } + title "Benchmark" + link { _href "/css/site.css"; _rel "stylesheet" } + } + body { + _class "page" + header { + _class "site-header" + h1 { "Benchmark Page" } + nav { + ul { + li { a { _href "/"; "Home" } } + li { a { _href "/docs"; "Docs" } } + li { a { _href "/about"; "About" } } + } + } + } + main { + section { + _id "intro" + h2 { "Intro" } + p { "This is a simple benchmark document." } + p { "It includes common HTML elements." } + pre { + code { _class "language-html"; text "

Hello

" } + } + } + article { + h3 { "Highlights" } + ul { + li { "Lists" } + li { "Forms" } + li { "Tables" } + } + p { + text "Some inline " + raw "bold" + text " text." + } + } + form { + _id "signup" + _class "form" + label { _for "email"; "Email" } + input { _id "email"; _name "email"; _type "email"; _placeholder "name@example.com" } + label { _for "plan"; "Plan" } + select { + _id "plan" + _name "plan" + option { _value "free"; "Free" } + option { _value "pro"; "Pro" } + option { _value "team"; "Team" } + } + label { + _for "terms" + input { _id "terms"; _type "checkbox"; _checked true } + text " Accept terms" + } + button { _type "submit"; "Submit" } + } + table { + _class "data" + thead { + tr { + th { "Name" } + th { "Value" } + } + } + tbody { + tr { td { "Alpha" }; td { "1" } } + tr { td { "Beta" }; td { "2" } } + tr { td { "Gamma" }; td { "3" } } + } + } + } + footer { + _class "site-footer" + small { "© 2026 Example Co." } + a { _href "/privacy"; "Privacy" } + } + } + } + +module OxpeckerApi = + open Oxpecker.ViewEngine + + let buildDocument () = + html().attr("lang", "en") { + head() { + meta().attr("charset", "utf-8") + meta().attr("name", "viewport").attr("content", "width=device-width, initial-scale=1") + title() { "Benchmark" } + link().attr("href", "/css/site.css").attr("rel", "stylesheet") + } + body().attr("class", "page") { + header().attr("class", "site-header") { + h1() { "Benchmark Page" } + nav() { + ul() { + li() { a().attr("href", "/") { "Home" } } + li() { a().attr("href", "/docs") { "Docs" } } + li() { a().attr("href", "/about") { "About" } } + } + } + } + main() { + section().attr("id", "intro") { + h2() { "Intro" } + p() { "This is a simple benchmark document." } + p() { "It includes common HTML elements." } + pre() { + code().attr("class", "language-html") { "

Hello

" } + } + } + article() { + h3() { "Highlights" } + ul() { + li() { "Lists" } + li() { "Forms" } + li() { "Tables" } + } + p() { + "Some inline " + raw "bold" + " text." + } + } + form().attr("id", "signup").attr("class", "form") { + label().attr("for", "email") { "Email" } + input() + .attr("id", "email") + .attr("name", "email") + .attr("type", "email") + .attr("placeholder", "name@example.com") + label().attr("for", "plan") { "Plan" } + select().attr("id", "plan").attr("name", "plan") { + option().attr("value", "free") { "Free" } + option().attr("value", "pro") { "Pro" } + option().attr("value", "team") { "Team" } + } + label().attr("for", "terms") { + input().attr("id", "terms").attr("type", "checkbox").bool("checked", true) + " Accept terms" + } + button().attr("type", "submit") { "Submit" } + } + table().attr("class", "data") { + thead() { + tr() { + th() { "Name" } + th() { "Value" } + } + } + tbody() { + tr() { td() { "Alpha" }; td() { "1" } } + tr() { td() { "Beta" }; td() { "2" } } + tr() { td() { "Gamma" }; td() { "3" } } + } + } + } + footer().attr("class", "site-footer") { + small() { "© 2026 Example Co." } + a().attr("href", "/privacy") { "Privacy" } + } + } + } + +module GiraffeApi = + open Giraffe.ViewEngine + open Giraffe.ViewEngine.HtmlElements + open Giraffe.ViewEngine.Attributes + + let buildDocument () = + html [ _lang "en" ] [ + head [] [ + meta [ _charset "utf-8" ] + meta [ _name "viewport"; _content "width=device-width, initial-scale=1" ] + title [] [ str "Benchmark" ] + link [ _href "/css/site.css"; _rel "stylesheet" ] + ] + body [ _class "page" ] [ + header [ _class "site-header" ] [ + h1 [] [ str "Benchmark Page" ] + nav [] [ + ul [] [ + li [] [ a [ _href "/" ] [ str "Home" ] ] + li [] [ a [ _href "/docs" ] [ str "Docs" ] ] + li [] [ a [ _href "/about" ] [ str "About" ] ] + ] + ] + ] + main [] [ + section [ _id "intro" ] [ + h2 [] [ str "Intro" ] + p [] [ str "This is a simple benchmark document." ] + p [] [ str "It includes common HTML elements." ] + pre [] [ + code [ _class "language-html" ] [ str "

Hello

" ] + ] + ] + article [] [ + h3 [] [ str "Highlights" ] + ul [] [ + li [] [ str "Lists" ] + li [] [ str "Forms" ] + li [] [ str "Tables" ] + ] + p [] [ + str "Some inline " + rawText "bold" + str " text." + ] + ] + form [ _id "signup"; _class "form" ] [ + label [ _for "email" ] [ str "Email" ] + input [ _id "email"; _name "email"; _type "email"; _placeholder "name@example.com" ] + label [ _for "plan" ] [ str "Plan" ] + select [ _id "plan"; _name "plan" ] [ + option [ _value "free" ] [ str "Free" ] + option [ _value "pro" ] [ str "Pro" ] + option [ _value "team" ] [ str "Team" ] + ] + label [ _for "terms" ] [ + input [ _id "terms"; _type "checkbox"; _checked ] + str " Accept terms" + ] + button [ _type "submit" ] [ str "Submit" ] + ] + table [ _class "data" ] [ + thead [] [ + tr [] [ + th [] [ str "Name" ] + th [] [ str "Value" ] + ] + ] + tbody [] [ + tr [] [ td [] [ str "Alpha" ]; td [] [ str "1" ] ] + tr [] [ td [] [ str "Beta" ]; td [] [ str "2" ] ] + tr [] [ td [] [ str "Gamma" ]; td [] [ str "3" ] ] + ] + ] + ] + footer [ _class "site-footer" ] [ + small [] [ str "© 2026 Example Co." ] + a [ _href "/privacy" ] [ str "Privacy" ] + ] + ] + ] + +module FelizApi = + open Feliz.ViewEngine + + let buildDocument () = + Html.html [ + prop.lang "en" + prop.children [ + Html.head [ + prop.children [ + Html.meta [ prop.charset "utf-8" ] + Html.meta [ prop.name "viewport"; prop.content "width=device-width, initial-scale=1" ] + Html.title "Benchmark" + Html.link [ prop.href "/css/site.css"; prop.rel "stylesheet" ] + ] + ] + Html.body [ + prop.className "page" + prop.children [ + Html.header [ + prop.className "site-header" + prop.children [ + Html.h1 [ prop.children [ Html.text "Benchmark Page" ] ] + Html.nav [ + prop.children [ + Html.ul [ + prop.children [ + Html.li [ prop.children [ Html.a [ prop.href "/"; prop.children [ Html.text "Home" ] ] ] ] + Html.li [ prop.children [ Html.a [ prop.href "/docs"; prop.children [ Html.text "Docs" ] ] ] ] + Html.li [ prop.children [ Html.a [ prop.href "/about"; prop.children [ Html.text "About" ] ] ] ] + ] + ] + ] + ] + ] + ] + Html.main [ + prop.children [ + Html.section [ + prop.id "intro" + prop.children [ + Html.h2 [ prop.children [ Html.text "Intro" ] ] + Html.p [ prop.children [ Html.text "This is a simple benchmark document." ] ] + Html.p [ prop.children [ Html.text "It includes common HTML elements." ] ] + Html.pre [ + prop.children [ + Html.code [ prop.className "language-html"; prop.children [ Html.text "

Hello

" ] ] + ] + ] + ] + ] + Html.article [ + prop.children [ + Html.h3 [ prop.children [ Html.text "Highlights" ] ] + Html.ul [ + prop.children [ + Html.li [ prop.children [ Html.text "Lists" ] ] + Html.li [ prop.children [ Html.text "Forms" ] ] + Html.li [ prop.children [ Html.text "Tables" ] ] + ] + ] + Html.p [ + prop.children [ + Html.text "Some inline " + Html.rawText "bold" + Html.text " text." + ] + ] + ] + ] + Html.form [ + prop.id "signup" + prop.className "form" + prop.children [ + Html.label [ prop.htmlFor "email"; prop.children [ Html.text "Email" ] ] + Html.input [ + prop.id "email" + prop.name "email" + prop.type' "email" + prop.placeholder "name@example.com" + ] + Html.label [ prop.htmlFor "plan"; prop.children [ Html.text "Plan" ] ] + Html.select [ + prop.id "plan" + prop.name "plan" + prop.children [ + Html.option [ prop.value "free"; prop.children [ Html.text "Free" ] ] + Html.option [ prop.value "pro"; prop.children [ Html.text "Pro" ] ] + Html.option [ prop.value "team"; prop.children [ Html.text "Team" ] ] + ] + ] + Html.label [ + prop.htmlFor "terms" + prop.children [ + Html.input [ prop.id "terms"; prop.type' "checkbox"; prop.isChecked true ] + Html.text " Accept terms" + ] + ] + Html.button [ prop.type' "submit"; prop.children [ Html.text "Submit" ] ] + ] + ] + Html.table [ + prop.className "data" + prop.children [ + Html.thead [ + prop.children [ + Html.tr [ + prop.children [ + Html.th [ prop.children [ Html.text "Name" ] ] + Html.th [ prop.children [ Html.text "Value" ] ] + ] + ] + ] + ] + Html.tbody [ + prop.children [ + Html.tr [ prop.children [ Html.td [ prop.children [ Html.text "Alpha" ] ]; Html.td [ prop.children [ Html.text "1" ] ] ] ] + Html.tr [ prop.children [ Html.td [ prop.children [ Html.text "Beta" ] ]; Html.td [ prop.children [ Html.text "2" ] ] ] ] + Html.tr [ prop.children [ Html.td [ prop.children [ Html.text "Gamma" ] ]; Html.td [ prop.children [ Html.text "3" ] ] ] ] + ] + ] + ] + ] + ] + ] + Html.footer [ + prop.className "site-footer" + prop.children [ + Html.small [ prop.children [ Html.text "© 2026 Example Co." ] ] + Html.a [ prop.href "/privacy"; prop.children [ Html.text "Privacy" ] ] + ] + ] + ] + ] + ] + ] +[] +type BuildAndRender() = + + [] + member _.ViewEngineApi() = + ViewEngineApi.buildDocument() |> FSharp.ViewEngine.Render.toHtmlDocString + + [] + member _.OxpeckerApi() = + OxpeckerApi.buildDocument() |> Oxpecker.ViewEngine.Render.toHtmlDocString + + [] + member _.GiraffeApi() = + GiraffeApi.buildDocument() |> Giraffe.ViewEngine.RenderView.AsString.htmlDocument + + [] + member _.FelizApi() = + FelizApi.buildDocument() |> Feliz.ViewEngine.Render.htmlDocument + +[] +type RenderOnly() = + let viewDoc = ViewEngineApi.buildDocument() + let oxDoc = OxpeckerApi.buildDocument() + let giraffeDoc = GiraffeApi.buildDocument() + let felizDoc = FelizApi.buildDocument() + + [] + member _.ViewEngineApi() = + viewDoc |> FSharp.ViewEngine.Render.toHtmlDocString + + [] + member _.OxpeckerApi() = + oxDoc |> Oxpecker.ViewEngine.Render.toHtmlDocString + + [] + member _.GiraffeApi() = + giraffeDoc |> Giraffe.ViewEngine.RenderView.AsString.htmlDocument + + [] + member _.FelizApi() = + felizDoc |> Feliz.ViewEngine.Render.htmlDocument + +[] +type BuildOnly() = + + [] + member _.ViewEngineApi() = + ViewEngineApi.buildDocument() + + [] + member _.OxpeckerApi() = + OxpeckerApi.buildDocument() + + [] + member _.GiraffeApi() = + GiraffeApi.buildDocument() + + [] + member _.FelizApi() = + FelizApi.buildDocument() + +let runBenchmarks () = + let medJob = + Job.MediumRun + .WithToolchain(InProcessNoEmitToolchain.Instance) + + let config = + ManualConfig.Create(DefaultConfig.Instance) + .AddJob(medJob) + BenchmarkRunner.Run(config) |> ignore + BenchmarkRunner.Run(config) |> ignore + BenchmarkRunner.Run(config) |> ignore diff --git a/lib/src/Benchmarks/Benchmarks.fsproj b/lib/src/Benchmarks/Benchmarks.fsproj new file mode 100644 index 0000000..5cb7d76 --- /dev/null +++ b/lib/src/Benchmarks/Benchmarks.fsproj @@ -0,0 +1,18 @@ + + + + Exe + net10.0 + false + true + + + + + + + + + + + diff --git a/lib/src/Benchmarks/Profile.fs b/lib/src/Benchmarks/Profile.fs new file mode 100644 index 0000000..cd1c5f5 --- /dev/null +++ b/lib/src/Benchmarks/Profile.fs @@ -0,0 +1,110 @@ +module Profile + +open System + +type Mode = + | BuildAndRender + | BuildOnly + | RenderOnly + +type Api = + | ViewEngine + | Oxpecker + | Giraffe + | Feliz + +let private parseMode (args: string array) = + if args |> Array.contains "--build-only" then + BuildOnly + elif args |> Array.contains "--render-only" then + RenderOnly + else + BuildAndRender + +let private parseApi (args: string array) = + if args |> Array.contains "--oxpecker" then + Oxpecker + elif args |> Array.contains "--giraffe" then + Giraffe + elif args |> Array.contains "--feliz" then + Feliz + else + ViewEngine + +let private parseDurationMs (args: string array) = + args + |> Array.tryFind (fun arg -> arg.StartsWith("--duration-ms=", StringComparison.OrdinalIgnoreCase)) + |> Option.bind (fun arg -> + let value = arg.Substring("--duration-ms=".Length) + match Int32.TryParse(value) with + | true, ms when ms > 0 -> Some ms + | _ -> None) + |> Option.defaultValue 10_000 + +let run (mode: Mode) (api: Api) (durationMs: int) = + let buildAndRender () = + match api with + | ViewEngine -> + Benchmarks.ViewEngineApi.buildDocument() |> FSharp.ViewEngine.Render.toHtmlDocString |> ignore + | Oxpecker -> + Benchmarks.OxpeckerApi.buildDocument() |> Oxpecker.ViewEngine.Render.toHtmlDocString |> ignore + | Giraffe -> + Benchmarks.GiraffeApi.buildDocument() |> Giraffe.ViewEngine.RenderView.AsString.htmlDocument |> ignore + | Feliz -> + Benchmarks.FelizApi.buildDocument() |> Feliz.ViewEngine.Render.htmlDocument |> ignore + + let buildOnly () = + match api with + | ViewEngine -> Benchmarks.ViewEngineApi.buildDocument() |> ignore + | Oxpecker -> Benchmarks.OxpeckerApi.buildDocument() |> ignore + | Giraffe -> Benchmarks.GiraffeApi.buildDocument() |> ignore + | Feliz -> Benchmarks.FelizApi.buildDocument() |> ignore + + let renderOnly () = + match api with + | ViewEngine -> + let doc = Benchmarks.ViewEngineApi.buildDocument() + doc |> FSharp.ViewEngine.Render.toHtmlDocString |> ignore + | Oxpecker -> + let doc = Benchmarks.OxpeckerApi.buildDocument() + doc |> Oxpecker.ViewEngine.Render.toHtmlDocString |> ignore + | Giraffe -> + let doc = Benchmarks.GiraffeApi.buildDocument() + doc |> Giraffe.ViewEngine.RenderView.AsString.htmlDocument |> ignore + | Feliz -> + let doc = Benchmarks.FelizApi.buildDocument() + doc |> Feliz.ViewEngine.Render.htmlDocument |> ignore + + let invoke = + match mode with + | BuildAndRender -> buildAndRender + | BuildOnly -> buildOnly + | RenderOnly -> renderOnly + + // Warm up + for _ = 1 to 1000 do + invoke () + + // Signal ready + Console.Error.WriteLine("READY") + Console.Error.Flush() + + // Hot loop for profiling + let mutable count = 0 + let sw = Diagnostics.Stopwatch.StartNew() + while sw.ElapsedMilliseconds < int64 durationMs do + invoke () + count <- count + 1 + Console.Error.WriteLine($"Completed {count} iterations in {sw.ElapsedMilliseconds}ms") + +[] +let main args = + if args |> Array.contains "--profile" then + let mode = parseMode args + let api = parseApi args + let durationMs = parseDurationMs args + run mode api durationMs + 0 + else + Benchmarks.runBenchmarks () + 0 diff --git a/lib/src/Benchmarks/paket.references b/lib/src/Benchmarks/paket.references new file mode 100644 index 0000000..e8590a7 --- /dev/null +++ b/lib/src/Benchmarks/paket.references @@ -0,0 +1,5 @@ +BenchmarkDotNet +FSharp.Core +Giraffe.ViewEngine +Feliz.ViewEngine +Oxpecker.ViewEngine diff --git a/lib/src/Build/Build.fsproj b/lib/src/Build/Build.fsproj new file mode 100644 index 0000000..8f18310 --- /dev/null +++ b/lib/src/Build/Build.fsproj @@ -0,0 +1,13 @@ + + + Exe + net10.0 + false + $(NoWarn);NU1510 + + + + + + + \ No newline at end of file diff --git a/fsharp-view-engine/src/Build/Program.fs b/lib/src/Build/Program.fs similarity index 67% rename from fsharp-view-engine/src/Build/Program.fs rename to lib/src/Build/Program.fs index b8ff837..4bbfe70 100644 --- a/fsharp-view-engine/src/Build/Program.fs +++ b/lib/src/Build/Program.fs @@ -16,10 +16,8 @@ let inline (==>!) x y = x ==> y |> ignore let srcDir = Path.getDirectory __SOURCE_DIRECTORY__ let rootDir = Path.getDirectory srcDir -let sln = rootDir "FSharp.ViewEngine.sln" let nugetsDir = rootDir "nugets" let testsDir = srcDir "Tests" -let appDir = srcDir "App" let exec workDir cmd args = CreateProcess.fromRawCommand cmd args @@ -39,7 +37,6 @@ let execEnv env workDir cmd args = |> Async.Ignore let dotnet workdir args = exec workdir "dotnet" args -let tailwindcss args = exec appDir "tailwindcss" args let getVersion () = let tag = Environment.environVarOrFail "GITHUB_REF_NAME" @@ -49,33 +46,17 @@ let getVersion () = Target.create "CleanNugets" <| fun _ -> Shell.cleanDir nugetsDir Target.create "Test" <| fun _ -> - dotnet testsDir ["test"] - |> Async.RunSynchronously - -Target.create "WatchApp" (fun _ -> - let watchApp = dotnet appDir ["watch"; "run"; "--no-restore"] - let watchCss = tailwindcss ["--input"; "input.css"; "--output"; "wwwroot/css/output.css"; "--watch"] - Async.Parallel [| watchApp; watchCss |] - |> Async.RunSynchronously - |> ignore -) - -Target.create "BuildAppCss" <| fun _ -> - tailwindcss [ "--input"; "input.css"; "--output"; "wwwroot/css/output.css"; "--minify" ] - |> Async.RunSynchronously - -Target.create "PublishApp" <| fun _ -> - dotnet appDir [ - "publish" - "--output"; "./out" - "--self-contained"; "false" - ] - |> Async.RunSynchronously + ["net8.0"; "net9.0"; "net10.0"] + |> List.iter (fun tfm -> + dotnet testsDir ["run"; "--framework"; tfm] + |> Async.RunSynchronously + ) Target.create "Pack" (fun _ -> - Trace.trace $"Packing {sln}" + let project = srcDir "FSharp.ViewEngine" "FSharp.ViewEngine.fsproj" + Trace.trace $"Packing {project}" let version = getVersion() - dotnet rootDir ["pack"; sln; "--configuration"; "Release"; "--output"; nugetsDir; $"/p:PackageVersion={version}"] + dotnet rootDir ["pack"; project; "--configuration"; "Release"; "--output"; nugetsDir; $"/p:PackageVersion={version}"] |> Async.RunSynchronously ) @@ -92,6 +73,5 @@ Target.create "Default" (fun _ -> Target.listAvailable()) "Test" ==>! "Pack" "CleanNugets" ==>! "Pack" "Pack" ==>! "PushNugets" -"BuildAppCss" ==>! "PublishApp" Target.runOrDefaultWithArguments "Default" diff --git a/lib/src/Build/paket.references b/lib/src/Build/paket.references new file mode 100644 index 0000000..9bc568a --- /dev/null +++ b/lib/src/Build/paket.references @@ -0,0 +1 @@ +Fake.Core.Target diff --git a/lib/src/FSharp.ViewEngine/Alpine.fs b/lib/src/FSharp.ViewEngine/Alpine.fs new file mode 100644 index 0000000..7948334 --- /dev/null +++ b/lib/src/FSharp.ViewEngine/Alpine.fs @@ -0,0 +1,41 @@ +namespace FSharp.ViewEngine + +type Alpine = + static member inline _by (value: string) = { Name = "by"; Value = ValueSome value } + static member inline _x (key: string, ?value: string) = + match value with + | Some v -> { Name = $"x-{key}"; Value = ValueSome v } + | None -> { Name = $"x-{key}"; Value = ValueNone } + static member inline _xOn (event: string, v: string) = { Name = $"x-on:{event}"; Value = ValueSome v } + static member inline _xOn (event: string) = { Name = $"x-on:{event}"; Value = ValueNone } + static member inline _xInit (value: string) = { Name = "x-init"; Value = ValueSome value } + static member inline _xData (value: string) = { Name = "x-data"; Value = ValueSome value } + static member inline _xRef (value: string) = { Name = "x-ref"; Value = ValueSome value } + static member inline _xText (value: string) = { Name = "x-text"; Value = ValueSome value } + static member inline _xBind (attr: string, value: string) = { Name = $"x-bind:{attr}"; Value = ValueSome value } + static member inline _xShow (value: string) = { Name = "x-show"; Value = ValueSome value } + static member inline _xIf (value: string) = { Name = "x-if"; Value = ValueSome value } + static member inline _xFor (value: string) = { Name = "x-for"; Value = ValueSome value } + static member inline _xModel (value: string, ?modifier: string) = + match modifier with + | Some m -> { Name = $"x-model{m}"; Value = ValueSome value } + | None -> { Name = "x-model"; Value = ValueSome value } + static member inline _xModelable (value: string) = { Name = "x-modelable"; Value = ValueSome value } + static member inline _xId (value: string) = { Name = "x-id"; Value = ValueSome value } + static member inline _xEffect (value: string) = { Name = "x-effect"; Value = ValueSome value } + static member inline _xTransition (?value: string, ?modifier: string) = + match value, modifier with + | Some v, Some m -> { Name = $"x-transition{m}"; Value = ValueSome v } + | Some v, None -> { Name = "x-transition"; Value = ValueSome v } + | None, Some m -> { Name = $"x-transition{m}"; Value = ValueNone } + | None, None -> { Name = "x-transition"; Value = ValueNone } + static member inline _xTrap (value: string, ?modifier: string) = + match modifier with + | Some m -> { Name = $"x-trap{m}"; Value = ValueSome value } + | None -> { Name = "x-trap"; Value = ValueSome value } + static member inline _xCloak = { Name = "x-cloak"; Value = ValueNone } + static member inline _xAnchor (value: string, ?modifier: string) = + match modifier with + | Some m -> { Name = $"x-anchor{m}"; Value = ValueSome value } + | None -> { Name = "x-anchor"; Value = ValueSome value } + static member inline _xTeleport (value: string) = { Name = "x-teleport"; Value = ValueSome value } diff --git a/lib/src/FSharp.ViewEngine/Core.fs b/lib/src/FSharp.ViewEngine/Core.fs new file mode 100644 index 0000000..fc0fd85 --- /dev/null +++ b/lib/src/FSharp.ViewEngine/Core.fs @@ -0,0 +1,322 @@ +namespace FSharp.ViewEngine + +open System +open System.Buffers +open System.Runtime.CompilerServices +open System.Text + +[] +type HtmlAttribute = { Name: string; Value: string voption } + +[] +type HtmlElement() = + abstract Render: StringBuilder -> unit + +module private HtmlEncoding = + [] + let private UnicodeReplacementChar = '\uFFFD' + + let private htmlAsciiNonEncodingChars = + SearchValues.Create( + "\0\u0001\u0002\u0003\u0004\u0005\u0006\a\b\t\n\v\f\r\u000e\u000f\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f !#$%()*+,-./0123456789:;=?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\u007f" + ) + + let rec private findEncodingCharLoop i (input: ReadOnlySpan) = + if i < input.Length then + let ch = input[i] + if ch <= '>' then + if ch = '<' || ch = '>' || ch = '"' || ch = '\'' || ch = '&' then + i + else + findEncodingCharLoop (i + 1) input + elif Char.IsBetween(ch, '\u00A0', '\u00FF') || Char.IsSurrogate(ch) then + i + else + findEncodingCharLoop (i + 1) input + else + -1 + + let private indexOfHtmlEncodingChar (input: ReadOnlySpan) = + match input.IndexOfAnyExcept(htmlAsciiNonEncodingChars) with + | -1 -> -1 + | index -> findEncodingCharLoop index input + + let private htmlEncodeInner (input: ReadOnlySpan) (sb: StringBuilder) : unit = + let mutable i = 0 + while i < input.Length do + let ch = input[i] + i <- i + 1 + if ch <= '>' then + if '<' = ch then sb.Append "<" + elif '>' = ch then sb.Append ">" + elif '"' = ch then sb.Append """ + elif '\'' = ch then sb.Append "'" + elif '&' = ch then sb.Append "&" + else sb.Append ch + else if Char.IsBetween(ch, '\u00A0', '\u00FF') then + sb.Append("&#").Append(int ch).Append(';') + elif Char.IsSurrogate(ch) then + if i < input.Length then + match Rune.TryCreate(ch, input[i]) with + | true, rune -> + i <- i + 1 + sb.Append("&#").Append(rune.Value).Append(';') + | _ -> + sb.Append(UnicodeReplacementChar) + else + sb.Append(UnicodeReplacementChar) + else + sb.Append(ch) + |> ignore + + let htmlEncode (value: string) (sb: StringBuilder) = + if isNull value then + () + else + let value = value.AsSpan() + match indexOfHtmlEncodingChar value with + | -1 -> sb.Append(value) |> ignore + | index -> + sb.Append(value.Slice(0, index)) |> ignore + htmlEncodeInner (value.Slice index) sb + +type TextElement(text: string) = + inherit HtmlElement() + override _.Render(sb) = HtmlEncoding.htmlEncode text sb + +type RawElement(text: string) = + inherit HtmlElement() + override _.Render(sb) = sb.Append(text) |> ignore + +type NoopElement() = + inherit HtmlElement() + override _.Render(_) = () + +module private RenderHelpers = + let inline renderAttr (sb: StringBuilder) (a: HtmlAttribute) = + match a.Value with + | ValueSome v -> sb.Append(' ').Append(a.Name).Append("=\"").Append(v).Append('\"') |> ignore + | ValueNone -> sb.Append(' ').Append(a.Name) |> ignore + +type VoidElement(tag: string) = + inherit HtmlElement() + let mutable attrCount = 0 + let mutable attr0 = Unchecked.defaultof + let mutable attr1 = Unchecked.defaultof + let mutable attrRest: ResizeArray = null + [] + member this.AddAttr(a: HtmlAttribute) = + match attrCount with + | 0 -> + attr0 <- a + attrCount <- 1 + | 1 -> + attr1 <- a + attrCount <- 2 + | _ -> + if isNull attrRest then + attrRest <- ResizeArray() + attrRest.Add(a) + attrCount <- attrCount + 1 + override _.Render(sb) = + sb.Append('<').Append(tag) |> ignore + match attrCount with + | 0 -> () + | 1 -> RenderHelpers.renderAttr sb attr0 + | 2 -> + RenderHelpers.renderAttr sb attr0 + RenderHelpers.renderAttr sb attr1 + | _ -> + RenderHelpers.renderAttr sb attr0 + RenderHelpers.renderAttr sb attr1 + let rest = attrRest + if not (isNull rest) then + for i = 0 to rest.Count - 1 do + RenderHelpers.renderAttr sb rest[i] + sb.Append('>') |> ignore + +type RegularElement(tag: string) = + inherit HtmlElement() + let mutable attrCount = 0 + let mutable attr0 = Unchecked.defaultof + let mutable attr1 = Unchecked.defaultof + let mutable attrRest: ResizeArray = null + let mutable childCount = 0 + let mutable child0 = Unchecked.defaultof + let mutable child1 = Unchecked.defaultof + let mutable childRest: ResizeArray = null + [] + member this.AddAttr(a: HtmlAttribute) = + match attrCount with + | 0 -> + attr0 <- a + attrCount <- 1 + | 1 -> + attr1 <- a + attrCount <- 2 + | _ -> + if isNull attrRest then + attrRest <- ResizeArray() + attrRest.Add(a) + attrCount <- attrCount + 1 + [] + member this.AddChild(c: HtmlElement) = + match childCount with + | 0 -> + child0 <- c + childCount <- 1 + | 1 -> + child1 <- c + childCount <- 2 + | _ -> + if isNull childRest then + childRest <- ResizeArray() + childRest.Add(c) + childCount <- childCount + 1 + override _.Render(sb) = + sb.Append('<').Append(tag) |> ignore + match attrCount with + | 0 -> () + | 1 -> RenderHelpers.renderAttr sb attr0 + | 2 -> + RenderHelpers.renderAttr sb attr0 + RenderHelpers.renderAttr sb attr1 + | _ -> + RenderHelpers.renderAttr sb attr0 + RenderHelpers.renderAttr sb attr1 + let rest = attrRest + if not (isNull rest) then + for i = 0 to rest.Count - 1 do + RenderHelpers.renderAttr sb rest[i] + sb.Append('>') |> ignore + match childCount with + | 0 -> () + | 1 -> child0.Render(sb) + | 2 -> + child0.Render(sb) + child1.Render(sb) + | _ -> + child0.Render(sb) + child1.Render(sb) + let rest = childRest + if not (isNull rest) then + for i = 0 to rest.Count - 1 do + rest[i].Render(sb) + sb.Append("') |> ignore + +type private StringBuilderPool = + [] + static val mutable private pool: ResizeArray + + static member inline Rent() = + let pool = StringBuilderPool.pool + if isNull pool || pool.Count = 0 then + StringBuilder() + else + let idx = pool.Count - 1 + let sb = pool[idx] + pool.RemoveAt(idx) + sb + + static member inline Return(sb: StringBuilder) = + sb.Clear() |> ignore + let pool = StringBuilderPool.pool + if isNull pool then + let p = ResizeArray() + p.Add(sb) + StringBuilderPool.pool <- p + else + pool.Add(sb) + +type TagBuilderCode = RegularElement -> unit +type VoidBuilderCode = VoidElement -> unit + +type TagBuilder(tag: string) = + [] + member inline _.Yield(el: HtmlElement) : TagBuilderCode = + fun st -> st.AddChild(el) + + [] + member inline _.Yield(text: string) : TagBuilderCode = + fun st -> st.AddChild(TextElement(text) :> HtmlElement) + + [] + member inline _.Yield(attr: HtmlAttribute) : TagBuilderCode = + fun st -> + if not (isNull attr.Name) then + st.AddAttr(attr) + + [] + member inline _.Zero() : TagBuilderCode = + fun _ -> () + + [] + member inline _.Combine([] f1: TagBuilderCode, [] f2: TagBuilderCode) : TagBuilderCode = + fun st -> f1 st; f2 st + + [] + member inline _.Delay([] f: unit -> TagBuilderCode) : TagBuilderCode = + fun st -> (f ()) st + + [] + member inline _.For(xs: 'a seq, [] f: 'a -> TagBuilderCode) : TagBuilderCode = + fun st -> + for x in xs do + (f x) st + + [] + member _.Run(f: TagBuilderCode) : HtmlElement = + let el = RegularElement(tag) + f el + el :> HtmlElement + + +type VoidBuilder(tag: string) = + [] + member inline _.Yield(attr: HtmlAttribute) : VoidBuilderCode = + fun st -> + if not (isNull attr.Name) then + st.AddAttr(attr) + + [] + member inline _.Zero() : VoidBuilderCode = + fun _ -> () + + [] + member inline _.Combine([] f1: VoidBuilderCode, [] f2: VoidBuilderCode) : VoidBuilderCode = + fun st -> f1 st; f2 st + + [] + member inline _.Delay([] f: unit -> VoidBuilderCode) : VoidBuilderCode = + fun st -> (f ()) st + + [] + member inline _.For(xs: 'a seq, [] f: 'a -> VoidBuilderCode) : VoidBuilderCode = + fun st -> + for x in xs do + (f x) st + + [] + member _.Run(f: VoidBuilderCode) : HtmlElement = + let el = VoidElement(tag) + f el + el :> HtmlElement + +[] +module Render = + let toString (element: HtmlElement) = + let sb = StringBuilderPool.Rent() + try + element.Render(sb) + sb.ToString() + finally + StringBuilderPool.Return(sb) + + let toHtmlDocString (view: #HtmlElement) = + let sb = StringBuilderPool.Rent() + sb.AppendLine("") |> ignore + try + view.Render(sb) + sb.ToString() + finally + StringBuilderPool.Return(sb) diff --git a/lib/src/FSharp.ViewEngine/Datastar.fs b/lib/src/FSharp.ViewEngine/Datastar.fs new file mode 100644 index 0000000..258a260 --- /dev/null +++ b/lib/src/FSharp.ViewEngine/Datastar.fs @@ -0,0 +1,45 @@ +namespace FSharp.ViewEngine + +type Datastar = + // Generic data-* attribute + static member inline _ds (key: string, value: string) = { Name = $"data-{key}"; Value = ValueSome value } + static member inline _ds (key: string) = { Name = $"data-{key}"; Value = ValueNone } + + // Core attributes + static member inline _dsAttr (name: string, v: string) = { Name = $"data-attr:{name}"; Value = ValueSome v } + static member inline _dsBind (name: string) = { Name = $"data-bind:{name}"; Value = ValueNone } + static member inline _dsBind (name: string, v: string) = { Name = $"data-bind:{name}"; Value = ValueSome v } + static member inline _dsClass (name: string, v: string) = { Name = $"data-class:{name}"; Value = ValueSome v } + static member inline _dsComputed (name: string, v: string) = { Name = $"data-computed:{name}"; Value = ValueSome v } + static member inline _dsEffect (v: string) = { Name = "data-effect"; Value = ValueSome v } + static member inline _dsIgnore = { Name = "data-ignore"; Value = ValueNone } + static member inline _dsIgnoreMorph = { Name = "data-ignore-morph"; Value = ValueNone } + static member inline _dsIndicator (name: string) = { Name = $"data-indicator:{name}"; Value = ValueNone } + static member inline _dsIndicator (name: string, v: string) = { Name = $"data-indicator:{name}"; Value = ValueSome v } + static member inline _dsInit (v: string) = { Name = "data-init"; Value = ValueSome v } + static member inline _dsJsonSignals (?v: string) = match v with Some v -> { Name = "data-json-signals"; Value = ValueSome v } | None -> { Name = "data-json-signals"; Value = ValueNone } + static member inline _dsOn (event: string, v: string) = { Name = $"data-on:{event}"; Value = ValueSome v } + static member inline _dsOnIntersect (v: string) = { Name = "data-on-intersect"; Value = ValueSome v } + static member inline _dsOnInterval (v: string) = { Name = "data-on-interval"; Value = ValueSome v } + static member inline _dsOnSignalPatch (v: string) = { Name = "data-on-signal-patch"; Value = ValueSome v } + static member inline _dsOnSignalPatchFilter (v: string) = { Name = "data-on-signal-patch-filter"; Value = ValueSome v } + static member inline _dsPreserveAttr (v: string) = { Name = "data-preserve-attr"; Value = ValueSome v } + static member inline _dsRef (name: string) = { Name = $"data-ref:{name}"; Value = ValueNone } + static member inline _dsRef (name: string, v: string) = { Name = $"data-ref:{name}"; Value = ValueSome v } + static member inline _dsShow (v: string) = { Name = "data-show"; Value = ValueSome v } + static member inline _dsSignals (name: string, v: string) = { Name = $"data-signals:{name}"; Value = ValueSome v } + static member inline _dsStyle (prop: string, v: string) = { Name = $"data-style:{prop}"; Value = ValueSome v } + static member inline _dsText (v: string) = { Name = "data-text"; Value = ValueSome v } + + // Pro attributes + static member inline _dsAnimate (v: string) = { Name = "data-animate"; Value = ValueSome v } + static member inline _dsCustomValidity (v: string) = { Name = "data-custom-validity"; Value = ValueSome v } + static member inline _dsOnRaf (v: string) = { Name = "data-on-raf"; Value = ValueSome v } + static member inline _dsOnResize (v: string) = { Name = "data-on-resize"; Value = ValueSome v } + static member inline _dsPersist (key: string) = { Name = $"data-persist:{key}"; Value = ValueNone } + static member inline _dsPersist (key: string, v: string) = { Name = $"data-persist:{key}"; Value = ValueSome v } + static member inline _dsQueryString (?v: string) = match v with Some v -> { Name = "data-query-string"; Value = ValueSome v } | None -> { Name = "data-query-string"; Value = ValueNone } + static member inline _dsReplaceUrl (v: string) = { Name = "data-replace-url"; Value = ValueSome v } + static member inline _dsRocket (v: string) = { Name = "data-rocket"; Value = ValueSome v } + static member inline _dsScrollIntoView = { Name = "data-scroll-into-view"; Value = ValueNone } + static member inline _dsViewTransition (v: string) = { Name = "data-view-transition"; Value = ValueSome v } diff --git a/fsharp-view-engine/src/FSharp.ViewEngine/FSharp.ViewEngine.fsproj b/lib/src/FSharp.ViewEngine/FSharp.ViewEngine.fsproj similarity index 69% rename from fsharp-view-engine/src/FSharp.ViewEngine/FSharp.ViewEngine.fsproj rename to lib/src/FSharp.ViewEngine/FSharp.ViewEngine.fsproj index a557833..86fb3b7 100644 --- a/fsharp-view-engine/src/FSharp.ViewEngine/FSharp.ViewEngine.fsproj +++ b/lib/src/FSharp.ViewEngine/FSharp.ViewEngine.fsproj @@ -1,7 +1,7 @@ - + - net10.0 + net8.0;net9.0;net10.0 FSharp.ViewEngine LICENSE README.md @@ -16,9 +16,10 @@ - - + + + - \ No newline at end of file + diff --git a/lib/src/FSharp.ViewEngine/Html.fs b/lib/src/FSharp.ViewEngine/Html.fs new file mode 100644 index 0000000..b8294a1 --- /dev/null +++ b/lib/src/FSharp.ViewEngine/Html.fs @@ -0,0 +1,226 @@ +namespace FSharp.ViewEngine + +open JetBrains.Annotations + +type Html = + static member val EmptyAttr = { Name = null; Value = ValueNone } with get + static member val empty = NoopElement() :> HtmlElement with get + static member raw ([]v: string) = RawElement(v) :> HtmlElement + static member js ([]v: string) = RawElement(v) :> HtmlElement + static member text (v: string) = TextElement(v) :> HtmlElement + static member val html = TagBuilder("html") with get + static member val head = TagBuilder("head") with get + static member title (value: string) = + let el = RegularElement("title") + el.AddChild(TextElement(value) :> HtmlElement) + el :> HtmlElement + static member val script = TagBuilder("script") with get + static member val body = TagBuilder("body") with get + static member val main = TagBuilder("main") with get + static member val header = TagBuilder("header") with get + static member val footer = TagBuilder("footer") with get + static member val nav = TagBuilder("nav") with get + static member val h1 = TagBuilder("h1") with get + static member val h2 = TagBuilder("h2") with get + static member val h3 = TagBuilder("h3") with get + static member val h4 = TagBuilder("h4") with get + static member val h5 = TagBuilder("h5") with get + static member val h6 = TagBuilder("h6") with get + static member val div = TagBuilder("div") with get + static member val p = TagBuilder("p") with get + static member val span = TagBuilder("span") with get + static member val a = TagBuilder("a") with get + static member val button = TagBuilder("button") with get + static member val code = TagBuilder("code") with get + static member val pre = TagBuilder("pre") with get + static member val ul = TagBuilder("ul") with get + static member val ol = TagBuilder("ol") with get + static member val li = TagBuilder("li") with get + static member val blockquote = TagBuilder("blockquote") with get + static member val article = TagBuilder("article") with get + static member val dialog = TagBuilder("dialog") with get + static member val time = TagBuilder("time") with get + static member val form = TagBuilder("form") with get + static member val label = TagBuilder("label") with get + static member val textarea = TagBuilder("textarea") with get + static member val select = TagBuilder("select") with get + static member val option = TagBuilder("option") with get + static member val table = TagBuilder("table") with get + static member val thead = TagBuilder("thead") with get + static member val tr = TagBuilder("tr") with get + static member val th = TagBuilder("th") with get + static member val tbody = TagBuilder("tbody") with get + static member val td = TagBuilder("td") with get + static member val dl = TagBuilder("dl") with get + static member val dt = TagBuilder("dt") with get + static member val dd = TagBuilder("dd") with get + static member val template = TagBuilder("template") with get + static member val iframe = TagBuilder("iframe") with get + static member val section = TagBuilder("section") with get + static member val aside = TagBuilder("aside") with get + static member val figure = TagBuilder("figure") with get + static member val figcaption = TagBuilder("figcaption") with get + static member val details = TagBuilder("details") with get + static member val summary = TagBuilder("summary") with get + static member val strong = TagBuilder("strong") with get + static member val em = TagBuilder("em") with get + static member val b = TagBuilder("b") with get + static member val i = TagBuilder("i") with get + static member val u = TagBuilder("u") with get + static member val s = TagBuilder("s") with get + static member val small = TagBuilder("small") with get + static member val mark = TagBuilder("mark") with get + static member val sub = TagBuilder("sub") with get + static member val sup = TagBuilder("sup") with get + static member val abbr = TagBuilder("abbr") with get + static member val cite = TagBuilder("cite") with get + static member val q = TagBuilder("q") with get + static member val dfn = TagBuilder("dfn") with get + static member val var = TagBuilder("var") with get + static member val samp = TagBuilder("samp") with get + static member val kbd = TagBuilder("kbd") with get + static member val ins = TagBuilder("ins") with get + static member val del = TagBuilder("del") with get + static member val address = TagBuilder("address") with get + static member val hgroup = TagBuilder("hgroup") with get + static member val search = TagBuilder("search") with get + static member val noscript = TagBuilder("noscript") with get + static member val slot = TagBuilder("slot") with get + static member val data = TagBuilder("data") with get + static member val video = TagBuilder("video") with get + static member val audio = TagBuilder("audio") with get + static member val picture = TagBuilder("picture") with get + static member val canvas = TagBuilder("canvas") with get + static member val object = TagBuilder("object") with get + static member val fieldset = TagBuilder("fieldset") with get + static member val legend = TagBuilder("legend") with get + static member val datalist = TagBuilder("datalist") with get + static member val output = TagBuilder("output") with get + static member val progress = TagBuilder("progress") with get + static member val meter = TagBuilder("meter") with get + static member val caption = TagBuilder("caption") with get + static member val colgroup = TagBuilder("colgroup") with get + static member val tfoot = TagBuilder("tfoot") with get + static member val map = TagBuilder("map") with get + static member val br = VoidElement("br") :> HtmlElement with get + static member val hr = VoidElement("hr") :> HtmlElement with get + static member val wbr = VoidElement("wbr") :> HtmlElement with get + static member val meta = VoidBuilder("meta") with get + static member val link = VoidBuilder("link") with get + static member val img = VoidBuilder("img") with get + static member val input = VoidBuilder("input") with get + static member val source = VoidBuilder("source") with get + static member val track = VoidBuilder("track") with get + static member val col = VoidBuilder("col") with get + static member val area = VoidBuilder("area") with get + static member val embed = VoidBuilder("embed") with get + + static member inline _id (v: string) = { Name = "id"; Value = ValueSome v } + static member inline _class (v: string) = { Name = "class"; Value = ValueSome v } + static member inline _class (v: string seq) = { Name = "class"; Value = ValueSome(v |> String.concat " ") } + static member inline _style (v: string) = { Name = "style"; Value = ValueSome v } + static member inline _lang (v: string) = { Name = "lang"; Value = ValueSome v } + static member inline _charset (v: string) = { Name = "charset"; Value = ValueSome v } + static member inline _name (v: string) = { Name = "name"; Value = ValueSome v } + static member inline _content (v: string) = { Name = "content"; Value = ValueSome v } + static member inline _href (v: string) = { Name = "href"; Value = ValueSome v } + static member inline _rel (v: string) = { Name = "rel"; Value = ValueSome v } + static member inline _src (v: string) = { Name = "src"; Value = ValueSome v } + static member inline _async (v: bool) = if v then { Name = "async"; Value = ValueNone } else Html.EmptyAttr + static member inline _defer (v: bool) = if v then { Name = "defer"; Value = ValueNone } else Html.EmptyAttr + static member inline _action (v: string) = { Name = "action"; Value = ValueSome v } + static member inline _method (v: string) = { Name = "method"; Value = ValueSome v } + static member inline _formmethod (v: string) = { Name = "formmethod"; Value = ValueSome v } + static member inline _type (v: string) = { Name = "type"; Value = ValueSome v } + static member inline _for (v: string) = { Name = "for"; Value = ValueSome v } + static member inline _rows (v: int) = { Name = "rows"; Value = ValueSome(string v) } + static member inline _cols (v: int) = { Name = "cols"; Value = ValueSome(string v) } + static member inline _data (attr: string, ?v: string) = + let key = $"data-{attr}" + match v with + | Some v -> { Name = key; Value = ValueSome v } + | None -> { Name = key; Value = ValueNone } + static member inline _datetime (v: string) = { Name = "datetime"; Value = ValueSome v } + static member inline _width (v: string) = { Name = "width"; Value = ValueSome v } + static member inline _height (v: string) = { Name = "height"; Value = ValueSome v } + static member inline _value (v: string) = { Name = "value"; Value = ValueSome v } + static member inline _hidden (v: bool) = if v then { Name = "hidden"; Value = ValueNone } else Html.EmptyAttr + static member inline _required (v: bool) = if v then { Name = "required"; Value = ValueNone } else Html.EmptyAttr + static member inline _disabled (v: bool) = if v then { Name = "disabled"; Value = ValueNone } else Html.EmptyAttr + static member inline _readonly (v: bool) = if v then { Name = "readonly"; Value = ValueNone } else Html.EmptyAttr + static member inline _multiple (v: bool) = if v then { Name = "multiple"; Value = ValueNone } else Html.EmptyAttr + static member inline _selected (v: bool) = if v then { Name = "selected"; Value = ValueNone } else Html.EmptyAttr + static member inline _min (v: string) = { Name = "min"; Value = ValueSome v } + static member inline _min (v: float) = { Name = "min"; Value = ValueSome(string v) } + static member inline _minlength (v: string) = { Name = "minlength"; Value = ValueSome v } + static member inline _minlength (v: int) = { Name = "minlength"; Value = ValueSome(string v) } + static member inline _max (v: string) = { Name = "max"; Value = ValueSome v } + static member inline _max (v: float) = { Name = "max"; Value = ValueSome(string v) } + static member inline _maxlength (v: string) = { Name = "maxlength"; Value = ValueSome v } + static member inline _maxlength (v: int) = { Name = "maxlength"; Value = ValueSome(string v) } + static member inline _step (v: string) = { Name = "step"; Value = ValueSome v } + static member inline _step (v: float) = { Name = "step"; Value = ValueSome(string v) } + static member inline _checked (v: bool) = if v then { Name = "checked"; Value = ValueNone } else Html.EmptyAttr + static member inline _role (v: string) = { Name = "role"; Value = ValueSome v } + static member inline _ariaLabelledby (v: string) = { Name = "aria-labelledby"; Value = ValueSome v } + static member inline _ariaDescribedby (v: string) = { Name = "aria-describedby"; Value = ValueSome v } + static member inline _ariaModal (v: string) = { Name = "aria-modal"; Value = ValueSome v } + static member inline _placeholder (v: string) = { Name = "placeholder"; Value = ValueSome v } + static member inline _autocomplete (v: string) = { Name = "autocomplete"; Value = ValueSome v } + static member inline _pattern (v: string) = { Name = "pattern"; Value = ValueSome v } + static member inline _accept (v: string) = { Name = "accept"; Value = ValueSome v } + static member inline _title (v: string) = { Name = "title"; Value = ValueSome v } + static member inline _wrap (v: string) = { Name = "wrap"; Value = ValueSome v } + static member inline _size (v: int) = { Name = "size"; Value = ValueSome(string v) } + static member inline _colspan (v: int) = { Name = "colspan"; Value = ValueSome(string v) } + static member inline _onload (v: string) = { Name = "onload"; Value = ValueSome v } + static member inline _crossorigin = { Name = "crossorigin"; Value = ValueNone } + static member inline _alt (v: string) = { Name = "alt"; Value = ValueSome v } + static member inline _target (v: string) = { Name = "target"; Value = ValueSome v } + static member inline _tabindex (v: int) = { Name = "tabindex"; Value = ValueSome(string v) } + static member inline _autofocus (v: bool) = if v then { Name = "autofocus"; Value = ValueNone } else Html.EmptyAttr + static member inline _open (v: bool) = if v then { Name = "open"; Value = ValueNone } else Html.EmptyAttr + static member inline _loading (v: string) = { Name = "loading"; Value = ValueSome v } + static member inline _srcset (v: string) = { Name = "srcset"; Value = ValueSome v } + static member inline _sandbox (v: string) = { Name = "sandbox"; Value = ValueSome v } + static member inline _allow (v: string) = { Name = "allow"; Value = ValueSome v } + static member inline _enctype (v: string) = { Name = "enctype"; Value = ValueSome v } + static member inline _novalidate (v: bool) = if v then { Name = "novalidate"; Value = ValueNone } else Html.EmptyAttr + static member inline _spellcheck (v: bool) = { Name = "spellcheck"; Value = ValueSome(if v then "true" else "false") } + static member inline _draggable (v: bool) = { Name = "draggable"; Value = ValueSome(if v then "true" else "false") } + static member inline _contenteditable (v: bool) = { Name = "contenteditable"; Value = ValueSome(if v then "true" else "false") } + static member inline _accesskey (v: string) = { Name = "accesskey"; Value = ValueSome v } + static member inline _dir (v: string) = { Name = "dir"; Value = ValueSome v } + static member inline _translate (v: bool) = { Name = "translate"; Value = ValueSome(if v then "yes" else "no") } + static member inline _inputmode (v: string) = { Name = "inputmode"; Value = ValueSome v } + static member inline _enterkeyhint (v: string) = { Name = "enterkeyhint"; Value = ValueSome v } + static member inline _list (v: string) = { Name = "list"; Value = ValueSome v } + static member inline _form (v: string) = { Name = "form"; Value = ValueSome v } + static member inline _formaction (v: string) = { Name = "formaction"; Value = ValueSome v } + static member inline _formenctype (v: string) = { Name = "formenctype"; Value = ValueSome v } + static member inline _formnovalidate (v: bool) = if v then { Name = "formnovalidate"; Value = ValueNone } else Html.EmptyAttr + static member inline _formtarget (v: string) = { Name = "formtarget"; Value = ValueSome v } + static member inline _ariaLabel (v: string) = { Name = "aria-label"; Value = ValueSome v } + static member inline _ariaHidden (v: string) = { Name = "aria-hidden"; Value = ValueSome v } + static member inline _ariaExpanded (v: string) = { Name = "aria-expanded"; Value = ValueSome v } + static member inline _ariaControls (v: string) = { Name = "aria-controls"; Value = ValueSome v } + static member inline _ariaLive (v: string) = { Name = "aria-live"; Value = ValueSome v } + static member inline _ariaCurrent (v: string) = { Name = "aria-current"; Value = ValueSome v } + static member inline _rowspan (v: int) = { Name = "rowspan"; Value = ValueSome(string v) } + static member inline _scope (v: string) = { Name = "scope"; Value = ValueSome v } + static member inline _headers (v: string) = { Name = "headers"; Value = ValueSome v } + static member inline _download (v: string) = { Name = "download"; Value = ValueSome v } + static member inline _download () = { Name = "download"; Value = ValueNone } + static member inline _referrerpolicy (v: string) = { Name = "referrerpolicy"; Value = ValueSome v } + static member inline _media (v: string) = { Name = "media"; Value = ValueSome v } + static member inline _sizes (v: string) = { Name = "sizes"; Value = ValueSome v } + static member inline _poster (v: string) = { Name = "poster"; Value = ValueSome v } + static member inline _controls (v: bool) = if v then { Name = "controls"; Value = ValueNone } else Html.EmptyAttr + static member inline _autoplay (v: bool) = if v then { Name = "autoplay"; Value = ValueNone } else Html.EmptyAttr + static member inline _loop (v: bool) = if v then { Name = "loop"; Value = ValueNone } else Html.EmptyAttr + static member inline _muted (v: bool) = if v then { Name = "muted"; Value = ValueNone } else Html.EmptyAttr + static member inline _preload (v: string) = { Name = "preload"; Value = ValueSome v } + static member inline _start (v: int) = { Name = "start"; Value = ValueSome(string v) } + static member inline _reversed (v: bool) = if v then { Name = "reversed"; Value = ValueNone } else Html.EmptyAttr + static member inline _cite (v: string) = { Name = "cite"; Value = ValueSome v } + diff --git a/lib/src/FSharp.ViewEngine/Htmx.fs b/lib/src/FSharp.ViewEngine/Htmx.fs new file mode 100644 index 0000000..96cd894 --- /dev/null +++ b/lib/src/FSharp.ViewEngine/Htmx.fs @@ -0,0 +1,17 @@ +namespace FSharp.ViewEngine + +type Htmx = + static member inline _hx (key: string, value: string) = { Name = $"hx-{key}"; Value = ValueSome value } + static member inline _hxGet (v: string) = { Name = "hx-get"; Value = ValueSome v } + static member inline _hxPost (v: string) = { Name = "hx-post"; Value = ValueSome v } + static member inline _hxDelete (v: string) = { Name = "hx-delete"; Value = ValueSome v } + static member inline _hxTrigger (v: string) = { Name = "hx-trigger"; Value = ValueSome v } + static member inline _hxTarget (v: string) = { Name = "hx-target"; Value = ValueSome v } + static member inline _hxIndicator (v: string) = { Name = "hx-indicator"; Value = ValueSome v } + static member inline _hxInclude (v: string) = { Name = "hx-include"; Value = ValueSome v } + static member inline _hxSwap (v: string) = { Name = "hx-swap"; Value = ValueSome v } + static member inline _hxSwapOOB (v: string) = { Name = "hx-swap-oob"; Value = ValueSome v } + static member inline _hxEncoding (v: string) = { Name = "hx-encoding"; Value = ValueSome v } + static member inline _hxOn (event: string, value: string) = { Name = $"hx-on:{event}"; Value = ValueSome value } + static member inline _hxHistory (v: string) = { Name = "hx-history"; Value = ValueSome v } + static member inline _hxVals (v: string) = { Name = "hx-vals"; Value = ValueSome v } diff --git a/lib/src/FSharp.ViewEngine/Svg.fs b/lib/src/FSharp.ViewEngine/Svg.fs new file mode 100644 index 0000000..c7b574a --- /dev/null +++ b/lib/src/FSharp.ViewEngine/Svg.fs @@ -0,0 +1,21 @@ +namespace FSharp.ViewEngine + +type Svg = + static member val svg = TagBuilder("svg") with get + static member val path = TagBuilder("path") with get + static member val circle = TagBuilder("circle") with get + static member inline _viewBox (v: string) = { Name = "viewBox"; Value = ValueSome v } + static member inline _width (v: int) = { Name = "width"; Value = ValueSome(string v) } + static member inline _height (v: int) = { Name = "height"; Value = ValueSome(string v) } + static member inline _fill (v: string) = { Name = "fill"; Value = ValueSome v } + static member inline _stroke (v: string) = { Name = "stroke"; Value = ValueSome v } + static member inline _strokeWidth (v: int) = { Name = "stroke-width"; Value = ValueSome(string v) } + static member inline _strokeLinecap (v: string) = { Name = "stroke-linecap"; Value = ValueSome v } + static member inline _strokeLinejoin (v: string) = { Name = "stroke-linejoin"; Value = ValueSome v } + static member inline _fillRule (v: string) = { Name = "fill-rule"; Value = ValueSome v } + static member inline _clipRule (v: string) = { Name = "clip-rule"; Value = ValueSome v } + static member inline _d (v: string) = { Name = "d"; Value = ValueSome v } + static member inline _cx (v: int) = { Name = "cx"; Value = ValueSome(string v) } + static member inline _cy (v: int) = { Name = "cy"; Value = ValueSome(string v) } + static member inline _r (v: int) = { Name = "r"; Value = ValueSome(string v) } + diff --git a/lib/src/FSharp.ViewEngine/Tailwind.fs b/lib/src/FSharp.ViewEngine/Tailwind.fs new file mode 100644 index 0000000..2a85895 --- /dev/null +++ b/lib/src/FSharp.ViewEngine/Tailwind.fs @@ -0,0 +1,25 @@ +namespace FSharp.ViewEngine + +type Tailwind = + static member val elAutocomplete = TagBuilder("el-autocomplete") with get + static member val elOptions = TagBuilder("el-options") with get + static member val elOption = TagBuilder("el-option") with get + static member val elSelect = TagBuilder("el-select") with get + static member val elSelectedContent = TagBuilder("el-selectedcontent") with get + static member val elDropdown = TagBuilder("el-dropdown") with get + static member val elMenu = TagBuilder("el-menu") with get + static member val elDialog = TagBuilder("el-dialog") with get + static member val elDialogBackdrop = TagBuilder("el-dialog-backdrop") with get + static member val elDialogPanel = TagBuilder("el-dialog-panel") with get + static member val elCommandPalette = TagBuilder("el-command-palette") with get + static member val elCommandList = TagBuilder("el-command-list") with get + static member val elCommandGroup = TagBuilder("el-command-group") with get + static member val elCommandPreview = TagBuilder("el-command-preview") with get + static member val elDefaults = TagBuilder("el-defaults") with get + static member val elNoResults = TagBuilder("el-no-results") with get + static member val elTabGroup = TagBuilder("el-tab-group") with get + static member val elTabList = TagBuilder("el-tab-list") with get + static member val elTabPanels = TagBuilder("el-tab-panels") with get + static member inline _popover = { Name = "popover"; Value = ValueNone } + static member inline _anchor (position: string) = { Name = "anchor"; Value = ValueSome position } + diff --git a/fsharp-view-engine/src/FSharp.ViewEngine/paket.references b/lib/src/FSharp.ViewEngine/paket.references similarity index 100% rename from fsharp-view-engine/src/FSharp.ViewEngine/paket.references rename to lib/src/FSharp.ViewEngine/paket.references diff --git a/fsharp-view-engine/src/Tests/Program.fs b/lib/src/Tests/Program.fs similarity index 100% rename from fsharp-view-engine/src/Tests/Program.fs rename to lib/src/Tests/Program.fs diff --git a/lib/src/Tests/Tests.fs b/lib/src/Tests/Tests.fs new file mode 100644 index 0000000..097b0d2 --- /dev/null +++ b/lib/src/Tests/Tests.fs @@ -0,0 +1,173 @@ +module Tests + +open FSharp.ViewEngine +open System.Text +open System.Web +open System.Text.Json +open System.Text.RegularExpressions +open Expecto +open type Html +open type Htmx +open type Alpine +open type Svg +open type Tailwind + +module String = + let replace (oldValue:string) (newValue:string) (s:string) = s.Replace(oldValue, newValue) + let clean (s:string) = Regex.Replace(s, @"\s{2,}|\r|\n|\r\n", "") + +module ViewEngineApi = + open type Html + open type Htmx + open type Alpine + open type Svg + open type Tailwind + + let buildDocument () = + html { + _lang "en" + head { + title "Test" + meta { _charset "utf-8" } + link { _href "/css/compiled.css"; _rel "stylesheet" } + } + body { + _class "bg-gray-50" + div { + _id "page" + _class [ "flex"; "flex-col" ] + h1 { _hxGet "/hello"; _hxTarget "#page"; "Hello" } + h1 { _hxGet "/world"; _hxTarget "#page"; "World" } + } + br + div { + _xShow "showContent" + h2 { "Content" } + p { "Some content" } + raw "

Some more content

" + pre { + _class "language-html" + code { + _class "language-html" + text "

Even more content

" + } + } + ul { + li { "One" } + li { "Two" } + } + a { + _href "https://github.com/meiermade/FSharp.ViewEngine" + _class "rounded-lg text-gray-800 font-semibold flex items-center gap-3 p-1" + svg { + _viewBox "0 0 24 24" + _class "h-6 w-6 fill-current" + path { + _fillRule "evenodd" + _clipRule "evenodd" + _d "M12 2C6.477 2 2 6.463 2 11.97c0 4.404 2.865 8.14 6.839 9.458.5.092.682-.216.682-.48 0-.236-.008-.864-.013-1.695-2.782.602-3.369-1.337-3.369-1.337-.454-1.151-1.11-1.458-1.11-1.458-.908-.618.069-.606.069-.606 1.003.07 1.531 1.027 1.531 1.027.892 1.524 2.341 1.084 2.91.828.092-.643.35-1.083.636-1.332-2.22-.251-4.555-1.107-4.555-4.927 0-1.088.39-1.979 1.029-2.675-.103-.252-.446-1.266.098-2.638 0 0 .84-.268 2.75 1.022A9.607 9.607 0 0 1 12 6.82c.85.004 1.705.114 2.504.336 1.909-1.29 2.747-1.022 2.747-1.022.546 1.372.202 2.386.1 2.638.64.696 1.028 1.587 1.028 2.675 0 3.83-2.339 4.673-4.566 4.92.359.307.678.915.678 1.846 0 1.332-.012 2.407-.012 2.734 0 .267.18.577.688.48 3.97-1.32 6.833-5.054 6.833-9.458C22 6.463 17.522 2 12 2Z" + } + } + raw "Documentation" + } + elSelect { + _name "status" + _value "active" + button { + _type "button" + elSelectedContent { "Active" } + } + elOptions { + _popover + elOption { _value "active"; "Active" } + elOption { _value "inactive"; "Inactive" } + elOption { _value "archived"; "Archived" } + } + } + } + } + } + +// language=HTML +let expectedHtml = """ + + + + Test + + + + +
+

Hello

+

World

+
+
+
+

Content

+

Some content

+

Some more content

+
+                
+                    <p>Even more content</p>
+                
+            
+
    +
  • One
  • +
  • Two
  • +
+ + + + + Documentation + + + + + Active + Inactive + Archived + + +
+ + +""" + +[] +let tests = + testList "ViewEngine Tests" [ + test "ViewEngine should render html document" { + let actual = ViewEngineApi.buildDocument() |> Render.toHtmlDocString + Expect.equal (String.clean actual) (String.clean expectedHtml) "Rendered HTML should match expected" + } + + test "HtmlEncode should match HttpUtility.HtmlEncode" { + let inputs = + [ + "" + "plain" + "" + "Tom & Jerry" + "\"quote\" and 'apostrophe'" + "accent \u00E9" + "emoji \U0001F600" + "mix <&> \u00E9 \U0001F600" + ] + + for input in inputs do + let expected = HttpUtility.HtmlEncode(input) + + let actual = + let sb = StringBuilder() + let el = TextElement(input) + el.Render(sb) + sb.ToString() + + Expect.equal actual expected $"HtmlEncode should match HttpUtility for input: {input}" + } + + ] diff --git a/fsharp-view-engine/src/Tests/Tests.fsproj b/lib/src/Tests/Tests.fsproj similarity index 87% rename from fsharp-view-engine/src/Tests/Tests.fsproj rename to lib/src/Tests/Tests.fsproj index 031f17a..fa5df33 100644 --- a/fsharp-view-engine/src/Tests/Tests.fsproj +++ b/lib/src/Tests/Tests.fsproj @@ -1,7 +1,7 @@ Exe - net10.0 + net8.0;net9.0;net10.0 false false @@ -14,4 +14,4 @@ - \ No newline at end of file + diff --git a/fsharp-view-engine/src/Tests/paket.references b/lib/src/Tests/paket.references similarity index 100% rename from fsharp-view-engine/src/Tests/paket.references rename to lib/src/Tests/paket.references diff --git a/pulumi/src/docker/image.ts b/pulumi/src/docker/image.ts index 319343e..5d0d510 100644 --- a/pulumi/src/docker/image.ts +++ b/pulumi/src/docker/image.ts @@ -9,7 +9,7 @@ export const image = new docker.Image(config.identifier, { tags: [pulumi.interpolate `${repo.repositoryUrl}:latest`], push: true, context: { - location: path.join(config.rootDir, 'fsharp-view-engine'), + location: path.join(config.rootDir, 'docs'), }, platforms: ['linux/arm64'], registries: [{