github.com に表示される Markdown HTML が TOC が無くて見辛いので「すべて見出し単位でたたむ」「クリックで開閉」する Tampermonkey スクリプトをつくった
github.com はいいかげん TOC(目次)機能を実装するべきだと思う。Markdown で書かれた HTML の全体構造がわからなくて辛い。ふと「見出しだけ表示させてクリックした部分だけ開く」的な小細工を入れたら見やすいかも、とひらめいたので、早速試してみた。
デモ
要件
- Firefox
- Tampermonkey
Tampermonkey とは
Firefox アドオンの一つ(発祥は Google Chrome 拡張だが)で、Greasemonkey の後継となるスクリプト機構。
「このサイトを開いた時に」「この Javascript を実行してね」ってのを登録できる。
DOM とかいじって要らん要素を消したり、煩雑な情報を上手くまとめたり、と Javascript の書き方次第でウェブサイト(の今あなたが見ている表示)をカスタマイズできる。
インストール
1: Tampermonkey をインストールする
詳細は割愛。
2: 以下スクリプトを登録
// ==UserScript== // @name Folding per <h> in github.com // @namespace http://tampermonkey.net/ // @version 0.1 // @description H要素単位で折りたたむ&クリックで開閉するスクリプト // @author You // @match https://github.com/*/* // @require http://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js // @grant none // ==/UserScript== /* ■設定 - ROOT_ELEMENT_SELECTOR - @match ■仕様など - click イベントを拾っているので, H要素中でクリック絡む操作する時がうざい - 他サイトで動くかどうかは正直わからん... */ function hide_console_log(msg){ return; } //var cc = console.log; var cc = hide_console_log; DisplayController = (function(){ var DisplayController = function(h_elm_by_jq, index_in_body){ this._elm_by_jq = h_elm_by_jq; this._index = index_in_body; this._controllees = []; } var p = DisplayController.prototype; p.add = function(elm_by_jq){ this._controllees.push(elm_by_jq); } p.hide_all = function(){ var controllees = this._controllees; for(var i=0;i<controllees.length;i++){ var controllee = controllees[i]; controllee.hide(); } } p.show_all = function(){ var controllees = this._controllees; for(var i=0;i<controllees.length;i++){ var controllee = controllees[i]; controllee.show(); } } p.define_events = function(){ var controllees = this._controllees; this._elm_by_jq.click(function(){ for(var i=0;i<controllees.length;i++){ var controllee = controllees[i]; controllee.toggle(); } }); } return DisplayController; })(); $(function(){ var ROOT_ELEMENT_SELECTOR = '.markdown-body'; //var ROOT_ELEMENT_SELECTOR = 'body'; // ロジック上, 最後の要素が state2 → state1 にならないので, // ダミーの h 要素を入れとく. var dummy_element_html_for_logic = '<h6></h6>'; $(ROOT_ELEMENT_SELECTOR).append(dummy_element_html_for_logic) var children = $(ROOT_ELEMENT_SELECTOR).children(); var STATE_0_NOT_STARTED_YET = 'state0'; var STATE_1_FOUND_HEADER = 'state1'; var STATE_2_PARSING_THE_REST_ELEMENT_FOR_HIDDING = 'state2'; var current_state = STATE_0_NOT_STARTED_YET; var controllers = []; var current_controller = null; var controller = null; children.each(function(i, elm_by_dom){ var elm_by_jq = $(elm_by_dom); var tagname = elm_by_dom.tagName.toLowerCase(); var is_header_element = false; if(tagname === 'h1' || tagname === 'h2' || tagname === 'h3' || tagname === 'h4' || tagname === 'h5' || tagname === 'h6'){ is_header_element = true; } if(current_state === STATE_0_NOT_STARTED_YET){ if(is_header_element){ current_state = STATE_1_FOUND_HEADER; cc('state1::ここでヘッダを切り替える >'+tagname); controller = new DisplayController(elm_by_jq, i); controllers.push(controller); current_controller = controller; } return true; } if(current_state === STATE_1_FOUND_HEADER){ current_state = STATE_2_PARSING_THE_REST_ELEMENT_FOR_HIDDING; // fall through } if(current_state === STATE_2_PARSING_THE_REST_ELEMENT_FOR_HIDDING){ if(is_header_element){ current_state = STATE_1_FOUND_HEADER; cc('state2(found)::新しいヘッダ切替前に, 全部 hide() と開閉用イベント登録'); current_controller.hide_all(); current_controller.define_events(); cc('state1::ここでヘッダを切り替える >'+tagname); controller = new DisplayController(elm_by_jq, i); controllers.push(controller); current_controller = controller; return true; } cc('state2(notfound)::ここでヘッダに対応する「見せる見せないを切り替えるやつ」を積んでいく >'+tagname); current_controller.add(elm_by_jq); return true; } throw new Error('ここには来ないはず'); }); });
雑談
主にコードについての話。
どうやって実装したか
まず HTML 文書構造が以下のようになっていることを想定する。
- 便宜上 h1, h2, ... h6 要素を hx 要素と書く
- hx 要素以外を not_hx 要素と書く
<RootElement> <hx></hx> <not_hx></not_hx> <not_hx></not_hx> ... <hx></hx> <not_hx></not_hx> <not_hx></not_hx> ...
つまり特定の要素を基準として、hx が 1 個 → not_hx が n 個、という順でシーケンシャルに並んでいるような構造を想定。
あとはこのシーケンシャル構造を「ある hx は、n 個の not_hx を持つ」という親子構造だと解釈した上で、hx 単位で「まとめて消す」「まとめて表示する」を行う処理とイベントを仕込んだ。
なお、仕込む際のパースは RootElement の children() を取得して、each() で行った。要素を一個ずつ見ていく&状態変数導入して漏れがないように。
使い心地
しばらく試してみる。
長い Markdown の時だけ使ってみて、見出しだけ読む → 読みたいところを開いてサクっと読む、みたいな拾い読み流し読みがスムーズにできるのではないかなぁと期待している。
改造の余地
この仕組みを使うかどうかの判定を Tampermonkey の @match で制御するのは面倒くさいので、以下のようにすればいいかなぁとか何とか思案中。
- @match は
*
とかにしてどこでも動作するように - サイト読み込んだだけでは何も動かない
- 何かアクションを行うと、動く
- アクション is 何?
- どっかダブルクリックするとか、ショートカットキー仕込むとか……?
ただ、root element がサイトによって違う(ローカルでテストした HTML だと body だし、GitHub だと .markdown-body だし、他のサイトやブログでは当然違ってくる)ので、これをどうやって吸収すればいいかがわからん。