Notas de rodapé, pincel de formatação e maiúsculas e minúsculas
Três ferramentas para documentos longos que os outros editores comerciais deixam em planos pagos. Notas de rodapé com marcadores e notas que se renumeram sozinhos, um pincel de formatação que copia a formatação de caracteres de uma seleção para outra e a alteração de maiúsculas e minúsculas da seleção. As três vêm na licença padrão.
Sumário
Insere um bloco de sumário montado a partir dos títulos do documento, com links que levam até eles, e o mantém atualizado enquanto você escreve. Renomeie um título, adicione um, exclua outro, mova uma seção — a lista é derivada novamente do documento, então não tem como ficar desatualizada.
Isso não é a mesma coisa que o painel de estrutura do documento. Esse painel faz parte da interface do editor, serve para navegação e nunca aparece no HTML salvo. Um sumário faz parte do documento que você publica ou imprime, então ele sobrevive a getHTMLCode() e é lido corretamente sem nenhum JavaScript em execução. O artigo um bloco de sumário é conteúdo; um painel de estrutura não é explica por que errar nessa distinção produz um bloco de sumário desatualizado ou um painel inutilizável.
editor.insertTableOfContents(); // one per document
editor.updateTableOfContents(); // force a rebuild (normally automatic)
editor.hasTableOfContents(); // -> true | false
editor.removeTableOfContents();
editor.getTableOfContents();
// -> [{ id: "rte-h-3k1a0", level: 1, text: "Master Agreement", page: 1 }, ...]| Configuração | Padrão | O que faz |
|---|---|---|
| tocTitle | "Contents" | Título acima da lista. Defina como "" para omiti-lo. |
| tocMinLevel / tocMaxLevel | 1 / 3 | Níveis de título incluídos, inclusive os limites. O recuo acompanha o nível. |
| tocPageNumbers | "auto" | "auto" mostra os números de página quando a visualização de página está ativa, true sempre tenta mostrar, false nunca mostra. |
Números de página. Com o plugin de paginação carregado e a visualização de página ativa, cada entrada recebe a página em que seu título cai, com uma linha pontilhada de preenchimento, como em um sumário impresso. Os números vêm dos mesmos limites de página que a sobreposição da visualização de página desenha, então sempre batem com o que você vê. Fora da visualização de página nenhum número novo é calculado — um número de página seria uma ficção —, mas os números que já estão no bloco são mantidos, então carregar um documento salvo na visualização de página nunca os remove sem aviso. Volte à visualização de página para atualizá-los.
O bloco é contenteditable="false" e é gerado novamente a partir dos títulos, então não pode ser editado à mão — exclua-o como uma unidade e insira-o de novo para movê-lo. Títulos dentro de regiões geradas (o próprio bloco de sumário, a seção de notas de rodapé) são ignorados, então ele nunca lista o próprio título.
Notas de rodapé
Insere um marcador de referência numerado no cursor e uma nota correspondente em uma seção de notas de rodapé no fim do documento, e mantém os dois sincronizados. A numeração segue a ordem do documento: adicione uma nota de rodapé acima de uma existente, exclua um marcador ou mova um parágrafo, e todos os marcadores e notas são renumerados e a lista de notas é reordenada para corresponder. Excluir um marcador exclui sua nota, e remover o último marcador remove a seção inteira — nenhuma nota órfã fica para trás no HTML salvo.
<script src="richtexteditor/rte.js"></script>
<script src="richtexteditor/plugins/all_plugins.js"></script>
<script>
var editor = new RichTextEditor("#editor");
editor.insertFootnote(); // marker at the caret, caret moves into the note
editor.insertFootnote("Ibid., p. 42"); // ...or supply the note text up front
editor.getFootnotes();
// -> [{ id: "fn3k1a0", number: 1, text: "Ibid., p. 42" }]
editor.syncFootnotes(); // force a renumber (normally automatic)
editor.getFootnotesCss(); // the stylesheet, to reuse on your public page
</script>| Configuração | Padrão | O que faz |
|---|---|---|
| footnotesTitle | "Footnotes" | Título acima da seção de notas. Defina como "" para omiti-lo. |
| footnotesNumbering | "decimal" | Estilo de lista das notas: decimal, lower-roman, upper-roman, lower-alpha, upper-alpha. |
| footnotesMarkerPrefix | "" | Envolve o número do marcador, por exemplo "[" para exibir [1]. |
| footnotesMarkerSuffix | "" | A metade de fechamento do item acima, por exemplo "]". |
O que vai parar no seu HTML
Notas de rodapé são conteúdo, então, ao contrário da sobreposição da visualização de página, elas devem persistir em getHTMLCode(). O número é escrito como texto literal, e não desenhado por um contador CSS, então o HTML salvo é autossuficiente: publique-o, exporte-o ou exiba-o em uma página que nunca carrega a nossa folha de estilo, e a numeração continua lá e continua correta.
<p>The clause was struck out<sup class="rte-fn-ref" data-fn-id="fn3k1a0"
data-fn-number="1" id="fnref-fn3k1a0" contenteditable="false">1</sup>.</p>
<section class="rte-footnotes">
<h2 class="rte-fn-title">Footnotes</h2>
<ol class="rte-fn-list" data-numbering="decimal">
<li class="rte-fn-note" data-fn-id="fn3k1a0" data-fn-number="1" id="fn-fn3k1a0">Ibid., p. 42<a
class="rte-fn-back" href="#fnref-fn3k1a0" contenteditable="false">↩</a></li>
</ol>
</section>Os marcadores são contenteditable="false", então o cursor os trata como uma única unidade e não consegue parar dentro do número. Clicar em um marcador leva à sua nota; clicar no ↩ volta.
Pincel de formatação
Captura a formatação de caracteres no cursor e a aplica na próxima seleção. A formatação é lida do estilo computado, então não importa se a origem era <b>, style="font-weight:700" ou uma classe CSS. Ao aplicar, o pincel também remove a formatação de caracteres que já existe no destino — é isso que faz funcionar de verdade aplicar texto sem formatação sobre texto em negrito.
editor.copyFormatting(); // capture at the caret
editor.pasteFormatting(); // apply to the current selection
editor.toggleFormatPainter(); // one-shot: capture, then paint the next selection
editor.toggleFormatPainter(true); // sticky: keep painting until cancelled
editor.cancelFormatPainter(); // or press Escape
editor.isFormatPainterActive(); // -> true | false
editor.getCapturedFormatting(); // -> { "font-weight": "700", "color": "rgb(204, 0, 0)" }Só são levadas as propriedades que realmente diferem do padrão do editor, então uma aplicação gera um atributo style curto, e não uma muralha de valores padrão herdados. Aplicar de novo no mesmo trecho não aninha elementos envolventes. Quais propriedades são levadas pode ser configurado com formatPainterProperties; propriedades de bloco, como alinhamento e margens, ficam de fora de propósito, porque um pincel que refluísse o documento não é para isso que a ferramenta serve.
Maiúsculas e minúsculas
Reescreve os caracteres de fato na seleção — e não com um text-transform, que só parece certo até o HTML ser usado em outro lugar. A marcação inline é preservada: altere as maiúsculas e minúsculas de the <strong>Data</strong> Act e o <strong> continua lá depois. Sem nada selecionado, a operação vale para a palavra sob o cursor.
editor.changeCase("upper"); // THE DATA ACT
editor.changeCase("lower"); // the data act
editor.changeCase("title"); // The Data Act
editor.changeCase("sentence"); // The data act
editor.changeCase("toggle"); // tHE dATA aCT
editor.getChangeCaseModes(); // -> ["upper","lower","title","sentence","toggle"]O modo Title Case mantém palavras curtas (a, an, and, of, the, to…) em minúsculas, a menos que sejam a primeira ou a última, e aplica a mesma regra dentro de palavras compostas com hífen — assim state-of-the-art vira State-of-the-Art, e não State-Of-The-Art. Edite a lista com titleCaseSmallWords.
Autocorreção e tipografia
Corrige erros de digitação comuns enquanto você digita, coloca em maiúscula o início das frases e transforma aproximações digitadas nos caracteres reais: aspas retas viram aspas curvas, -- vira travessão, ... vira reticências, (c) vira ©, 1/2 vira ½.
editor.setAutocorrectEnabled(false); // off at runtime
editor.isAutocorrectEnabled();
editor.addAutocorrectRule("acheive", "achieve");
editor.getAutocorrectRules();
// Run the same typography normalisation over imported or pasted text:
editor.applyTypography('He said "hello" -- it is 1/2 done...');
// -> 'He said “hello” — it is ½ done…'| Configuração | Padrão | O que faz |
|---|---|---|
| autocorrectEnabled | true | Chave geral. |
| autocorrectSpelling | true | Tabela de erros de ortografia comuns (teh → the). |
| autocorrectCapitalize | true | Coloca em maiúscula a primeira palavra de uma frase. |
| autocorrectTypography | true | Aspas inteligentes, travessões, reticências, setas, símbolos, frações. |
| autocorrectReplacements | null | Regras extras, mescladas sobre as padrão. Mapeie uma palavra para ela mesma para desativar uma regra embutida. |
Nada é acionado dentro de código. <code>, <pre>, <kbd>, <samp> e <var> são totalmente ignorados, assim como qualquer subárvore marcada com data-rte-no-autocorrect. Curvar as aspas dentro de um exemplo de código o corrompe, e essa é a coisa mais danosa que um recurso como este pode fazer.
Pressione Backspace logo depois de uma correção para desfazê-la e manter exatamente o que você digitou — a mesma saída de emergência que o Word e o Google Docs oferecem. A lista de palavras embutida é conservadora de propósito: sequências que também são palavras reais (ill, lets, its) não são alteradas, porque errar o palpite em uma palavra real é pior do que deixar de fazer uma correção.
Campos de mesclagem
Marcadores de posição que representam dados, para que um documento vire um modelo que você renderiza com vários registros — mala direta, emissão de faturas, cartas personalizadas, contratos gerados. Cada campo é um chip atômico contenteditable="false", então nunca pode ser excluído pela metade, virando {{customer.na}} e deixando de ser resolvido sem aviso.
var editor = new RichTextEditor("#editor", {
mergeFields: [
{ id: "recipient.name", label: "Recipient name", sample: "Ada Lovelace" },
{ id: "invoice.total", label: "Invoice total", sample: "£1,240.00" },
{ id: "date.today", label: "Today's date", sample: function () { return new Date().toLocaleDateString(); } }
]
});
editor.insertMergeField("recipient.name");
editor.listMergeFields();
// -> [{ id: "recipient.name", label: "Recipient name", known: true, count: 1 }]
editor.setMergeFieldPreview(true); // show sample data instead of labels
editor.toggleMergeFieldPreview();
editor.isMergeFieldPreview();
// Render one record. Flat or nested data both work.
editor.renderMergeFields({ "recipient.name": "Grace Hopper", invoice: { total: "£90" } });
// -> HTML with the values substituted. The document itself is untouched.| Configuração | Padrão | O que faz |
|---|---|---|
| mergeFields | conjunto de exemplo | Definições { id, label, sample }. A lista incluída é um exemplo — substitua-a pelo seu próprio esquema. |
| mergeFieldPrefix / Suffix | "{{" / "}}" | Delimitadores desenhados em volta do rótulo para que um marcador de posição fique visualmente óbvio. |
| mergeFieldMissing | "placeholder" | O que renderMergeFields faz quando os dados não têm valor: deixa o marcador de posição visível, ou "empty" para removê-lo. |
A pré-visualização nunca chega ao HTML salvo. Este é o único ponto realmente perigoso do recurso: se ativar a pré-visualização e clicar em salvar gravasse “Ada Lovelace” no modelo onde antes estava {{Recipient name}}, o usuário destruiria o modelo só por olhar para ele. Por isso os serializadores são envolvidos — getHTMLCode, getJSON, getHTMLContent e getText restauram cada chip ao seu rótulo durante a chamada. Salve durante a pré-visualização e você ainda recebe o modelo de volta.
Pelo mesmo princípio, renderMergeFields() trabalha em um clone desanexado: gerar a saída mesclada nunca altera o documento em edição. Um campo sem dados correspondentes mantém seu marcador de posição em vez de ficar em branco, porque uma linha de fatura vazia sem aviso é pior do que uma obviamente não preenchida. Campos colados de um modelo criado em outro lugar são informados com known: false em vez de descartados, para que você possa dizer ao usuário o que os dados dele precisam fornecer.
Verificação de links
Encontra links quebrados, inseguros, invisíveis para tecnologias assistivas ou enganosos. O ponto importante: toda a parte offline funciona sem rede e sem nenhum serviço, porque a maioria dos defeitos reais em links pode ser determinada a partir do próprio documento.
editor.highlightLinkIssues(true); // mark problems in the document
editor.checkLinks().then(function (issues) {
// [{ element, href, text, severity: "error"|"warning", code, message }]
});
editor.getLinkIssues(); // the last run's results
editor.clearLinkHighlights();| Código | Gravidade | O que detecta |
|---|---|---|
| broken-anchor | error | Links para um #id que não existe mais — muito comum, porque mudar o texto de um título muda o id dele. |
| empty-href | error | Sem destino — inclusive um link cujo endereço foi removido por ser inseguro. |
| unsafe-scheme | error | javascript:, data:, vbscript:, file:. |
| empty-text | error | Sem texto, alt de imagem, aria-label ou title, então o link não tem nome acessível. |
| deceptive-text | warning | O texto mostra um nome de host e o link leva a outro — o formato típico de phishing, e também o que acontece quando alguém edita o texto visível, mas não o href. |
| ambiguous-text | warning | Vários links “Read more” apontando para lugares diferentes — idênticos quando listados pelo nome em um leitor de tela. |
| mixed-content | warning | Um link http:// em uma página segura. |
| unreachable | error | A URL remota não pôde ser acessada. Só aparece quando você fornece um resolver. |
A verificação de URLs remotas é opcional, e quem a faz é o seu código. Este editor não faz chamadas externas por conta própria, e um verificador de URLs embutido quebraria isso sem alarde — enviaria cada URL em que seus autores mexem para o servidor de outra pessoa. Por isso, verificar se as URLs respondem fica a cargo de um resolver que você fornece, no mesmo formato dos resolvers de IA, de verificação ortográfica e de chips:
new RichTextEditor("#editor", {
linkCheckResolver: function (urls) {
// urls: only http(s) links that passed the offline checks
return fetch("/api/check-links", {
method: "POST", body: JSON.stringify(urls)
}).then(function (r) { return r.json(); });
// -> { "https://example.com": { ok: false, status: 404 }, ... }
}
});Só são enviadas as URLs que passaram nas verificações offline, e nunca âncoras internas do documento nem links mailto:. Se o seu resolver lançar uma exceção, os resultados offline continuam sendo retornados. Para comparação, o Link Checker do TinyMCE é um plugin premium, e clientes que hospedam por conta própria precisam, além disso, implantar o serviço de verificação de links e apontar linkchecker_service_url para ele.
O destaque é interface, não conteúdo: data-rte-link-issue é removido em toda serialização, então marcar os problemas nunca muda o HTML que você salva.
Ordenação de tabelas e numeração de linhas
Ordene uma tabela pela coluna em que o cursor está e adicione uma coluna de numeração automática que se renumera depois de cada ordenação.
editor.sortTableAtCaret("asc"); // or "desc"
editor.sortTableByColumn(2, "desc");
// -> { ok: true, sortedBy: 2, direction: "desc", detectedType: "number" }
editor.canSortTable(); // -> { ok: false, reason: "...merged cells..." }
editor.addTableRowNumbers({ series: "upper-roman" });
editor.removeTableRowNumbers();
editor.hasTableRowNumbers();O tipo da coluna é detectado, não presumido. Uma coluna de preços é ordenada numericamente mesmo quando as células mostram $1,240.00 ou (500) (notação contábil para um valor negativo), e uma coluna de datas é ordenada cronologicamente, e não alfabeticamente. Ordenar uma coluna numérica como texto é o erro clássico que coloca 10 antes de 9. Células vazias vão para o fim nas duas direções, porque uma célula em branco significa “sem valor”, e não “o menor valor”. A ordenação é estável, então ordenar por uma segunda coluna preserva a ordem da primeira.
Ela se recusa a ordenar tabelas com células mescladas e diz por quê, em vez de ordená-las mal. Um rowspan perde o sentido quando as linhas se movem; destruir sem aviso uma tabela em que alguém passou uma hora trabalhando é muito pior do que se recusar a ordená-la. <thead> e <tfoot> nunca se movem, e uma primeira linha composta só de <th> é tratada como cabeçalho mesmo sem um <thead>. A numeração de linhas é posicional, então depois de uma ordenação a linha 1 é a que estiver no topo.
Caneta permanente
Ative uma formatação de caracteres fixa e tudo o que você digitar a partir daí a recebe — o marcador que você pega para anotar um documento em uma cor que obviamente não é a do texto original. Ao contrário do pincel de formatação, o que a caneta produz é conteúdo real e persiste no HTML salvo.
editor.setPermanentPen(true);
editor.togglePermanentPen();
editor.isPermanentPenActive();
editor.setPermanentPenStyle({ "color": "#0b6", "font-style": "italic" });
editor.getPermanentPenStyle();Um traço inteiro vai para um único span, que a caneta vai estendendo, em vez de um span por tecla. Mover o cursor encerra o traço, então o próximo trecho ganha o próprio span, em vez de o texto ser teletransportado para o anterior. Desativar a caneta tira o cursor do trecho — caso contrário, a digitação continuaria dentro do span estilizado e a caneta pareceria ignorar o fato de ter sido desativada.
Referências cruzadas
“Veja a cláusula 3.2”, “Figura 4”, “descrito em Escopo do Trabalho” — referências que continuam apontando para o lugar certo depois que o documento é editado em volta delas. É a peça que faz o restante das ferramentas de documento funcionar em conjunto: a numeração jurídica renumera as cláusulas e o bloco de sumário é derivado novamente, mas uma frase que diz “veja a cláusula 3.2” fica congelada, a menos que algo a atualize. Em um contrato, uma referência desatualizada muda o que o documento diz.
editor.listCrossReferenceTargets();
// -> [{ id, type: "heading"|"clause"|"footnote"|"table"|"figure", label, number }]
editor.insertCrossReference(targetId, "label"); // "clause 3.2" / "Table 2"
editor.insertCrossReference(targetId, "text"); // the heading's own text
editor.insertCrossReference(targetId, "number"); // "3.2"
editor.insertCrossReference(targetId, "page"); // page number, in page view
editor.insertCrossReference(targetId, "position");// "above" / "below"
editor.updateCrossReferences();
editor.getCrossReferences();Os números das cláusulas são derivados da posição na lista, do mesmo jeito que os contadores CSS que os exibem, então inserir uma cláusula acima renumera todas as referências abaixo dela. Referências a títulos acompanham um título renomeado. Tabelas e figuras são numeradas automaticamente, ou usam seu <caption> / <figcaption> quando têm um.
Uma referência cujo destino foi excluído falha de forma visível — ela exibe [reference not found] em vermelho, em vez de manter o último valor. O Word faz o mesmo, e pelo mesmo motivo: um número de cláusula plausível, mas errado, é muito mais perigoso do que um obviamente quebrado. O texto exibido é texto real no HTML salvo, então a referência é lida corretamente depois que o documento sai do editor.
Numeração de linhas na margem
Numera cada linha na margem esquerda, como um processador de texto faz em petições, leis, transcrições e tudo o que as pessoas citam por linha (“página 4, linhas 12–15”). As regras de protocolo judicial de várias jurisdições exigem linhas numeradas, e é por isso que editores de documentos têm esse recurso e editores para navegador em geral não têm.
editor.setLineNumbers(true);
editor.toggleLineNumbers();
editor.isLineNumbers();
editor.getLineCount(); // visual lines, not blocks
editor.setLineNumberOptions({
restart: "page", // "continuous" | "page" (needs page view) | "block"
interval: 5, // show every 5th number
start: 1,
gutter: 38 // gutter width in px
});Conta linhas visuais, não parágrafos. Um parágrafo que se quebra em quatro linhas recebe quatro números, porque é isso que “linha 12” significa para quem a cita. As caixas de linha são encontradas com Range.getClientRects(), então a contagem segue o layout realmente renderizado — mude a largura da janela e a numeração reflui junto com o texto.
Com restart: "page" e a visualização de página ativa, a numeração recomeça no topo de cada página, que é o que as regras de protocolo especificam. Assim como a própria sobreposição da visualização de página, a margem numerada é puramente visual: ela é contenteditable="false" e é removida em toda serialização, então o HTML que você salva é idêntico byte a byte com a numeração de linhas ativada ou desativada — incluindo o espaçamento da margem, que de outra forma vazaria para a área editável como um estilo inline.
Marcas de formatação
O botão ¶ do Word: marcas de parágrafo, contornos de blocos e — a parte realmente útil — destaque para caracteres que são invisíveis, mas não inofensivos. Espaços não separáveis, espaços de largura zero, hifens condicionais e marcas bidi chegam com o conteúdo colado e causam bugs de layout que ninguém consegue ver.
editor.setFormattingMarks(true);
editor.toggleFormattingMarks();
editor.isFormattingMarks();
// config
formattingMarkPilcrow: true, // ¶ at the end of each block
formattingMarkBlocks: true, // dashed outline around block elements
formattingMarkInvisibles: true // nbsp / zero-width / soft hyphen / bidiAs marcas de parágrafo e os contornos são desenhados inteiramente em CSS, então não podem alterar o documento de jeito nenhum. Caracteres invisíveis não podem ser alcançados pelo CSS, então eles recebem um elemento envolvente — mas todo elemento envolvente é removido em cada serialização e quando o recurso é desativado, e o caractere original é restaurado, não o glifo usado como rótulo. As marcas nunca são aplicadas dentro de <code> ou <pre>, onde o caractere pode ser intencional.
Justiça seja feita: neste caso há paridade, não vantagem. O TinyMCE inclui visualchars e visualblocks no núcleo gratuito, e o CKEditor tem show-blocks.
Marca d’água
Desenha DRAFT, CONFIDENTIAL ou o nome de um cliente atrás do conteúdo, para que o leitor perceba de relance que não deve tratar o documento como final ou que não deve distribuí-lo.
editor.setWatermark(true);
editor.toggleWatermark();
editor.isWatermark();
editor.setWatermarkOptions({
text: "CONFIDENTIAL",
mode: "tile", // "tile" | "single"
opacity: 0.18,
angle: -30,
fontSize: 48,
color: "#94a3b8",
print: false, // opt in to include it when printing / exporting PDF
image: null // data URI or URL; overrides the text
});Ela nunca faz parte do seu conteúdo. A marca é renderizada como um fundo SVG, então não pode ser selecionada, copiada nem encontrada por localizar e substituir, e é removida em toda serialização. Isso importa por mais do que organização: uma marca d’água gravada no documento sobreviveria em uma página publicada muito depois de o rascunho deixar de ser rascunho, e um “CONFIDENTIAL” como conteúdo é apagado com facilidade, enquanto uma marca na camada de renderização não é.
Imprimir é uma decisão separada da marcação na tela, então a marca d’água é excluída da impressão por padrão. Defina print: true para levá-la à saída impressa e à exportação em PDF.
Texto da direita para a esquerda e bidirecional
Árabe, hebraico, persa e urdu são escritos da direita para a esquerda. A direção pode ser definida por bloco ou para o documento inteiro, detectada a partir do conteúdo ou deixada a cargo do navegador com dir="auto".
editor.setTextDirection("rtl"); // the block(s) in the selection
editor.toggleTextDirection();
editor.getTextDirection(); // -> "rtl" | "ltr"
editor.setBaseDirection("rtl"); // the whole document
editor.getBaseDirection();
editor.detectTextDirection("123 שלום"); // -> "rtl" (first-strong)
editor.autoDetectTextDirection(); // stamp every block from its own text
// Mixed content: isolate a run so the surrounding direction cannot reorder it
editor.isolateBidi("https://example.com", "ltr"); // -> <bdi dir="ltr">…</bdi>
editor.insertIsolated("https://example.com", "ltr");A direção é conteúdo. Ao contrário da sobreposição da visualização de página ou da marca d’água, dir é gravado no documento e persiste em getHTMLCode() — um documento cuja direção sumisse ao salvar ficaria ilegível.
A detecção usa a regra do primeiro caractere forte (first-strong), o mesmo algoritmo que a especificação do HTML define para dir="auto": um parágrafo assume a direção do seu primeiro caractere com direcionalidade forte, ignorando dígitos e pontuação. É por isso que "123 שלום" fica da direita para a esquerda, enquanto "123 hello" fica da esquerda para a direita, e por isso um parágrafo só com números é deixado como está, em vez de receber um palpite.
Inverter a direção troca um alinhamento explícito. Definir dir só muda o alinhamento padrão, então um parágrafo definido explicitamente como text-align:left ficaria preso à margem esquerda em um bloco RTL e pareceria quebrado. O alinhamento físico explícito é espelhado, e removido por completo quando cai no lado natural da nova direção, para que o bloco passe a seguir dir dali em diante.
Conteúdo misto é isolado, não apenas marcado. Colocar uma URL LTR em uma frase RTL sem isolamento deixa o algoritmo bidi reordenar a pontuação em volta — é assim que (https://example.com) acaba sendo exibido com os parênteses nos lados errados. insertIsolated() envolve o trecho em <bdi> para que isso não aconteça.
Listas, citações em bloco e tabelas são invertidas via CSS, para que os marcadores de lista e as barras de citação fiquem na borda correta.
O próprio editor também é espelhado. A direção do documento é só metade do trabalho — um autor que escreve em árabe ou hebraico não deveria digitar texto da direita para a esquerda dentro de um aplicativo da esquerda para a direita. A barra de ferramentas, os menus, os menus suspensos, as caixas de diálogo e a barra de status são todos invertidos:
editor.setRtlUserInterface(true);
editor.toggleRtlUserInterface();
editor.isRtlUserInterface();
// config: rtlUserInterface — "auto" (follow the document), true, or falseA direção da interface é mantida estritamente separada da direção do conteúdo: espelhar a interface não grava nada no seu documento, e getHTMLCode() não muda. Com vários editores na mesma página, só o que você espelhar é afetado. Os ícones, de propósito, não são invertidos — o Word também não inverte os ícones da barra de ferramentas, e os glifos de negrito, itálico e alinhamento significam a mesma coisa nas duas direções. Só os que codificam a ordem de leitura (aumentar recuo, diminuir recuo, setas de menu) são espelhados.
Justiça seja feita: tanto o CKEditor quanto o TinyMCE incluem direção de texto básica nos núcleos gratuitos; a detecção automática, o isolamento bidi e o espelhamento da interface descritos aqui vão além desse básico, mas o básico em si não é algo que eles cobram.
Alça de arrasto e reordenação de blocos
Passe o mouse sobre qualquer parágrafo, título, lista, tabela ou destaque (callout) e uma alça ⠿ aparece ao lado — arraste-a para mover o bloco inteiro, com uma linha de inserção mostrando exatamente onde ele vai parar. É a interação de reordenação de blocos que o Notion tornou padrão, e um jeito muito melhor de mover uma cláusula do que recortar e colar.
// The drag is a thin layer over a scriptable API:
editor.moveBlockUp(); // block containing the caret
editor.moveBlockDown();
editor.moveBlock(el, beforeEl); // programmatic move
editor.setDragHandleEnabled(false); // hide the grip
// config: dragHandleEnabled, dragHandleKeyboardAlt+Shift+↑/↓ move o bloco em que está o cursor — as mesmas teclas que o Word usa para mover parágrafos. Não é um atalho extra: um recurso de reordenação que só funciona com o ponteiro é inutilizável pelo teclado, então o caminho pelo teclado é o que torna o recurso acessível. Nos dois caminhos, o cursor acompanha o bloco movido.
A alça e a linha de inserção ficam totalmente fora da área editável, então, por estrutura, não têm como aparecer no HTML salvo. O arrasto usa eventos de mouse simples em vez do arrastar e soltar nativo do HTML5, que dentro de contenteditable é o comportamento do navegador que funciona pela metade em todo lugar — ele inicia um arrasto de texto, desenha a imagem fantasma errada e solta HTML serializado em vez de mover o nó. Em um documento RTL, a alça aparece à direita, onde a linha começa.
Exportação para PDF com texto real
Existem dois exportadores de PDF, e a diferença importa. html2pdf renderiza o documento com o html2canvas e coloca uma imagem em cada página: fiel pixel a pixel, e a escolha certa quando o layout visual exato é o que precisa ser entregue. Mas o resultado é uma imagem de um documento — nada pode ser selecionado nem pesquisado, um leitor de tela encontra um arquivo vazio, os links são pintados em vez de clicáveis, e uma página de texto simples custa centenas de kilobytes.
exportpdf escreve diretamente no formato de arquivo PDF. O texto continua sendo texto. Ele usa as 14 fontes padrão do PDF, que não precisam ser incorporadas — isso elimina a criação de subconjuntos de fontes, a parte realmente difícil de um gerador de PDF, e mantém uma página típica abaixo de 15 KB. Nenhuma biblioteca é carregada e nada é enviado.
- Títulos, negrito/itálico/sublinhado/tachado, cor, tamanho da fonte, listas com marcadores e numeradas, tabelas com linhas de cabeçalho repetidas, citações em bloco, linhas horizontais e imagens
- Links viram anotações de link reais do PDF, e não texto sublinhado
- Tamanho da página, orientação e margens vêm da configuração de página do próprio documento, então o PDF corresponde à visualização de página e à exportação para Word
- Quebras de página explícitas são respeitadas, e um título nunca fica isolado no pé de uma página
- Título, autor e idioma do documento são gravados como metadados
Uma sutileza que vale conhecer: um visualizador posiciona o texto usando as métricas da própria fonte, então um gerador que quebra linhas com seus próprios números vai discordar dele e ultrapassar a margem. Este exportador mede cada caractere e grava essas larguras no PDF como /Widths, que um visualizador é obrigado a respeitar — assim, o layout e a renderização usam os mesmos números por construção. Verificado com o pdf.js (o parser que o Firefox inclui): o texto é extraído sem problemas, os links funcionam e nada ultrapassa a margem.
Com tags para acessibilidade
Texto selecionável é necessário, mas não suficiente. Um PDF sem tags ainda é reprovado em uma auditoria da Section 508 ou da EN 301 549: a tecnologia assistiva recebe um fluxo plano de palavras, sem títulos, sem estrutura de lista, sem semântica de tabela e sem ordem de leitura garantida. A exportação grava uma árvore de estrutura completa, então o documento pode ser navegado, e não apenas lido.
H1–H6para títulos,Ppara parágrafos,BlockQuotepara citaçõesL/LI/Lbl/LBody, para que um leitor de tela anuncie “lista, 3 itens” em vez de três parágrafos soltosTable/TR/TH/TDcom/Scopenas células de cabeçalho — a diferença entre ouvir “Receita, 1,240” e só “1,240”- Elementos
Figurelevam o/Alta partir do texto alternativo da imagem - Linhas horizontais, bordas de célula, a barra de citação e o cabeçalho/rodapé de página são marcados como artefatos, o que os remove da ordem de leitura — um cabeçalho repetido lido em voz alta em cada página é um dos achados de auditoria mais comuns
/MarkInfo,/StructTreeRoot, uma/ParentTree,/Lange/DisplayDocTitle, para que o visualizador anuncie o título do documento em vez do nome do arquivo
A árvore é verificada lendo-a de volta pelo pdf.js, do jeito que uma tecnologia assistiva faria, e não inspecionando os bytes: 25 asserções que cobrem cada papel, a ordem de leitura do documento e a ausência de artefatos na árvore de conteúdo.
editor.exportToPdf("quarterly-report");
const bytes = editor.getPdfBytes({ title: "Q3", author: "Finance" });
// config
pdfExportFont: "helvetica" | "times" | "courier"
pdfExportBaseFontSize: 11
pdfExportHeader: "Confidential"
pdfExportFooter: "Page {page} of {total}"Exportação real para .docx, no navegador
A importação já lia um pacote OOXML genuíno no lado do cliente. A exportação não: wordexport envolve o HTML em um contêiner no estilo MSO e o nomeia como .doc. O Word abre esse arquivo, mas ele não é um .docx — a ida e volta funciona mal, o Word reclama ao salvar e algumas políticas corporativas bloqueiam de cara HTML com extensão .doc. exportdocx grava um pacote OOXML real: um ZIP gerado com CompressionStream, o espelho do DecompressionStream que o importador usa. Sem biblioteca, sem upload.
Ele gera, de propósito, exatamente o que o importador lê de volta — as mesmas relações de hiperlink, ids de numeração, partes de notas de rodapé e comentários e convenções de revisão —, então HTML → .docx → HTML é uma ida e volta genuína, e não dois recursos que por acaso compartilham uma extensão de arquivo. Verificado nos dois sentidos: o editor importa de novo a própria saída (títulos, formatação inline, cor, URLs de links, listas aninhadas, tabelas, citações, notas de rodapé, alterações controladas e equações, tudo recuperado), e o próprio pacote é validado parte por parte — cada parte XML bem formada, cada r:id resolvido, cada numId definido e cada parte declarada em [Content_Types].xml.
Dois detalhes que quebram sem alarde arquivos .docx montados à mão, ambos tratados aqui: w:sz é expresso em meios pontos, então gravar pontos faz o documento inteiro aparecer com metade do tamanho; e um único caractere de controle em qualquer parte faz o Word rejeitar o arquivo de vez, em vez de ignorar o caractere.
await editor.exportToDocx("quarterly-report");
const blob = await editor.getDocxBlob({ title: "Q3", author: "Finance" });Sanitizador de conteúdo
“Nada é executado no editor” é o critério errado. O critério é nada é executado no aplicativo que renderiza o que o editor salvou — porque esse é o formato de toda implantação: um usuário escreve o conteúdo, ele é armazenado, e o navegador de outro usuário o renderiza.
O núcleo do editor já remove manipuladores de eventos inline e rejeita javascript: em links e em actions de formulários, e nenhuma de uma bateria de 20 cargas maliciosas foi executada dentro do editor. Mas três construções sobreviveram até a saída salva, e uma delas — <iframe srcdoc> — era executada assim que aquele HTML salvo era renderizado em um contêiner comum. O sanitizador fecha essa brecha.
- Lista de permissões, não lista de bloqueio — uma lista de bloqueio é a promessa de ter pensado em todo elemento que o HTML um dia vai ganhar. Elementos desconhecidos são desembrulhados, para que o texto que o usuário digitou dentro deles nunca se perca.
- É executado na entrada (definição de conteúdo, colar, soltar) e na saída. A saída é o caminho garantido: é o que é armazenado, funciona em uma cópia desanexada, então nunca atrapalha o cursor, e conteúdo anterior ao plugin é limpo no próximo salvamento.
- As URLs são verificadas depois que os caracteres de controle são removidos —
java	script:é interpretado comojavascript:pelos navegadores, então testar a string bruta é contornado com facilidade. styleé filtrado por declaração, e não por fragmento: apagarjavascript:deurl(javascript:f())deixariaurl(f()), que então seria lido como uma URL relativa inofensiva e passaria justamente pela verificação feita para pegá-lo.- Faz o parse novamente até o resultado se estabilizar, para neutralizar XSS por mutação — marcação que é inerte em um parse e perigosa depois que o serializador a reescreve.
- Links com
target="_blank"ganhamrel="noopener".
Incorporações continuam funcionando: <iframe> é mantido para origens https. Sem uma lista de hosts permitidos configurada, ele recebe um sandbox que permite o que uma incorporação de vídeo precisa e bloqueia navegação da janela principal, pop-ups, modais e formulários — um frame injetado não consegue manipular a página pai via script, mas, fora isso, conseguiria navegar a janela principal, o que é uma tática de phishing. Definir sanitizerAllowedIframeHosts é o controle mais forte.
Isto é defesa em profundidade, e não substitui a validação no servidor. Ele roda no navegador, e qualquer coisa que roda no navegador pode ser contornada por um cliente que simplesmente não a executa. Sanitize no servidor antes de armazenar e faça o escape na saída. Esta camada protege o caminho honesto e estreita o desonesto; ela não torna seguro renderizar HTML não confiável incondicionalmente.
contentSanitizer: true // default
sanitizerAllowedIframeHosts: ["www.youtube.com"]
sanitizerAllowStyleTags: false // default
sanitizerAllowTags: ["my-element"]
sanitizerAllowAttributes: ["my-attr"]
editor.sanitizeHtml(html) // clean a string yourself
editor.getSanitizerReport() // what has been stripped so farO CKEditor 5 restringe as notas de rodapé aos planos Essential, Professional e Custom, e coloca sumário, pincel de formatação, alteração de maiúsculas e minúsculas e campos de mesclagem em “selected CKEditor Plans”. O TinyMCE oferece cada um desses recursos como plugin premium, “only available for paid TinyMCE subscriptions” — com autocorreção e tipografia como dois plugins separados. Aqui todos fazem parte da licença perpétua de $129. (Justiça seja feita: a transformação automática de texto do CKEditor está no núcleo open source, então ali a autocorreção é paridade, e não uma lacuna.) Veja a comparação completa.