Skip to content

Markdown

Renderiza Markdown (inclusive a saída de uma IA token a token) como um web component independente de framework. O <r-markdown> é modelado no Streamdown da Vercel: enquanto o texto chega, ele fecha na hora o **bold, o `code, os links e a matemática $$ digitados pela metade, divide o documento em blocos e redesenha só o bloco que mudou, de modo que uma resposta longa nunca é reanalisada do início a cada token.

Blocos cercados com ```mermaid viram <r-mermaid>, a matemática vira <r-math>, e o código pode ser destacado com shiki; cada um deles é carregado sob demanda na primeira vez que o conteúdo precisa. A saída é sanitizada com DOMPurify.

Use quando exibir Markdown que você não controla por inteiro (respostas de chat, fluxos de LLM, comentários de usuários, documentação) e quiser streaming, suporte a código, diagramas e matemática, e HTML seguro, sem ter de ligar você mesmo um parser, um sanitizador e um destacador.

Início rápido

html
<r-markdown copy highlight content="# Olá ..."></r-markdown>
js
import 'ranui'; // ou a entrada independente:
import 'ranui/markdown';

A fonte é lida da propriedade content (a preferida: não é refletida, então transmitir uma resposta longa não agita o DOM), do atributo content ou do texto do próprio elemento:

js
const el = document.createElement('r-markdown');
el.setAttribute('caret', ''); // mostra um cursor piscando enquanto o texto chega
for await (const chunk of stream) {
  el.content += chunk; // só o último bloco é redesenhado
}
el.removeAttribute('caret');
container.append(el);

Streaming

mode="streaming" (o padrão) passa o texto primeiro pelo remend, o finalizador de markdown incompleto extraído do Streamdown: assim um **bold recebido pela metade sai em negrito em vez de mostrar asteriscos, [text](https://exa fica como texto simples até a URL fechar, um - não transforma o parágrafo anterior em título, e por aí vai. Para documentos já prontos, use mode="static" e pule essa passagem, renderizando tudo de uma vez.

html
<r-markdown caret content="*Ênfase* pela metade, `código em linha` e **negrito que ainda está chegando"></r-markdown>
  • cursor: caret mostra um piscando e caret="circle" um , depois do último bloco. Ele se esconde sozinho enquanto uma cerca de código continua aberta ou o último bloco é uma tabela.
  • cercas de código incompletas ficam em texto simples (sem lampejo de destaque, sem diagrama pela metade) até a cerca de fechamento chegar; nesse meio-tempo o contêiner carrega data-incomplete.

Blocos de código

Todo bloco de código ganha um cabeçalho com o nome da linguagem e, se você ativar, botões de copiar e baixar. Acrescente highlight para destacar a sintaxe com shiki (carregado sob demanda; as linguagens chegam conforme o uso; github-light / github-dark por padrão, seguindo o tema da página).

html
<r-markdown copy download line-numbers highlight></r-markdown>
<!-- escolha os temas: claro escuro -->
<r-markdown highlight="vitesse-light vitesse-dark"></r-markdown>

Mermaid e matemática

  • ```mermaid<r-mermaid> (com tela cheia; copy / download são repassados).
  • $$…$$, \[…\] e ```math<r-math> em bloco; \(…\) → em linha. O cifrão simples $…$ precisa ser ativado com inline-math, porque se confunde com valores em dinheiro.

Referência da API

Atributos

AtributoTipoPadrãoDescrição
contentstringFonte Markdown. A propriedade content tem precedência e não é refletida; na falta das duas, usa o texto do elemento.
mode'streaming' | 'static''streaming'streaming fecha o markdown incompleto e compara por bloco; static desenha o texto inteiro como está, de uma vez.
caretbooleano / 'circle'desligadoCursor piscando depois do último bloco (, ou com circle).
copybooleanodesligadoBotão de copiar nos blocos de código (repassado ao <r-mermaid> embutido).
downloadbooleanodesligadoBotão de baixar nos blocos de código (code.<ext> conforme a linguagem).
line-numbersbooleanodesligadoNúmeros de linha nos blocos de código.
highlightbooleano / nomes de tema "claro escuro"desligadoDestaque de sintaxe via shiki. Sozinho → github-light github-dark; um nome → os dois; dois nomes → claro / escuro.
inline-mathbooleanodesligadoTrata $…$ como matemática em linha (\(…\) sempre é).
link-targetstring'_blank'target dos links externos (com rel="noopener noreferrer" acrescentado). _self deixa os links intactos. As #âncoras da própria página nunca recebem.
theme'auto' | 'light' | 'dark''auto'Tema do destaque e dos diagramas. auto segue a página (.dark, [data-ran-theme], senão prefers-color-scheme).
sheetstringCSS extra injetado no shadow root.
label-*stringinglêsSobrescreve os rótulos dos controles: label-copy, label-download.

Apelidos de propriedade: content, mode, caret, copyable, downloadable, lineNumbers, highlight, inlineMath, linkTarget, theme, sheet.

Eventos

Todos os eventos borbulham e cruzam a fronteira do shadow DOM (composed).

EventodetailDispara quando
render{ blocks: number, changed: number }uma passagem de desenho mudou pelo menos um bloco
copied{ kind: 'code', language, code }um bloco de código foi copiado
download{ kind: 'code', language, filename }um bloco de código foi baixado
error{ message: string }a análise ou o desenho falhou (também aparece no lugar)

Parts CSS

PartDescrição
markdownO invólucro externo.
bodyO contêiner dos blocos.
blockCada bloco desenhado.
codeO contêiner de um bloco de código.
code-headerA barra de linguagem e ações de um bloco.
code-langO rótulo da linguagem.
code-actionsO grupo de botões de ação.
buttonCada botão de copiar ou baixar.
tableO invólucro de tabela com rolagem horizontal.
errorA caixa de erro (quando o desenho falha).
css
r-markdown::part(code) {
  border-radius: 8px;
}

Variáveis CSS

Sobrescreva no elemento (cada uma recai num token semântico e depois num valor literal): --ran-markdown-color, --ran-markdown-font-size, --ran-markdown-line-height, --ran-markdown-gap, --ran-markdown-heading-color, --ran-markdown-link-color, --ran-markdown-inline-code-bg, --ran-markdown-code-bg, --ran-markdown-code-border, --ran-markdown-code-radius, --ran-markdown-code-font-size, --ran-markdown-mono-font, --ran-markdown-blockquote-border, --ran-markdown-table-border, --ran-markdown-table-header-bg, --ran-markdown-caret, --ran-markdown-caret-color, --ran-markdown-button-color, --ran-markdown-error-color.

Notas

  • Carregado sob demanda: o pedaço do parser (marked + DOMPurify + remend) carrega no primeiro desenho; shiki, mermaid e Temml carregam só quando o conteúdo os usa. Aplicações que nunca desenham markdown não pagam nada.
  • Sanitizado: o HTML cru dentro do markdown passa pelo DOMPurify: scripts, manipuladores de eventos, URLs javascript:, <style>, formulários e iframes são removidos. As caixas de seleção de listas de tarefas sobrevivem.
  • A comparação por blocos os indexa por posição, então o estado do DOM dentro de blocos intocados (um diagrama aberto em tela cheia, uma tabela rolada) sobrevive às atualizações do streaming. O documento é lexado uma vez e cada bloco é desenhado a partir dos próprios tokens, de modo que uma definição de referência de link se resolve entre blocos ([text][id] num bloco, [id]: url em outro).
  • As notas de rodapé do GFM ([^1]) não têm suporte: o marked não tem tokenizador de notas de rodapé, então os marcadores saem como texto literal.
  • O shiki é resolvido a partir da sua própria instalação. A compilação ES deixa o import('shiki') intocado, então o seu empacotador o separa em pedaços e baixa só as gramáticas que as suas cercas de código usam. O shiki é uma dependência normal do ranui, então npm i ranui já o traz; não há nada a acrescentar.
  • IIFE independente: o dist/iife/markdown.iife.js não tem resolvedor, então embute mermaid, Temml e o pacote de linguagens web do shiki (uns 50 idiomas comuns). Para cobertura completa de linguagens e um download menor, prefira a entrada ES (ranui/markdown).

Publicado sob a licença MIT.