JavaScript でリッチテキストをプレーンテキストに変換して TXT をダウンロード

現在の文字を UTF-8 TXT として書き出し、安全なプレビューと古いファイルの無効化を実装します。
テキストと HTML を使い分ける
getPlainText() は抽出された文字列、getHTMLCode() はマークアップを返します。正規表現でタグを削除すると単語が連結されたり実体参照が残ったりします。テキスト出力は書式を失います。閲覧用コピー、索引、サポート記録に使えますが、文書のバックアップではありません。
完全なダウンロード例を実行する
アプリからエディターファイルを配信します。準備ボタンは現在の文書を読み、CRLF と CR を LF にそろえ、BOM を追加せず UTF-8 text/plain Blob を作ります。別リンクで document.txt を保存します。プレビューは textContent を使い、HTML のような文字を描画しません。編集後は古いリンクが無効になります。通信は行いません。
<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>
失われる情報を理解する
段落、リスト記号、表の区切り、画像の代替文字、リンクの扱いは抽出方針によります。実際の文書で検証し、Markdown や Word と同一とは約束しないでください。TXT は画像、書式、コメント、変更履歴、再編集用 HTML を保持しません。復旧には元の HTML を別に保存します。 空のエディターでも改行を返す場合があります。この例は空白を削らず保持します。フォーカスの変化で末尾の空行が変わることがあります。
ファイルの寿命と保存を管理する
オブジェクト URL は Blob をメモリに保持します。例ではクリック直後に解放せず、古くなった URL を 30 秒後に解放します。残りはページの終了時にブラウザーが解放します。この待機は汎用の完了通知ではありません。繰り返しの保存、大きな文書、対象端末で検証します。download 属性は名前の提案であり、保存方法はブラウザーが決めます。 繰り返しの保存にはブラウザーの複数ダウンロード許可が必要な場合があります。ポリシーを回避せず、利用者の意図なく一括保存を始めないでください。
確認項目
絵文字、結合アクセント、中国語、日本語、段落、表を試します。バイト列を HTML ではなく正規化した getPlainText() と比較します。空文書やタグのような文字も確認します。準備後に編集すると旧リンクが消える必要があります。TXT は CSV エスケープ、HTML サニタイズ、サーバー検証の代わりにはなりません。