【JavaScript】改行を <br> タグに変換する方法|replace・XSS対策・innerHTML vs textContent・ホワイトスペース保持まで解説

テキストエリアで入力した文章を画面に表示したとき、改行が無視されて1行にまとまってしまう——JavaScript でこの問題を解決するには、改行コードを <br> タグに変換する方法と、CSS で改行を保持する方法の2つのアプローチがあります。

ただし、変換した文字列を innerHTML にセットする実装は、XSS(クロスサイトスクリプティング)の脆弱性を生みやすい落とし穴があります。この記事では安全な実装方法を中心に、改行変換の基礎から実践パターンまで解説します。

スポンサーリンク

改行コードの種類を理解する

テキストの改行は OS や入力方法によって3種類の文字コードが使われます。変換前に把握しておきましょう。

改行コード エスケープ表記 主な環境
LF(Line Feed) \n Linux・macOS・Web標準
CR(Carriage Return) \r 古いMac(macOS 9以前)
CRLF(CR+LF) \r\n Windows・HTTPヘッダー
ブラウザのテキストエリアでは LF に統一される:HTML の <textarea> に入力した値は、ブラウザが自動的に LF(\n)に正規化します。ただし外部からテキストを取得する場合(APIレスポンス、ファイル読み込みなど)は CRLF が含まれることがあります。確実に対応するために、変換前に CRLF → LF の正規化を行うのが安全です。

基本:replace で改行を <br> に変換する

String.prototype.replace() に正規表現を使って改行コードを <br> に置き換えます。

改行を <br> に変換する関数
/**
 * テキストの改行コードを <br> に変換する
 * \r\n → \n に正規化してから \n → <br> に変換
 */
function nl2br(text) {
  return text
    .replace(/\r\n/g, '\n') // CRLF を LF に統一
    .replace(/\r/g, '\n')   // CR のみも LF に統一
    .replace(/\n/g, '<br>'); // LF を <br> に変換
}

// 使い方
const text = '1行目\n2行目\r\n3行目';
console.log(nl2br(text));
// '1行目<br>2行目<br>3行目'
正規表現の順番が重要:\r\n(CRLF)を先に変換しないと、\r\n がそれぞれ別の <br> に変換されて二重の改行になります。必ず CRLF → LF → <br> の順で処理してください。

innerHTML へのセットと XSS の危険性

変換した文字列を innerHTML にセットすると改行が反映されますが、ユーザーが入力したテキストをそのまま渡すと XSS 攻撃の入口になります。

NG: XSS 脆弱性のある実装
// NG: ユーザー入力をエスケープせずに innerHTML にセット
const userInput = '<img src=x onerror=alert("XSS")>\n悪意ある入力';
element.innerHTML = nl2br(userInput);
// → <img src=x onerror=alert('XSS')> がHTMLとして実行されてしまう!
ユーザー入力を innerHTML に渡す前に必ずエスケープする:テキストエリアやURLパラメータなど、ユーザーが制御できるテキストには任意のHTML・スクリプトが含まれる可能性があります。innerHTML に渡す前に <>&"'の5文字をHTMLエンティティに変換(エスケープ)してください。
OK: エスケープしてから変換する
/**
 * HTMLの特殊文字をエスケープする
 */
function escapeHtml(text) {
  return text
    .replace(/&/g, '&amp;')   // & は最初に変換(順番重要)
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}

/**
 * エスケープしてから <br> 変換する(安全版)
 */
function safeNl2br(text) {
  return escapeHtml(text)
    .replace(/\r\n/g, '\n')
    .replace(/\r/g, '\n')
    .replace(/\n/g, '<br>');
}

// 使い方
const userInput = '<script>alert("XSS")</script>\n普通のテキスト';
element.innerHTML = safeNl2br(userInput);
// → &lt;script&gt;alert("XSS")&lt;/script&gt;<br>普通のテキスト
// → スクリプトは実行されず、テキストとして表示される
& の変換を最初に行う理由:< を後で変換した場合、すでに変換済みの &&amp; に二重変換されてしまいます。&&amp; を必ず最初に処理してください。

より安全な方法:textContent + white-space: pre-wrap

XSS を根本的に防ぐには、innerHTML を使わず textContent を使う方法が最も安全です。textContent はHTMLとして解釈されないため、スクリプトが実行されません。CSS の white-space: pre-wrap を組み合わせると、<br> 変換なしで改行を表示できます。

HTML
<div id="output" class="pre-wrap"></div>
CSS
.pre-wrap {
  white-space: pre-wrap; /* 改行・スペースを保持しつつ折り返す */
  word-break: break-word; /* 長い単語も折り返す */
}
textContent で安全に表示
const output    = document.getElementById('output');
const userInput = '<script>alert("XSS")</script>\n普通のテキスト';

// textContent はHTMLを解釈しないため XSS の心配がない
output.textContent = userInput;

// CSS の white-space: pre-wrap によって
// \n がそのまま改行として表示される(<br> 変換不要!)
white-space: pre-wrap が最も安全で簡単:textContent + white-space: pre-wrap の組み合わせは、エスケープ処理も <br> 変換も不要で、XSS リスクがゼロです。innerHTML を使う必要があるのは、変換後に 自分でコントロールできるHTMLタグ(太字・リンクなど)を含めたいときだけにしましょう。

テキストエリア入力を安全に表示する完全実装

2つのアプローチを使った実装例をまとめます。

HTML(テキストエリア + プレビュー)
<div class="editor">
  <textarea id="inputText" rows="6" placeholder="テキストを入力してください..."></textarea>
  <div class="preview-tabs">
    <button type="button" id="previewBtn">プレビュー</button>
  </div>
  <div id="preview" class="preview pre-wrap"></div>
</div>
アプローチ①:textContent + white-space: pre-wrap(推奨)
document.getElementById('previewBtn').addEventListener('click', () => {
  const text    = document.getElementById('inputText').value;
  const preview = document.getElementById('preview');

  // XSS リスクなし・エスケープ不要・<br> 変換不要
  preview.textContent = text;
});
アプローチ②:safeNl2br + innerHTML(太字・リンク等を含めたい場合)
document.getElementById('previewBtn').addEventListener('click', () => {
  const text    = document.getElementById('inputText').value;
  const preview = document.getElementById('preview');

  // エスケープ → <br> 変換 → innerHTML
  preview.innerHTML = safeNl2br(text);
  // ※ white-space: pre-wrap の CSS は不要になる
});
CSS(プレビュー共通スタイル)
.preview {
  margin-top: 12px;
  padding: 12px 16px;
  border: 1px solid #e2e8f0;
  border-radius: 6px;
  background: #f8fafc;
  min-height: 80px;
}

.pre-wrap {
  white-space: pre-wrap;
  word-break: break-word;
}

リアルタイムプレビュー(input イベント版)

ボタンを押さなくても入力に応じてリアルタイムにプレビューを更新するパターンです。

リアルタイムプレビュー
const inputText = document.getElementById('inputText');
const preview   = document.getElementById('preview');

inputText.addEventListener('input', () => {
  // textContent 方式(推奨)
  preview.textContent = inputText.value;
});

末尾・連続改行の制御

入力末尾の余分な改行や、連続する改行を制御したいケースの処理です。

末尾の改行トリムと連続改行の圧縮
function normalizeNewlines(text, options = {}) {
  const {
    trimEnd     = true,  // 末尾の改行を除去
    maxConsecutive = 0,  // 連続改行の最大数(0 = 制限なし)
  } = options;

  // CRLF / CR を LF に統一
  let result = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');

  // 末尾の改行を除去
  if (trimEnd) result = result.replace(/\n+$/, '');

  // 連続改行を N 個に圧縮
  if (maxConsecutive > 0) {
    const pattern = new RegExp(`\\n{${maxConsecutive + 1},}`, 'g');
    result = result.replace(pattern, '\n'.repeat(maxConsecutive));
  }

  return result;
}

// 使い方:末尾改行除去 + 連続改行を2つまでに圧縮
const cleaned = normalizeNewlines(text, { trimEnd: true, maxConsecutive: 2 });
preview.textContent = cleaned;

よくある質問

QinnerHTML で <br> を使う場合、必ずエスケープが必要ですか?
Aユーザーが入力・編集できるテキストの場合は必須です。ソースコードに直接書いた固定文字列(例:const msg = "Hello\nWorld")であればXSS リスクはありません。「どこからそのテキストが来るか」で判断してください。API・フォーム・URLパラメータ・localStorage など外部由来のテキストは必ずエスケープしてください。
Qwhite-space: pre-wrap と pre-line の違いは何ですか?
Apre-wrap は改行・スペースをそのまま保持しつつ、コンテナの幅で折り返します。pre-line は改行は保持しますが、連続するスペースは1つに圧縮します。ユーザーが入力したテキストをそのまま表示するなら pre-wrap が適切です。
QVue や React で同じことをするにはどうすればいいですか?
AReact では dangerouslySetInnerHTML を使うと innerHTML と同様の処理ができますが、名前の通り危険です。ユーザー入力には <div style={{whiteSpace:"pre-wrap"}}>{text}</div> のように JSX で直接テキストを渡す方が安全です。Vue では v-html が innerHTML 相当ですが同様にエスケープが必要です。{{ text }}(Mustache構文)はデフォルトでエスケープされるため安全です。
QPHP の nl2br() とまったく同じ動作をJavaScriptで再現するには?
APHP の nl2br() は改行の直前<br /> を挿入し、改行コード自体は残します。JavaScript で完全再現するには: text.replace(/(\r\n|\r|\n)/g, '<br />$1') です。単純に改行を <br>置換するだけであれば冒頭の nl2br() 関数で十分です。
Q変換した結果をさらにJSON文字列として保存する場合の注意点は?
A<br> を含む文字列を JSON.stringify() すると <> はそのまま保存されます。ただし後からJSON文字列を再度 innerHTML にセットする場合は、二重エスケープに注意してください。一般的には、保存時はオリジナルのテキスト(改行コードのまま)を保存し、表示するたびに変換する設計が保守しやすいです。

まとめ

改行の扱いと安全な実装方法をまとめます。

  • 最も安全な方法は textContent + CSS white-space: pre-wrap。XSS リスクなし・変換コードなし
  • <br> 変換して innerHTML に渡す場合は、必ず事前に HTML エスケープする
  • CRLF・CR・LF の3種類があるため、変換前に \r\n → \n → \r → \n の順で正規化する
  • & のエスケープは最初に行う(二重変換防止)
  • 外部由来のテキスト(API・フォーム・URLパラメータ)は必ずエスケープを疑う

フォームバリデーションと組み合わせた実装は【JavaScript】フォームバリデーション完全ガイド、要素の動的追加については【JavaScript】マウスオーバーで要素を動的に追加・削除する実践ガイドもあわせてご覧ください。