Skip to content
6 changes: 5 additions & 1 deletion RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Release Notes

## 8.1.12 - Apr 28 2026
## 8.1.13 - May 08 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.12 - Apr 29 2026

- Fix: `JsonValue.WriteTo` now always uses `CultureInfo.InvariantCulture` when serializing `Number` (decimal) values, preventing invalid JSON output (e.g. `1,5` instead of `1.5`) when called with a `TextWriter` configured with a non-English culture.
- Perf: `JsonValue.WriteTo` no longer allocates an intermediate `System.String(' ', n)` per indentation level; spaces are written directly to the writer.
Expand Down
67 changes: 39 additions & 28 deletions src/FSharp.Data.Html.Core/HtmlNode.fs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,33 @@ open System.Text

// --------------------------------------------------------------------------------------

[<AutoOpen>]
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

// --------------------------------------------------------------------------------------

/// <summary>Represents an HTML attribute. The name is always normalized to lowercase</summary>
/// <namespacedoc>
/// <summary>Contains the primary types for the FSharp.Data package.</summary>
Expand Down Expand Up @@ -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

Expand All @@ -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) ->
Expand Down Expand Up @@ -224,11 +229,17 @@ type HtmlDocument =
override x.ToString() =
match x with
| HtmlDocument(docType, elements) ->
(if String.IsNullOrEmpty docType then
""
else
"<!DOCTYPE " + docType + ">" + Environment.NewLine)
+ (elements |> List.map (fun x -> x.ToString()) |> String.Concat)
let sb = StringBuilder()

if not (String.IsNullOrEmpty docType) then
sb.Append("<!DOCTYPE ") |> ignore
sb.Append(docType) |> ignore
sb.AppendLine(">") |> ignore

for element in elements do
sb.Append(element.ToString()) |> ignore

sb.ToString()

/// <exclude />
[<EditorBrowsableAttribute(EditorBrowsableState.Never)>]
Expand Down
Loading