diff --git a/CHANGELOG.md b/CHANGELOG.md
index a94fa9b9..406ccec0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Changed
- Upgrade `react-components` to [23.2.0](https://github.com/cucumber/react-components/releases/tag/v23.2.0)
+### Fixed
+- Escape html comments in json ([#408](https://github.com/cucumber/html-formatter/pull/408))
+
## [21.13.0] - 2025-07-07
### Changed
- Upgrade `cucumber-messages` to [28.0.0](https://github.com/cucumber/messages/releases/tag/v28.0.0)
diff --git a/dotnet/Cucumber.HtmlFormatter/JsonInHtmlWriter.cs b/dotnet/Cucumber.HtmlFormatter/JsonInHtmlWriter.cs
index 407c9178..71958a38 100644
--- a/dotnet/Cucumber.HtmlFormatter/JsonInHtmlWriter.cs
+++ b/dotnet/Cucumber.HtmlFormatter/JsonInHtmlWriter.cs
@@ -33,7 +33,8 @@ public override void Write(char[] source, int offset, int length)
throw new ArgumentException("Cannot read past the end of the input source char array.");
var destination = PrepareBuffer();
- var flushAt = BUFFER_SIZE - 2;
+ // Largest write without boundary check is 4 bytes
+ var flushAt = BUFFER_SIZE - 4;
var written = 0;
for (var i = offset; i < offset + length; i++)
{
@@ -46,12 +47,19 @@ public override void Write(char[] source, int offset, int length)
written = 0;
}
- // Write with escapes
- if (c == '/')
+ // Replace < with \x3C
+ // https://html.spec.whatwg.org/multipage/scripting.html#restrictions-for-contents-of-script-elements
+ if (c == '<')
{
destination[written++] = '\\';
+ destination[written++] = 'x';
+ destination[written++] = '3';
+ destination[written++] = 'C';
+ }
+ else
+ {
+ destination[written++] = c;
}
- destination[written++] = c;
}
// Flush any remaining
if (written > 0)
@@ -66,7 +74,8 @@ public override async Task WriteAsync(char[] source, int offset, int length)
throw new ArgumentException("Cannot read past the end of the input source char array.");
var destination = PrepareBuffer();
- var flushAt = BUFFER_SIZE - 2;
+ // Largest write without boundary check is 4 bytes
+ var flushAt = BUFFER_SIZE - 4;
var written = 0;
for (var i = offset; i < offset + length; i++)
{
@@ -79,12 +88,19 @@ public override async Task WriteAsync(char[] source, int offset, int length)
written = 0;
}
- // Write with escapes
- if (c == '/')
+ // Replace < with \x3C
+ // https://html.spec.whatwg.org/multipage/scripting.html#restrictions-for-contents-of-script-elements
+ if (c == '<')
{
destination[written++] = '\\';
+ destination[written++] = 'x';
+ destination[written++] = '3';
+ destination[written++] = 'C';
+ }
+ else
+ {
+ destination[written++] = c;
}
- destination[written++] = c;
}
// Flush any remaining
if (written > 0)
diff --git a/dotnet/Cucumber.HtmlFormatterTest/JsonInHtmlWriterTests.cs b/dotnet/Cucumber.HtmlFormatterTest/JsonInHtmlWriterTests.cs
index 16a78de0..09a048f7 100644
--- a/dotnet/Cucumber.HtmlFormatterTest/JsonInHtmlWriterTests.cs
+++ b/dotnet/Cucumber.HtmlFormatterTest/JsonInHtmlWriterTests.cs
@@ -32,29 +32,29 @@ public async Task WritesAsync()
[TestMethod]
public void EscapesSingle()
{
- _writer.Write("/");
- Assert.AreEqual("\\/", Output());
+ _writer.Write("<");
+ Assert.AreEqual("\\x3C", Output());
}
[TestMethod]
public async Task EscapesSingleAsync()
{
- await _writer.WriteAsync("/");
- Assert.AreEqual("\\/", await OutputAsync());
+ await _writer.WriteAsync("<");
+ Assert.AreEqual("\\x3C", await OutputAsync());
}
[TestMethod]
public void EscapesMultiple()
{
_writer.Write("");
- Assert.AreEqual("<\\/script>");
- Assert.AreEqual("<\\/script>")]
));
string html = RenderAsHtml(envelope);
- Assert.IsTrue(html.Contains("window.CUCUMBER_MESSAGES = [{\"gherkinDocument\":{\"comments\":[{\"location\":{\"line\":0,\"column\":0},\"text\":\"<\\/script>")]
));
string html = await RenderAsHtmlAsync(envelope);
- Assert.IsTrue(html.Contains("window.CUCUMBER_MESSAGES = [{\"gherkinDocument\":{\"comments\":[{\"location\":{\"line\":0,\"column\":0},\"text\":\"<\\/script>");
- assertEquals("<\\/script>