1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-05-30

はじめに

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

実装する機能

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

用意するファイル

1. manifest.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

// 除外するサイトのリスト
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

.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

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

カスタマイズ(除外サイトの追加)

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

まとめ

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

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

ぜひお試しください。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?