Skip to content

fix: encode carriage returns so HTML helpers round-trip them#213

Merged
linkdata merged 2 commits into
mainfrom
fix/htmlio-carriage-return-roundtrip-202
Jul 22, 2026
Merged

fix: encode carriage returns so HTML helpers round-trip them#213
linkdata merged 2 commits into
mainfrom
fix/htmlio-carriage-return-roundtrip-202

Conversation

@linkdata

@linkdata linkdata commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Fixes #202

Problem

AppendAttrValue delegates to html.EscapeString, which does not escape a carriage return, so a multiline attribute value read back from the DOM has its \r turned into \n (browser input-stream preprocessing rewrites raw CR/CRLF to LF before tokenization). Encoding the CR as 
 makes getAttribute report it unchanged.

Fix

AppendAttrValue now encodes each \r as the numeric character reference 
 (via a small shared appendEscapeCR helper). The parser decodes it back to CR after preprocessing, so the value round-trips through getAttribute. Attr, AppendAttr, WriteHTMLTag, and WriteHTMLInput inherit the fix.

Why WriteHTMLInner (textarea/pre) is not changed

The issue also asked to encode CR in textarea/pre inner content, but that turns out to be wrong for both:

  • textareaHTMLTextAreaElement.value normalizes every CR and CRLF to LF (WHATWG textarea value normalization), and jaws.js sends elem.value back to the server (jaws.js#L157). So a carriage return in textarea content can never round-trip through the value the browser reports, regardless of &#13; encoding — <textarea>\n&#13;hello</textarea> gives textContent == "\rhello" but value == "\nhello".
  • prepre permits phrasing content including <script> and comments, where character references are not decoded (script-data / comment parsing). Rewriting every CR would corrupt trusted markup, e.g. //x\rdoThing() would become the literal //x&#13;doThing(), commenting out doThing().

So WriteHTMLInner writes innerHTML verbatim (unchanged behavior); the doc comment now explains the textarea/pre CR semantics and points callers who need CR retention to AppendAttrValue.

Tests

  • Exact-output assertions for the new &#13; attribute encoding (single CR and CRLF).
  • TestAppendAttrValue_DOMRoundTrip — models getAttribute (input-stream preprocessing + character-reference decoding via stdlib html.UnescapeString) and confirms the logical value is recovered.
  • TestWriteHTMLInner_TextareaValueNormalization — models HTMLTextAreaElement.value and pins the true contract: textarea content round-trips with CR/CRLF collapsed to LF.
  • Newline-sensitive table now asserts textarea/pre content is written verbatim, including a pre+<script> regression case guarding against CR encoding inside nested markup.

Dependency surface stays stdlib-only. Package coverage remains 100% under -race; go vet, gofumpt, staticcheck, golangci-lint, and gosec are clean.

linkdata added 2 commits July 22, 2026 21:09
html.EscapeString leaves a raw carriage return unescaped, and WriteHTMLInner
copied a caller's raw CR into textarea/pre content verbatim. Browser
input-stream preprocessing rewrites a raw CR (and CRLF) to LF before
tokenization, so the DOM attribute value or textarea/pre content differed from
the supplied logical value, breaking the documented leading-CR guarantee and
exact-name form bindings.

Encode each CR as the numeric character reference &#13; in AppendAttrValue and
in the newline-sensitive innerHTML path of WriteHTMLInner. The reference is
decoded back to CR after preprocessing, so carriage returns survive parsing.
Non-newline-sensitive elements still carry their trusted innerHTML verbatim.

Add DOM round-trip tests that model browser input-stream preprocessing and
character-reference decoding (via html.UnescapeString) alongside exact-output
assertions.

Fixes #202
Address review of the initial fix. Encoding carriage returns as &#13; in
WriteHTMLInner was wrong for both newline-sensitive elements:

- textarea: HTMLTextAreaElement.value normalizes every CR and CRLF to LF (the
  HTML standard's textarea value normalization), and jaws.js sends elem.value
  back to the server, so a carriage return in textarea content can never
  round-trip regardless of &#13; encoding.
- pre: pre permits phrasing content such as script and comments, where
  character references are not decoded, so rewriting every CR corrupts trusted
  markup (e.g. //x&#13;doThing() leaves doThing() commented out).

Revert WriteHTMLInner to write innerHTML verbatim and document why carriage
returns are not encoded there. The AppendAttrValue &#13; encoding stays, as
attribute values genuinely round-trip through getAttribute.

Replace the flawed DOM round-trip test (which modeled textContent) with one
that models HTMLTextAreaElement.value and pins the CR/CRLF-to-LF contract, and
add a pre regression case guarding against CR encoding inside nested markup.
@linkdata
linkdata merged commit 2d84b62 into main Jul 22, 2026
7 checks passed
@linkdata
linkdata deleted the fix/htmlio-carriage-return-roundtrip-202 branch July 22, 2026 19:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HTML helpers do not round-trip carriage returns

1 participant