Render a model’s answer as text instead of active markup

A report page shows generated answers with a link to the cited source. The renderer inserts both as HTML, so an answer containing an image handler becomes active markup and a javascript: source becomes a clickable script. Repair the text serializer and the DOM renderer.

Requires Node.js 22 or newer. The DOM cases need jsdom: run npm install jsdom in this folder first. Without it those cases report that they did not run. No server needed. To work in a container instead, open the folder with VS Code Dev Containers or GitHub Codespaces: .devcontainer/devcontainer.json uses the Dev Containers Node.js 24 image, pinned by digest.
1. Unzip all files into one folder.
2. In that folder, run: node --test exercise.test.mjs
3. Edit render-answer.mjs, then run the same command again.

The starter deliberately fails some tests. A successful repair passes every case, including cases that catch common wrong fixes. The browser runs this same cases.mjs with the same fixtures; locally Node runs your code directly. If you completed the optional first stage in the browser, Download my work adds your input as stage1-input.json and the tests include it. Only run code you wrote or reviewed.

1. Reproduce both sinks
Run the starter. escapeText returns hostile markup unchanged; renderAnswer’s innerHTML turns the same answer into an img element with an onerror handler, and turns a javascript: source into a clickable link.

2. Encode for serialized HTML
Repair escapeText with one replacement pass and a lookup table for the five characters. Repeated replacements in the wrong order double-encode the ampersands they introduce.

3. Use the DOM’s text interface
In renderAnswer, create the p element and set its textContent to the answer. textContent makes a text node, so no answer can create elements or attributes. Do not pass escapeText output to textContent: that would display the entities.

4. Allow only the documented links
Parse the source with URL.parse or new URL and compare whole fields: protocol, hostname, username, password and port. Set link.href to the parsed href, or add no link. The ordinary answer and the valid documentation link must still render.

SUPPLIED CONTRACT
escapeText(value) returns an HTML-escaped string for the text position of serialized HTML that is parsed later, such as an exported report: convert the value with String, then escape &, <, >, double quote and single quote as &amp;, &lt;, &gt;, &quot; and &#39;, preserving ordinary Unicode. renderAnswer(answer, sourceUrl) builds the card shown on the page and returns it: an article element containing a p element whose text is exactly the answer and, only when the source is allowed, an a element with the text Source whose href is the parsed URL. A source is allowed when the WHATWG URL parser accepts it as an absolute URL with scheme https:, host exactly docs.example.test, no username or password and no explicit port; otherwise the card has no link. The lab’s document supports createElement, createTextNode, textContent, innerHTML, setAttribute, href, append and insertAdjacentHTML. The cases replay your DOM writes in a real browser DOM and inspect the elements, attributes and text it built.

REFERENCE
reference.mjs contains an explained solution: escapeText turns <img src=x onerror="steal()"> into entity-escaped source, so parsing it in a text position creates visible text, and turns an existing &lt;admin&gt; into &amp;lt;admin&amp;gt; so the entity spelling stays literal. renderAnswer puts the answer in a text node, so the same hostile answer shows as text and the DOM holds no img element; a closing </p> in an answer stays text too. For the source, URL.parse applies the browser’s own rules: it strips leading and trailing spaces and control characters and removes every tab and newline, so java\tscript: and " javascript:" both parse as javascript: and fail the https: check; docs.example.test.attacker.invalid fails the exact host; :8443 fails the port; credentials fail. A quote in an allowed URL is percent-encoded by the parser and, set through the href property, stays inside one attribute. The documentation link renders with its parsed href.

SCOPE
The cases replay your DOM writes in a real DOM and inspect the structure; they do not open the page or click the link. Prefer the platform’s text interfaces and your framework’s normal escaping; rich HTML needs a maintained sanitizer (see the HTML sanitizer lab). This encoder is not for JavaScript, CSS or URL contexts, and it is not a SQL parameter binder, shell argument validator or authorization control. A Content Security Policy is a second layer, not a substitute for choosing the right sink.

NEXT TASK
Which other destinations can generated output reach after rendering, such as exports, links, tools or queries?

SOURCE
https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
