Notes de bas de page, reproduction de la mise en forme et changement de casse
Trois outils pour les longs documents que les autres éditeurs commerciaux réservent à une offre payante. Des notes de bas de page dont les appels et les notes se renumérotent d’eux-mêmes, un outil de reproduction de la mise en forme qui copie la mise en forme des caractères d’une sélection à une autre, et le changement de casse de la sélection. Les trois sont inclus dans la licence standard.
Table des matières
Insère un bloc de sommaire construit à partir des titres du document, avec des liens qui y mènent, et le tient à jour pendant que vous écrivez. Renommez un titre, ajoutez-en un, supprimez-en un, déplacez une section — la liste se reconstruit à partir du document : elle ne peut donc pas se désynchroniser.
Ce n’est pas la même chose que le panneau de plan du document. Ce panneau fait partie de l’interface de l’éditeur, sert à la navigation et n’apparaît jamais dans votre HTML enregistré. Une table des matières fait partie du document que vous publiez ou imprimez : elle survit donc à getHTMLCode() et se lit correctement sans aucun JavaScript. Pourquoi se tromper sur cette distinction produit soit un sommaire périmé, soit un panneau inutilisable, c’est ce qu’explique un sommaire est du contenu ; un panneau de plan, non.
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 }, ...]| Configuration | Valeur par défaut | Ce qu’elle fait |
|---|---|---|
| tocTitle | "Contents" | Titre au-dessus de la liste. Définissez-le sur "" pour l’omettre. |
| tocMinLevel / tocMaxLevel | 1 / 3 | Niveaux de titre inclus, bornes comprises. Le retrait suit le niveau. |
| tocPageNumbers | "auto" | "auto" affiche les numéros de page lorsque l’affichage paginé est activé, true essaie toujours, false jamais. |
Numéros de page. Avec le plugin de pagination chargé et l’affichage paginé activé, chaque entrée reçoit la page sur laquelle tombe son titre, avec des points de conduite comme dans un sommaire imprimé. Les numéros proviennent des mêmes limites de page que celles que dessine la surcouche de l’affichage paginé : ils concordent donc toujours avec ce que vous voyez. Hors de l’affichage paginé, aucun nouveau numéro n’est calculé — un numéro de page serait une fiction —, mais les numéros déjà présents dans le bloc sont conservés, si bien que le chargement d’un document enregistré en affichage paginé ne les supprime jamais silencieusement. Revenez à l’affichage paginé pour les actualiser.
Le bloc est en contenteditable="false" et il est régénéré à partir des titres : il n’est donc pas modifiable à la main — supprimez-le d’un bloc et réinsérez-le pour le déplacer. Les titres situés dans des zones générées (le bloc de sommaire lui-même, la section des notes de bas de page) sont ignorés : le sommaire ne liste donc jamais son propre titre.
Notes de bas de page
Insère un appel de note numéroté à l’emplacement du curseur et une note correspondante dans une section de notes à la fin du document, puis maintient les deux en accord. La numérotation suit l'ordre du document : ajoutez une note au-dessus d’une note existante, supprimez un appel ou déplacez un paragraphe, et chaque appel et chaque note se renumérote, la liste des notes se triant de nouveau en conséquence. Supprimer un appel supprime sa note, et retirer le dernier appel retire toute la section — aucune note orpheline ne reste dans le HTML enregistré.
<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>| Configuration | Valeur par défaut | Ce qu’elle fait |
|---|---|---|
| footnotesTitle | "Footnotes" | Titre au-dessus de la section des notes. Définissez-le sur "" pour l’omettre. |
| footnotesNumbering | "decimal" | Style de liste des notes : decimal, lower-roman, upper-roman, lower-alpha, upper-alpha. |
| footnotesMarkerPrefix | "" | Entoure le numéro de l’appel, par exemple "[" pour afficher [1]. |
| footnotesMarkerSuffix | "" | La partie fermante de l’option ci-dessus, par exemple "]". |
Ce qui arrive dans votre HTML
Les notes de bas de page sont du contenu : contrairement à la surcouche de l’affichage paginé, elles sont faites pour persister dans getHTMLCode(). Le numéro est écrit sous forme de texte littéral plutôt que dessiné par un compteur CSS : le HTML enregistré est donc autonome. Publiez-le, exportez-le ou affichez-le sur une page qui ne charge jamais notre feuille de style, et la numérotation est toujours là, et toujours correcte.
<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>Les appels sont en contenteditable="false" : le curseur les traite comme une unité et ne peut pas se placer à l’intérieur du numéro. Cliquer sur un appel mène à sa note ; cliquer sur le ↩ ramène à l’appel.
Reproduction de la mise en forme
Récupère la mise en forme des caractères à l’emplacement du curseur et l’applique à la sélection suivante. La mise en forme est lue à partir du style calculé : peu importe que la source soit un <b>, un style="font-weight:700" ou une classe CSS. L’application supprime aussi la mise en forme des caractères déjà présente dans la cible — c’est ce qui permet réellement d’appliquer un texte sans mise en forme sur du texte en gras.
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)" }Seules les propriétés qui diffèrent réellement de la base de l’éditeur sont transportées : une application produit donc un attribut style court plutôt qu’une avalanche de valeurs par défaut héritées. Appliquer de nouveau la mise en forme au même passage n’imbrique pas d’enveloppes. Les propriétés transportées se configurent via formatPainterProperties ; les propriétés de niveau bloc, comme l’alignement et les marges, sont volontairement exclues, car un outil qui remettrait en page le document ne répondrait pas à sa finalité.
Changement de casse
Réécrit les caractères réels de la sélection — et non un text-transform, qui ne fait illusion que jusqu’à ce que le HTML soit utilisé ailleurs. Le balisage en ligne survit : changez la casse de the <strong>Data</strong> Act et le <strong> est toujours là ensuite. Sans sélection, l’outil agit sur le mot sous le curseur.
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"]Title Case laisse en minuscules les petits mots (a, an, and, of, the, to…) sauf s’ils sont en première ou en dernière position, et applique la même règle à l’intérieur des mots composés avec trait d’union — state-of-the-art devient donc State-of-the-Art, et non State-Of-The-Art. Modifiez la liste avec titleCaseSmallWords.
Correction automatique et typographie
Corrige les fautes de frappe courantes au fil de la saisie, met une majuscule au début des phrases et transforme les approximations tapées en vrais caractères : les guillemets droits deviennent courbes, -- devient un tiret cadratin, ... des points de suspension, (c) devient ©, 1/2 devient ½.
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…'| Configuration | Valeur par défaut | Ce qu’elle fait |
|---|---|---|
| autocorrectEnabled | true | Interrupteur général. |
| autocorrectSpelling | true | Table des fautes d’orthographe courantes (teh → the). |
| autocorrectCapitalize | true | Met une majuscule au premier mot d’une phrase. |
| autocorrectTypography | true | Guillemets typographiques, tirets, points de suspension, flèches, symboles, fractions. |
| autocorrectReplacements | null | Règles supplémentaires, fusionnées par-dessus les valeurs par défaut. Associez un mot à lui-même pour désactiver une règle intégrée. |
Rien ne se déclenche dans du code. <code>, <pre>, <kbd>, <samp> et <var> sont entièrement ignorés, de même que tout sous-arbre marqué data-rte-no-autocorrect. Transformer les guillemets d’un exemple de code le corrompt, et c’est la chose la plus dommageable qu’une fonctionnalité comme celle-ci puisse faire.
Appuyez sur Retour arrière juste après une correction pour l’annuler et garder exactement ce que vous avez tapé — la même porte de sortie que celle de Word et de Google Docs. La liste de mots intégrée est volontairement prudente : les chaînes qui sont aussi de vrais mots (ill, lets, its) ne sont pas touchées, car se tromper sur un vrai mot est pire que manquer une correction.
Champs de fusion
Des espaces réservés qui remplacent des données, pour qu’un document devienne un modèle que vous générez pour de nombreux enregistrements — publipostage, séries de factures, lettres personnalisées, contrats générés. Chaque champ est une pastille atomique en contenteditable="false" : il ne peut donc jamais être à moitié supprimé en {{customer.na}} et cesser silencieusement d’être résolu.
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.| Configuration | Valeur par défaut | Ce qu’elle fait |
|---|---|---|
| mergeFields | jeu d’exemple | Définitions { id, label, sample }. La liste fournie est un exemple — remplacez-la par votre propre schéma. |
| mergeFieldPrefix / Suffix | "{{" / "}}" | Délimiteurs dessinés autour du libellé pour qu’un espace réservé soit visuellement évident. |
| mergeFieldMissing | "placeholder" | Ce que fait renderMergeFields lorsque les données n’ont pas de valeur : laisser l’espace réservé visible, ou "empty" pour le supprimer. |
L’aperçu n’atteint jamais votre HTML enregistré. C’est le seul vrai danger de cette fonctionnalité : si activer l’aperçu puis enregistrer écrivait « Ada Lovelace » dans le modèle à la place de {{Recipient name}}, l’utilisateur détruirait son modèle rien qu’en le regardant. Les sérialiseurs sont donc enveloppés — getHTMLCode, getJSON, getHTMLContent et getText remettent tous chaque pastille à son libellé le temps de l’appel. Enregistrez pendant l’aperçu, et vous récupérez toujours le modèle.
Selon le même principe, renderMergeFields() travaille sur un clone détaché : produire le résultat fusionné ne modifie jamais le document en cours d’édition. Un champ sans donnée correspondante conserve son espace réservé au lieu d’être rendu vide, car une ligne de facture vide sans que personne ne s’en aperçoive est pire qu’une ligne visiblement non remplie. Les champs collés depuis un modèle construit ailleurs sont signalés avec known: false au lieu d’être abandonnés : vous pouvez ainsi indiquer à l’utilisateur ce que ses données doivent fournir.
Vérification des liens
Repère les liens cassés, dangereux, invisibles pour les technologies d’assistance ou carrément trompeurs. L’essentiel : toute la partie hors ligne ne nécessite ni réseau ni service pour fonctionner, car la plupart des défauts réels des liens peuvent être déterminés à partir du document lui-même.
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();| Code | Gravité | Ce qu’il détecte |
|---|---|---|
| broken-anchor | error | Des liens vers un #id qui n’existe plus — très courant, car renommer un titre change son id. |
| empty-href | error | Aucune destination — y compris un lien dont l’adresse a été supprimée parce qu’elle était dangereuse. |
| unsafe-scheme | error | javascript:, data:, vbscript:, file:. |
| empty-text | error | Aucun texte, texte alternatif d’image, aria-label ni title : le lien n’a donc pas de nom accessible. |
| deceptive-text | warning | Le texte affiche un nom d’hôte et le lien mène à un autre — la forme typique du phishing, et aussi ce qui arrive quand quelqu’un modifie le texte visible mais pas le href. |
| ambiguous-text | warning | Plusieurs liens « Read more » qui pointent vers des endroits différents — identiques lorsqu’ils sont listés par nom dans un lecteur d’écran. |
| mixed-content | warning | Un lien http:// sur une page sécurisée. |
| unreachable | error | L’URL distante n’a pas pu être résolue. Produit uniquement lorsque vous fournissez un résolveur. |
La vérification des URL distantes est à activer explicitement, et c’est votre code qui l’effectue. Cet éditeur n’effectue aucun appel sortant de lui-même, et un vérificateur d’URL intégré romprait discrètement cet engagement — il enverrait chaque URL touchée par vos auteurs au serveur de quelqu’un d’autre. L’accessibilité des URL est donc déléguée à un résolveur que vous fournissez, de la même forme que les résolveurs d’IA, de correction orthographique et de pastilles :
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 }, ... }
}
});Seules les URL qui ont passé les vérifications hors ligne sont envoyées, et jamais les ancres internes au document ni les liens mailto:. Si votre résolveur lève une exception, les résultats hors ligne sont quand même renvoyés. À titre de comparaison, le Link Checker de TinyMCE est un plugin premium, et les clients auto-hébergés doivent en plus déployer son service de vérification des liens et faire pointer linkchecker_service_url vers lui.
La mise en évidence relève de l’interface, pas du contenu : data-rte-link-issue est supprimé autour de chaque sérialisation, si bien que le marquage des problèmes ne modifie jamais le HTML que vous enregistrez.
Tri des tableaux et numérotation des lignes
Triez un tableau selon la colonne où se trouve le curseur, et ajoutez une colonne de numérotation automatique qui se renumérote après chaque tri.
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();Le type de colonne est détecté, pas supposé. Une colonne de prix est triée numériquement même quand les cellules contiennent $1,240.00 ou (500) (notation comptable d’un nombre négatif), et une colonne de dates est triée chronologiquement plutôt qu’alphabétiquement. Trier une colonne numérique comme du texte est l’erreur classique qui place 10 avant 9. Les cellules vides descendent en bas dans les deux sens, car un blanc signifie « aucune valeur », et non « la plus petite valeur ». Le tri est stable : trier selon une seconde colonne préserve l’ordre de la première.
Il refuse les tableaux à cellules fusionnées et explique pourquoi, au lieu de mal les trier. Un rowspan n’a plus de sens une fois les lignes déplacées ; mettre silencieusement en pièces un tableau sur lequel quelqu’un a passé une heure est bien pire que de refuser de le trier. <thead> et <tfoot> ne bougent jamais, et une première ligne composée uniquement de <th> est traitée comme un en-tête, même sans <thead>. La numérotation des lignes est positionnelle : après un tri, la ligne 1 est celle qui se trouve désormais en haut.
Stylo permanent
Activez un format de caractères fixe, et tout ce que vous tapez ensuite le reçoit — le marqueur que l’on prend pour annoter un document dans une couleur qui ne se confond pas avec le texte d’origine. Contrairement à la reproduction de la mise en forme, ce que produit le stylo est du vrai contenu et persiste dans votre HTML enregistré.
editor.setPermanentPen(true);
editor.togglePermanentPen();
editor.isPermanentPenActive();
editor.setPermanentPenStyle({ "color": "#0b6", "font-style": "italic" });
editor.getPermanentPenStyle();Tout un trait va dans un seul span, que le stylo continue d’étendre, plutôt qu’un span par frappe. Déplacer le curseur termine le trait : le passage suivant reçoit alors son propre span, au lieu que du texte se téléporte dans le précédent. Désactiver le stylo fait sortir le curseur du passage — sinon la saisie continuerait dans le span stylé, et le stylo semblerait ignorer sa désactivation.
Renvois
« Voir la clause 3.2 », « Figure 4 », « décrit dans Scope of Work » — des renvois qui pointent toujours vers la bonne cible une fois le document modifié autour d’eux. C’est la pièce qui assure la cohérence du reste des outils documentaires : la numérotation juridique renumérote les clauses et le sommaire se reconstruit, mais une phrase qui dit « voir la clause 3.2 » reste figée si rien ne la met à jour. Dans un contrat, un renvoi périmé change ce que dit le document.
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();Les numéros de clause sont déduits de la position dans la liste, de la même façon que les compteurs CSS qui les affichent : insérer une clause au-dessus renumérote donc chaque renvoi situé en dessous. Les renvois à des titres suivent un titre renommé. Les tableaux et les figures sont numérotés automatiquement, ou utilisent leur <caption> / <figcaption> lorsqu’ils en ont une.
Un renvoi dont la cible est supprimée échoue de façon visible — il affiche [reference not found] en rouge au lieu de garder sa dernière valeur. Word fait la même chose, pour la même raison : un numéro de clause plausible mais faux est bien plus dangereux qu’un numéro manifestement cassé. Le texte affiché est du vrai texte dans le HTML enregistré : le renvoi se lit donc correctement une fois le document sorti de l’éditeur.
Numéros de ligne dans la marge
Numérote chaque ligne dans la marge de gauche, comme le fait un traitement de texte pour les conclusions, les textes de loi, les transcriptions et tout ce que l’on cite par ligne (« page 4, lignes 12 à 15 »). Les règles de dépôt devant les tribunaux de plusieurs juridictions exigent des lignes numérotées, et c’est pourquoi les éditeurs de documents ont cette fonction, alors que les éditeurs dans le navigateur ne l’ont généralement pas.
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
});Il compte les lignes visuelles, pas les paragraphes. Un paragraphe qui s’étend sur quatre lignes reçoit quatre numéros, car c’est ce que signifie « ligne 12 » pour la personne qui la cite. Les boîtes de ligne sont trouvées avec Range.getClientRects() : le décompte suit donc la mise en page réellement affichée — changez la largeur de la fenêtre et la numérotation suit le texte.
Avec restart: "page" et l’affichage paginé activé, la numérotation recommence en haut de chaque page, comme le prévoient les règles de dépôt. Comme la surcouche de l’affichage paginé elle-même, la marge est purement visuelle : elle est en contenteditable="false" et supprimée autour de chaque sérialisation, si bien que le HTML que vous enregistrez est identique à l’octet près, que les numéros de ligne soient activés ou non — y compris la marge intérieure de la gouttière, qui sinon fuirait dans la zone modifiable sous forme de style en ligne.
Marques de mise en forme
Le bouton ¶ de Word : marques de paragraphe, contours des blocs et — la partie vraiment utile — mise en évidence des caractères invisibles mais pas anodins. Les espaces insécables, les espaces de largeur nulle, les traits d’union conditionnels et les marques bidirectionnelles arrivent tous avec du contenu collé et provoquent des bugs de mise en page que personne ne voit.
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 / bidiLes pieds-de-mouche et les contours sont dessinés entièrement en CSS : ils ne peuvent en aucun cas modifier le document. Les caractères invisibles ne sont pas atteignables depuis le CSS : ils reçoivent donc une enveloppe — mais chaque enveloppe est supprimée autour de la sérialisation et retirée lorsque la fonctionnalité est désactivée, et c’est le caractère d’origine qui est restauré, et non le glyphe de libellé. Les marques ne sont jamais appliquées à l’intérieur de <code> ou de <pre>, où le caractère peut être voulu.
Soyons honnêtes : sur ce point, il s’agit d’une parité, pas d’un avantage. TinyMCE fournit visualchars et visualblocks dans son cœur gratuit, et CKEditor propose show-blocks.
Filigrane
Dessine DRAFT, CONFIDENTIAL ou le nom d’un client derrière le contenu, pour qu’un lecteur voie d’un coup d'œil qu’il ne doit pas considérer le document comme définitif ni le diffuser.
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
});Il ne fait jamais partie de votre contenu. La marque est rendue sous forme d’arrière-plan SVG : elle ne peut être ni sélectionnée, ni copiée, ni trouvée par rechercher-remplacer, et elle est supprimée autour de chaque sérialisation. C’est plus qu’une question de propreté : un filigrane écrit dans le document survivrait dans une page publiée bien après que le brouillon a cessé d’en être un, et un « CONFIDENTIAL » placé dans le contenu se supprime en un instant, alors qu’une marque dans la couche de rendu, non.
L’impression est une décision distincte du marquage à l’écran : le filigrane est donc exclu de l’impression par défaut. Définissez print: true pour le faire figurer dans la sortie imprimée et dans l’export PDF.
Texte de droite à gauche et bidirectionnel
L’arabe, l’hébreu, le persan et l’ourdou s’écrivent de droite à gauche. La direction peut être définie par bloc ou pour tout le document, détectée à partir du contenu, ou laissée au navigateur avec 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");La direction est du contenu. Contrairement à la surcouche de l’affichage paginé ou au filigrane, dir est écrit dans le document et persiste dans getHTMLCode() — un document dont la direction disparaîtrait à l’enregistrement serait illisible.
La détection utilise la règle du premier caractère fort, le même algorithme que celui que la spécification HTML définit pour dir="auto" : un paragraphe prend la direction de son premier caractère à directionnalité forte, en ignorant les chiffres et la ponctuation. C’est pourquoi "123 שלום" est de droite à gauche alors que "123 hello" est de gauche à droite, et pourquoi un paragraphe composé uniquement de nombres est laissé tel quel au lieu d’être deviné.
Inverser la direction permute un alignement explicite. Définir dir ne change que l’alignement par défaut : un paragraphe explicitement réglé sur text-align:left resterait donc collé au bord gauche dans un bloc RTL et semblerait cassé. Un alignement physique explicite est inversé, et supprimé entièrement lorsqu’il tombe du côté naturel de la nouvelle direction, pour que le bloc suive dir à partir de là.
Le contenu mixte est isolé, et pas seulement marqué. Placer une URL LTR dans une phrase RTL sans isolation laisse l’algorithme bidirectionnel réordonner la ponctuation environnante — c’est ainsi que (https://example.com) finit affiché avec ses parenthèses du mauvais côté. insertIsolated() enveloppe le passage dans un <bdi> pour que cela ne puisse pas arriver.
Les listes, les citations et les tableaux sont inversés en CSS, pour que les puces et les barres de citation se trouvent du bon côté.
L’éditeur lui-même s’inverse aussi. La direction du document n’est que la moitié du travail — un auteur arabe ou hébreu ne devrait pas taper du texte de droite à gauche dans une application de gauche à droite. La barre d’outils, les menus, les listes déroulantes, les boîtes de dialogue et la barre d’état s’inversent tous :
editor.setRtlUserInterface(true);
editor.toggleRtlUserInterface();
editor.isRtlUserInterface();
// config: rtlUserInterface — "auto" (follow the document), true, or falseLa direction de l’interface est strictement séparée de la direction du contenu : inverser l’interface n’écrit rien dans votre document, et getHTMLCode() est inchangé. Avec plusieurs éditeurs sur une même page, seul celui que vous inversez est concerné. Les icônes ne sont volontairement pas inversées — Word n’inverse pas non plus les icônes de sa barre d’outils, et les glyphes gras/italique/alignement signifient la même chose dans les deux directions. Seules celles qui encodent un sens de lecture (augmenter et diminuer le retrait, flèches de menu) sont inversées.
Soyons honnêtes : CKEditor comme TinyMCE fournissent la direction de texte de base dans leurs cœurs gratuits ; la détection automatique, l’isolation bidirectionnelle et l’inversion de l’interface vont ici au-delà de cette base, mais la base elle-même n’est pas payante chez eux.
Poignée de glissement et réorganisation des blocs
Survolez un paragraphe, un titre, une liste, un tableau ou un encadré, et une poignée ⠿ apparaît à côté — faites-la glisser pour déplacer tout le bloc, avec une ligne d’insertion qui montre exactement où il atterrira. C’est l’interaction de réorganisation des blocs que Notion a imposée comme standard, et une bien meilleure façon de déplacer une clause qu’un couper-coller.
// 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+↑/↓ déplace le bloc du curseur — les mêmes touches que celles qu’utilise Word pour déplacer des paragraphes. Ce n’est pas un raccourci accessoire : une réorganisation réservée au pointeur est inutilisable au clavier, et c’est ce chemin clavier qui rend la fonctionnalité accessible. Dans les deux cas, le curseur suit le bloc déplacé.
La poignée et la ligne d’insertion se trouvent entièrement en dehors de la zone modifiable : elles sont donc structurellement incapables d’apparaître dans votre HTML enregistré. Le glissement utilise de simples événements de souris plutôt que le glisser-déposer HTML5 natif, qui, dans un contenteditable, est le comportement de navigateur qui ne fonctionne qu’à moitié partout — il lance un glissement de texte, dessine le mauvais fantôme et dépose du HTML sérialisé au lieu de déplacer le nœud. Dans un document RTL, la poignée apparaît à droite, là où commence la ligne.
Export PDF avec du vrai texte
Il existe deux exportateurs PDF, et la différence compte. html2pdf rend le document avec html2canvas et place une image sur chaque page : fidèle au pixel près, et le bon choix lorsque la mise en page visuelle exacte est le livrable. Mais le résultat est une image de document — rien n’est sélectionnable ni consultable par recherche, un lecteur d’écran trouve un fichier vide, les liens sont peints au lieu d’être cliquables, et une page de texte simple pèse des centaines de kilo-octets.
exportpdf écrit à la place directement le format de fichier PDF. Le texte reste du texte. Il utilise les 14 polices PDF standard, qui n’ont pas besoin d’être intégrées — cela supprime le sous-ensemble de polices, la partie vraiment difficile d’un générateur PDF, et maintient une page typique sous les 15 Ko. Aucune bibliothèque n’est chargée et rien n’est envoyé.
- Titres, gras/italique/souligné/barré, couleur, taille de police, listes à puces et numérotées, tableaux avec lignes d’en-tête répétées, citations, filets et images
- Les liens deviennent de vraies annotations de lien PDF, et non du texte souligné
- Le format de page, l’orientation et les marges proviennent de la mise en page propre au document : le PDF correspond donc à l’affichage paginé et à l’export Word
- Les sauts de page explicites sont respectés, et un titre n’est jamais laissé seul en bas d’une page
- Le titre, l’auteur et la langue du document sont écrits dans les métadonnées
Une subtilité à connaître : un lecteur de PDF met le texte en page avec les métriques propres à la police, si bien qu’un générateur qui coupe les lignes avec ses propres valeurs sera en désaccord avec lui et débordera de la marge. Cet exportateur mesure chaque caractère et écrit ces largeurs dans le PDF sous forme de /Widths, qu’un lecteur est tenu de respecter — la mise en page et le rendu utilisent alors par construction les mêmes valeurs. Vérifié avec pdf.js (l’analyseur intégré à Firefox) : le texte s’extrait proprement, les liens se résolvent, et rien ne dépasse la marge.
Balisé pour l’accessibilité
Un texte sélectionnable est nécessaire, mais pas suffisant. Un PDF non balisé échoue quand même à un audit Section 508 ou EN 301 549 : les technologies d’assistance reçoivent un flux de mots à plat, sans titres, sans structure de liste, sans sémantique de tableau et sans ordre de lecture garanti. L’export écrit un arbre de structure complet : le document est donc navigable, et pas seulement lisible.
H1–H6pour les titres,Ppour les paragraphes,BlockQuotepour les citationsL/LI/Lbl/LBody, pour qu’un lecteur d’écran annonce « liste, 3 éléments » plutôt que trois paragraphes isolésTable/TR/TH/TDavec/Scopesur les cellules d’en-tête — la différence entre entendre « Revenue, 1,240 » et seulement « 1,240 »- Les éléments
Figureportent un/Alttiré du texte alternatif de l’image - Les filets, les bordures de cellules, la barre de citation et l’en-tête et le pied de page courants sont marqués comme artefacts, ce qui les retire de l’ordre de lecture — un en-tête répété lu à voix haute sur chaque page est l’une des remarques d’audit les plus fréquentes
/MarkInfo,/StructTreeRoot, un/ParentTree,/Langet/DisplayDocTitle, pour que le lecteur annonce le titre du document au lieu de son nom de fichier
L’arbre est vérifié en le relisant avec pdf.js, comme le feraient les technologies d’assistance, et non en inspectant les octets : 25 assertions couvrant chaque rôle, l’ordre de lecture du document et l’absence d’artefacts dans l’arbre de contenu.
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}"Un vrai export .docx, dans le navigateur
L’import lisait déjà un véritable package OOXML côté client. L’export, non : wordexport enveloppe le HTML dans un conteneur de type MSO et le nomme .doc. Word l’ouvre, mais ce n’est pas un .docx — l’aller-retour se passe mal, Word insiste à l’enregistrement, et certaines politiques d’entreprise bloquent purement et simplement le HTML portant une extension doc. exportdocx écrit à la place un vrai package OOXML : un ZIP construit avec CompressionStream, symétrique du DecompressionStream qu’utilise l’importateur. Aucune bibliothèque, aucun envoi.
Il émet volontairement exactement ce que l’importateur relit — les mêmes relations de liens hypertexte, identifiants de numérotation, parties de notes et de commentaires, et conventions de révision —, si bien que HTML → .docx → HTML est un véritable aller-retour, et non deux fonctionnalités qui partagent par hasard une extension de fichier. Vérifié dans les deux sens : l’éditeur réimporte sa propre sortie (titres, mise en forme en ligne, couleur, URL des liens, listes imbriquées, tableaux, citations, notes de bas de page, suivi des modifications et équations sont tous récupérés), et le package lui-même est validé partie par partie — chaque partie XML bien formée, chaque r:id résolu, chaque numId défini, et chaque partie déclarée dans [Content_Types].xml.
Deux détails qui cassent discrètement les fichiers .docx construits à la main, tous deux gérés ici : w:sz est en demi-points, si bien qu’écrire des points affiche tout le document à moitié de sa taille ; et un seul caractère de contrôle n’importe où dans une partie fait purement et simplement rejeter le fichier par Word, au lieu d’ignorer le caractère.
await editor.exportToDocx("quarterly-report");
const blob = await editor.getDocxBlob({ title: "Q3", author: "Finance" });Filtre de nettoyage du contenu
« Rien ne s’exécute dans l’éditeur » n’est pas le bon critère. Le bon critère, c’est que rien ne s’exécute dans l’application qui affiche ce que l’éditeur a enregistré — car c’est la forme de tout déploiement : un utilisateur rédige du contenu, celui-ci est stocké, et le navigateur d’un autre utilisateur l’affiche.
Le cœur de l’éditeur supprime déjà les gestionnaires d’événements en ligne et rejette javascript: sur les liens et les actions de formulaire, et aucune des 20 charges utiles d’une batterie de tests ne s’est exécutée dans l’éditeur. Mais trois constructions survivaient dans la sortie enregistrée, et l’une d’elles — <iframe srcdoc> — s’exécutait dès que ce HTML enregistré était affiché dans un conteneur ordinaire. Le filtre de nettoyage ferme cette porte.
- Liste d’autorisation, et non liste de blocage — une liste de blocage est une promesse d’avoir pensé à tous les éléments que le HTML gagnera un jour. Les éléments inconnus sont désenveloppés, pour que le texte qu’un utilisateur y a tapé ne soit jamais perdu.
- Il s’exécute à l'entrée (définition, collage, dépôt) et à la sortie. La sortie est la voie garantie : c’est ce qui est stocké, le filtre travaille sur une copie détachée et ne peut donc jamais perturber le curseur, et le contenu antérieur au plugin est nettoyé à son prochain enregistrement.
- Les URL sont vérifiées après la suppression des caractères de contrôle —
java	script:est analysé par les navigateurs commejavascript:: tester la chaîne brute se contourne donc sans effort. styleest filtré par déclaration, et non par fragment : supprimerjavascript:deurl(javascript:f())laisseraiturl(f()), qui se lit alors comme une URL relative inoffensive et passe la vérification même qui devait l’arrêter.- Il réanalyse jusqu’à stabilité, pour déjouer le mutation XSS — un balisage inerte lors d’une analyse et dangereux une fois que le sérialiseur l’a réécrit.
- Les liens
target="_blank"reçoiventrel="noopener".
Les intégrations fonctionnent toujours : <iframe> est conservé pour les sources https. Sans liste d’hôtes autorisés configurée, il reçoit un bac à sable qui autorise ce dont a besoin une vidéo intégrée et refuse la navigation de la fenêtre principale, les fenêtres contextuelles, les fenêtres modales et les formulaires — une iframe injectée ne peut pas manipuler le parent par script, mais elle pourrait sinon faire naviguer la fenêtre principale, ce qui est une manœuvre de phishing. Définir sanitizerAllowedIframeHosts est le contrôle le plus strict.
C’est une défense en profondeur, et non un substitut à la validation côté serveur. Le filtre s’exécute dans le navigateur, et tout ce qui s’exécute dans le navigateur peut être contourné par un client qui ne l’exécute tout simplement pas. Nettoyez sur le serveur avant de stocker, et échappez en sortie. Cette couche protège le parcours honnête et réduit la marge du parcours malhonnête ; elle ne rend pas un HTML non fiable sûr à afficher sans condition.
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 farCKEditor 5 réserve les notes de bas de page à ses offres Essential, Professional et Custom, et place la table des matières, la reproduction de la mise en forme, le changement de casse et les champs de fusion derrière des « selected CKEditor Plans ». TinyMCE fournit chacune de ces fonctionnalités sous forme de plugin premium « only available for paid TinyMCE subscriptions » — la correction automatique et la typographie étant deux plugins distincts. Ici, elles font toutes partie de la licence perpétuelle à $129. (Soyons honnêtes : la transformation automatique du texte de CKEditor fait partie de son cœur open source ; pour la correction automatique, il s’agit donc d’une parité plutôt que d’un manque.) Consultez la comparaison complète.