Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -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.
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