Skip to content

Commit 3bc4ce2

Browse files
authored
Merge branch 'main' into repo-assist/fix-json-writeto-culture-2026-04-28-09f2ea7bfb7db127
2 parents 6dcea2c + a615f06 commit 3bc4ce2

7 files changed

Lines changed: 519 additions & 111 deletions

File tree

.github/aw/actions-lock.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,20 @@
55
"version": "v6.0.2",
66
"sha": "de0fac2e4500dabe0009e67214ff5f5447ce83dd"
77
},
8+
"actions/github-script@v9": {
9+
"repo": "actions/github-script",
10+
"version": "v9",
11+
"sha": "3a2844b7e9c422d3c10d287c895573f7108da1b3"
12+
},
813
"actions/github-script@v9.0.0": {
914
"repo": "actions/github-script",
1015
"version": "v9.0.0",
1116
"sha": "d746ffe35508b1917358783b479e04febd2b8f71"
1217
},
13-
"github/gh-aw-actions/setup@v0.68.3": {
18+
"github/gh-aw-actions/setup@v0.71.3": {
1419
"repo": "github/gh-aw-actions/setup",
15-
"version": "v0.68.3",
16-
"sha": "ba90f2186d7ad780ec640f364005fa24e797b360"
20+
"version": "v0.71.3",
21+
"sha": "07c7335cd76c4d4d9f00dd7874f85ff55ed71f24"
1722
},
1823
"github/gh-aw/actions/setup@v0.68.7": {
1924
"repo": "github/gh-aw/actions/setup",

.github/workflows/repo-assist.lock.yml

Lines changed: 280 additions & 104 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/workflows/repo-assist.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ description: |
1414
Always polite, constructive, and mindful of the project's goals.
1515
1616
on:
17-
schedule: daily
17+
schedule: weekly
1818
workflow_dispatch:
1919
slash_command:
2020
name: repo-assist

paket.dependencies

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ group Test
5555
nuget NUnit3TestAdapter
5656
nuget FsUnit 4.2.0
5757
nuget FsCheck 2.16.6
58-
nuget GitHubActionsTestLogger
58+
nuget GitHubActionsTestLogger 3.0.3
59+
nuget OpenTelemetry.Api >= 1.15.1
5960

6061
group Benchmarks
6162
frameworks: net8.0

paket.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ NUGET
440440
FSharp.Core (>= 5.0.2)
441441
NETStandard.Library (>= 2.0.3)
442442
NUnit (>= 3.13.2 < 3.14)
443-
GitHubActionsTestLogger (3.0.1)
443+
GitHubActionsTestLogger (3.0.3)
444444
Microsoft.ApplicationInsights (3.0)
445445
Azure.Monitor.OpenTelemetry.Exporter (>= 1.6)
446446
Microsoft.Bcl.AsyncInterfaces (10.0.3)
@@ -528,7 +528,7 @@ NUGET
528528
Microsoft.Extensions.Diagnostics.Abstractions (>= 8.0)
529529
Microsoft.Extensions.Logging.Configuration (>= 8.0)
530530
OpenTelemetry.Api.ProviderBuilderExtensions (>= 1.15)
531-
OpenTelemetry.Api (1.15)
531+
OpenTelemetry.Api (1.15.3)
532532
System.Diagnostics.DiagnosticSource (>= 10.0)
533533
OpenTelemetry.Api.ProviderBuilderExtensions (1.15)
534534
Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0)

tests/FSharp.Data.Core.Tests/FSharp.Data.Core.Tests.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
<Compile Include="CsvParserProperties.fs" />
4141
<Compile Include="HtmlCharRefs.fs" />
4242
<Compile Include="HtmlParser.fs" />
43+
<Compile Include="HtmlNodeSerialization.fs" />
4344
<Compile Include="HtmlOperations.fs" />
4445
<Compile Include="HtmlAttributeExtensions.fs" />
4546
<Compile Include="HtmlCssSelectors.fs" />
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
module FSharp.Data.Tests.HtmlNodeSerialization
2+
3+
open NUnit.Framework
4+
open FsUnit
5+
open FSharp.Data
6+
7+
// ─────────────────────────────────────────────────────────────────────────────
8+
// HtmlNode.ToString() — leaf nodes
9+
// ─────────────────────────────────────────────────────────────────────────────
10+
11+
[<Test>]
12+
let ``HtmlNode.NewText serializes to its content`` () =
13+
HtmlNode.NewText "hello world" |> string |> should equal "hello world"
14+
15+
[<Test>]
16+
let ``HtmlNode.NewText with empty string serializes to empty`` () =
17+
HtmlNode.NewText "" |> string |> should equal ""
18+
19+
[<Test>]
20+
let ``HtmlNode.NewComment serializes to HTML comment`` () =
21+
HtmlNode.NewComment " a comment " |> string |> should equal "<!-- a comment -->"
22+
23+
[<Test>]
24+
let ``HtmlNode.NewCData serializes to CDATA section`` () =
25+
HtmlNode.NewCData "some <raw> content" |> string |> should equal "<![CDATA[some <raw> content]]>"
26+
27+
// ─────────────────────────────────────────────────────────────────────────────
28+
// HtmlNode.ToString() — void elements (should self-close with " />")
29+
// ─────────────────────────────────────────────────────────────────────────────
30+
31+
[<Test>]
32+
let ``Void element 'area' serializes as self-closing`` () =
33+
HtmlNode.NewElement "area" |> string |> should equal "<area />"
34+
35+
[<Test>]
36+
let ``Void element 'br' serializes as self-closing`` () =
37+
HtmlNode.NewElement "br" |> string |> should equal "<br />"
38+
39+
[<Test>]
40+
let ``Void element 'img' with attribute serializes as self-closing`` () =
41+
HtmlNode.NewElement("img", [ "src", "logo.png"; "alt", "Logo" ]) |> string
42+
|> should equal """<img src="logo.png" alt="Logo" />"""
43+
44+
[<Test>]
45+
let ``All standard void elements serialize as self-closing`` () =
46+
let voidNames =
47+
[ "area"
48+
"base"
49+
"br"
50+
"col"
51+
"command"
52+
"embed"
53+
"hr"
54+
"img"
55+
"input"
56+
"keygen"
57+
"link"
58+
"meta"
59+
"param"
60+
"source"
61+
"track"
62+
"wbr" ]
63+
64+
for name in voidNames do
65+
HtmlNode.NewElement name |> string |> should equal $"<{name} />"
66+
67+
// ─────────────────────────────────────────────────────────────────────────────
68+
// HtmlNode.ToString() — non-void elements
69+
// ─────────────────────────────────────────────────────────────────────────────
70+
71+
[<Test>]
72+
let ``Non-void element with no children uses opening and closing tags`` () =
73+
HtmlNode.NewElement "div" |> string |> should equal "<div></div>"
74+
75+
[<Test>]
76+
let ``Non-void element with text child serializes inline`` () =
77+
HtmlNode.NewElement("p", [], [ HtmlNode.NewText "Hello" ])
78+
|> string
79+
|> should equal "<p>Hello</p>"
80+
81+
[<Test>]
82+
let ``Non-void element with multiple text children serializes inline`` () =
83+
HtmlNode.NewElement("p", [], [ HtmlNode.NewText "foo"; HtmlNode.NewText "bar" ])
84+
|> string
85+
|> should equal "<p>foobar</p>"
86+
87+
[<Test>]
88+
let ``Non-void element with element children serializes with indentation`` () =
89+
let node =
90+
HtmlNode.NewElement("ul", [], [ HtmlNode.NewElement("li", [], [ HtmlNode.NewText "item" ]) ])
91+
92+
let result = node |> string |> fun s -> s.Replace("\r\n", "\n").Replace("\r", "\n")
93+
result |> should equal "<ul>\n <li>item</li>\n</ul>"
94+
95+
[<Test>]
96+
let ``Nested element children are indented at each level`` () =
97+
let inner = HtmlNode.NewElement("span", [], [ HtmlNode.NewText "text" ])
98+
let middle = HtmlNode.NewElement("p", [], [ inner ])
99+
let outer = HtmlNode.NewElement("div", [], [ middle ])
100+
let result = outer |> string |> fun s -> s.Replace("\r\n", "\n").Replace("\r", "\n")
101+
result |> should equal "<div>\n <p>\n <span>text</span>\n </p>\n</div>"
102+
103+
[<Test>]
104+
let ``Element with sibling element children each get their own indented line when not text-only`` () =
105+
// Elements that themselves have element children (not text-only) get their own line.
106+
let node =
107+
HtmlNode.NewElement(
108+
"div",
109+
[],
110+
[ HtmlNode.NewElement("ul", [], [ HtmlNode.NewElement("li", [], [ HtmlNode.NewText "a" ]) ])
111+
HtmlNode.NewElement("ul", [], [ HtmlNode.NewElement("li", [], [ HtmlNode.NewText "b" ]) ]) ]
112+
)
113+
114+
let result = node |> string |> fun s -> s.Replace("\r\n", "\n").Replace("\r", "\n")
115+
116+
// Each <ul> has element children so each gets its own line with indentation.
117+
result |> should startWith "<div>\n <ul>"
118+
result |> should contain "\n <ul>"
119+
120+
[<Test>]
121+
let ``Text-only sibling elements are serialized inline without separating newlines`` () =
122+
// <p> elements whose children are all text nodes are considered inline by the serializer.
123+
let node =
124+
HtmlNode.NewElement(
125+
"div",
126+
[],
127+
[ HtmlNode.NewElement("p", [], [ HtmlNode.NewText "first" ])
128+
HtmlNode.NewElement("p", [], [ HtmlNode.NewText "second" ]) ]
129+
)
130+
131+
let result = node |> string |> fun s -> s.Replace("\r\n", "\n").Replace("\r", "\n")
132+
// Text-only siblings appear on the same line (no newline inserted between them).
133+
result |> should equal "<div>\n <p>first</p><p>second</p>\n</div>"
134+
135+
// ─────────────────────────────────────────────────────────────────────────────
136+
// HtmlNode.ToString() — attributes
137+
// ─────────────────────────────────────────────────────────────────────────────
138+
139+
[<Test>]
140+
let ``Element with a single attribute serializes the attribute`` () =
141+
HtmlNode.NewElement("a", [ "href", "https://example.com" ])
142+
|> string
143+
|> should equal """<a href="https://example.com"></a>"""
144+
145+
[<Test>]
146+
let ``Element with multiple attributes serializes all attributes in order`` () =
147+
HtmlNode.NewElement("input", [ "type", "text"; "name", "q"; "value", "" ])
148+
|> string
149+
|> should equal """<input type="text" name="q" value="" />"""
150+
151+
[<Test>]
152+
let ``Attribute name is lowercased when created via HtmlAttribute.New`` () =
153+
HtmlNode.NewElement("div", [ "Class", "main" ])
154+
|> string
155+
|> should equal """<div class="main"></div>"""
156+
157+
[<Test>]
158+
let ``Element name is lowercased when created via NewElement`` () =
159+
HtmlNode.NewElement("DIV") |> string |> should equal "<div></div>"
160+
161+
// ─────────────────────────────────────────────────────────────────────────────
162+
// HtmlDocument.ToString()
163+
// ─────────────────────────────────────────────────────────────────────────────
164+
165+
[<Test>]
166+
let ``HtmlDocument with empty doctype serializes without DOCTYPE declaration`` () =
167+
let doc = HtmlDocument.New([ HtmlNode.NewElement "html" ])
168+
let result = doc.ToString()
169+
result |> should equal "<html></html>"
170+
171+
[<Test>]
172+
let ``HtmlDocument with doctype includes DOCTYPE declaration and newline`` () =
173+
let doc =
174+
HtmlDocument.New("html", [ HtmlNode.NewElement("html", [], [ HtmlNode.NewText "content" ]) ])
175+
176+
let result = doc.ToString() |> fun s -> s.Replace("\r\n", "\n").Replace("\r", "\n")
177+
result |> should startWith "<!DOCTYPE html>\n"
178+
result |> should contain "<html>content</html>"
179+
180+
[<Test>]
181+
let ``HtmlDocument with multiple root elements serializes all of them`` () =
182+
let doc =
183+
HtmlDocument.New([ HtmlNode.NewElement "head"; HtmlNode.NewElement "body" ])
184+
185+
let result = doc.ToString()
186+
result |> should contain "<head></head>"
187+
result |> should contain "<body></body>"
188+
189+
[<Test>]
190+
let ``HtmlDocument with empty element list produces empty string`` () =
191+
let doc = HtmlDocument.New([])
192+
doc.ToString() |> should equal ""
193+
194+
[<Test>]
195+
let ``HtmlDocument with void element inside body serializes self-closing`` () =
196+
let doc =
197+
HtmlDocument.New([ HtmlNode.NewElement("body", [], [ HtmlNode.NewElement "br" ]) ])
198+
199+
let result = doc.ToString() |> fun s -> s.Replace("\r\n", "\n").Replace("\r", "\n")
200+
result |> should contain "<br />"
201+
202+
// ─────────────────────────────────────────────────────────────────────────────
203+
// Round-trip: HtmlNode constructed manually then parsed back
204+
// ─────────────────────────────────────────────────────────────────────────────
205+
206+
[<Test>]
207+
let ``HtmlNode constructed with NewElement round-trips to same string`` () =
208+
let node =
209+
HtmlNode.NewElement(
210+
"ul",
211+
[],
212+
[ HtmlNode.NewElement("li", [], [ HtmlNode.NewText "alpha" ])
213+
HtmlNode.NewElement("li", [], [ HtmlNode.NewText "beta" ]) ]
214+
)
215+
216+
let serialized = node.ToString()
217+
let reparsed = HtmlDocument.Parse("<body>" + serialized + "</body>")
218+
219+
let uls =
220+
reparsed.Descendants() |> Seq.filter (fun n -> n.Name() = "ul") |> Seq.toList
221+
222+
uls.Length |> should equal 1
223+
let lis = uls.[0].Descendants() |> Seq.filter (fun n -> n.Name() = "li") |> Seq.toList
224+
lis.Length |> should equal 2
225+

0 commit comments

Comments
 (0)