Decide Whether User Content Is Literal Text or Markup
Drafted and reviewed using AI assistance.
On this page
Before asking an agent to “display the note correctly,” choose what the note means. Should <b>Roadmap</b> appear as those exact characters, or should it produce a bold word? Both are understandable product intentions, but they require different rendering contracts.
For the fictional Fieldnote app in this example, the first version displays new plain notes literally. A separate formatting proposal stays undecided. Existing formatted notes keep their existing route instead of being silently reinterpreted as plain text.
Write the smallest useful content contract
The new read-only preview receives a record with three fields:
{"id":"N1","format":"plain","body":"<b>Roadmap</b>"}
body is already a JavaScript string after the application's normal record loading. This article starts at that in-memory boundary; it does not define a server protocol or change stored notes.
For this version, format="plain" means display the characters of body without treating them as HTML or Markdown. Do not trim them, decode entity-like sequences or apply a formatting parser first. The display may wrap a long line, but it keeps explicit line breaks and runs of spaces.
An empty string is an intentionally empty note. A null or missing body is unavailable data, which gets a different message. That distinction prevents a missing value from looking like a successfully loaded blank note.
Compare the two intentions on harmless inputs
The examples below are invented and contain no executable payloads. The last column describes desired appearances for a possible later formatting mode, not an implemented parser or an approved syntax.
| Input string | Selected first-version plain display | Possible later formatting intention |
|---|---|---|
<b>Roadmap</b> |
Show both tags and the word exactly | Show Roadmap as emphasized text, with a defined representation for that formatting |
Research & review |
Show the ampersand as typed | Show the same visible phrase |
Say "ready"; keep 'draft' |
Keep both kinds of quotation mark | Show the same visible phrase |
First line + LF + Second line |
Show two lines | Preserve a hard line break under the selected formatting grammar |
Research & review |
Show & as five literal characters |
Decide whether entity syntax has any meaning before promising a display |
**Roadmap** |
Show the asterisks | Decide whether Markdown-style emphasis is part of the format |
The first four rows provide the basic fixture: angle brackets, an ampersand, quotes and a newline. The extra rows catch an easy mistake: applying a decoder or Markdown step before assigning “plain” content would already change the meaning.
Do not implement both columns and choose by guessing from the note's characters. A person may deliberately write example HTML or Markdown in an ordinary note. The record's declared mode decides which route owns it.
Draw the input-to-display boundary
Loaded record
|
+-- format is plain, body is a string
| -> original string -> dedicated note-body element.textContent
| -> display style preserves whitespace and explicit line breaks
|
+-- format is plain, body is ""
| -> empty note-body + separate "No note yet." status
|
+-- format is plain, body is null/missing/not a string
| -> empty note-body + separate "Note unavailable." status
|
+-- format is formatted
| -> do not reinterpret body here
| -> show "Open this note in the existing formatted view."
| -> retain the existing formatted route and stored record
|
+-- any other format
-> empty note-body + "Unsupported note format."
The empty string is also a string; in an implementation, it is the empty subcase of the string branch. It is shown separately here because its surrounding status differs.
MDN's textContent documentation distinguishes assigning text from assigning HTML. It also notes that setting the property replaces the node's children. Use a dedicated content element so updating the note does not remove nearby buttons or status controls.
This choice is specific to an ordinary DOM text container. A URL destination, stylesheet, script, HTML attribute or another output context needs its own handling; this text assignment is not an application-wide security guarantee.
Give the agent a plain-text example
This small proposed renderer has no parser, network request or external dependency. note-body is an ordinary paragraph, and the separate status is an ordinary paragraph too.
<p id="note-status"></p>
<p id="note-body" style="white-space: pre-wrap; overflow-wrap: anywhere;"></p>
const bodyNode = document.getElementById("note-body");
const statusNode = document.getElementById("note-status");
function renderNote(record) {
bodyNode.textContent = "";
statusNode.textContent = "";
if (!record || record.format == null) {
statusNode.textContent = "Note unavailable.";
return;
}
if (record.format === "formatted") {
statusNode.textContent = "Open this note in the existing formatted view.";
return;
}
if (record.format !== "plain") {
statusNode.textContent = "Unsupported note format.";
return;
}
if (typeof record.body !== "string") {
statusNode.textContent = "Note unavailable.";
return;
}
bodyNode.textContent = record.body;
if (record.body === "") statusNode.textContent = "No note yet.";
}
For N1, the proposed result is the literal <b>Roadmap</b> string in the body. There is no child bold element created from that note. For a note containing one newline, the string retains that newline, and the chosen whitespace style asks the layout to show it. Visible wrapping and font appearance still need a browser check.
The example shows a message for an existing formatted note; it does not implement navigation to that view. The application should keep its existing way to open that record. Passing an old formatted note through this new renderer is not a format migration.
Keep the first delivery narrow
Build the new plain-note preview for the fictional Fieldnote app.
Use literal-note-cases.json and the supplied small renderer as the proposal.
First inspection target: Safari 26 on macOS 26, ordinary HTML paragraph
containers, no framework or third-party formatting library.
This target is selected for verification, not claimed as installed or tested.
Record its exact build before reporting browser results.
For format=plain and a string body, preserve the exact string in a dedicated
body element using textContent. Preserve explicit LF breaks and spaces in
layout. Do not parse HTML/Markdown or decode entity-like text first.
Keep status content and controls outside that replaceable body element.
An empty string has an empty body and separate "No note yet." status.
A null, missing or non-string plain body is "Note unavailable.".
An unsupported format is "Unsupported note format.".
For format=formatted, show the existing-view message and leave that record
and its existing display route unchanged. Do not migrate or rewrite it.
Use the six harmless string cases plus empty, null, missing and legacy-format
cases. Compare the body's textContent as well as its visible presentation.
Verify that switching records clears stale body/status content.
Keep persistence, links, URL/attribute handling and existing formatted
rendering out of scope. Return the change and actual observations.
Later formatting decision:
Identify the authoring representation and allowed formatting features first.
Define how unsupported content is handled, select appropriate parsing and
sanitization for that representation, and review its output contexts.
Do not add an HTML branch as a shortcut to finishing this first delivery.
The formatting questions are pending product and implementation work. A bold appearance in a mockup does not settle which language is accepted, how it is stored or what content the renderer should allow.
Check representation and appearance separately
These are proposed browser checks. The author has not run the DOM renderer or inspected a rendered page.
| Check | Expected | Actual observation | Status |
|---|---|---|---|
| Angle-bracket and Markdown examples | Body text matches input; no inferred formatting | NOT_RUN | |
| Ampersand, entity-like text and quotes | Characters remain unchanged | NOT_RUN | |
| LF and repeated spaces | Exact text retained; layout preserves intended breaks/spaces | NOT_RUN | |
| Empty versus null/missing | Empty-note status differs from unavailable-data status | NOT_RUN | |
| Existing formatted record | New body stays empty; existing-view message appears | NOT_RUN | |
| Switch from a long note to an empty or unavailable note | Previous text is cleared | NOT_RUN | |
| Update the body | Nearby controls remain intact | NOT_RUN |
Checking only a screenshot can miss a changed source string; checking only a string can miss collapsed line breaks. Give the agent both expectations and a clear format boundary to preserve.