テキスト自動リンク化(URL・メール対応)
このコンポーネントについて
テキスト内のURLとメールアドレスを自動検出し、クリッカブルなリンクに変換して表示するUIです。 テキストエリアに文章を貼り付けて「リンク化する」ボタンを押すと、変換結果が下に表示されます。
連絡網・社内通知・お知らせ文など、ユーザーが入力・貼り付けた生テキストをリンク付きで表示したい場面で役立ちます。 Wordからのコピペで混入するタブ・全角スペース・ノーブレークスペースを変換前に正規化する処理も含んでいます。
- URL自動リンク化 —
https:///http://で始まるURLを検出し、新しいタブで開くリンクに変換する - メールアドレス自動リンク化 — 標準的なメールアドレスを検出し
mailto:リンクに変換する - 日本語埋め込みURL対応 — スペースなしで日本語が前後に来るURLを正しく終端検出する(例:「詳しくはhttps://example.comご参照」)
- 末尾記号の除外 — URLの末尾に付いた句読点(
.・,)や括弧())をURL本体から自動で取り除く - メール誤検知防止 —
@が2つある・TLDがないパターンはリンク化しない - Wordコピペ文字の正規化 — タブ・全角スペース・ノーブレークスペースを通常スペースに、改行コードを統一する
- XSS安全 —
innerHTMLに変数を代入せず、createElementとtextContentで安全にDOMを組み立てる
実装のポイント・注意点
最も重要なのはXSS対策です。テキストを innerHTML で一括置換する方法もありますが、テキストに <script> が含まれていた場合にスクリプトが実行される危険があります。このサンプルでは document.createTextNode() でプレーンテキストを、document.createElement('a') でリンク要素を個別に生成し、appendChild でつなぎ合わせています。ブラウザがテキストを自動エスケープするため、ユーザーが貼り付けた生テキストをそのまま安全に扱えます。
URLの終端検出は正規表現のキャラクタークラスで処理しています。[^\s、-] のように「日本語文字・全角記号は含めない」と指定することで、スペースなしで日本語に隣接するURL(「詳しくはhttps://example.com参照」など)を正確に切り出せます。URLの末尾に文末ピリオドや括弧が付いた場合は trimTrailing() 関数で取り除いています。
グローバル正規表現(/g フラグ)を RegExp.exec() でループ実行すると、lastIndex プロパティに次の検索開始位置が自動で記録されます。マッチ位置と直前の lastIndex の差分が「プレーンテキスト部分」になるため、テキスト→リンク→テキスト…の順にDOMノードを正確に生成できます。URLの末尾を削った際は LINK_RE.lastIndex を手動で調整する点に注意してください。
HTML・CSS・バニラJavaScriptのみで実装しており、フレームワーク不要でコピペすぐに動きます。
デモ
変換結果
※ デモテキスト内の example.com はIANAがドキュメント・サンプル用途に公式予約したドメインです(RFC 2606)。商業利用されておらず、サンプルコードに安心して使えます。
サンプルソース
3つのファイルを同じフォルダに保存し、index.html をブラウザで開くとすぐに動作確認できます。
ファイル名:index.html / style.css / script.js
— 保存時の文字コードは UTF-8 を指定してください(Shift-JISだと日本語が文字化けします)。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>テキスト自動リンク化 サンプル</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div class="autolink-wrap">
<label class="autolink-label" for="autolink-input">テキストを貼り付けてください</label>
<textarea class="autolink-textarea" id="autolink-input" rows="10"
placeholder="URLやメールアドレスを含むテキストを入力..."></textarea>
<div class="autolink-actions">
<button class="autolink-btn" id="autolink-btn" type="button">リンク化する</button>
<button class="autolink-reset" id="autolink-reset" type="button">リセット</button>
</div>
<div class="autolink-result-wrap" id="autolink-result-wrap" hidden>
<p class="autolink-result-label">変換結果</p>
<div class="autolink-result" id="autolink-result"></div>
</div>
</div>
<script src="./script.js"></script>
</body>
</html>
/* テキスト自動リンク化 — style.css
:root の変数を書き換えるだけで色をカスタマイズできます */
:root {
--color-accent: #2B7FE8; /* リンク色・ボタン色 */
--color-border: #E5E7EB; /* ボーダー色 */
--color-text: #1A2332; /* テキスト色 */
}
*, *::before, *::after { box-sizing: border-box; }
body {
font-family: sans-serif;
padding: 24px;
max-width: 640px;
margin: 0 auto;
color: var(--color-text);
}
/* ========== テキストエリア ========== */
.autolink-label {
display: block;
font-size: 14px;
font-weight: 600;
margin-bottom: 6px;
}
.autolink-textarea {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--color-border);
border-radius: 6px;
font-size: 14px;
line-height: 1.7;
resize: vertical;
font-family: inherit;
}
.autolink-textarea:focus {
outline: none;
border-color: var(--color-accent);
box-shadow: 0 0 0 3px rgba(43, 127, 232, 0.15);
}
/* ========== ボタン ========== */
.autolink-actions {
display: flex;
gap: 8px;
margin-top: 10px;
}
.autolink-btn {
background: var(--color-accent);
color: #fff;
border: none;
border-radius: 6px;
padding: 9px 20px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
font-family: inherit;
}
.autolink-btn:hover { opacity: 0.85; }
.autolink-reset {
background: #fff;
color: var(--color-text);
border: 1px solid var(--color-border);
border-radius: 6px;
padding: 9px 16px;
font-size: 14px;
cursor: pointer;
font-family: inherit;
}
.autolink-reset:hover { background: #f5f5f5; }
/* ========== 変換結果エリア ========== */
.autolink-result-wrap { margin-top: 20px; }
.autolink-result-label {
font-size: 13px;
font-weight: 600;
color: #888;
margin: 0 0 8px;
}
/* white-space: pre-wrap で改行をそのまま表示する */
.autolink-result {
padding: 16px;
border: 1px solid var(--color-border);
border-radius: 6px;
background: #fafafa;
font-size: 14px;
line-height: 1.8;
white-space: pre-wrap;
word-break: break-all;
}
.autolink-result a {
color: var(--color-accent);
text-decoration: underline;
}
// === テキスト自動リンク化 script.js ===
// サンプルテキスト(初期値)
var SAMPLE_TEXT =
'【社内連絡】5月度の勉強会についてお知らせします。\n\n' +
'詳細はこちらをご確認ください。https://example.com/meeting/2026-05\n\n' +
'ご不明な点は担当の山田までお問い合わせください。\n' +
'メール: [email protected]\n\n' +
'また、関連資料はhttps://example.com/docs/materialを参照ください。\n' +
'申し込みフォームはこちら→https://example.com/form\n\n' +
'※ Wordからコピペした場合もそのまま貼り付けてお試しください。';
// URL(グループ1)とメールアドレス(グループ2)をまとめて検出する正規表現
// 、-: ひらがな・カタカナ・漢字・全角記号。この範囲が来たらURLの終端とする
var LINK_RE = /(https?:\/\/[^\s、-]+)|([a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,})/g;
// テキストを正規化する(Wordコピペ対策)
function normalizeText(text) {
return text
.replace(/\r\n/g, '\n') // Windows改行コードをUnix改行に統一
.replace(/\r/g, '\n') // Mac旧改行コードをUnix改行に統一
.replace(/\t/g, ' ') // タブ文字を半角スペースに変換
.replace(/ /g, ' ') // ノーブレークスペースを半角スペースに変換
.replace(/ /g, ' '); // 全角スペースを半角スペースに変換
}
// URLの末尾についた句読点・括弧を除去する
// 例: 「(https://example.com)」の末尾 ) を取り除く
function trimTrailing(url) {
return url.replace(/[.,!;:))]+$/, '');
}
// テキストをリンク化してDOMノードの配列を返す
// innerHTML は使わず createElement/createTextNode で安全に組み立てる
function buildNodes(text) {
var nodes = [];
var lastIndex = 0;
var match;
LINK_RE.lastIndex = 0; // グローバル正規表現の検索位置をリセット
while ((match = LINK_RE.exec(text)) !== null) {
// マッチ前のプレーンテキスト部分をテキストノードとして追加する
if (match.index > lastIndex) {
nodes.push(document.createTextNode(text.slice(lastIndex, match.index)));
}
var a = document.createElement('a');
if (match[1]) {
// URLのリンク化
var url = trimTrailing(match[1]);
a.setAttribute('href', url);
a.textContent = url;
a.setAttribute('target', '_blank');
a.setAttribute('rel', 'noopener noreferrer');
// 末尾記号を取り除いた分だけ次の検索開始位置を手動で調整する
LINK_RE.lastIndex = match.index + url.length;
} else {
// メールアドレスのリンク化
a.setAttribute('href', 'mailto:' + match[2]);
a.textContent = match[2];
}
nodes.push(a);
lastIndex = LINK_RE.lastIndex;
}
// 残りのプレーンテキストを追加する
if (lastIndex < text.length) {
nodes.push(document.createTextNode(text.slice(lastIndex)));
}
return nodes;
}
// 変換を実行してDOMに反映する
function convertLinks() {
var textarea = document.getElementById('autolink-input');
var resultEl = document.getElementById('autolink-result');
var resultWrap = document.getElementById('autolink-result-wrap');
var nodes = buildNodes(normalizeText(textarea.value));
resultEl.textContent = ''; // 前回の変換結果をクリアする
for (var i = 0; i < nodes.length; i++) {
resultEl.appendChild(nodes[i]);
}
resultWrap.hidden = false;
}
// リセット: テキストエリアをサンプルに戻し、結果エリアを非表示にする
function resetDemo() {
document.getElementById('autolink-input').value = SAMPLE_TEXT;
document.getElementById('autolink-result').textContent = '';
document.getElementById('autolink-result-wrap').hidden = true;
}
// 初期化: テキストエリアにサンプルテキストをセット、ボタンにイベントを登録する
document.getElementById('autolink-input').value = SAMPLE_TEXT;
document.getElementById('autolink-btn').addEventListener('click', convertLinks);
document.getElementById('autolink-reset').addEventListener('click', resetDemo);
AI用プロンプト
以下のプロンプトをコピーしてAIに渡すと、同様のコンポーネントを生成できます。
ChatGPTやClaudeにこのプロンプトを渡すと、同様のコンポーネントをゼロから生成・カスタマイズできます。対象テキストの種類や正規表現の調整など、要件を追記して使うのがおすすめです。
※ このプロンプトを使ってもデモとまったく同じ動作にならない場合があります。AIの解釈や生成タイミングによって差が出ることをご了承ください。
💡 jQuery・Vue・React など特定のライブラリで実装したい場合は、プロンプトの末尾に「〇〇を使って実装してください」と追記してください。
# テキスト自動リンク化(URL・メール対応)作成依頼
## 概要
テキストエリアに入力した文章内のURLとメールアドレスを自動検出し、クリッカブルなリンクに変換して結果を表示するUIを実装してください。
## 要件
- テキストエリアにサンプル文書をあらかじめ表示する(初期値)
- 「リンク化する」ボタン押下で変換を実行し、テキストエリア下部に結果を表示する
- 「リセット」ボタンでテキストエリアを初期のサンプル文書に戻す
- URLは target="_blank" rel="noopener noreferrer" で新タブで開く
- メールアドレスは mailto: リンクにする
- 変換結果は white-space: pre-wrap でテキストの改行を保持して表示する
## テキスト正規化(変換前に必ず実施)
- タブ文字 \t → 半角スペース
- ノーブレークスペース → 半角スペース
- 全角スペース → 半角スペース
- \r\n / \r → \n
## URL検出仕様
- https:// または http:// で始まる文字列を検出する
- 終端: 半角スペース・改行・日本語文字(、- の範囲)でURLを終端させる
- 末尾の ". , ! ; : ) )" はURLから除外する(文末句読点・括弧のため)
## メールアドレス検出仕様
- [ローカル部]@[ドメイン].[TLD] の形式のみ検出する(例: [email protected])
- TLDが2文字以上あることを必須とする
- @ が2つ以上含まれる文字列は検出しない
## XSSセキュリティ要件(重要)
- innerHTML にユーザー入力テキストを直接代入しない
- テキスト部分は document.createTextNode() で生成する
- リンク要素は createElement('a') + setAttribute('href', ...) + textContent で生成する
- すべてのノードを appendChild でDOMに追加する
## 動作詳細
- URLとメールアドレスを1つの正規表現でまとめてマッチする(RegExp.exec() のループ)
- lastIndex を使って文字列を先頭から順に走査し、テキスト→リンク→テキスト…の順にDOMノードを生成する
- URLの末尾を trimTrailing() で削った場合は LINK_RE.lastIndex を手動で調整する
- 変換結果はテキストエリア下部に表示し、初期状態では非表示(hidden 属性)にしておく
## 技術仕様
- HTML / CSS / バニラJavaScript で実装
- 外部ライブラリ:なし
- レスポンシブ対応:不要(max-width: 640px 程度で中央寄せ)
## 出力形式
HTML・CSS・JavaScriptを分けて出力してください。
各ファイルは単独でコピー&ペーストして使えるよう記述してください。