From c26ae207e4911080ff8423708c7f8f6744f22460 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 05:05:10 +0000 Subject: [PATCH 1/4] perf: HtmlNode.serialize avoids per-indent string alloc; isVoidElement as module-level constant; HtmlDocument uses single StringBuilder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move void-element set to a module-level [] private module so it is computed once at startup instead of being re-created on every HtmlNode.ToString() call. - Replace `String(' ', indentation + plus) |> append` in the serialize `newLine` helper with `sb.Append(' ', indentation + plus)` which writes spaces directly into the StringBuilder without allocating an intermediate string. - Rewrite HtmlDocument.ToString() to use a single StringBuilder rather than `List.map … |> String.Concat`, avoiding the intermediate list allocation. 2980 tests pass (net8.0, -p:NuGetAudit=false). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- RELEASE_NOTES.md | 4 ++ src/FSharp.Data.Html.Core/HtmlNode.fs | 67 ++++++++++++++++----------- 2 files changed, 43 insertions(+), 28 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 41a94ebb0..9c897add3 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,9 @@ # Release Notes +## 8.1.12 - Apr 29 2026 + +- Performance: `HtmlNode.serialize` no longer allocates a temporary `string` on each newline/indentation step; uses `StringBuilder.Append(char, int)` overload directly. `isVoidElement` set is now computed once at module initialisation instead of being re-created on every `HtmlNode.ToString()` call. `HtmlDocument.ToString()` uses a single `StringBuilder` for the whole document instead of `List.map … |> String.Concat`. + ## 8.1.11 - Apr 22 2026 - Code: `HtmlParser` `EmitTag` removes dead code in the `else` branch (the expression `x.HasFormattedParent || x.IsFormattedTag` was always equivalent to `x.HasFormattedParent` since `x.IsFormattedTag` is always `false` in that branch). Uses `name` directly to avoid re-computing `CurrentTagName()` for formatted/script tag checks. Also removes redundant `.ToLowerInvariant()` calls in `IsFormattedTag` and `IsScriptTag` since tag names are already lowercased at read time. diff --git a/src/FSharp.Data.Html.Core/HtmlNode.fs b/src/FSharp.Data.Html.Core/HtmlNode.fs index 13485c6bb..5db6e8cef 100644 --- a/src/FSharp.Data.Html.Core/HtmlNode.fs +++ b/src/FSharp.Data.Html.Core/HtmlNode.fs @@ -7,6 +7,33 @@ open System.Text // -------------------------------------------------------------------------------------- +[] +module private HtmlNodeHelpers = + // Void elements per the HTML spec — stored as a module-level constant so the Set is not + // re-created on every HtmlNode.ToString() call. + let private htmlVoidElementSet = + Set.ofArray + [| "area" + "base" + "br" + "col" + "command" + "embed" + "hr" + "img" + "input" + "keygen" + "link" + "meta" + "param" + "source" + "track" + "wbr" |] + + let isVoidElement name = Set.contains name htmlVoidElementSet + +// -------------------------------------------------------------------------------------- + /// Represents an HTML attribute. The name is always normalized to lowercase /// /// Contains the primary types for the FSharp.Data package. @@ -92,28 +119,6 @@ type HtmlNode = static member NewCData content = HtmlCData(content) override x.ToString() = - let isVoidElement = - let set = - [| "area" - "base" - "br" - "col" - "command" - "embed" - "hr" - "img" - "input" - "keygen" - "link" - "meta" - "param" - "source" - "track" - "wbr" |] - |> Set.ofArray - - fun name -> Set.contains name set - let rec serialize (sb: StringBuilder) indentation canAddNewLine insidePre html = let append (str: string) = sb.Append str |> ignore @@ -124,7 +129,7 @@ type HtmlNode = let newLine plus = sb.AppendLine() |> ignore - String(' ', indentation + plus) |> append + sb.Append(' ', indentation + plus) |> ignore match html with | HtmlElement(name, attributes, elements) -> @@ -224,11 +229,17 @@ type HtmlDocument = override x.ToString() = match x with | HtmlDocument(docType, elements) -> - (if String.IsNullOrEmpty docType then - "" - else - "" + Environment.NewLine) - + (elements |> List.map (fun x -> x.ToString()) |> String.Concat) + let sb = StringBuilder() + + if not (String.IsNullOrEmpty docType) then + sb.Append(" ignore + sb.Append(docType) |> ignore + sb.AppendLine(">") |> ignore + + for element in elements do + sb.Append(element.ToString()) |> ignore + + sb.ToString() /// [] From 4252d1742e8d28f404391f8f5950fe457a430b6b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 29 Apr 2026 05:05:13 +0000 Subject: [PATCH 2/4] ci: trigger checks From 131ee82b535b27db867baa270591d21a290bfc1d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 05:06:43 +0000 Subject: [PATCH 3/4] fix: pin OpenTelemetry.Api >= 1.15.1 and GitHubActionsTestLogger 3.0.3 to resolve GHSA-g94r-2vxg-569j Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- paket.dependencies | 3 ++- paket.lock | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/paket.dependencies b/paket.dependencies index 7eccdc0ed..5a8cce897 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -55,7 +55,8 @@ group Test nuget NUnit3TestAdapter nuget FsUnit 4.2.0 nuget FsCheck 2.16.6 - nuget GitHubActionsTestLogger + nuget GitHubActionsTestLogger 3.0.3 + nuget OpenTelemetry.Api >= 1.15.1 group Benchmarks frameworks: net8.0 diff --git a/paket.lock b/paket.lock index 982d944ef..e180d2a28 100644 --- a/paket.lock +++ b/paket.lock @@ -440,7 +440,7 @@ NUGET FSharp.Core (>= 5.0.2) NETStandard.Library (>= 2.0.3) NUnit (>= 3.13.2 < 3.14) - GitHubActionsTestLogger (3.0.1) + GitHubActionsTestLogger (3.0.3) Microsoft.ApplicationInsights (3.0) Azure.Monitor.OpenTelemetry.Exporter (>= 1.6) Microsoft.Bcl.AsyncInterfaces (10.0.3) @@ -528,7 +528,7 @@ NUGET Microsoft.Extensions.Diagnostics.Abstractions (>= 8.0) Microsoft.Extensions.Logging.Configuration (>= 8.0) OpenTelemetry.Api.ProviderBuilderExtensions (>= 1.15) - OpenTelemetry.Api (1.15) + OpenTelemetry.Api (1.15.3) System.Diagnostics.DiagnosticSource (>= 10.0) OpenTelemetry.Api.ProviderBuilderExtensions (1.15) Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0) From 2bc0689ba1be1f9eba6818ea4371d93ac8db5fff Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 1 May 2026 05:06:44 +0000 Subject: [PATCH 4/4] ci: trigger checks