はじめに
Kibelarのみなさんも、そうでないみなさんも、こんにちは!
Kibelarの皆さんはご存知かと思いますが、情報共有ツールKibelaでは、PlantUMLを記事に記述するとUML図が表示されるというとても便利な機能があります。
そんなKibelaでPlantUMLをより快適に閲覧する用のChrome拡張をつくったお話のはじまりはじまり〜。
経緯
Kibelaを使い込んでいると、たくさんの記事で溢れかえって探すのが大変になると思います。
その対策として、フローチャートを辿ることで目当ての記事のリンクにたどれるようなUML図をKibelaで書くことになりました。
しかし、このUMLが大きくなってくるとKibela中の画像では見づらくなります。
また、URLを記していても本文中の画像ではアドレスバーにコピペできません。
右クリック>新しいタブで画像を開く で画像を開くと文字がコピペできますが、これよりももっと扱いやすくしようと考え今回の拡張をつくることになりました。
KibelaでのPlantUMLの書き方
## フローチャート
```plantuml
@startuml
(*) --> "Let's search!"
if "Would you like google?" then
-->[true] "Access https://www.google.co.jp/"
--> (*)
else
->[false] "Access https://www.yahoo.co.jp/"
--> (*)
endif
という感じで書くと
こんな感じで表示されます。
QiitaでもPlantUMLを書くことができますが、それと同じ書き方ですね。
記事中のUML図はどうなっている?
さきほどの記事のHTMLを見てみると、UML図は次のようになっていました。
<div class="plantuml" data-plantuml-source="@startuml
(*) --> "Let's search!"
if "Would you like google?" then
-->[true] "Access https://www.google.co.jp/"
--> (*)
else
->[false] "Access https://www.yahoo.co.jp/"
--> (*)
endif
@enduml
">
<img alt="PlantUML diagram" src="data:image/svg+xml;charset=utf-8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U3R5bGVUeXBlPSJ0ZXh0L2NzcyIgaGVpZ2h0PSIzNDFweCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSIgc3R5bGU9IndpZHRoOjM3NXB4O2hlaWdodDozNDFweDtiYWNrZ3JvdW5kOiNGRkZGRkY7IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAzNzUgMzQxIiB3aWR0aD0iMzc1cHgiIHpvb21BbmRQYW49Im1hZ25pZnkiPjxkZWZzLz48Zz48ZWxsaXBzZSBjeD0iMTg0IiBjeT0iMTYiIGZpbGw9IiMyMjIyMjIiIHJ4PSIxMCIgcnk9IjEwIiBzdHlsZT0ic3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjEuMDsiLz48cmVjdCBmaWxsPSIjRjFGMUYxIiBoZWlnaHQ9IjMzLjk2ODgiIHJ4PSIxMi41IiByeT0iMTIuNSIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDowLjU7IiB3aWR0aD0iMTAxIiB4PSIxMzMuNSIgeT0iNjciLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMiIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI4MSIgeD0iMTQzLjUiIHk9Ijg4LjEzODciPkxldCdzIHNlYXJjaCE8L3RleHQ+PGcgaWQ9ImVsZW1fIzUiPjxwb2x5Z29uIGZpbGw9IiNGMUYxRjEiIHBvaW50cz0iMTg0LDE0MiwxOTYsMTU0LDE4NCwxNjYsMTcyLDE1NCwxODQsMTQyIiBzdHlsZT0ic3Ryb2tlOiMxODE4MTg7c3Ryb2tlLXdpZHRoOjAuNTsiLz48L2c+PHJlY3QgZmlsbD0iI0YxRjFGMSIgaGVpZ2h0PSIzMy45Njg4IiByeD0iMTIuNSIgcnk9IjEyLjUiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MC41OyIgd2lkdGg9IjIyNSIgeD0iMTQ0LjUiIHk9IjI0MCIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEyIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjIwNSIgeD0iMTU0LjUiIHk9IjI2MS4xMzg3Ij5BY2Nlc3MgaHR0cHM6Ly93d3cuZ29vZ2xlLmNvLmpwLzwvdGV4dD48ZWxsaXBzZSBjeD0iMTI2IiBjeT0iMzI1IiBmaWxsPSJub25lIiByeD0iMTAiIHJ5PSIxMCIgc3R5bGU9InN0cm9rZTojMjIyMjIyO3N0cm9rZS13aWR0aDoxLjA7Ii8+PGVsbGlwc2UgY3g9IjEyNi41IiBjeT0iMzI1LjUiIGZpbGw9IiMyMjIyMjIiIHJ4PSI2IiByeT0iNiIgc3R5bGU9InN0cm9rZTpub25lO3N0cm9rZS13aWR0aDoxLjA7Ii8+PHJlY3QgZmlsbD0iI0YxRjFGMSIgaGVpZ2h0PSIzMy45Njg4IiByeD0iMTIuNSIgcnk9IjEyLjUiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MC41OyIgd2lkdGg9IjIyMCIgeD0iNyIgeT0iMTg2Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTIiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMjAwIiB4PSIxNyIgeT0iMjA3LjEzODciPkFjY2VzcyBodHRwczovL3d3dy55YWhvby5jby5qcC88L3RleHQ+PCEtLU1ENT1bOThmOGM5ODE5ZTdlNjZjMzk0NjBlMjA5ZjM0YjIyNzddCmxpbmsgc3RhcnQgdG8gTGV0J3Mgc2VhcmNoIS0tPjxnIGlkPSJsaW5rX3N0YXJ0X0xldCdzIHNlYXJjaCEiPjxwYXRoIGQ9Ik0xODQsMjYuMTggQzE4NCwzNS4yNyAxODQsNDkuNTMgMTg0LDYxLjQ5ICIgZmlsbD0ibm9uZSIgaWQ9InN0YXJ0LXRvLUxldCdzIHNlYXJjaCEiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MS4wOyIvPjxwb2x5Z29uIGZpbGw9IiMxODE4MTgiIHBvaW50cz0iMTg0LDY2Ljc5LDE4OCw1Ny43OSwxODQsNjEuNzksMTgwLDU3Ljc5LDE4NCw2Ni43OSIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDoxLjA7Ii8+PC9nPjwhLS1NRDU9W2IwYjgwNDE5ZTdlOWE0OGE4MDlmODdlYTA5YWU5ZGFhXQpsaW5rIExldCdzIHNlYXJjaCEgdG8gIzUtLT48ZyBpZD0ibGlua19MZXQncyBzZWFyY2ghXyM1Ij48cGF0aCBkPSJNMTg0LDEwMS4xMiBDMTg0LDExMS45MiAxODQsMTI2LjA4IDE4NCwxMzYuODggIiBmaWxsPSJub25lIiBpZD0iTGV0J3Mgc2VhcmNoIS10by0jNSIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDoxLjA7Ii8+PHBvbHlnb24gZmlsbD0iIzE4MTgxOCIgcG9pbnRzPSIxODQsMTQxLjg5LDE4OCwxMzIuODksMTg0LDEzNi44OSwxODAsMTMyLjg5LDE4NCwxNDEuODkiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MS4wOyIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjEyOCIgeD0iMzYuNCIgeT0iMTM0LjY3NzUiPldvdWxkIHlvdSBsaWtlIGdvb2dsZT88L3RleHQ+PC9nPjwhLS1NRDU9W2QwYmRmOTExNTE0YTg4YzViYjExNDMzZmU4NzI1N2E4XQpsaW5rICM1IHRvIEFjY2VzcyBodHRwczovL3d3dy5nb29nbGUuY28uanAvLS0+PGcgaWQ9ImxpbmtfIzVfQWNjZXNzIGh0dHBzOi8vd3d3Lmdvb2dsZS5jby5qcC8iPjxwYXRoIGQ9Ik0xOTEuOTIsMTU4LjQ3IEMyMDIuMDYsMTYzLjI4IDIxOS41NiwxNzIuOSAyMzAsMTg2IEMyNDEuMzUsMjAwLjI0IDI0OC4zNiwyMTkuODEgMjUyLjM4LDIzNC42NCAiIGZpbGw9Im5vbmUiIGlkPSIjNS10by1BY2Nlc3MgaHR0cHM6Ly93d3cuZ29vZ2xlLmNvLmpwLyIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDoxLjA7Ii8+PHBvbHlnb24gZmlsbD0iIzE4MTgxOCIgcG9pbnRzPSIyNTMuNzMsMjM5Ljg3LDI1NS4zNjcyLDIzMC4xNTgyLDI1Mi40ODcxLDIzNS4wMjY5LDI0Ny42MTgzLDIzMi4xNDY4LDI1My43MywyMzkuODciIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MS4wOyIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjIzIiB4PSIyNDkiIHk9IjIwNy4yMTA0Ij50cnVlPC90ZXh0PjwvZz48IS0tTUQ1PVtjNWJkNDg2NDFlMTRlYzkwY2U1YzJiZDgzMjEzN2I1Ml0KbGluayBBY2Nlc3MgaHR0cHM6Ly93d3cuZ29vZ2xlLmNvLmpwLyB0byBlbmQtLT48ZyBpZD0ibGlua19BY2Nlc3MgaHR0cHM6Ly93d3cuZ29vZ2xlLmNvLmpwL19lbmQiPjxwYXRoIGQ9Ik0yMjQuOTUsMjc0LjE1IEMxOTcuNDQsMjg4LjAxIDE1OS4xNiwzMDcuMjkgMTM5LjI0LDMxNy4zMyAiIGZpbGw9Im5vbmUiIGlkPSJBY2Nlc3MgaHR0cHM6Ly93d3cuZ29vZ2xlLmNvLmpwLy10by1lbmQiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MS4wOyIvPjxwb2x5Z29uIGZpbGw9IiMxODE4MTgiIHBvaW50cz0iMTM0LjU5LDMxOS42NywxNDQuNDI4MSwzMTkuMjEwMiwxMzkuMDU5MywzMTcuNDI4MiwxNDAuODQxMywzMTIuMDU5NCwxMzQuNTksMzE5LjY3IiBzdHlsZT0ic3Ryb2tlOiMxODE4MTg7c3Ryb2tlLXdpZHRoOjEuMDsiLz48L2c+PCEtLU1ENT1bNTZmZTUxMjJkMjBmODk4OTEzZDg3NDkyOTViYTEzMzldCmxpbmsgIzUgdG8gQWNjZXNzIGh0dHBzOi8vd3d3LnlhaG9vLmNvLmpwLy0tPjxnIGlkPSJsaW5rXyM1X0FjY2VzcyBodHRwczovL3d3dy55YWhvby5jby5qcC8iPjxwYXRoIGQ9Ik0xNzcuMzMsMTU5LjY4IEMxNjkuNjEsMTY1LjEgMTU2LjM5LDE3NC4zNyAxNDQuMywxODIuODUgIiBmaWxsPSJub25lIiBpZD0iIzUtdG8tQWNjZXNzIGh0dHBzOi8vd3d3LnlhaG9vLmNvLmpwLyIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDoxLjA7Ii8+PHBvbHlnb24gZmlsbD0iIzE4MTgxOCIgcG9pbnRzPSIxMzkuOSwxODUuOTMsMTQ5LjU2NjUsMTg0LjA0MzUsMTQzLjk5NTUsMTgzLjA2MTcsMTQ0Ljk3NzMsMTc3LjQ5MDcsMTM5LjksMTg1LjkzIiBzdHlsZT0ic3Ryb2tlOiMxODE4MTg7c3Ryb2tlLXdpZHRoOjEuMDsiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIyOCIgeD0iMTI5LjU5IiB5PSIxNzAuMDQwNCI+ZmFsc2U8L3RleHQ+PC9nPjwhLS1NRDU9W2QwNjZlYjY0YTM1ZTUzNTZjNGE3OWNlYTdiMDNiNDBlXQpsaW5rIEFjY2VzcyBodHRwczovL3d3dy55YWhvby5jby5qcC8gdG8gZW5kLS0+PGcgaWQ9ImxpbmtfQWNjZXNzIGh0dHBzOi8vd3d3LnlhaG9vLmNvLmpwL19lbmQiPjxwYXRoIGQ9Ik0xMTguMjEsMjIwLjE4IEMxMTkuOTgsMjQzLjc3IDEyMy4yNCwyODcuMjMgMTI0LjkzLDMwOS43MSAiIGZpbGw9Im5vbmUiIGlkPSJBY2Nlc3MgaHR0cHM6Ly93d3cueWFob28uY28uanAvLXRvLWVuZCIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDoxLjA7Ii8+PHBvbHlnb24gZmlsbD0iIzE4MTgxOCIgcG9pbnRzPSIxMjUuMzIsMzE0Ljk0LDEyOC42NDAzLDMwNS42Njc3LDEyNC45NDg1LDMwOS45NTM4LDEyMC42NjI0LDMwNi4yNjIsMTI1LjMyLDMxNC45NCIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDoxLjA7Ii8+PC9nPjwhLS1NRDU9WzI1NzdmZGYxZDk2OWU3OTljZDczYzJlNzQwNTI0Nzk2XQpAc3RhcnR1bWwNCigqKSAtIC0+ICJMZXQncyBzZWFyY2ghIg0KDQppZiAiV291bGQgeW91IGxpa2UgZ29vZ2xlPyIgdGhlbg0KICAtIC0+W3RydWVdICJBY2Nlc3MgaHR0cHM6Ly93d3cuZ29vZ2xlLmNvLmpwLyINCi0gLT4gKCopDQplbHNlDQogIC0+W2ZhbHNlXSAiQWNjZXNzIGh0dHBzOi8vd3d3LnlhaG9vLmNvLmpwLyINCi0gLT4gKCopDQplbmRpZg0KQGVuZHVtbA0KClBsYW50VU1MIHZlcnNpb24gMS4yMDIyLjEyKFN1biBPY3QgMjMgMTg6MTI6MjYgVVRDIDIwMjIpCihHUEwgc291cmNlIGRpc3RyaWJ1dGlvbikKSmF2YSBSdW50aW1lOiBPcGVuSkRLIFJ1bnRpbWUgRW52aXJvbm1lbnQKSlZNOiBPcGVuSkRLIDY0LUJpdCBTZXJ2ZXIgVk0KRGVmYXVsdCBFbmNvZGluZzogVVRGLTgKTGFuZ3VhZ2U6IGVuCkNvdW50cnk6IFVTCi0tPjwvZz48L3N2Zz4=" class="modalImage">
</div>
見やすく要約すると、
<div class="plantuml" data-plantuml-source="${uml_source_code}">
<img alt="PlantUML diagram" src="data:image/svg+xml;charset=utf-8;base64,${base64data}" class="modalImage">
</div>
という中身になっていました。
srcがdata:image/svg+xml;base64,*のデータをデコードしよう
HTMLを確認した通り、imgタグのsrcとして、Data URIが指定されていたことがわかりました。
RFC2397によると、Data URIスキームは以下の構成になっているようです。
data:[<mediatype>][;base64],<data>
今回はmediatype
がimage/svg+xml
(また、文字コードがutf-8
)なので、
data:image/svg+xml;charset=utf-8;base64,<data>
となりますね。
これを踏まえると、先述のHTML中のimgのsrcから、UML図の<data>
は次のような処理(js)で取り出せることがわかります。
/**
* パラメータのdataURIからbase64のdata部分を取り出し返す。
* パラメータがdataURL(svg+xml)以外のときはnullを返す。
* @param {String} src 調べたい画像ソース
* @returns {String|Null}
*/
function getBase64DataFromSvgXmlImgDataUri(src) {
const dataUriMatchResult = src.match(/data\:image\/svg\+xml(.*)\;base64,(.*)/);
if(!dataUriMatchResult) return null;
return dataUriMatchResult[2];
}
この関数で先ほどのHTML中のimgのsrcから<data>
を抜き出すと、以下のようになります。
PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U3R5bGVUeXBlPSJ0ZXh0L2NzcyIgaGVpZ2h0PSIzNDFweCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSIgc3R5bGU9IndpZHRoOjM3NXB4O2hlaWdodDozNDFweDtiYWNrZ3JvdW5kOiNGRkZGRkY7IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAzNzUgMzQxIiB3aWR0aD0iMzc1cHgiIHpvb21BbmRQYW49Im1hZ25pZnkiPjxkZWZzLz48Zz48ZWxsaXBzZSBjeD0iMTg0IiBjeT0iMTYiIGZpbGw9IiMyMjIyMjIiIHJ4PSIxMCIgcnk9IjEwIiBzdHlsZT0ic3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjEuMDsiLz48cmVjdCBmaWxsPSIjRjFGMUYxIiBoZWlnaHQ9IjMzLjk2ODgiIHJ4PSIxMi41IiByeT0iMTIuNSIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDowLjU7IiB3aWR0aD0iMTAxIiB4PSIxMzMuNSIgeT0iNjciLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMiIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI4MSIgeD0iMTQzLjUiIHk9Ijg4LjEzODciPkxldCdzIHNlYXJjaCE8L3RleHQ+PGcgaWQ9ImVsZW1fIzUiPjxwb2x5Z29uIGZpbGw9IiNGMUYxRjEiIHBvaW50cz0iMTg0LDE0MiwxOTYsMTU0LDE4NCwxNjYsMTcyLDE1NCwxODQsMTQyIiBzdHlsZT0ic3Ryb2tlOiMxODE4MTg7c3Ryb2tlLXdpZHRoOjAuNTsiLz48L2c+PHJlY3QgZmlsbD0iI0YxRjFGMSIgaGVpZ2h0PSIzMy45Njg4IiByeD0iMTIuNSIgcnk9IjEyLjUiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MC41OyIgd2lkdGg9IjIyNSIgeD0iMTQ0LjUiIHk9IjI0MCIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjEyIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjIwNSIgeD0iMTU0LjUiIHk9IjI2MS4xMzg3Ij5BY2Nlc3MgaHR0cHM6Ly93d3cuZ29vZ2xlLmNvLmpwLzwvdGV4dD48ZWxsaXBzZSBjeD0iMTI2IiBjeT0iMzI1IiBmaWxsPSJub25lIiByeD0iMTAiIHJ5PSIxMCIgc3R5bGU9InN0cm9rZTojMjIyMjIyO3N0cm9rZS13aWR0aDoxLjA7Ii8+PGVsbGlwc2UgY3g9IjEyNi41IiBjeT0iMzI1LjUiIGZpbGw9IiMyMjIyMjIiIHJ4PSI2IiByeT0iNiIgc3R5bGU9InN0cm9rZTpub25lO3N0cm9rZS13aWR0aDoxLjA7Ii8+PHJlY3QgZmlsbD0iI0YxRjFGMSIgaGVpZ2h0PSIzMy45Njg4IiByeD0iMTIuNSIgcnk9IjEyLjUiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MC41OyIgd2lkdGg9IjIyMCIgeD0iNyIgeT0iMTg2Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTIiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMjAwIiB4PSIxNyIgeT0iMjA3LjEzODciPkFjY2VzcyBodHRwczovL3d3dy55YWhvby5jby5qcC88L3RleHQ+PCEtLU1ENT1bOThmOGM5ODE5ZTdlNjZjMzk0NjBlMjA5ZjM0YjIyNzddCmxpbmsgc3RhcnQgdG8gTGV0J3Mgc2VhcmNoIS0tPjxnIGlkPSJsaW5rX3N0YXJ0X0xldCdzIHNlYXJjaCEiPjxwYXRoIGQ9Ik0xODQsMjYuMTggQzE4NCwzNS4yNyAxODQsNDkuNTMgMTg0LDYxLjQ5ICIgZmlsbD0ibm9uZSIgaWQ9InN0YXJ0LXRvLUxldCdzIHNlYXJjaCEiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MS4wOyIvPjxwb2x5Z29uIGZpbGw9IiMxODE4MTgiIHBvaW50cz0iMTg0LDY2Ljc5LDE4OCw1Ny43OSwxODQsNjEuNzksMTgwLDU3Ljc5LDE4NCw2Ni43OSIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDoxLjA7Ii8+PC9nPjwhLS1NRDU9W2IwYjgwNDE5ZTdlOWE0OGE4MDlmODdlYTA5YWU5ZGFhXQpsaW5rIExldCdzIHNlYXJjaCEgdG8gIzUtLT48ZyBpZD0ibGlua19MZXQncyBzZWFyY2ghXyM1Ij48cGF0aCBkPSJNMTg0LDEwMS4xMiBDMTg0LDExMS45MiAxODQsMTI2LjA4IDE4NCwxMzYuODggIiBmaWxsPSJub25lIiBpZD0iTGV0J3Mgc2VhcmNoIS10by0jNSIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDoxLjA7Ii8+PHBvbHlnb24gZmlsbD0iIzE4MTgxOCIgcG9pbnRzPSIxODQsMTQxLjg5LDE4OCwxMzIuODksMTg0LDEzNi44OSwxODAsMTMyLjg5LDE4NCwxNDEuODkiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MS4wOyIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjEyOCIgeD0iMzYuNCIgeT0iMTM0LjY3NzUiPldvdWxkIHlvdSBsaWtlIGdvb2dsZT88L3RleHQ+PC9nPjwhLS1NRDU9W2QwYmRmOTExNTE0YTg4YzViYjExNDMzZmU4NzI1N2E4XQpsaW5rICM1IHRvIEFjY2VzcyBodHRwczovL3d3dy5nb29nbGUuY28uanAvLS0+PGcgaWQ9ImxpbmtfIzVfQWNjZXNzIGh0dHBzOi8vd3d3Lmdvb2dsZS5jby5qcC8iPjxwYXRoIGQ9Ik0xOTEuOTIsMTU4LjQ3IEMyMDIuMDYsMTYzLjI4IDIxOS41NiwxNzIuOSAyMzAsMTg2IEMyNDEuMzUsMjAwLjI0IDI0OC4zNiwyMTkuODEgMjUyLjM4LDIzNC42NCAiIGZpbGw9Im5vbmUiIGlkPSIjNS10by1BY2Nlc3MgaHR0cHM6Ly93d3cuZ29vZ2xlLmNvLmpwLyIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDoxLjA7Ii8+PHBvbHlnb24gZmlsbD0iIzE4MTgxOCIgcG9pbnRzPSIyNTMuNzMsMjM5Ljg3LDI1NS4zNjcyLDIzMC4xNTgyLDI1Mi40ODcxLDIzNS4wMjY5LDI0Ny42MTgzLDIzMi4xNDY4LDI1My43MywyMzkuODciIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MS4wOyIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjExIiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjIzIiB4PSIyNDkiIHk9IjIwNy4yMTA0Ij50cnVlPC90ZXh0PjwvZz48IS0tTUQ1PVtjNWJkNDg2NDFlMTRlYzkwY2U1YzJiZDgzMjEzN2I1Ml0KbGluayBBY2Nlc3MgaHR0cHM6Ly93d3cuZ29vZ2xlLmNvLmpwLyB0byBlbmQtLT48ZyBpZD0ibGlua19BY2Nlc3MgaHR0cHM6Ly93d3cuZ29vZ2xlLmNvLmpwL19lbmQiPjxwYXRoIGQ9Ik0yMjQuOTUsMjc0LjE1IEMxOTcuNDQsMjg4LjAxIDE1OS4xNiwzMDcuMjkgMTM5LjI0LDMxNy4zMyAiIGZpbGw9Im5vbmUiIGlkPSJBY2Nlc3MgaHR0cHM6Ly93d3cuZ29vZ2xlLmNvLmpwLy10by1lbmQiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MS4wOyIvPjxwb2x5Z29uIGZpbGw9IiMxODE4MTgiIHBvaW50cz0iMTM0LjU5LDMxOS42NywxNDQuNDI4MSwzMTkuMjEwMiwxMzkuMDU5MywzMTcuNDI4MiwxNDAuODQxMywzMTIuMDU5NCwxMzQuNTksMzE5LjY3IiBzdHlsZT0ic3Ryb2tlOiMxODE4MTg7c3Ryb2tlLXdpZHRoOjEuMDsiLz48L2c+PCEtLU1ENT1bNTZmZTUxMjJkMjBmODk4OTEzZDg3NDkyOTViYTEzMzldCmxpbmsgIzUgdG8gQWNjZXNzIGh0dHBzOi8vd3d3LnlhaG9vLmNvLmpwLy0tPjxnIGlkPSJsaW5rXyM1X0FjY2VzcyBodHRwczovL3d3dy55YWhvby5jby5qcC8iPjxwYXRoIGQ9Ik0xNzcuMzMsMTU5LjY4IEMxNjkuNjEsMTY1LjEgMTU2LjM5LDE3NC4zNyAxNDQuMywxODIuODUgIiBmaWxsPSJub25lIiBpZD0iIzUtdG8tQWNjZXNzIGh0dHBzOi8vd3d3LnlhaG9vLmNvLmpwLyIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDoxLjA7Ii8+PHBvbHlnb24gZmlsbD0iIzE4MTgxOCIgcG9pbnRzPSIxMzkuOSwxODUuOTMsMTQ5LjU2NjUsMTg0LjA0MzUsMTQzLjk5NTUsMTgzLjA2MTcsMTQ0Ljk3NzMsMTc3LjQ5MDcsMTM5LjksMTg1LjkzIiBzdHlsZT0ic3Ryb2tlOiMxODE4MTg7c3Ryb2tlLXdpZHRoOjEuMDsiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMSIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIyOCIgeD0iMTI5LjU5IiB5PSIxNzAuMDQwNCI+ZmFsc2U8L3RleHQ+PC9nPjwhLS1NRDU9W2QwNjZlYjY0YTM1ZTUzNTZjNGE3OWNlYTdiMDNiNDBlXQpsaW5rIEFjY2VzcyBodHRwczovL3d3dy55YWhvby5jby5qcC8gdG8gZW5kLS0+PGcgaWQ9ImxpbmtfQWNjZXNzIGh0dHBzOi8vd3d3LnlhaG9vLmNvLmpwL19lbmQiPjxwYXRoIGQ9Ik0xMTguMjEsMjIwLjE4IEMxMTkuOTgsMjQzLjc3IDEyMy4yNCwyODcuMjMgMTI0LjkzLDMwOS43MSAiIGZpbGw9Im5vbmUiIGlkPSJBY2Nlc3MgaHR0cHM6Ly93d3cueWFob28uY28uanAvLXRvLWVuZCIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDoxLjA7Ii8+PHBvbHlnb24gZmlsbD0iIzE4MTgxOCIgcG9pbnRzPSIxMjUuMzIsMzE0Ljk0LDEyOC42NDAzLDMwNS42Njc3LDEyNC45NDg1LDMwOS45NTM4LDEyMC42NjI0LDMwNi4yNjIsMTI1LjMyLDMxNC45NCIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDoxLjA7Ii8+PC9nPjwhLS1NRDU9WzI1NzdmZGYxZDk2OWU3OTljZDczYzJlNzQwNTI0Nzk2XQpAc3RhcnR1bWwNCigqKSAtIC0+ICJMZXQncyBzZWFyY2ghIg0KDQppZiAiV291bGQgeW91IGxpa2UgZ29vZ2xlPyIgdGhlbg0KICAtIC0+W3RydWVdICJBY2Nlc3MgaHR0cHM6Ly93d3cuZ29vZ2xlLmNvLmpwLyINCi0gLT4gKCopDQplbHNlDQogIC0+W2ZhbHNlXSAiQWNjZXNzIGh0dHBzOi8vd3d3LnlhaG9vLmNvLmpwLyINCi0gLT4gKCopDQplbmRpZg0KQGVuZHVtbA0KClBsYW50VU1MIHZlcnNpb24gMS4yMDIyLjEyKFN1biBPY3QgMjMgMTg6MTI6MjYgVVRDIDIwMjIpCihHUEwgc291cmNlIGRpc3RyaWJ1dGlvbikKSmF2YSBSdW50aW1lOiBPcGVuSkRLIFJ1bnRpbWUgRW52aXJvbm1lbnQKSlZNOiBPcGVuSkRLIDY0LUJpdCBTZXJ2ZXIgVk0KRGVmYXVsdCBFbmNvZGluZzogVVRGLTgKTGFuZ3VhZ2U6IGVuCkNvdW50cnk6IFVTCi0tPjwvZz48L3N2Zz4=
これをデコードしていきます。
こちらの記事を参考にさせていただきました。
ありがとうございます!
<data>
をbase64data
に格納した状態で、以下の処理を実行するとdecodedData
にデコードされた<data>
が格納されます。
const decodedUtf8str = atob(base64data);
const decodedArray = new Uint8Array(Array.prototype.map.call(decodedUtf8str, c => c.charCodeAt()));
const decodedData = new TextDecoder().decode(decodedArray);
先ほどの<data>
でこの処理を行うと、デコードされたsvg+xml
が現れました。先ほどの<data>
をデコードしたものを整形したものは次のとおりです。
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" contentStyleType="text/css" height="341px" preserveAspectRatio="none" style="width:375px;height:341px;background:#FFFFFF;" version="1.1" viewBox="0 0 375 341" width="375px" zoomAndPan="magnify">
<defs />
<g>
<ellipse cx="184" cy="16" fill="#222222" rx="10" ry="10" style="stroke:none;stroke-width:1.0;" />
<rect fill="#F1F1F1" height="33.9688" rx="12.5" ry="12.5" style="stroke:#181818;stroke-width:0.5;" width="101" x="133.5" y="67" />
<text fill="#000000" font-family="sans-serif" font-size="12" lengthAdjust="spacing" textLength="81" x="143.5" y="88.1387">Let's search!</text>
<g id="elem_#5">
<polygon fill="#F1F1F1" points="184,142,196,154,184,166,172,154,184,142" style="stroke:#181818;stroke-width:0.5;" />
</g>
<rect fill="#F1F1F1" height="33.9688" rx="12.5" ry="12.5" style="stroke:#181818;stroke-width:0.5;" width="225" x="144.5" y="240" />
<text fill="#000000" font-family="sans-serif" font-size="12" lengthAdjust="spacing" textLength="205" x="154.5" y="261.1387">Access https://www.google.co.jp/</text>
<ellipse cx="126" cy="325" fill="none" rx="10" ry="10" style="stroke:#222222;stroke-width:1.0;" />
<ellipse cx="126.5" cy="325.5" fill="#222222" rx="6" ry="6" style="stroke:none;stroke-width:1.0;" />
<rect fill="#F1F1F1" height="33.9688" rx="12.5" ry="12.5" style="stroke:#181818;stroke-width:0.5;" width="220" x="7" y="186" />
<text fill="#000000" font-family="sans-serif" font-size="12" lengthAdjust="spacing" textLength="200" x="17" y="207.1387">Access https://www.yahoo.co.jp/</text>
\x3C!--MD5=[98f8c9819e7e66c39460e209f34b2277]\nlink start to Let's search!-->
<g id="link_start_Let's search!">
<path d="M184,26.18 C184,35.27 184,49.53 184,61.49 " fill="none" id="start-to-Let's search!" style="stroke:#181818;stroke-width:1.0;" />
<polygon fill="#181818" points="184,66.79,188,57.79,184,61.79,180,57.79,184,66.79" style="stroke:#181818;stroke-width:1.0;" />
</g>
\x3C!--MD5=[b0b80419e7e9a48a809f87ea09ae9daa]\nlink Let's search! to #5-->
<g id="link_Let's search!_#5">
<path d="M184,101.12 C184,111.92 184,126.08 184,136.88 " fill="none" id="Let's search!-to-#5" style="stroke:#181818;stroke-width:1.0;" />
<polygon fill="#181818" points="184,141.89,188,132.89,184,136.89,180,132.89,184,141.89" style="stroke:#181818;stroke-width:1.0;" />
<text fill="#000000" font-family="sans-serif" font-size="11" lengthAdjust="spacing" textLength="128" x="36.4" y="134.6775">Would you like google?</text>
</g>
\x3C!--MD5=[d0bdf911514a88c5bb11433fe87257a8]\nlink #5 to Access https://www.google.co.jp/-->
<g id="link_#5_Access https://www.google.co.jp/">
<path d="M191.92,158.47 C202.06,163.28 219.56,172.9 230,186 C241.35,200.24 248.36,219.81 252.38,234.64 " fill="none" id="#5-to-Access https://www.google.co.jp/" style="stroke:#181818;stroke-width:1.0;" />
<polygon fill="#181818" points="253.73,239.87,255.3672,230.1582,252.4871,235.0269,247.6183,232.1468,253.73,239.87" style="stroke:#181818;stroke-width:1.0;" />
<text fill="#000000" font-family="sans-serif" font-size="11" lengthAdjust="spacing" textLength="23" x="249" y="207.2104">true</text>
</g>
\x3C!--MD5=[c5bd48641e14ec90ce5c2bd832137b52]\nlink Access https://www.google.co.jp/ to end-->
<g id="link_Access https://www.google.co.jp/_end">
<path d="M224.95,274.15 C197.44,288.01 159.16,307.29 139.24,317.33 " fill="none" id="Access https://www.google.co.jp/-to-end" style="stroke:#181818;stroke-width:1.0;" />
<polygon fill="#181818" points="134.59,319.67,144.4281,319.2102,139.0593,317.4282,140.8413,312.0594,134.59,319.67" style="stroke:#181818;stroke-width:1.0;" />
</g>
\x3C!--MD5=[56fe5122d20f898913d8749295ba1339]\nlink #5 to Access https://www.yahoo.co.jp/-->
<g id="link_#5_Access https://www.yahoo.co.jp/">
<path d="M177.33,159.68 C169.61,165.1 156.39,174.37 144.3,182.85 " fill="none" id="#5-to-Access https://www.yahoo.co.jp/" style="stroke:#181818;stroke-width:1.0;" />
<polygon fill="#181818" points="139.9,185.93,149.5665,184.0435,143.9955,183.0617,144.9773,177.4907,139.9,185.93" style="stroke:#181818;stroke-width:1.0;" />
<text fill="#000000" font-family="sans-serif" font-size="11" lengthAdjust="spacing" textLength="28" x="129.59" y="170.0404">false</text>
</g>
\x3C!--MD5=[d066eb64a35e5356c4a79cea7b03b40e]\nlink Access https://www.yahoo.co.jp/ to end-->
<g id="link_Access https://www.yahoo.co.jp/_end">
<path d="M118.21,220.18 C119.98,243.77 123.24,287.23 124.93,309.71 " fill="none" id="Access https://www.yahoo.co.jp/-to-end" style="stroke:#181818;stroke-width:1.0;" />
<polygon fill="#181818" points="125.32,314.94,128.6403,305.6677,124.9485,309.9538,120.6624,306.262,125.32,314.94" style="stroke:#181818;stroke-width:1.0;" />
</g>
\x3C!--MD5=[2577fdf1d969e799cd73c2e740524796]\n@startuml\r\n(*) - -> "Let's search!"\r\n\r\nif "Would you like google?" then\r\n - ->[true] "Access https://www.google.co.jp/"\r\n- -> (*)\r\nelse\r\n ->[false] "Access https://www.yahoo.co.jp/"\r\n- -> (*)\r\nendif\r\n@enduml\r\n\nPlantUML version 1.2022.12(Sun Oct 23 18:12:26 UTC 2022)\n(GPL source distribution)\nJava Runtime: OpenJDK Runtime Environment\nJVM: OpenJDK 64-Bit Server VM\nDefault Encoding: UTF-8\nLanguage: en\nCountry: US\n-->
</g>
</svg>
これで<data>
の中身がわかりましたね。
この中身をブラウザで直接見てやれば、UML図の画像の内容と同じものでテキスト選択ができるものを表示することができます。
また、中身が解析できたので、置換などを行って任意の文字列を書き換えたりすることもできます。
今回つくるChrome拡張では、URLを見つけるとaタグに置換する処理を埋め込みました。
これが作成したChrome拡張だ!
先述した過程をたどり、そのあと色々あってこんな感じのものに落ち着きました。
機能としては次のようなところです。
- plantUML図の右上に展開ボタンを表示し、クリックすると別タブで展開される。
- 展開されたものはテキスト選択、コピペが可能
- 展開されたものの文中のURLをクリックするとリンク先に飛べる
-
?openUml=0
などとKibela記事のURLにパラメータを付与することで、記事を開いた時に指定のUML図を自動展開できる
この拡張をChromeに導入すると、
こんな感じで右上に展開ボタン<->
が表示され、押下すると別タブで展開される仕様です。
自分の欲しかった機能は一通り実装でき、Kibe lifeをより一層充実させてくれるものが完成しました。
おわりに
今回つくったChrome拡張で、「リンク集フローチャートUML図」をKibelaで圧倒的に使いやすくすることができました。
Data URI のbase64でエンコードされたデータ部分はjsで簡単にデコードできたおかげで楽に実装でき助かりました。
Data URIが使われているものはいろいろいあるので、それを触りたいときはChrome拡張で実装すると楽かもしれませんね。
みなさんもChrome拡張でよりよい生活をお過ごしください。