Repository navigation
fix: keep tag-like text in console and warnings - #2015
Conversation
Escape the text around URLs in linkExternalUrls, so warnings rendered through v-safe-html no longer lose text that looks like a tag. Rename link-external-urls to html-helpers, with named exports for escapeHtml, urlToAnchor and linkExternalUrls. Document the plain text rendering rules, and how the console and macro prompts handle HTML. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Pedro Lamas <pedrolamas@gmail.com>
Escape console messages without HTML tags, and only add command links to those. <none>, <x>, <y> and <z> are placeholders, not tags. Highlight search matches on the sanitized DOM fragment, in text nodes only, so HTML messages match quotes and ampersands too. v-safe-html now inserts a DOMPurify fragment with replaceChildren rather than re-parsing through innerHTML, and takes an optional search term as its argument. Document that console search highlights rather than filters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Pedro Lamas <pedrolamas@gmail.com>
Bundle size report (gzip)
120 chunks compared, 22 changed. Sizes are gzip, matching what nginx serves. |
v-safe-html now takes an HTML string or { html, search }, instead of
the search term as a dynamic argument. As an object value is new on
every render, the directive compares html and search field by field.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Pedro Lamas <pedrolamas@gmail.com>
There was a problem hiding this comment.
Note
Copilot was unable to complete this review at the selected tier due to model policy or plan restrictions.
🟡 Changes recommended
A critical security finding remains in src/directives/safe-html.ts where sanitized links can retain target="_blank" without enforcing rel="noopener noreferrer", leaving reverse-tabnabbing risk unresolved.
1 open issue
What changed in this PR
This PR is a follow-up to #2014 to prevent “tag-like” substrings (e.g. <none>, <x>) from disappearing in console output and warnings, and to make console search/highlighting work against rendered text nodes rather than raw HTML.
Changes:
- Consolidates escaping, URL linking, and HTML tag detection into
src/util/html-helpers.ts(and removes the legacylink-external-urlsmodule). - Updates
v-safe-htmlto sanitize into a DOM fragment and render viareplaceChildren, with optional search-term highlighting applied to text nodes only. - Adjusts console/warnings rendering and docs to preserve tag-like text, clarify search behavior, and improve search highlighting for HTML messages.
| File | Summary |
|---|---|
src/util/mark-text-nodes.ts |
Adds a helper to highlight matches by wrapping only text-node matches in <mark>. |
src/util/link-external-urls.ts |
Removed legacy URL-linking helper (superseded by html-helpers). |
src/util/html-helpers.ts |
Introduces shared HTML utilities (escapeHtml, urlToAnchor, linkExternalUrls, hasHtmlTags). |
src/util/__tests__/mark-text-nodes.spec.ts |
Adds jsdom unit tests for text-node-only highlighting behavior. |
src/util/__tests__/link-external-urls.spec.ts |
Removes tests tied to the deleted link-external-urls module. |
src/util/__tests__/html-helpers.spec.ts |
Adds unit tests covering escaping, URL linking, and HTML tag detection. |
src/directives/safe-html.ts |
Switches to DOMPurify fragment sanitization + replaceChildren; adds { html, search } binding support and integrates markTextNodes. Unresolved: preserves target="_blank" without ensuring rel="noopener noreferrer" (reverse-tabnabbing risk). |
src/directives/__tests__/safe-html.spec.ts |
Adds jsdom tests exercising DOMPurify sanitization, highlighting, and update-skipping logic. |
src/components/widgets/console/ConsoleItem.vue |
Updates console message escaping/tag-detection rules and passes { html, search } into v-safe-html. |
src/components/common/AppWarnings.vue |
Updates warnings URL-linking to use linkExternalUrls from html-helpers. |
docs/docs/features/macros.md |
Updates docs describing prompt <text> rendering behavior. |
docs/docs/features/console.md |
Clarifies console search behavior and documents HTML-output limitations/behavior. |
AGENTS.md |
Updates repository guidance to reflect the new helpers/directive behavior and console rendering rules. |
Review effort: Balanced
Treat a console response as HTML only when DOMPurify keeps an element in it, instead of matching any tag-like text. Klipper sends a whole help listing as one entry, so a single macro description such as "M163 [P<factor>]" made every line lose its command links, and its placeholders were dropped. Move the DOMPurify config into sanitize-html, shared by v-safe-html and the console, so the check and the rendering always agree. The <none>, <x>, <y> and <z> exemptions are no longer needed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Pedro Lamas <pedrolamas@gmail.com>
Force target="_blank" on any link in v-safe-html content whose href is absolute, and add noopener and noreferrer to the rel of every link that opens a new tab, so macro HTML can neither navigate Fluidd away nor skip the rel the project requires. Links without an href, such as console command links, and relative, hash, mailto: and tel: links keep opening in the same tab. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Pedro Lamas <pedrolamas@gmail.com>
There was a problem hiding this comment.
Note
Copilot was unable to complete this review at the selected tier due to model policy or plan restrictions.
🟡 Changes recommended
A critical active finding remains because src/util/sanitize-html.ts has noopener commented out so target="_blank" links only get rel="noreferrer", weakening reverse-tabnabbing protection and conflicting with the unit tests’ expected rel="noopener noreferrer".
1 open issue
1 resolved since last review
Review effort: Balanced
Restore the noopener rel token, which was committed commented out. Decide whether a link is external by resolving its href with new URL against a dummy base, instead of a regex on the raw value. The browser also treats backslash, tab and slash-less forms such as /\x.com or https:x.com as external, and those opened in the same tab without rel. Match links by localName, so SVG <a> elements are handled too, reading xlink:href when there is no href. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Pedro Lamas <pedrolamas@gmail.com>
There was a problem hiding this comment.
Note
Copilot was unable to complete this review at the selected tier due to model policy or plan restrictions.
🔵 Needs a closer look
This change alters global sanitization and HTML rendering behavior (DOMPurify hooks, v-safe-html fragment insertion, and automatic target/_blank handling) across multiple UI surfaces, so it is security-sensitive and warrants a brief human XSS/UX regression pass before approval.
No open issues 🎉
1 resolved since last review
Review effort: Balanced

Follow-up to #2014. Stops warnings and console messages losing text that looks like a tag, such as Klipper's
<none>or<x>placeholders, and fixes console search in HTML messages.Changes
linkExternalUrls: escapes the text around the URLs it links, soAppWarningskeeps tag-like text.link-external-urls.tsis renamed tohtml-helpers.ts, with named exports forescapeHtml,urlToAnchorandlinkExternalUrlsConsoleItem: aresponserenders as HTML, with no command links, only when DOMPurify keeps an element in it (hasRenderableHtml). Anything else is escaped and gets command links, so<none>,<x>,<temp>orUnknown algorithm <foo>stay visible. A/<[a-z]/ipre-check means most lines skip DOMPurify. Commands, actions and update logs are always escaped, as beforesanitize-html.ts: holds the DOMPurify config and its hooks, shared byv-safe-htmlandConsoleItem, so the check and the rendering always agreev-safe-htmlcontent: anhrefnaming its ownhttp(s)host now always opens in a new tab. This is decided by resolving it withnew URLagainst a dummy base, so forms the browser also treats as external, such as/\x.comorhttps:x.com, are caught. SVG<a>links are included, readingxlink:hrefwhen there is nohref. Every link opening a new tab getsnoopenerandnoreferreradded to itsrel. An author'starget="_blank"is still kept on relative links, which the markdown preview relies on for#headinglinksv-safe-html: inserts the sanitized fragment withreplaceChildren, instead of serializing it and re-parsing it throughinnerHTML. Its value is now an HTML string or{ html, search? }, and a search term is highlighted bymarkTextNodesin text nodes only. An object value is new on every render, so the directive compareshtmlandsearchfield by field and skips the update when both match{ html, search }tov-safe-html, so it can't hit tags, attributes or entities, and finds',"and&in HTML messages too. It was a regex over the HTML string beforeBehaviour changes
Klipper sends a whole
helplisting as one entry, so a single macro description such asM163 [P<factor>]used to make every line lose its command links. Such text no longer counts as HTMLA
responsewhose only tags are removed by DOMPurify, such as<script>, now shows them as literal text rather than as an empty lineEvery
v-safe-htmluser now goes through the fragment path, not only the consoleExternal links in macro HTML, markdown previews, notifications and locale strings open in a new tab even without
target="_blank", and always carryrel="noopener noreferrer". Links without anhref(console command links), and relative, hash,mailto:andtel:links, keep the same tabConsole search matches text inside a single element only, so a match can't span the
$prefix and a command link. The regex had the same limitNotes
<word>is still dropped, and a placeholder named like a real tag (<b>,<i>,<p>,<s>,<u>) makes the message HTMLTesting
pnpm run lint,type-check,test:unit(685 passed) andcircular-checkpass, and markdownlint passes on the docs. New specs coverhasRenderableHtml,sanitizeHtml(onerrorremoved, and the new-tab andrelrules),markTextNodes, and the directive running real DOMPurify in jsdom. No spec coversConsoleItemitself, so it was also tested manually in a browser.🤖 Generated with Claude Code