Chrome機能拡張 タイトルとURLをコピーするコマンドを作った

# はじめに
## 解決したかった課題
githubのREADME.mdやConfluence、その他いろいろ、近頃MarkDownで書くことが増えてきました。リンクをコピーするときに、URLだけだったらまぁいいのですが、**タイトルをコピーするのがめんどくさい**。皆さんどうしているのでしょうか。

ちなみに私が調べた一番簡単な方法(Chrome)は、URLの右端にある☆をクリックして、ブックマーク追加の画面を出し、タイトルをコピーして、削除ボタンを押してブックマークには追加しないという変な技ｗそんなことをしながら、まともな方法でやりたいなと、ついでに効率アップしたいなと、そういうわけでChrome機能拡張を作ってみました。

## 作ったもの
任意のページを右クリックして出てくるコンテキストメニューに、タイトルとURLをコピーする機能が入ります。３つあって、MarkDown形式、素でタイトルだけ、HTMLのAタグ形式です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/722610/990e1cec-04aa-fd42-bce3-b5c2c9d2eb13.png)

↓この記事のChrome機能拡張をインストールするにはこちら

https://chrome.google.com/webstore/detail/copy-markdown-title/cpegekkinibgeoehmohcljgahicfeehe?hl=ja&authuser=0

↓ソースはこちら

https://github.com/yo16/chrome_copy_title

## 参考
公式のChrome機能拡張のページ（英語）

https://developer.chrome.com/docs/extensions/mv3/


# 技術的な説明
技術的にはそう難しくないですが、お作法を知らないと全然動かなくてお作法がわからなくて苦労するパターンです。ドキュメントは英語で、割と頻繁に変わるから古い記事は当てになったりならなかったり。

## manifest.json
[Manifest file format - Chrome Developers](https://developer.chrome.com/docs/extensions/mv3/manifest/)

manifestでは、概要説明と機能を宣言します。ここで宣言した機能しか使えないし、Chrome機能拡張へ正式に登録するときには、その機能の必要性の説明が求められます。ちょっと特殊かもと思ったのは`scripting`。Clipboardへアクセスするために必要でした。（アクセスする方法は後述）

`background`でJavaScriptを呼んでいるのは、今回作るのがコンテキストメニューなので、Chromeを起動したらすぐコンテキストメニューに機能を追加する必要があるためです。そうでないものとしては、Google翻訳とか、呼び出されたときに読み込んで動作すればよいもの。この記述のおかげで、Chromeが起動したらすぐ`copy_md_title.js`が呼び出されます。
```manifest.json
{
    "manifest_version": 3,

    "name": "Copy Markdown Title",
    "description": "Copy the title in markdown format or only title by page context menu.",
    "version": "1.0.0",
    "icons": {
        "16": "CopyMarkdownTitle_icon16.png",
        "48": "CopyMarkdownTitle_icon48.png",
        "128": "CopyMarkdownTitle_icon128.png"
    },
    
    "permissions": [
        "activeTab",
        "contextMenus",
        "scripting"
    ],
    "background": {
        "service_worker":"./copy_md_title.js"
    }
}
```

## copy_md_title.js
大きく２つの処理を実装しました。
1. コンテキストメニューを作成して、それがクリックされたら関数を呼ぶようListener登録する
1. 呼び出される関数で、タイトルとURLを取得して加工して、クリップボードへ出力する

### 最初に呼び出される処理
`chrome.runtime.onInstalled`によって、起動されたらすぐ呼び出される処理です。
```JavaScript:copy_md_title.js (8行目)
chrome.runtime.onInstalled.addListener((details) => {
    ...
}
```
contextMenusにcreateでメニューを追加。
[API reference](https://developer.chrome.com/docs/extensions/reference/)のページ内で、`contextMenus`を検索すると[chrome.contextMenus](https://developer.chrome.com/docs/extensions/reference/contextMenus/)に飛べる。このページのメソッドのcreateを見れば引数がわかる。APIを調べるときは大体このパターン。
```JavaScript:copy_md_title.js (9行目)
    // コンテキストメニューに追加
    chrome.contextMenus.create({
        id: 'copy_url_md_format_topmenu',
        title: 'Copy [Title](URL)',
        contexts: ['page'],
        type: 'normal',
        visible: true,
    });
    chrome.contextMenus.create({
        id: 'copy_url_md_format_mdformat',
        title: 'Copy [Title](URL)',
        contexts: ['page'],
        type: 'normal',
        visible: true,
        parentId: 'copy_url_md_format_topmenu',
    });
    ...
```
コンテキストメニューが呼ばれたときに呼び出される。
作成したときに決めたidが、`info.menuItemId`に渡ってくるので、そこで未知のIDだったらはじくようにはしている。（いらないかもしれない。）
```JavaScript:copy_md_title.js (42行目)
    // 選択時の処理
    chrome.contextMenus.onClicked.addListener((info, tab) => {
        ...
    }
```
そのあと、各IDごとにタイトルとURLをいじってコピーされる文字列を成型する。
```JavaScript:copy_md_title.js (55行目)
        // メニューによってコピーする文字列を変える
        let text_to_be_copied = '';
        if (menu_id == 'copy_url_md_format_mdformat') {
            text_to_be_copied = '['+tab.title+']('+tab.url+')';
        } else if (menu_id == 'copy_url_md_format_title') {
            text_to_be_copied = tab.title;
        } else if (menu_id == 'copy_url_md_format_htmlformat') {
            text_to_be_copied = '<a href="'+tab.url+'">'+tab.title+'</a>';
        }
```
コピーを実行する関数を呼び出します。ここで`scripting`の権限が必要。
```JavaScript:copy_md_title.js (66行目)
        try {
            // コピー
            const tabId = tab.id;
            chrome.scripting.executeScript({
                target: { tabId: tabId },
                func: copyText,
                args: [{text: text_to_be_copied}],
            });
        } catch (err) {
            console.error('Fail to copy: ', err);
        }
```
### メニュー選択時に呼び出される関数 copyText
呼び出される関数の内容は、`navigator.clipboard.writeText()`を呼び出しているだけ。古いバージョンでは`clipboardWrite`を使っていたようですが、使わなくなったみたい。
- クリップボード関連の参考
  - [Transitioning from Chrome Apps - Chrome Developers](https://developer.chrome.com/docs/apps/migration/#clipboard)
  - [クリップボードアクセスのブロックを解除する](https://web.dev/async-clipboard/)
```JavaScript:copy_md_title.js (2行目)
function copyText(arg) {
    const text = arg.text;
    if (text.length > 0) {
        navigator.clipboard.writeText(text);
    }
}
```

# まとめ
作ったコンテキストメニューは、デベロッパーモードでさっそく頻繁に使っています。このQiitaの記事でも何度も。（そしてバグを見つけたり。。）

作った機能は単純だし、ソースも短い。けど周辺のものの敷居が高くて変な時間がかかりました。コーディング以外にかかった時間は、アイコンの作成と、Chrome機能拡張に正式登録する作業。こういうのも個人作業ならではなのかもしれません。会社だったら分担したいところ。

ただ、今回これで１つ突破できたので、次に何かを作りたくなったら少しマシになった気がします。この記事を読んで、誰かの敷居を下げることにも協力できていたら幸いです。