Footnotes, format painter & change case
Three long-document tools that the other commercial editors put behind a paid plan. Footnotes with markers and notes that renumber themselves, a format painter that copies character formatting from one selection to another, and change case for the selection. All three ship in the standard license.
Table of contents
Inserts a contents block built from the document’s headings, with links that jump to them, and keeps it in step as you write. Rename a heading, add one, delete one, move a section — the list re-derives itself from the document, so it cannot drift.
This is not the same thing as the document outline panel. That panel is editor chrome for navigation and never appears in your saved HTML. A table of contents is part of the document you publish or print, so it survives getHTMLCode() and reads correctly with no JavaScript running.
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 }, ...]| Config | Default | What it does |
|---|---|---|
| tocTitle | "Contents" | Heading above the list. Set to "" to omit it. |
| tocMinLevel / tocMaxLevel | 1 / 3 | Heading levels included, inclusive. Indentation follows the level. |
| tocPageNumbers | "auto" | "auto" shows page numbers when page view is on, true always tries, false never. |
Page numbers.With the pagination plugin loaded and page view on, each entry gets the page its heading falls on, set with a dotted leader like a printed contents page. The numbers come from the same page boundaries the page-view overlay draws, so they always agree with what you see. Outside page view no new number is computed — a page number would be a fiction — but numbers already in the block are kept, so loading a document that was saved in page view never silently strips them. Re-enter page view to refresh.
The block is contenteditable="false"and is regenerated from the headings, so it is not hand-editable — delete it as a unit and re-insert to move it. Headings inside generated regions (the contents block itself, the footnotes section) are skipped, so it never lists its own title.
Footnotes
Inserts a numbered reference marker at the caret and a matching note in a footnotes section at the end of the document, then keeps the two in step. Numbering follows document order: add a footnote above an existing one, delete a marker, or move a paragraph, and every marker and note renumbers and the notes list re-sorts to match. Deleting a marker deletes its note, and removing the last marker removes the whole section — no orphans are left behind in the saved HTML.
<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>| Config | Default | What it does |
|---|---|---|
| footnotesTitle | "Footnotes" | Heading above the notes section. Set to "" to omit it. |
| footnotesNumbering | "decimal" | List style of the notes: decimal, lower-roman, upper-roman, lower-alpha, upper-alpha. |
| footnotesMarkerPrefix | "" | Wraps the marker number, e.g. "[" to render [1]. |
| footnotesMarkerSuffix | "" | The closing half of the above, e.g. "]". |
What lands in your HTML
Footnotes are content, so unlike the page-view overlay they are meant to persist in getHTMLCode(). The number is written as literal text rather than drawn by a CSS counter, so the saved HTML is self-contained: publish it, export it, or render it on a page that never loads our stylesheet and the numbering is still there and still correct.
<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>Markers are contenteditable="false" so the caret treats them as a single unit and cannot land inside the number. Clicking a marker jumps to its note; clicking the returns.
Format painter
Picks up the character formatting at the caret and paints it onto the next selection. Formatting is read from the computed style, so it does not matter whether the source was <b>, style="font-weight:700"or a CSS class. Painting also strips the character formatting already inside the target — which is what makes painting unformatted text onto bold text actually work.
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)" }Only properties that actually differfrom the editor’s baseline are carried, so a paint emits a short style attribute rather than a wall of inherited defaults. Repainting the same run does not nest wrappers. Which properties travel is configurable via formatPainterProperties; block-level properties such as alignment and margins are deliberately excluded, because a painter that reflowed the document is not what the tool is for.
Change case
Rewrites the actual characters in the selection — not a text-transform, which only looks right until the HTML is consumed somewhere else. Inline markup survives: change the case of the <strong>Data</strong> Act and the <strong> is still there afterwards. With nothing selected it operates on the word under the caret.
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 keeps small words (a, an, and, of, the, to…) lowercase unless they are first or last, and applies the same rule inside hyphenated compounds — so state-of-the-art becomes State-of-the-Art, not State-Of-The-Art. Edit the list with titleCaseSmallWords.
Autocorrect & typography
Fixes common typos as you type, capitalises the start of sentences, and turns typed approximations into the real characters: straight quotes become curly, -- becomes an em dash, ... becomes an ellipsis, (c) becomes ©, 1/2becomes ½.
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…'| Config | Default | What it does |
|---|---|---|
| autocorrectEnabled | true | Master switch. |
| autocorrectSpelling | true | Common-misspelling table (teh → the). |
| autocorrectCapitalize | true | Capitalise the first word of a sentence. |
| autocorrectTypography | true | Smart quotes, dashes, ellipsis, arrows, symbols, fractions. |
| autocorrectReplacements | null | Extra rules, merged over the defaults. Map a word to itself to disable a built-in one. |
Nothing fires inside code. <code>, <pre>, <kbd>, <samp> and <var> are skipped entirely, as is any subtree marked data-rte-no-autocorrect. Curling the quotes inside a code sample corrupts it, which is the most damaging thing a feature like this can do.
Press Backspace immediately after a correction to undo itand keep exactly what you typed — the same escape hatch Word and Google Docs give you. The built-in word list is deliberately conservative: strings that are also real words (ill, lets, its) are left alone, because guessing wrong on a real word is worse than missing a correction.
Merge fields
Placeholders that stand in for data, so one document becomes a template you render against many records — mail merge, invoice runs, personalised letters, generated contracts. Each field is an atomiccontenteditable="false" chip, so it can never be half-deleted into {{customer.na}} and silently stop resolving.
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.| Config | Default | What it does |
|---|---|---|
| mergeFields | example set | { id, label, sample }definitions. The shipped list is an example — replace it with your own schema. |
| mergeFieldPrefix / Suffix | "{{" / "}}" | Wrappers drawn around the label so a placeholder is visually obvious. |
| mergeFieldMissing | "placeholder" | What renderMergeFields does when the data has no value: leave the placeholder visible, or "empty" to remove it. |
Preview never reaches your saved HTML.This is the one genuinely dangerous thing about the feature: if turning on preview and pressing save wrote “Ada Lovelace” into the template where {{Recipient name}}used to be, the user would destroy their template merely by looking at it. So the serializers are wrapped — getHTMLCode, getJSON, getHTMLContent and getText all restore every chip to its label for the duration of the call. Save while previewing and you still get the template back.
By the same principle, renderMergeFields() works on a detached clone: producing merged output never mutates the document being edited. A field with no matching data keeps its placeholder rather than rendering blank, because a silently empty invoice line is worse than an obviously unfilled one. Fields pasted from a template built elsewhere are reported with known: false rather than dropped, so you can tell the user what their data needs to supply.
Link checking
Finds links that are broken, unsafe, invisible to assistive technology, or actively misleading. The important part: the whole offline half needs no network and no service to run, because most real link defects are decidable from the document itself.
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 | Severity | What it catches |
|---|---|---|
| broken-anchor | error | Links to #id that no longer exists — very common, because retitling a heading changes its id. |
| empty-href | error | No destination — including a link whose address was stripped for being unsafe. |
| unsafe-scheme | error | javascript:, data:, vbscript:, file:. |
| empty-text | error | No text, image alt, aria-label or title, so the link has no accessible name. |
| deceptive-text | warning | Text shows one hostname, the link goes to another — the phishing shape, and also what happens when someone edits the visible text but not the href. |
| ambiguous-text | warning | Several “Read more” links pointing to different places — identical when listed by name in a screen reader. |
| mixed-content | warning | An http:// link on a secure page. |
| unreachable | error | Remote URL did not resolve. Only produced when you supply a resolver. |
Checking remote URLs is opt-in, and it is your code that does it.This editor makes no outbound calls of its own, and a built-in URL prober would quietly break that — it would send every URL your authors touch to somebody else’s server. So reachability is delegated to a resolver you supply, the same shape as the AI, spell-check and chip resolvers:
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 }, ... }
}
});Only URLs that survived the offline checks are sent, and never in-document anchors or mailto:links. If your resolver throws, the offline findings are still returned. For comparison, TinyMCE’s Link Checker is a premium plugin and self-hosted customers must additionally deploy its link-checking service and point linkchecker_service_url at it.
The highlight is chrome, not content: data-rte-link-issue is stripped around every serialize, so marking up problems never changes the HTML you save.
Table sorting & row numbering
Sort a table by the column the caret is in, and add an automatic numbering column that renumbers itself after every sort.
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();The column type is detected, not assumed. A column of prices sorts numerically even when the cells read $1,240.00 or (500) (accounting notation for a negative), and a column of dates sorts chronologically rather than alphabetically. Text-sorting a numeric column is the classic wrong answer that puts 10 before 9. Empty cells sink to the bottom in bothdirections, because a blank means “no value”, not “the smallest value”. The sort is stable, so sorting by a second column preserves the order of the first.
It refuses on tables with merged cells and tells you why, rather than sorting them badly. Arowspan has no meaning once the rows move; silently shredding a table someone spent an hour on is far worse than declining to sort it. <thead> and <tfoot> never move, and a first row of all <th> is treated as a header even without a <thead>. Row numbering is positional, so after a sort row 1 is whatever is now on top.
Permanent pen
Switch on a fixed character format and everything you type from then on gets it — the marker you pick up to annotate a document in a colour that is obviously not the original text. Unlike the format painter, pen output is real content and persists in your saved HTML.
editor.setPermanentPen(true);
editor.togglePermanentPen();
editor.isPermanentPenActive();
editor.setPermanentPenStyle({ "color": "#0b6", "font-style": "italic" });
editor.getPermanentPenStyle();A whole stroke goes into onespan that the pen keeps extending, rather than one span per keystroke. Moving the caret ends the stroke, so the next run gets its own span instead of text teleporting into the previous one. Switching the pen off steps the caret out of the run — otherwise typing would continue inside the styled span and the pen would appear to ignore being switched off.
CKEditor 5 gates footnotes behind its Essential, Professional and Custom plans, and puts table of contents, format painter, case change and merge fields behind “selected CKEditor Plans”. TinyMCE ships every one of these as a premium plugin “only available for paid TinyMCE subscriptions” — autocorrect and typography as two separate ones. Here they are all part of the $129 perpetual license. (In fairness: CKEditor’s automatic text transformation is in its open-source core, so autocorrect is parity there rather than a gap.) See the full comparison.