Markdownのコードブロックにコピー機能を追加するChrome拡張機能を作ってみた

## はじめに

ローカルPCにあるMarkdown形式のドキュメントを読んでいる時に、コードブロックをコピーするのが面倒だったので、コピーボタンを自動で追加するChrome拡張機能を生成AI作成してみました。

## 実装する機能

- Markdownのコードブロック箇所にコピーボタンを自動で追加する
- GitHubやQiitaなど、既にコピーボタンがあるサイトでは表示しない制御ができるようにする
- ボタンをクリックするとその内容をクリップボードにコピーする

## 用意するファイル

### 1. manifest.json

```json
{
  "manifest_version": 3,
  "name": "Markdown Code Copy",
  "version": "1.0",
  "description": "Add copy buttons to markdown code blocks",
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"],
      "css": ["styles.css"],
      "all_frames": true
    }
  ],
  "permissions": ["activeTab"],
  "host_permissions": ["file://*/*"]
}
```

### 2. content.js

```javascript
// 除外するサイトのリスト
const excludedSites = [
  'github.com',
  'qiita.com',
  'zenn.dev',
  'stackoverflow.com'
];

// 現在のサイトが除外対象かチェック
function isExcludedSite() {
  const hostname = window.location.hostname;
  
  // ローカルファイル（file://）の場合はhostnameが空文字になるので除外しない
  if (window.location.protocol === 'file:') {
    return false;
  }
  
  // hostnameが空の場合（ローカルファイル等）は除外しない
  if (!hostname) {
    return false;
  }
  
  return excludedSites.some(site => hostname.includes(site));
}

// デバッグ用ログ（開発時のみ使用）
function debugLog() {
  console.log('Current URL:', window.location.href);
  console.log('Protocol:', window.location.protocol);
  console.log('Hostname:', window.location.hostname);
  console.log('Is excluded:', isExcludedSite());
}

// コードブロックを検出してコピーボタンを追加
function addCopyButtons() {
  // デバッグログ（必要に応じてコメントアウト）
  debugLog();
  
  // 除外サイトの場合は何もしない
  if (isExcludedSite()) {
    console.log('Site is excluded, skipping...');
    return;
  }
  
  const codeBlocks = document.querySelectorAll('pre code, pre, code');
  console.log('Found code blocks:', codeBlocks.length);
  
  codeBlocks.forEach((block, index) => {
    // 既にボタンが追加されている場合はスキップ
    if (block.parentElement.querySelector('.copy-btn')) {
      console.log(`Block ${index}: Already has copy button`);
      return;
    }
    
    // 複数行のコードブロックのみ対象
    if (block.textContent.split('\n').length < 2) {
      console.log(`Block ${index}: Single line, skipping`);
      return;
    }
    
    console.log(`Block ${index}: Adding copy button`);
    
    const copyBtn = document.createElement('button');
    copyBtn.textContent = 'Copy';
    copyBtn.className = 'copy-btn';
    copyBtn.onclick = () => copyToClipboard(block.textContent);
    
    // ボタンの配置
    const wrapper = block.parentElement;
    wrapper.style.position = 'relative';
    wrapper.appendChild(copyBtn);
  });
}

// クリップボードにコピー
async function copyToClipboard(text) {
  try {
    await navigator.clipboard.writeText(text);
    showCopyFeedback();
  } catch (err) {
    // フォールバック方法
    const textArea = document.createElement('textarea');
    textArea.value = text;
    document.body.appendChild(textArea);
    textArea.select();
    document.execCommand('copy');
    document.body.removeChild(textArea);
    showCopyFeedback();
  }
}

// コピー完了のフィードバック
function showCopyFeedback() {
  const feedback = document.createElement('div');
  feedback.textContent = 'Copied!';
  feedback.className = 'copy-feedback';
  document.body.appendChild(feedback);
  
  setTimeout(() => {
    document.body.removeChild(feedback);
  }, 2000);
}

// ページ読み込み時とDOM変更時に実行
addCopyButtons();
const observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
```

### 3. styles.css

```css
.copy-btn {
  position: absolute;
  top: 8px;
  right: 8px;
  background: #24292e;
  color: white;
  border: none;
  padding: 4px 8px;
  border-radius: 4px;
  font-size: 12px;
  cursor: pointer;
  opacity: 0.7;
  transition: opacity 0.2s;
}

.copy-btn:hover {
  opacity: 1;
}

.copy-feedback {
  position: fixed;
  top: 20px;
  right: 20px;
  background: #28a745;
  color: white;
  padding: 8px 16px;
  border-radius: 4px;
  z-index: 10000;
  animation: fadeInOut 2s ease-in-out;
}

@keyframes fadeInOut {
  0%, 100% { opacity: 0; }
  10%, 90% { opacity: 1; }
}
```

## インストール方法

1. 新しいフォルダを作成して上記の3ファイルを保存する
2. Chromeで `chrome://extensions/` にアクセス
3. 右上の「デベロッパーモード」を有効にする
4. 「パッケージ化されていない拡張機能を読み込む」をクリック
5. 手順の1で作成したフォルダを選択する
6. **重要**: ローカルPCのMarkdownファイルで利用する場合は、拡張機能の詳細ページで「ファイルの URL へのアクセスを許可する」をONにする

## 完成イメージ
* ローカルPCにあるMarkdownファイルをChrome開くと、コードブロックの右端にCopyボタンが追加されました。

![SnapCrab_NoName_2025-5-30_15-13-36_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106992/4a6da194-a462-4d6a-bc2f-a885e148e5ce.png)

* また、content.jsの除外サイトに「qiita.com」があるので、以下のコードブロックにはCopyボタンは表示されず、ちゃんと制御されていることがわかります。
```
このコードブロックにはコピーボタンは表示されません。
```

## カスタマイズ（除外サイトの追加）
* コピーボタンを表示したくないサイトを追加する場合は、content.jsの以下の箇所に対象サイトのドメインを追加してください
```javascript
const excludedSites = [
  'github.com',
  'qiita.com',
  'zenn.dev',
  'stackoverflow.com',
  'ほげほげ.com'  // 追加
];
```

## まとめ

Chrome拡張機能でMarkdownコードブロックを効率的にコピーできるようになりました。

作業の生産性向上に貢献できればうれしいです。

ぜひお試しください。