VS Code の Markdown Preview に「先頭に戻るボタン」と「ページ内検索」ボタンを追加してみた


> **2017/1/7:**
> Preview Editor に「ページ内検索」ボタンを追加する拡張機能を作成してみました。Markdown preview 以外にリリースノートや Preview Editor に何かを出力する拡張機能で検索が利用できます。
>
> "Find in Page" in Preview Editor 
> https://marketplace.visualstudio.com/items?itemName=satokaz.vscode-findinpage


VS Code そのものを改造するわけでも、エクステンションを追加するわけでもなく、単にその機能を実現する HTML と Javascript を Markdown file に貼り付けるだけでできちゃう系のお話なのですが。
やったね。  
先人の知恵や成果に感謝。

これは、VS Code の Markdown モードで利用される Markdown パーサ: markdown-it が HTML タグの埋め込みを制御する `Enable HTML tags in source` 設定が許可されていて、Electron を使っているから可能なことかなと。

```typescript
		const md = require('markdown-it')({
			html: true,
```

![スクリーンショット 2016-11-03 15.28.28.png](https://qiita-image-store.s3.amazonaws.com/0/65800/b10840e3-14d9-6416-4a83-4c0f89c02ff8.png "スクリーンショット 2016-11-03 15.28.28.png")


## 「先頭に戻る」ボタンは VS Code のドキュメントから

Preview エディタ内に、よくある先頭に戻るためのボタンを追加します。  
VS Code 1.6 から、リリースノートの内容を Preview エディタで参照できるようになりましたが、このリリースノートに先頭に戻るボタンがついていたわけですよ。  
これ、どうやるのかなぁと興味を持地、下記の commit を参考にしました。 

* [Modularize release notes css. Remove it from web.](https://github.com/Microsoft/vscode-docs/commit/bf5a91d46970ddf50b79e3daced3bf5a7afe5667)
```
commit bf5a91d46970ddf50b79e3daced3bf5a7afe5667
Author: bgashler1@users.noreply.github.com <bgashler1@users.noreply.github.com>
Date:   Fri Oct 7 11:56:21 2016 -0700

    Modularize release notes css.  Remove it from web.
    Code reviewed by @gregvanl
```

下記の CSS ファイルをダウンロードし、適当なディレクトリに配置して、

* [inproduct_releasenotes.css](https://raw.githubusercontent.com/Microsoft/vscode-docs/master/release-notes/css/inproduct_releasenotes.css)

Markdown ファイルの最後にでも下記の HTML タグを貼り付けると、

```html
<!-- In-product release notes styles.  Do not modify without also modifying regex in gulpfile.common.js -->
<a id="scroll-to-top" role="button" aria-label="scroll to top" onclick="scroll(0,0)"><span class="icon"></span></a>
<link rel="stylesheet" type="text/css" href="inproduct_releasenotes.css"/>
```

Preview 時にボタンが出現します。  
これで、いつでも先頭に戻れますね。

![スクリーンショット 2016-11-03 13.50.41.png](https://qiita-image-store.s3.amazonaws.com/0/65800/27fea59d-f705-e7ff-4894-c4d8c6bb954e.png "スクリーンショット 2016-11-03 13.50.41.png")


## 「ページ内検索」には Cool Javascript Find on this Page... Fixed Position Edition を利用

Preview エディタの中で検索したくてもできない仕様ですが、検索したくなったので。  
すでに、Issue として上がっているので、そのうち VS Code に実装されると思います。思うだけです。
ちなみ、ここで紹介するやり方はリリースノートの Preview には適用できません。

* [Find is not available in Markdown Preview mode #2187](https://github.com/Microsoft/vscode/issues/2187)

Preview エディタに表示されるコンテンツのページ内検索には、[Cool Javascript Find on this Page... Fixed Position Edition](http://www.seabreezecomputers.com/tips/find6.htm) を利用します。

* [Cool Javascript Find on this Page...](http://www.seabreezecomputers.com/tips/find.htm)

これは、"Find on This Page..." ボタンを HTML に追加し、ページ内の検索と検索対象の文字をハイライトさせることを可能にする JavaScript ですが、"Find on This Page..." ボタンをアイコン化し、右下に固定配置する Fixed Position Edition がコンパクトで良い感じなので、これを利用することにします。

[Cool Javascript Find on this Page... Fixed Position Edition](http://www.seabreezecomputers.com/tips/find6.htm) から、ダウンロードした `find6.js` を適当なディレクトリに配置し、下記のように `<script></script>` タグを Markdown または HTML ドキュメントの最後などに貼り付けます。(下記の例では、ドキュメントと同じディレクトリに配置してあります)

```html
<script type="text/javascript" id="cool_find_script" src="find6.js">
</script>
```

そして、Preview を実行すると、右下に虫眼鏡のアイコンが表示され、ページ内を検索することが可能になります。


虫眼鏡アイコンをクリックすると、

![スクリーンショット 2016-11-03 13.16.12.png](https://qiita-image-store.s3.amazonaws.com/0/65800/cdb259ee-c6dd-b762-033c-b5bda54ff8c5.png "スクリーンショット 2016-11-03 13.16.12.png")

検索文字列を入力できます。

![スクリーンショット 2016-11-03 13.16.23.png](https://qiita-image-store.s3.amazonaws.com/0/65800/5f81b416-73c4-8b90-d880-3f86c7e73053.png "スクリーンショット 2016-11-03 13.16.23.png")


## 「先頭に戻る」ボタンと「ページ内検索」ボタンを同時に使ってみる

こんな感じで、何も手を加えなくても 2 つの機能が同居できるので個人的には良い感じです。

![スクリーンショット 2016-11-03 13.57.32.png](https://qiita-image-store.s3.amazonaws.com/0/65800/634c1730-962f-c750-e9ec-6580571c4027.png "スクリーンショット 2016-11-03 13.57.32.png")


## シングルファイルで完結させる

minify したものも貼り付けておきます。  
これを Markdown ファイルの最後に貼り付けて、preview を実行すると、右下に「先頭に戻る」ボタンとページ内検索を行うことが可能二なる虫眼鏡アイコンが追加されます。

って、Qiita 投稿のためにこの記事を書いている Kobito でも Preview 画面に表示されていますね・・・(投稿したらどうなるのだろう→投稿されたものでは無効になりました)

* 「先頭に戻る」ボタン

```html
<!-- scroll-to-top-->
<style type="text/css">h1,h2,h3,h4{margin-bottom:1rem}html{font-size:10px}body{padding-bottom:100px}h1,h2,h3{font-weight:lighter;margin-top:2rem}h2,h4{margin-top:3rem}h1{font-size:4rem;margin-bottom:1.5rem}h2{font-size:3rem}h3{font-size:2.2rem}h4{font-size:1.2rem;text-transform:uppercase}#scroll-to-top{position:fixed;width:40px;height:40px;right:25px;bottom:25px;background-color:#444;border-radius:50%;cursor:pointer;box-shadow:1px 1px 1px rgba(0,0,0,.25)}#scroll-to-top:hover{background-color:#007acc;box-shadow:2px 2px 2px rgba(0,0,0,.25)}body.vscode-light #scroll-to-top{background-color:#949494}body.vscode-light #scroll-to-top:hover{background-color:#007acc}body.vscode-high-contrast #scroll-to-top{background-color:#000;border:2px solid #6fc3df;box-shadow:none}body.vscode-high-contrast #scroll-to-top:hover{background-color:#007acc}#scroll-to-top span.icon::before{content:"";background:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE5LjIuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAxNiAxNiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTYgMTY7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojRkZGRkZGO30KCS5zdDF7ZmlsbDpub25lO30KPC9zdHlsZT4KPHRpdGxlPnVwY2hldnJvbjwvdGl0bGU+CjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik04LDUuMWwtNy4zLDcuM0wwLDExLjZsOC04bDgsOGwtMC43LDAuN0w4LDUuMXoiLz4KPHJlY3QgY2xhc3M9InN0MSIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2Ii8+Cjwvc3ZnPgo=);width:1.6rem;height:1.6rem;position:absolute;left:calc(50% - 1.6rem / 2);top:calc(50% - 1.6rem / 2)}</style>
<a id="scroll-to-top" role="button" aria-label="scroll to top" onclick="scroll(0,0)"><span class="icon"></span></a>
```

* 「ページ内検索」ボタン

```html
<!--find6.js-->
<script type="text/javascript" id="cool_find_script">
var coolfind={lock_button:1,find_root_node:null,test_mode:!1,find_button_html:"",highlights:[],find_pointer:-1,find_text:"",found_highlight_rule:0,found_selected_rule:0};coolfind.create_find_div=function(){var e=document.createElement("div"),n=document.getElementById("cool_find_script"),o="",i="display: inline-block; vertical-align: middle; z-index:200;",t="display: inline-block;  min-height: 1.15em; min-width: 1.5em; max-width: 3em; vertical-align: middle; text-align: center; font-size: 1em;border: 1px solid black; background: lightgray; cursor: pointer; padding: 1px; margin: 4px; -webkit-user-select:none; -ms-user-select: none;",l="background-color: #e5e5e5; display: none;",d="display: inline; max-width: 55%;";coolfind.lock_button&&(l+="float: left;"),coolfind.addCss(".cool_find_btn {"+t+"}"),coolfind.addCss(".cool_find_menu {"+l+"}"),coolfind.addCss(".cool_find_input {"+d+"}"),"undefined"==typeof SVGRect?coolfind.find_button_html="Find":coolfind.find_button_html='<svg width="1.15em" height="1.15em" viewbox="0 0 30 30"><circle cx="18" cy="12" r="8" stroke="black" stroke-width="2" fill="#fff" fill-opacity="0.4" /><line x1="13" y1="17" x2="0" y2="30" stroke="black" stroke-width="2" /><line x1="10" y1="20" x2="0" y2="30" stroke="black" stroke-width="4" /></svg>',e.id="cool_find_div",e.style.cssText=i,o+="<button class='cool_find_btn' id='cool_find_btn' title='Find on this page' onclick='coolfind.find_menu(this)'>"+coolfind.find_button_html+"</button> ",coolfind.lock_button&&(e.style.position="fixed",e.style.bottom="3em",e.style.right="1em"),n.parentNode.insertBefore(e,n.nextSibling),o+="<span class='cool_find_menu' id='cool_find_menu'>"+'<form onsubmit="return false;" style="display: inline"><input type="search" class="cool_find_input" id="cool_find_text" onchange="coolfind.resettext();" placeholder="Enter text to find"><span id="cool_find_msg"> </span></form>',o+="<button class='cool_find_btn' title='Find Previous' onclick='coolfind.findprev();'>&#9650;</button><button class='cool_find_btn' id='cool_find_next' title='Find Next' onclick='coolfind.findit();'>&#9660;</button> ",o+="</span>",e.innerHTML=o;for(var c=document.styleSheets,f=0;f<c.length;f++){var r=c[f].rules?c[f].rules:c[f].cssRules;if(null!=r)for(var s=0;s<r.length;s++)".highlight"==r[s].selectorText?coolfind.found_highlight_rule=1:".find_selected"==r[s].selectorText&&(coolfind.found_selected_rule=1)}},coolfind.find_menu=function(e){var n=document.getElementById("cool_find_text");"inline-block"!=e.nextElementSibling.style.display?(e.nextElementSibling.style.display="inline-block",e.innerHTML="X",e.title="Close",document.addEventListener?document.addEventListener("keydown",coolfind.checkkey,!1):document.attachEvent("onkeydown",coolfind.checkkey),n.focus(),n.select(),n.setSelectionRange(0,9999)):(e.nextElementSibling.style.display="none",e.innerHTML=coolfind.find_button_html,e.title="Find on this page",coolfind.unhighlight(),document.removeEventListener?document.removeEventListener("keydown",coolfind.checkkey,!1):document.detachEvent("onkeydown",coolfind.checkkey))},coolfind.addCss=function(e){var n=document.createElement("style");n.type="text/css",n.styleSheet?n.styleSheet.cssText=e:n.appendChild(document.createTextNode(e)),document.getElementsByTagName("head")[0].appendChild(n)},coolfind.highlight=function(e,n){for(n||(n=document.body),n=n.firstChild;n;n=n.nextSibling)if(3==n.nodeType){var o=n,i=0;if(i=o.nodeValue.toLowerCase().indexOf(e.toLowerCase()),i>-1){var t=o.nodeValue.substr(0,i),l=o.nodeValue.substr(i,e.length),d=document.createTextNode(o.nodeValue.substr(i+e.length)),c=document.createElement("span");1==coolfind.found_highlight_rule?c.className="highlight":c.style.backgroundColor="yellow",c.appendChild(document.createTextNode(l)),o.nodeValue=t,o.parentNode.insertBefore(d,o.nextSibling),o.parentNode.insertBefore(c,o.nextSibling),coolfind.highlights.push(c),c.id="highlight_span"+coolfind.highlights.length,n=n.nextSibling}}else 1==n.nodeType&&n.nodeName.match(/textarea/i)&&!coolfind.getStyle(n,"display").match(/none/i)?coolfind.textarea2pre(n):1!=n.nodeType||coolfind.getStyle(n,"visibility").match(/hidden/i)||1!=n.nodeType||coolfind.getStyle(n,"display").match(/none/i)||coolfind.highlight(e,n)},coolfind.unhighlight=function(){for(var e=0;e<coolfind.highlights.length;e++){var n=coolfind.highlights[e].firstChild,o=coolfind.highlights[e].parentNode;coolfind.highlights[e].parentNode&&(coolfind.highlights[e].parentNode.replaceChild(n,coolfind.highlights[e]),e==coolfind.find_pointer&&coolfind.selectElementContents(n),o.normalize(),coolfind.normalize(o))}coolfind.highlights=[],coolfind.find_pointer=-1},coolfind.normalize=function(e){if(e){if(3==e.nodeType)for(;e.nextSibling&&3==e.nextSibling.nodeType;)e.nodeValue+=e.nextSibling.nodeValue,e.parentNode.removeChild(e.nextSibling);else coolfind.normalize(e.firstChild);coolfind.normalize(e.nextSibling)}},coolfind.findit=function(){var e=document.getElementById("cool_find_msg"),n=document.getElementById("cool_find_menu"),o=document.getElementById("cool_find_text").value;if(n.style.visibility="hidden",coolfind.find_text.toLowerCase()==document.getElementById("cool_find_text").value.toLowerCase()&&coolfind.find_pointer>=0)coolfind.findnext();else{if(coolfind.unhighlight(),""==o)return e.innerHTML="",void(n.style.visibility="visible");if(coolfind.find_text=o,null!=coolfind.find_root_node)var i=document.getElementById(coolfind.find_root_node);else var i=null;coolfind.highlight(o,i),coolfind.highlights.length>0?(coolfind.find_pointer=-1,coolfind.findnext()):(e.innerHTML="&nbsp;<b>0 of 0</b>",coolfind.find_pointer=-1)}n.style.visibility="visible"},coolfind.findnext=function(){var e;coolfind.find_pointer!=-1&&(e=coolfind.highlights[coolfind.find_pointer],1==coolfind.found_highlight_rule?e.className="highlight":e.style.backgroundColor="yellow"),coolfind.find_pointer++,coolfind.find_pointer>=coolfind.highlights.length&&(coolfind.find_pointer=0);var n=coolfind.find_pointer+1;cool_find_msg.innerHTML=n+" of "+coolfind.highlights.length,e=coolfind.highlights[coolfind.find_pointer],1==coolfind.found_selected_rule?e.className="find_selected":e.style.backgroundColor="orange",coolfind.scrollToPosition(coolfind.highlights[coolfind.find_pointer])},coolfind.findprev=function(){var e,n=document.getElementById("cool_find_msg");if(!(coolfind.highlights.length<1)){coolfind.find_pointer!=-1&&(e=coolfind.highlights[coolfind.find_pointer],1==coolfind.found_highlight_rule?e.className="highlight":e.style.backgroundColor="yellow"),coolfind.find_pointer--,coolfind.find_pointer<0&&(coolfind.find_pointer=coolfind.highlights.length-1);var o=coolfind.find_pointer+1;n.innerHTML=o+" of "+coolfind.highlights.length,e=coolfind.highlights[coolfind.find_pointer],1==coolfind.found_selected_rule?e.className="find_selected":e.style.backgroundColor="orange",coolfind.scrollToPosition(coolfind.highlights[coolfind.find_pointer])}},coolfind.checkkey=function(e){var n;n=window.event?window.event.keyCode:e.which,13==n?(window.event&&event.srcElement.id.match(/cool_find_text/i)?(event.srcElement.blur(),document.getElementById("cool_find_next").focus()):e&&e.target.id.match(/cool_find_text/i)&&(e.target.blur(),document.getElementById("cool_find_next").focus()),"cool_find_btn"!=document.activeElement.className&&coolfind.findit()):27==n&&coolfind.find_menu(document.getElementById("cool_find_btn"))},coolfind.resettext=function(){coolfind.find_text.toLowerCase()!=document.getElementById("cool_find_text").value.toLowerCase()&&coolfind.unhighlight()},coolfind.scrollToPosition=function(e){var n=document.body.scrollLeft||document.documentElement.scrollLeft,o=document.body.scrollTop||document.documentElement.scrollTop,i=(window.innerHeight||document.documentElement.clientHeight||document.body.clientHeight)+o,t=(window.innerWidth||document.documentElement.clientWidth||document.body.clientWidth)+n;if(e){var l=e,d=l.offsetLeft,c=l.offsetTop;for(l=l.offsetParent;null!=l;)d+=l.offsetLeft,c+=l.offsetTop,l=l.offsetParent;(d<n||d>t||c<o||c>i)&&e.scrollIntoView()}},coolfind.getStyle=function(e,n){var o=document.getElementById(e)?document.getElementById(e):e;if(o.currentStyle)var i=o.currentStyle[n];else if(window.getComputedStyle)var i=document.defaultView.getComputedStyle(o,null).getPropertyValue(n);return i},coolfind.textarea2pre=function(e){if(e.nextSibling&&e.nextSibling.id&&e.nextSibling.id.match(/pre_/i))var n=e.nextsibling;else var n=document.createElement("pre");var o=e.value;o=o.replace(/>/g,"&gt;").replace(/</g,"&lt;").replace(/"/g,"&quot;"),n.innerHTML=o;var i="";if(e.currentStyle){var t=e.currentStyle;for(var l in t)i+=l+":"+t[l]+";";n.style.border="1px solid black"}else i=window.getComputedStyle(e,null).cssText,n.style.cssText=i;e.parentNode.insertBefore(n,e.nextSibling),e.onblur=function(){this.style.display="none",n.style.display="block"},e.onchange=function(){n.innerHTML=e.value.replace(/>/g,"&gt;").replace(/</g,"&lt;").replace(/"/g,"&quot;")},e.style.display="none",n.id="pre_"+coolfind.highlights.length,n.onclick=function(){this.style.display="none",e.style.display="block",e.focus(),e.click()}},coolfind.selectElementContents=function(e){if(window.getSelection&&document.createRange){var n=document.createRange();n.selectNodeContents(e);var o=window.getSelection();o.removeAllRanges(),o.addRange(n)}else if(document.body.createTextRange){var i=document.body.createTextRange();i.moveToElementText(e),i.select()}},coolfind.create_find_div();
</script>
```

検索機能があるので不要なのですが、iPhone の Safari でも動いてくれました。

![iOS_Safari_sample.jpg](https://qiita-image-store.s3.amazonaws.com/0/65800/a761c7f6-ea85-75fb-2c78-111e83528c75.jpeg "iOS_Safari_sample.jpg")