はじめに
「コードの差分を確認したい」というとき、ChatGPT や Claude にコードを貼り付けて比べてもらっていませんか?
diff は確定的な処理です。どこが違うかは計算で一瞬わかる。それをわざわざ LLM に推論させてトークンを消費するのは過剰です。しかも LLM は長いコードを省略したり、見落としたりすることがあります。
差分確認は専用ツールに任せ、AI はもっと高度な判断に使う。
そう考えて diff チェッカーをリニューアルしました。
実装した主な改善点
1. 並列表示(サイドバイサイド diff)
unified diff(+・- 記号)から左右並列表示に変更しました。
# before: unified diff(初学者には読みにくい)
- border-radius: 50%
+ border-radius: 5px
# after: 左右並列(直感的)
左(元コード) 右(自分のコード)
border-radius: 50% | border-radius: 5px
実装は diff.js の diffLines() を使い、removed と added が連続する場合は対応する行としてペアリングします。
while (i < parts.length) {
const part = parts[i];
const next = parts[i + 1];
if (part.removed && next && next.added) {
// 変更行:1:1でペアリングして単語diff→文字diffへ
const removedLines = splitLines(part.value);
const addedLines = splitLines(next.value);
const pairLen = Math.min(removedLines.length, addedLines.length);
// ...
i += 2;
}
}
2. ハイブリッド diff(行→単語→文字)
行単位だけでは「行のどこが違うか」がわかりません。3段階で精度を上げました。
// 変更行のペアに対して単語レベル diff
const words = Diff.diffWords(removedLines[j], addedLines[j]);
words.forEach((w, wi) => {
if (w.removed && words[wi + 1]?.added) {
// 削除側:文字レベルで精密に(消えた文字だけ赤)
Diff.diffChars(w.value, words[wi + 1].value).forEach(({ added, removed, value }) => {
if (removed) h1 += `<span class="char-removed">${escapeHTML(value)}</span>`;
else if (!added) h1 += escapeHTML(value);
});
// 追加側:単語丸ごと緑
h2 += `<span class="char-added">${escapeHTML(words[wi + 1].value)}</span>`;
}
});
結果:border vs boder の場合、削除側は d だけ赤、追加側は boder 丸ごと緑になります。
3. 全角スペース検出(センチネル方式)
全角スペース(\u3000)を通常の空白として扱うと、diffWords がトークン境界として認識してしまい、誤った側に表示されるバグが発生します。
解決策として、Unicode プライベート領域のセンチネル文字(\uE000)に置換してから diff を実行します。
const FW = "\uE000"; // U+E000: プライベート領域(空白でないため diffWords が境界と認識しない)
const normalizeCode = (text, ignoreWhitespace) => {
let normalized = text
.replace(/\r\n/g, "\n")
.replace(/\r/g, "\n")
.replace(/\u3000/g, FW); // 全角スペース → センチネル
if (!ignoreWhitespace) return normalized;
return normalized.split("\n").map((l) => l.trimEnd()).join("\n");
};
// 表示時にセンチネルを □ マークに変換
const escapeHTML = (str) =>
str
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/\uE000/g, '<span class="fullwidth-marker">□</span>');
これにより:
- 全角スペースが my code 側にある → 右パネルのみ □ が表示される ✓
- 両側にある → 両パネルに □ が表示される ✓
- ページ上部に警告バナーを表示 ✓
4. ファイルのドラッグ&ドロップ
FileReader API でテキストファイルを直接読み込めるようにしました。
const handleDrop = (target) => (e) => {
e.preventDefault();
target.classList.remove("drag-over");
const file = e.dataTransfer.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
target.innerHTML = escapeHTML(ev.target.result);
};
reader.readAsText(file);
};
5. ペースト処理を Selection API に移行
document.execCommand('insertText') は非推奨のため、Selection API に置き換えました。
const handlePaste = (e) => {
e.preventDefault();
const text = (e.clipboardData || window.clipboardData).getData("text");
const sel = window.getSelection();
if (!sel.rangeCount) return;
const range = sel.getRangeAt(0);
range.deleteContents();
const tpl = document.createElement("template");
tpl.innerHTML = escapeHTML(text);
range.insertNode(tpl.content);
range.collapse(false);
};
GitHub Actions で FTP 自動デプロイ
レンタルサーバー(ConoHa WING)へのデプロイを GitHub Actions で自動化しました。
name: Deploy via FTP
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: FTP Deploy
uses: SamKirkland/FTP-Deploy-Action@v4.3.4
with:
server: ${{ secrets.FTP_SERVER }}
username: ${{ secrets.FTP_USERNAME }}
password: ${{ secrets.FTP_PASSWORD }}
local-dir: ./public/
server-dir: ${{ secrets.FTP_SERVER_DIR }}
exclude: |
**/.git*
**/.git*/**
**/node_modules/**
**/.github/**
main へ push するだけで自動デプロイされます。local-dir: ./public/ を指定することで、サーバーサイドのコードを誤ってアップロードしないようにしています。
まとめ
| 改善点 | 実装 |
|---|---|
| 並列diff表示 |
diffLines() + removed/added ペアリング |
| 文字レベルハイライト |
diffWords() + diffChars() のハイブリッド |
| 全角スペース検出 |
\uE000 センチネル方式 |
| ファイルD&D | FileReader API |
| ペースト処理 | Selection API |
| 自動デプロイ | GitHub Actions + FTP |
diff に AI を使っていたトークン、もっと価値あることに使いましょう。
SEOスコアチェックツール: SEO_CHECK — RINIAディレクターツール。
Web制作・SEO関連の技術情報サイト: CodeQuest.work