diff --git a/CLAUDE.md b/CLAUDE.md index 5bfee79..21723fd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -81,6 +81,7 @@ These make the declarative syntax work; check them before changing signatures: - Three render paths on every `HtmlItem`: `ToString(indent)`, `AppendTo(ref StringBuilder)`, and `WriteTo(ref TextWriter)` (the streaming path used by the web integration). New node types must implement all three. For `HtmlNode` the first two funnel into `WriteTo` (one buffer for the whole subtree — `ToString` builds a `StringBuilder` then calls `AppendTo`, which wraps a `StringWriter`); keep the three outputs byte-identical (pinned by `GeneralRenderingTests.WriteTo_Should_ProduceTheSameOutputAsToString`). Indentation is emitted via the non-allocating `Indentation` helper, not `new string(' ', n)`. - Lowest-allocation render: `node.WriteTo(IBufferWriter)` (extension in `HtmlNodeExtensions`) encodes UTF-8 straight into the buffer via `Utf8HtmlWriter`, streaming in pooled chunks — render allocation is O(1) regardless of page size (no large string, no LOH/Gen2). `CC.CSX.Web.HtmlResult` uses this against the response `PipeWriter`. The browser path is different: `BrowserApp.Refresh()` calls `view().ToString()` and hands the HTML string to the JS `morph`, so the browser benefits from the `ToString` fast path (not the `IBufferWriter` path). - `HtmlNode.Attributes`/`Children` backing lists are allocated lazily (leaf nodes allocate neither). Render and traversal read the internal `RawAttributes`/`RawChildren` (nullable) so they never force a list allocation; the public getters allocate on demand for mutation. +- HTML escaping is **on by default**: `HtmlTextNode` and `HtmlAttribute` values are escaped (`& < > " '`) by `HtmlEscape` (vectorized `SearchValues` scan with a verbatim fast path — no cost when there's nothing to escape). `Raw(string)`/`RawHtml` is the opt-out (writes verbatim); `"; + const string note = "a\"b&c<"; + var live = Render(Views.Escaped(user, note)); + var opt = Render(Views__Optimized.Escaped(user, note)); + Assert.Equal(live, opt); // plan escapes identically to live + Assert.Contains("<script>", live); // and it really is escaped + Assert.DoesNotContain("