Skip to content

feat(live_ui): :markdown_viewer renders markdown in format='rendered' (Tier 2 substrate gap)#145

Draft
ty13r wants to merge 1 commit into
mainfrom
claude/markdown-viewer-rendered-mode-2026-05-26
Draft

feat(live_ui): :markdown_viewer renders markdown in format='rendered' (Tier 2 substrate gap)#145
ty13r wants to merge 1 commit into
mainfrom
claude/markdown-viewer-rendered-mode-2026-05-26

Conversation

@ty13r
Copy link
Copy Markdown
Member

@ty13r ty13r commented May 26, 2026

What

Enhances LiveUi.Widgets.MarkdownViewer to actually render markdown when format == "rendered" (default). Previously the widget always wrapped :source in <pre> regardless of mode/format — meaning consumers couldn't get HTML-rendered markdown from the canonical kind.

Two formats now supported:

  • "rendered" (default): Earmark.as_html(source, escape: true)HtmlSanitizeEx.markdown_html/1 (two-layer sanitization; safe for untrusted input)
  • "raw": wraps source verbatim in <pre> (HTML-escaped)

Why

Application-side ariston-ui shipped a AristonUiWeb.OperatorV2.DocBody Phoenix.Component (in ariston-ui#567) to render markdown on /v2/doc/<id> because the canonical widget didn't render. Once this lands, that workaround can be retired in favor of the canonical kind.

Per ariston-ui orchestrator memory feedback_substrate_gap_co_maintain_extends_to_draft_authoring, this is a Tier 2 substrate-gap proposal — DRAFT, Pascal-review REQUIRED before merge.

Framework note discovered during implementation

LiveUi.Widget's render pipeline reserves :mode and drops it from the assigns passed to the wrapper module (see live_ui/widget.ex build_render_assigns/1:mode appears in the Map.drop list). A widget declaring attr :mode would silently always receive the attribute default. The original markdown_viewer had attr :mode, :string, default: "rendered" — dead code; the user-provided value was always dropped.

This PR uses :format instead of :mode to dodge the collision. A note in the moduledoc documents the framework reservation for future widget authors. The data-live-ui-mode HTML attribute was renamed to data-live-ui-format for consistency.

If :mode should be unreserved or the reservation should be policy, that's a separate framework-level discussion. Flagging for review.

Two-layer sanitization rationale

Earmark's escape: true escapes raw HTML in markdown plain text but doesn't strip dangerous URL schemes (e.g., javascript: in markdown link hrefs). HtmlSanitizeEx.markdown_html/1 strips those + any remaining unsafe tags. Both layers required.

The {:error, html, _errors} return from Earmark is treated as recoverable (Earmark returns this for valid-but-imperfect markdown like unclosed emphasis) — the html field is sanitized + returned. Not a hard error.

Deps

Added to packages/live_ui/mix.exs:

  • {:earmark, "~> 1.4"}
  • {:html_sanitize_ex, "~> 1.4"}

Consumers depending on live_ui inherit these transitively. Worth a heads-up in release notes; the rendered format is what makes them necessary.

Test plan

  • mix deps.get — pulls earmark + html_sanitize_ex + mochiweb
  • mix compile --warnings-as-errors (EXIT 0)
  • mix test packages/live_ui/test/live_ui/widgets/markdown_viewer_test.exs11 tests, 0 failures

Test coverage:

  • Widget metadata (name/family, registry presence)
  • "rendered" format: h1/h2/h3 headings, bold + italic, unordered lists, script-tag stripping, javascript: href stripping, empty source, default-when-omitted
  • "raw" format: source verbatim in <pre>, HTML-escapes embedded tags

Pascal-review checklist

  • Is :format the right attr name for the rendering-mode toggle? Alternatives: :viewer_format, :render_mode (with :mode reserved-as-framework noted in moduledoc), or unreserving :mode at the framework level.
  • Are earmark + html_sanitize_ex acceptable transitive deps for live_ui consumers? Or should the rendering be deferred to an adapter / external sanitizer the consumer provides?
  • Cross-renderer consistency: unified_ui / elm_ui / desktop_ui markdown_viewer widgets should probably honor the same :format API. Out of scope for this PR; flagging for follow-up.
  • Should the data-live-ui-modedata-live-ui-format HTML attr rename break any external CSS / JS selectors? (Old attr never carried real semantic info — always "rendered" regardless of input — so risk of breakage is low.)

After this lands

ariston-ui's AristonUiWeb.OperatorV2.DocBody Phoenix.Component can be retired in favor of <.markdown_viewer source={@doc.body_md} /> via the canonical IUR pipeline. Cross-link comment posted on ariston-ui#567 once this PR opens.

… (Tier 2 substrate gap proposal)

Earmark.as_html (escape: true) + HtmlSanitizeEx.markdown_html for the
rendered format; <pre> with HTML-escape for raw. Uses :format attr —
LiveUi.Widget reserves :mode in build_render_assigns/1's Map.drop list,
so :mode-named attrs are silently always-default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant