-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: render html with headless chrome
- Loading branch information
Showing
7 changed files
with
285 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
.DEFAULT_GOAL := help | ||
|
||
.PHONY: help | ||
## help | show help | ||
help: | ||
@grep -E '^##' $(MAKEFILE_LIST) \ | ||
| sed -E 's,## ,,' \ | ||
| column -s '|' -t \ | ||
| sed -E "s,^([^ ]+),$(shell tput setaf 6)\1$(shell tput sgr0)," | ||
|
||
.PHONY: run-server | ||
## run-server | Run server for local development | ||
run-server: | ||
$(info --- $@) | ||
python -m http.server --bind 127.0.0.1 8000 | ||
|
||
.PHONY: render-posts | ||
## render-posts | Render posts | ||
render-posts: | ||
find posts -name '*.md' \ | ||
| xargs -n 1 basename \ | ||
| sed 's,\.md$$,,g' \ | ||
| xargs -I {} bash -c "google-chrome-stable --disable-gpu --disable-software-rasterizer --headless --virtual-time-budget=5000 --dump-dom 'http://127.0.0.1:8000/posts/?post={}' > posts/{}.html" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
<!DOCTYPE html> | ||
<html lang="ja"><head> | ||
<meta charset="UTF-8"> | ||
<meta name="viewport" content="width=device-width"> | ||
<link rel="shortcut icon" href="https://avatars.githubusercontent.com/u/8685693?s=48" type="image/x-icon"> | ||
<link rel="stylesheet" href="/site.css" type="text/css" media="all"> | ||
<link rel="stylesheet" href="page.css" type="text/css" media="all"> | ||
<title>Markdown sandbox</title> | ||
</head> | ||
<body> | ||
<nav><div class="navigation"><a href="/">Back to Index</a></div></nav> | ||
|
||
<div class="loading-screen" style="display: none;"> | ||
<span class="loading-screen__loading-emoji">⚡</span> | ||
</div> | ||
|
||
<div class="error-screen"> | ||
<span class="error-screen__status-code"></span> | ||
</div> | ||
|
||
<section class="post"><div class="post__date"><time datetime="0000-00-00">0000-00-00</time></div><h1>Markdown sandbox</h1><h2>Horizontal Rule</h2><hr><h2>Paragraphs</h2><p>The quick brown fox jumps over the lazy dog.</p><p>The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog.</p><h2>Emphasis</h2><p>This <b>is</b> bold.</p><p>This<b>is</b>bold.</p><p>This <em>is</em> italic.</p><p>This<em>is</em>italic.</p><h2>Blockquotes</h2><blockquote><p>May the Force be with you.</p></blockquote><blockquote><p>Blockquote with Multiple Paragraphs</p><p>May the Force be with you.</p></blockquote><blockquote><p>Nested Blockquote</p><blockquote><p>May the Force be with you.</p></blockquote></blockquote><blockquote><p>Blockquote with Other Elements</p><ul><li>one</li><li>two</li><li>three</li></ul><p>May the <b>Force</b> be with <em>you</em>.</p></blockquote><h2>Lists</h2><ul><li>foo</li><li>bar</li><li>baz</li></ul><ol><li>foo</li><li>bar</li><li>baz</li></ol><ul><li>foo<ul><li>indented foo<ul><li>indented bar</li></ul></li><li>indented baz</li></ul></li><li>bar</li><li>baz</li></ul><ul><li>fooParagraph in List</li><li>bar<blockquote><p>Blockquote in List</p></blockquote></li><li>baz</li></ul><h2>Code</h2><p>This is inline <code>code</code>.</p><div class="code-block__language-label">sh</div><pre><code>echo "Hello, World!" | ||
echo "May the Force be with you."</code></pre><div class="code-block__language-label">javascript</div><pre><code>const hello = () => { | ||
return "Hello, World!"; | ||
};</code></pre><h2>Link</h2><p><a href="http://localhost">localhost</a></p><h2>Image</h2><p><div class="image-link"><a href="https://github.com/identicons/iinm.png" target="_blank" rel="noopener"><img alt="alt" src="https://github.com/identicons/iinm.png" loading="lazy"></a></div></p></section> | ||
|
||
<script type="module"> | ||
import * as markdown from '/modules/markdown.js' | ||
import * as main from './main.js' | ||
window.onload = () => main.render({ modules: { markdown } }) | ||
</script> | ||
|
||
|
||
</body></html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
<!DOCTYPE html> | ||
<html lang="ja"><head> | ||
<meta charset="UTF-8"> | ||
<meta name="viewport" content="width=device-width"> | ||
<link rel="shortcut icon" href="https://avatars.githubusercontent.com/u/8685693?s=48" type="image/x-icon"> | ||
<link rel="stylesheet" href="/site.css" type="text/css" media="all"> | ||
<link rel="stylesheet" href="page.css" type="text/css" media="all"> | ||
<title>技術ブログを始める</title> | ||
</head> | ||
<body> | ||
<nav><div class="navigation"><a href="/">Back to Index</a></div></nav> | ||
|
||
<div class="loading-screen" style="display: none;"> | ||
<span class="loading-screen__loading-emoji">⚡</span> | ||
</div> | ||
|
||
<div class="error-screen"> | ||
<span class="error-screen__status-code"></span> | ||
</div> | ||
|
||
<section class="post"><div class="post__date"><time datetime="2021-04-18">2021-04-18</time></div><h1>技術ブログを始める</h1><h2>技術ブログを書く目的</h2><ul><li>未来の自分のために知識を再利用できるように残すこと</li><li>人に説明できるように考えを整理すること</li><li>人に伝わるように上手く説明する訓練をすること</li></ul><h2>ついでにやりたいこと</h2><p>ライブラリ、フレームワークを使わずに作ることでフロントエンドの基本的な要素技術を学び、応用できるようになること。</p><h2>気をつけること</h2><p>あくまでもアウトプットが目的なのでブログの仕組みづくりにこだわりすぎない。 まずは見た目がダサくてもテキストが表示できれば良いので、徐々に必要なものを作っていく。</p><p>※ これもMarkdownをHTMLに変換できて、まともなCSSがかけたらと思っていたけどまずpushする。</p><h2>ブログの作りに対する要求</h2><ul><li>記事を書くときはマークアップよりも内容に集中したい → Markdownで書けること</li><li>サーバ管理が不要であること</li><li>シンプルに簡単に投稿できること(例:Markdownをpushするだけで記事が公開される)</li><li>作っていて楽しいこと</li></ul><h2>ブログのアーキテクチャ</h2><p>(アーキテクチャっていうほどでもないが)</p><ul><li>静的サイトホスティングを使う (GitHub Pages)</li><li>コンテンツはクライアントサイドのJavaScriptでMarkdownをfetchしてレンダリングする</li></ul><p>document.titleなどをJavaScriptで書くため、検索エンジンにインデックスされるのか? → 商用サイトを作っているわけじゃないので今は気にしない</p><h2>これからやること</h2><ul><li>Markdownで書いた記事をcode要素に突っ込んで表示する (これはDone)</li><li>MarkdownをHTMLに変換する</li><li>CSSを書いて見た目を整える</li><li>MarkdownをHTMLに変換するネタの記事を書く</li><li>たくさんアウトプットする</li><li>記事にタグを付けられるようにする?</li><li>記事を検索できるようにする?</li></ul></section> | ||
|
||
<script type="module"> | ||
import * as markdown from '/modules/markdown.js' | ||
import * as main from './main.js' | ||
window.onload = () => main.render({ modules: { markdown } }) | ||
</script> | ||
|
||
|
||
</body></html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
<!DOCTYPE html> | ||
<html lang="ja"><head> | ||
<meta charset="UTF-8"> | ||
<meta name="viewport" content="width=device-width"> | ||
<link rel="shortcut icon" href="https://avatars.githubusercontent.com/u/8685693?s=48" type="image/x-icon"> | ||
<link rel="stylesheet" href="/site.css" type="text/css" media="all"> | ||
<link rel="stylesheet" href="page.css" type="text/css" media="all"> | ||
<title>Client-side JavaScriptでMarkdownをHTMLに変換する</title> | ||
</head> | ||
<body> | ||
<nav><div class="navigation"><a href="/">Back to Index</a></div></nav> | ||
|
||
<div class="loading-screen" style="display: none;"> | ||
<span class="loading-screen__loading-emoji">⚡</span> | ||
</div> | ||
|
||
<div class="error-screen"> | ||
<span class="error-screen__status-code"></span> | ||
</div> | ||
|
||
<section class="post"><div class="post__date"><time datetime="2021-04-24">2021-04-24</time></div><h1>Client-side JavaScriptでMarkdownをHTMLに変換する</h1><blockquote><p><em>この記事が表示できているということは、HTMLに変換できているということでしょう。</em></p></blockquote><h2>サマリー</h2><ul><li>フロントエンドの要素技術を学びながらブログを作っている。</li><li>Markdownで書いたブログ記事をHTMLに変換するためにMarkdown parserを書いた。まずは記事を書くための最低限のSyntaxのみをサポート。※ <a href="/posts/?post=0000-00-00--markdown-sandbox">Markdown Sandbox : 現時点でサポートする要素</a></li><li>この記事はClient-side JavaScriptで上記parserを使って表示している。※ 今後変えるかも</li></ul><h2>目的</h2><p>先週末に技術ブログをはじめました。 仕事はバックエンドが中心なので、せっかくならフロントエンドの基本的な要素技術を学ぼう、という意図でライブラリ、フレームワークは使わずにブログを作っています。</p><p>その第一歩としてMarkdownで書いた記事をHTMLに変換することが目的です。 静的なコンテンツなのでコマンドラインツールでも目的は達成できますが、 JavaScript Modulesなど最近のブラウザでできることを 試したかったためClient-side JavaScriptでMarkdownを処理する方法を選びました。</p><h2>どんなものを作ったか?</h2><p>Markdownの内容 (String) を受け取り、構造化して返します。</p><p>入力例:</p><div class="code-block__language-label">markdown</div><pre><code># Hello, World! | ||
|
||
This **is** inline element. | ||
|
||
- list item 1 | ||
- nested list item 1</code></pre><p>出力例:</p><div class="code-block__language-label">javascript</div><pre><code>[ | ||
{ | ||
// 要素の種類 | ||
type: 'heading', | ||
// 子要素 | ||
contents: [], | ||
// 要素特有のプロパティはここに設定 | ||
props: { | ||
level: 1, | ||
heading: 'Hello, World!' | ||
} | ||
}, | ||
{ | ||
type: 'empty_line', | ||
contents: [], | ||
props: {} | ||
}, | ||
{ | ||
type: 'inline', | ||
contents: [], | ||
props: { | ||
segments: [ | ||
{ | ||
type: 'text', | ||
props: { | ||
text: 'This ' | ||
} | ||
}, | ||
{ | ||
type: 'bold', | ||
props: { | ||
text: 'is' | ||
} | ||
}, | ||
// 長いので省略 | ||
... | ||
] | ||
} | ||
}, | ||
{ | ||
type: 'unorderd_list', | ||
contents: [ | ||
{ | ||
type: 'list_item', | ||
contents: [ | ||
{ | ||
type: 'inline', | ||
contents: [], | ||
props: { | ||
segments: [ | ||
{ | ||
type: 'text', | ||
props: { | ||
text: 'list item 1' | ||
} | ||
} | ||
] | ||
} | ||
}, | ||
{ | ||
type: 'unorderd_list', | ||
// 同じ構造のため省略 | ||
... | ||
} | ||
], | ||
props: {} | ||
} | ||
], | ||
props: {} | ||
} | ||
]</code></pre><h2>どのように実装したのか?</h2><p>できるだけ簡単な方法で実装したく、はじめは構造解析すらせず <code>String.prototype.replace</code> を使ってHTMLへの変換を試しました。 途中まで書いてcode block内の要素まで変換してしまう問題に気が付き、構造解析とレンダリングの2ステップで実現する方向に切り替えました。 また、経験上MarkdownのPreviewerによって改行の扱いが異なるという気づきがあり、 こういった細かな制御はレンダリング時にコントロールしたく、レンダリングのステップを分けることで実現しやすくなると考えました。 (例: <code>\n\n</code>は段落の区切り、<code>\n</code>だけなら同じ段落にするなど)</p><p>ボツになったコード例:</p><div class="code-block__language-label">javascript</div><pre><code>const html = markdownContent | ||
// heading | ||
.replace(/^(#+) (.+)$/gm, (match, levelChars, content) => | ||
`<h${levelChars.length}>${content}</h${levelChars.length}>`) | ||
// image | ||
.replace(/!\[([^\[]+)\]\(([^\)]+)\)/g, '<img src="$2" alt="$1">') | ||
// ...</code></pre><p>構造解析する際には、問題を以下の2つに分けて考えました。</p><ol><li>見出しやリストなどの構造を捉える問題 (HTMLのBlock要素に近い)</li><li>文字の装飾やLinkなど構造の末端にある要素を捉える問題 (HTMLのInline要素に近い)</li></ol><p>(1) HTMLと違って行の途中で別のBlock要素は出てこないので改行で区切ってグルーピングします。 Blockの途中でインデントされていた場合は行頭からインデントを外して再帰的にグルーピングする処理を呼びます。</p><div class="code-block__language-label">javascript</div><pre><code>const parseBlocks = (markdownContentLines) => { | ||
const blocks = [] | ||
for (let start = 0; start < markdownContentLines.length;) { | ||
for (const reader of blockReaders) { | ||
// Blockの開始を見つけたら、 | ||
if (reader.match(markdownContentLines, start)) { | ||
// Blockの終了まで読む | ||
const { block, readLineCount } = reader.read(markdownContentLines, start) | ||
blocks.push(block) | ||
start += readLineCount | ||
break | ||
} | ||
} | ||
// ※ 説明のため例外処理は省略 | ||
} | ||
return blocks | ||
}</code></pre><p>(2) テキスト要素まで分解できたら、正規表現で文字の装飾やLinkなどの要素を見つけて、テキストを分割、再帰的に分割したテキストからも要素を探します。</p><div class="code-block__language-label">javascript</div><pre><code>const parseInline = (inlineContent) => { | ||
if (inlineContent === '') return [] | ||
for (const segmenter of inlineContentSegmenters) { | ||
// 文字の装飾やLinkなどの要素を見つけたら、前後の文字列と分割して返す。 | ||
const { before, segment, after } = segmenter(inlineContent) | ||
if (segment) { | ||
// 再帰的に前後の文字列からも要素を探す | ||
return [...parseInline(before), segment, ...parseInline(after)] | ||
} | ||
} | ||
// 特に見つからなければplain text | ||
return [{ type: 'text', props: { text: inlineContent } }] | ||
}</code></pre><p>HTML要素への変換は上記の出力結果をトレースしながら<code>document.createElement</code>を呼んでいるだけなので割愛します。</p><h2>今後の課題</h2><ul><li>TableやInline HTMLなどサポートしてない要素は今後必要に応じて実装する予定です。</li><li>HTML要素への変換部分が<code>createElement</code>、<code>appendChild</code>の繰り返しで煩雑になってしまったので、仮想DOMみたいな層を挟んでみようと考えています。</li><li>分かってはいましたがClient-sideレンダリングするとMarkdownファイルをダウンロードするまで白い画面が表示されてしまいます。他にもいくつか気づきがあり、対策してみたのでこれは次の記事で紹介できればと思います。</li><li>コードハイライトしたい。</li></ul><p>まだまだ改善の余地がありますが、これでブログを書くための最低限の環境ができました。 これからいろいろ書いていきます。</p><h2>参考</h2><ul><li><a href="https://www.bigomega.dev/markdown-parser">Simple Markdown Parser with JavaScript and Regular Expressions</a> - 最終的にはやりませんでしたが、正規表現でHTMLに直接書き換えるという発想を得ました。</li><li><a href="https://github.com/markedjs/marked">marked</a> - block要素とinline要素に分けて処理するという発想を得ました。</li></ul></section> | ||
|
||
<script type="module"> | ||
import * as markdown from '/modules/markdown.js' | ||
import * as main from './main.js' | ||
window.onload = () => main.render({ modules: { markdown } }) | ||
</script> | ||
|
||
|
||
</body></html> |
Oops, something went wrong.