◆ 動的に記事中のコードを表示する

HTML プレビューしたい

ブログで記事中に HTML を書いたとき HTML なんだし実際に動かせるページがあると便利ですよね
そういうのが用意されていないと 見ていて気になって実際に動かしてみたいと思っても わざわざ HTML ファイルを作って開くという手間があります

ありがちなのがブログ内に HTML ページを用意してリンクを貼っておくこと
すごく単純ですし ブログ内で完結する安定の方法です
ただ 作る側が面倒なんですよね
記事本文と同じものをファイルにして保存してアップロードしてリンク貼って……

今のメインがこれですがそろそろもっと良い方法にしたいです

jsbin とか

少し楽な方法に jsbin, codepen, jsfiddle, stackblitz などなど HTML を実際に動かせるサービスを使うものがあります
国内ブログではそこまで見ません(日本語のそういう系のブログ自体をあまりみてないからかも)が github とか英語のページだとけっこう見かけます
その場限りなものなら私も使ってますが ブログだとけっこう長く続く前提なので サービスのほうがいつまで続くか怪しいものは避けたいです
サービス終わってしまうと過去記事の修正がすごく面倒ですから
そんな古い過去記事なんて誰が見るのって思いもありますけど できる限り見れなくなってるページは減らしたいです

github

長く続くことを考えると github はほとんどの人が使っていて microsoft のサービスになったので 安心です
Windows や IE を見て分かる通り必要以上に長くサポートしてくれてますからね
なのであまり外部サービスを埋め込みたくはないですが github のサービスの Gist を使って 記事からリンクを貼ってることはときどきあります
なんどか記事に直接コードを埋め込んだこともあります

Gist を記事内に直接埋め込んでしまうというのは国内ブログでも時々見かける方法です
ちょっとロードが遅いですが 手間を考えると楽になります
ただ HTML をプレビューするという意味では向いてません

Gist は HTML を書いてもそれをプレビューすることができません
GitHack などの外部のサービスを経由する必要があります
github が長続きしてもこれらのサービスも長続きする保証はありません
すでに rawgit がサービスやめていて そのときにけっこう URL 置き換える苦労を味わいました

動的に作る

考えていると 記事中にコードがあるんだからそれを使えばいいことに気づきました
DEMO ボタンを押したらすでに用意しておいた HTML を開く代わりに動的にページを作ります

やりかたはいくつかあります

  • Data URL
  • URL.createObjectURL
  • document.write

Data URL

シンプルなものは data URL 形式で本文の中のコードのページを開きます
ただ Chrome はいつのころからかトップフレームで Data URL のページを開けないように迷惑な変更をしたので Chrome を対応させるならこの方法は使えません

JavaScript に限らず a タグクリックなどもダメですが ユーザが URL バーへ直接入力した場合は開けるという例外があります
クリップボードに URL コピーだけしてユーザが URL バーに入力ということもできはします
ただやっぱり クリックだけで開けてほしいので別の方法にします

URL.createObjectURL

Data URL がダメだと blob の方になります
URL.createObjectURL を使えば blob からバイナリデータの URL を作れます
文字列なら結構簡単に blob を作れるのでシンプルです

elem.onclick = ()=> {
const blob = new Blob(["<h1>A</h1>"], { type: "text/html" })
const url = URL.createObjectURL(blob)
window.open(url)
URL.revokeObjectURL(url)
}

URL が blob:null/ee12493c-97a9-446b-8adb-93e284be4fbb みたいなものになります

about:blank & document.write

もうちょっと簡単な方法もあります
window.open で about:blank を開いたあとに その window の document.write で HTML を書き込みます

elem.onclick = () => {
window.open().document.write("<h1>A</h1>")
}

こっちは URL バーでは about:blank ですが ページ内の location は document.write を実行した親のページの URL になっています
どっちでもいいですが 個人的にはこっちのほうがちょっと好みです

HTML プレビューする

やり方が決まったので HTML プレビューをするコードを作ってみました
pre の中の code で data-lang に html が指定されているものを対象にします
対象のブロックにマウスを乗せたときに ブロックの右上に PREVIEW ボタンを表示します
ボタンを押すとウィンドウを開いて その code 要素内のテキストを HTML として document.write します

window.onload = eve => {
const style = document.createElement("style")
style.textContent = `
.code-wrapper {
position: relative;
}
.preview-open {
display: none;
position: absolute;
top: 0;
right: 0;
margin: 3px 10px;
padding: 3px 12px;
border-radius: 3px;
border: 1px solid #555;
background: #808080;
color: white;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.2);
text-shadow: 0 1px 0 rgba(0,0,0,0.2);
outline: none;
cursor: pointer;
}
.code-wrapper:hover .preview-open {
display: block;
}
`
document.head.append(style)
for (const elem of document.querySelectorAll("pre>code[data-lang=html]")) {
const html = elem.textContent
const pre = elem.parentElement
const wrapper = document.createElement("div")
wrapper.className = "code-wrapper"
pre.replaceWith(wrapper)
wrapper.append(pre)
const button = document.createElement("button")
button.textContent = "PREVIEW"
button.className = "preview-open"
button.addEventListener("click", eve => {
const subwindow = window.open()
setTimeout(() => {
subwindow.document.write(html)
subwindow.document.close()
}, 1)
})
wrapper.prepend(button)
}
}

追記

document.close が必要だったので追加しました
詳しくはこっちの記事