Convert rich text to plain text and download a TXT file in JavaScript

Extract current editor text and download a UTF-8 TXT file, with a safe preview, Unicode checks and stale-file handling.
Choose text or HTML deliberately
Use getPlainText() for the editor’s extracted text and getHTMLCode() for markup. Removing tags with a regular expression can join words or leave entities encoded. A text export removes formatting, not just angle brackets. It is useful for a reading copy, an index input or a support transcript; it is not a backup of the rich document.
Run the complete download example
Serve editor assets through your application. Prepare reads the current document, normalizes CRLF and CR to LF and creates a UTF-8 text/plain Blob without adding a BOM. A separate link downloads document.txt. The preview uses textContent, so text that looks like HTML is not rendered. Editing invalidates the old link; prepare again to export the latest version. No server request is made.
<link rel="stylesheet" href="/richtexteditor/rte_theme_default.css">
<script src="/richtexteditor/rte.js"></script>
<script src="/richtexteditor/plugins/all_plugins.js"></script>
<style>body{font:16px/1.6 system-ui;max-width:960px;margin:24px auto;padding:0 16px}button,a{box-sizing:border-box;display:inline-flex;align-items:center;min-height:44px;padding:8px 16px}a[hidden]{display:none}pre{white-space:pre-wrap;overflow-wrap:anywhere}.editor-scroll{max-width:100%;overflow-x:auto}</style>
<h1>Export rich text as a UTF-8 text file</h1>
<div class="editor-scroll"><div id="editor"></div></div>
<button type="button" id="prepare">Prepare text file</button>
<a id="download" download="document.txt" hidden>Download .txt</a>
<p id="status" role="status" aria-live="polite"></p>
<pre id="preview"></pre>
<script>
const editor = new RichTextEditor("#editor", { toolbar: "full", height: "320px" });
editor.setHTMLCode("<h2>Project notes</h2><p>Keep the words, including <strong>formatting</strong>, 中文 and 👋.</p>");
const link = document.getElementById("download");
const preview = document.getElementById("preview");
const status = document.getElementById("status");
let currentURL = null, preparedHTML = null;
function clearPreparedFile() {
if (currentURL) {
const obsoleteURL = currentURL;
// Do not revoke synchronously: a download may still be starting.
setTimeout(() => URL.revokeObjectURL(obsoleteURL), 30000);
currentURL = null;
}
preparedHTML = null;
link.removeAttribute("href");
link.hidden = true;
preview.textContent = "";
}
function prepare() {
clearPreparedFile();
// Export the API result, not the HTML source or a regex-stripped version.
const text = editor.getPlainText().replace(/\r\n?/g, "\n");
preparedHTML = editor.getHTMLCode();
const file = new Blob([text], { type: "text/plain;charset=utf-8" });
currentURL = URL.createObjectURL(file);
link.href = currentURL;
link.hidden = false;
preview.textContent = text;
status.textContent = "Text file ready: " + file.size + " UTF-8 bytes. Click Download .txt. Nothing sent to a server.";
}
document.getElementById("prepare").addEventListener("click", prepare);
editor.attachEvent("change", () => {
// Delayed notifications for the same saved HTML do not invalidate this snapshot.
if (preparedHTML !== null && editor.getHTMLCode() === preparedHTML) return;
clearPreparedFile();
status.textContent = "Content changed. Prepare a new file before downloading.";
});
window.textDemo = { editor, prepare };
</script>
Know what a text export loses
Paragraph boundaries, list markers, table separators, image alternatives and links depend on the editor’s extraction policy. Test representative documents rather than promising a Markdown or Word-equivalent result. A TXT file does not retain images, styling, comments, revision history or the HTML needed for editing round trips. Preserve the original HTML separately if you need a recoverable draft. An empty editor can still yield line breaks. This example preserves extracted whitespace rather than trimming it; focus changes can affect trailing blank lines.
Manage file lifetime and delivery
An object URL holds the Blob in memory. This example releases obsolete URLs after a 30-second grace period instead of revoking at the instant the link is used. The browser releases the remaining URL when the page unloads. This delay is a practical sample policy, not a universal download-completion signal. Test repeated downloads, large documents and your target devices. The download attribute suggests a name; browser settings decide how and where to save. Repeated downloads may require the browser’s multiple-download permission. Do not bypass browser policy or start a batch without the user’s intent.
Acceptance checklist
Export emoji, combined accents, Chinese, Japanese, multiple paragraphs and a table. Compare downloaded bytes with the normalized getPlainText() result, not the HTML. Try an empty document and text that looks like a tag. Edit after preparing: the stale link must disappear. TXT is not CSV escaping, HTML sanitization or a server-side validation boundary.