MarkTheRipper - マークダウンで書く事が出来る、静的サイトの高速生成ツール。
Package | NuGet |
---|---|
MarkTheRipper | |
MarkTheRipper.Core | |
MarkTheRipper.Engine |
main | develop |
---|---|
TODO: 最終的にこのドキュメントは、MarkTheRipper自身で変換する予定です。
MarkTheRipperは、非常にシンプルかつ高速な静的サイトジェネレータで、コンテンツをマークダウンで書くことができます。 主に想定される用途はブログサイトですが、まるでGitHub Gistで記事を書いているかのように、とにかく複雑な構造やツールの要求、というものを排除しました。
.NET 6.0をインストールしている環境なら、
dotnet tool install -g MarkTheRipper
とするだけでインストールできます。または、.NET Framework 4.71以上に対応した、ビルド済みのバイナリをダウンロードする事もできます。
- 0.4.0現在、dotnet toolingでのインストールは、正しくないバージョンがインストールされる問題があり、修正中です。
初めて使う場合は、
$ mtr init mininum
とすると、現在のディレクトリの配下に、以下のようにサンプルのひな形が生成されます。 (恐れる必要はありません!たった2つ、しかも中身は余計な定義が殆ど存在しない、数行のファイルです!)
contents
ディレクトリ:index.md
ファイルが置かれます。中身は一般的に想定されるマークダウンによる記事テキストです。layouts
ディレクトリ:page.html
ファイルが置かれます。サイト生成するときに、マークダウンがHTMLに変換され、このレイアウトファイルの中に挿入されます。
これだけです!念のために、中身も見せましょう:
---
title: Hello MarkTheRipper!
tags: foo,bar
---
This is sample post.
## H2
H2 body.
### H3
H3 body.
<!DOCTYPE html>
<html lang="{lang}">
<head>
<meta charset="utf-8" />
<meta name="keywords" content="{tags}" />
<title>{title}</title>
</head>
<body>
<header>
<h1>{title}</h1>
<p>Category:{foreach category.breadcrumbs} {item.name}{end}</p>
<p>Tags:{foreach tags} {item.name}{end}</p>
<p>
{foreach (take (older self) 1)}<a href="{relative item.path}">Older</a>{end}
{foreach (take (newer self) 1)}<a href="{relative item.path}">Newer</a>{end}
</p>
</header>
<hr />
<article>
{contentBody}
</article>
</body>
</html>
中身を見れば、何がどうなるのか想像も付くと思います。MarkTheRipperは、キーワードと本文をHTMLに変換して、レイアウトに挿入しているだけです。 なので、レイアウトをカスタマイズする時には、一般的なHTML/CSS/JavaScriptの技法をそのまま適用でき、制約は殆どありません。
(MarkTheRipperは.NETで書かれていますが、使用者は.NETの事を知らなくても問題ありません)
それでは、そのままサイトを生成してみましょう。サイト生成も非常に簡単です:
$ mtr
ディレクトリ構成がサンプルと同じであれば、mtr
を実行するだけでサイトを生成します。
サイト生成はマルチスレッド・マルチ非同期I/Oで行うので、大量のコンテンツがあっても高速です。
デフォルトでは、docs
ディレクトリ配下に出力されます。
この例では、contents/index.md
ファイルが、docs/index.html
ファイルに変換されて配置されます。
その後、すぐにデフォルトのブラウザでプレビューが表示されます:
サイト生成は、毎回docs
ディレクトリ内のファイルをすべて削除して、生成し直します。
ディレクトリ全体をGitで管理している場合は、docs
ディレクトを含めてコミットしてOKです。
そうすれば、実際に生成されたファイルの差分を確認することが出来ます。
また、github.io
にそのままpushして、簡単にあなたのサイトを公開出来ます!
マークダウンファイルのファイル名や、配置するサブディレクトリにも制約はありません。
contents
ディレクトリ配下で拡張子が.md
のファイルがあれば、どのようなサブディレクトリにどのようなファイル名で配置されていても、また、多数存在していても構いません。
サブディレクトリの構造を保ったまま、全ての.md
ファイルが.html
ファイルに変換されます。
.md
ではない拡張子のファイルは、単純に同じ場所にコピーされます。
例えば、写真などの追加ファイルは、あなたが管理したいように配置して、そこを指すように相対パスでマークダウンを書けば良いのです。
MarkTheRipperは、contents
ディレクトリ以下に配置された、全てのマークダウンファイルを自動的に認識するため、
何か記事を書く場合は、単純に好きなディレクトリに好きなようにマークダウンファイルを作って記述すればOKです。
この操作すら面倒ですか? ええ、もちろん知っています。なので、もっと簡単にマークダウンファイルを作って配置して、記事を書くことに集中できる方法があります。
$ mtr new
これで、contents
ディレクトリの直下に、現在の日付を元にした、ひな形のマークダウンファイルが生成され、かつデフォルトのマークダウンエディタが自動的に開きます。
カテゴリについては後で詳しく解説しますが、記事をカテゴリに配置したい場合は:
$ mtr new foo/bar/baz
とすれば、カテゴリ「foo」の「bar」の更に「baz」に、記事が配置されます。
mtr init minimum
が生成するサンプルはあまりにシンプルであり(minimumは伊達じゃありません!)、もう少しカスタマイズの例が見たいという場合のために、
標準でサンプルをいくつか内蔵しています。
$ mtr init sidebar
$ mtr init standard
$ mtr init rich
sidebar
、standard
またはrich
というサンプルを指定できます。以下のような機能があります:
名称 | 内容 |
---|---|
sidebar |
minimum サンプルに、CSS flexによるサイドバーナビゲーションを追加したものです。全体的に一からデザインしたい場合は、余計な定義が一切含まれていないので、都合がよいでしょう。 |
standard |
GitHubのコードブロックのデザインに似た見た目になります。シンタックスハイライトはありません。bootstrap.js 5.0を使用しています。 |
rich |
シンタックスハイライトに prism.js を使います。これも、bootstrap.js 5.0 を使っています。 |
心配無用です。これらのサンプルも、最小のレイアウトコードとなるよう、最新の注意を払って実装しました。 理解も容易で、HTMLのビギナーでも、これらのサンプルでカスタマイズを始める事が出来ます。
MarkTheRipperのレイアウトは、非常にシンプルでありながら、必要十分な柔軟性と応用性を備えています。 レイアウトは、「メタデータ辞書」を参照することで、すべてのキーワード置換を実現します。
レイアウトの置換例を示します。
もっとも簡単な例は、単純なキーワードの置き換えです。次のようなレイアウトを定義したとします:
<title>{title}</title>
これは、メタデータ辞書のtitle
というキーワードに対応する値に置き換えます。
title
の値はどこにあるかというと、対応するマークダウン文書の先頭で定義します:
---
title: Hello MarkTheRipper!
---
(... 本文 ...)
他のサイトジェネレーターを試したことがある場合は、マークダウンにこのような特別な「ヘッダ行」を追加して、タイトルや日時などを挿入する方法を知っているかもしれません。 MarkTheRipperも、構文上はこの慣例に従っていますが、もっと柔軟です。例えば、以下の例は全く同じ結果を生成します:
<title>{foobar}</title>
---
foobar: Hello MarkTheRipper!
---
(... 本文 ...)
- 補足: このヘッダの事を、他のサイトジェネレーターでは「FrontMatter」と呼び、YAML構文で記述することになっていますが、
MarkTheRipperは厳密にはYAMLではなく、記述の柔軟性を高めるための構文を使用しています。
例えば、
title
は、ダブルクオートで括る事は必須ではありませんし、tags
は角括弧で括らなくても正しく認識されます。 念のため、MarkTheRipperでは、FrontMatterという用語を使っていません。
何となく、メタデータ辞書をどう活用すれば良いか、見えてきましたか? つまりMarkTheRipperは、マークダウンのヘッダに書いた「キーワードと値」の組をメタデータ辞書として扱うことが出来て、レイアウト上にいくつでも反映することが出来ます。
任意のキーワードを、レイアウト上の任意の箇所で置き換えできるので、例えば以下のような応用が可能です:
<link rel="stylesheet" href="{stylesheet}.css">
---
title: Hello MarkTheRipper!
stylesheet: darcula
---
(... 本文 ...)
恐らく、この機能だけでも、大部分の問題は解決すると思います。
このメタデータ辞書には、特殊な、しかし重要と思われるキーワードがいくつか存在します。以下に示します:
キーワード | 内容 |
---|---|
generated |
MarkTheRipperでサイトを生成した日時 |
layout |
適用するレイアウト名 |
lang |
ロケール(en-us やja-jp など) |
date |
記事の日時 |
timezone |
MarkTheRipperでサイトを生成した環境のタイムゾーン。IANA表記、または時間 |
published |
明示的にfalse と指定する事で、このマークダウンを無視する |
- この他にもいくつか特殊なキーワードがありますが、後で解説します。
これらのキーワードは、マークダウンのヘッダに書いて、上書きする事が出来ます。
generated
を上書きする事に意味は無いかも知れませんが、
MarkTheRipperがメタデータ辞書の定義を特別扱いしない、と言う事だけ知っておけば問題ありません。
上記のキーワードのうち、lang
やlayout
やtimezone
のデフォルト値は何なのかが気になった人もいると思います。
メタデータ辞書は、サイト生成時のベースとなる定義を、metadata.json
に配置することが出来ます。
(無くても構いません。実際、minimumサンプルには存在しません)
例えば、以下のような定義です:
{
"title": "(Draft)",
"author": "Mark the Ripper",
"layout": "page",
"lang": "ja-jp",
"timezone": "Asia/Tokyo",
}
これを見ると面白いことがわかります。title
キーワードの値が"(Draft)"となっています。例えば、以下のようなレイアウトを考えます:
<meta name="author" content="{author}" />
<title>{title}</title>
もし、マークダウンにtitle
を指定した場合は、そのタイトルが挿入されますが、指定しなかった場合は"(Draft)"というタイトルが挿入されます。
同様に、author
を指定した場合は、その投稿者名が挿入されますが、指定しなかった場合は"Mark the Ripper"という投稿者名が挿入されます。
ブログに使うことを考えた場合、殆どの投稿文書はあなた自身が書いたものとなるので、いちいちマークダウンのヘッダに自分の名前を書きたいとは思わないでしょう。 しかし、タイトルはもちろん、その投稿毎に異なるはずです。 そのような場合分けに、このメタデータ辞書の「フォールバック」機能を使うことが出来ます。
そして、layout
とlang
のフォールバックですが:
layout
がフォールバックにも見つからない場合に限り、page
というレイアウト名が使われます。lang
がフォールバックにも見つからない場合に限り、システムのデフォルト言語が適用されます。timezone
がフォールバックにも見つからない場合に限り、システムのタイムゾーン設定が適用されます。
レイアウト名には、少し補足が必要でしょう。レイアウト名は、変換元のレイアウトファイルの特定に使用されます。
例えば、レイアウト名がpage
の場合は、layouts/page.html
ファイルが使用されます。もし:
---
title: Hello MarkTheRipper!
layout: fancy
---
(... 本文 ...)
のように指定した場合は、layouts/fancy.html
が使用されます。
date
は記事の日時を表していて、普通のキーワードと同様に扱われますが、
マークダウンヘッダに定義されていなかった場合は、自動的に生成時の日時が挿入されます。
timezone
は、date
のような日時を扱う場合に参照され、タイムゾーン補正を行う基準となります。
MarkTheRipperを動作させる環境の、システムのタイムゾーン設定が、日常的に記事に埋め込む日時と異なる場合は、
metadata.json
ファイルに含めておくと良いでしょう
(このような代表的な環境として、GitHub Actionsなどのクラウドサービス上で動作させる場合が考えられます)。
lang
は、単に普通のキーワードの一つのようにしか感じられないかも知れません。
これについては、後の節で解説します。
メタデータ辞書で引いた結果をキーワードとして、再度メタデータ辞書から引きたいと思うことがあります。例えば:
---
title: Hello MarkTheRipper!
category: blog
---
(... 本文 ...)
category
とは、記事のカテゴリです。ここでは、blog
と名付けていますが、以下のようにキーワード参照した場合:
<p>Category: {category}</p>
HTMLには Category: blog
のように表示されます。これで問題ない場合もありますが、もっと丁寧な文に置き換えたいかもしれません。
そこで、blog
をキーワードとして、再度メタデータ辞書から値を検索させることが出来ます。
MarkTheRipperに組み込まれている、lookup
関数キーワードを使用します:
<p>Category: {lookup category}</p>
- 関数キーワードの詳細については、後の章で説明します。
こうしておいて、メタデータ辞書に、blog
と私的な日記
を対にして登録しておけば、HTMLには Category: 私的な日記
と表示されます。
このようなキーワードと値のペアは、前節で示したmetadata.json
に書いておけば参照出来るようになります。
加えて、実はメタデータ辞書のファイルは、metadata/*.json
でマッチするすべてのJSONファイルが対象です。
ファイルが分かれていても、MarkTheRipperが起動する時に、すべて読み込まれて内容がマージされます。
例えば、記事カテゴリだけを管理するファイルとして、metadata/category.json
のように別のファイルにしておけば、管理が容易になるでしょう。
タグやカテゴリのような分類は、メニューから選択させてそのページに遷移させたいと思うでしょう。 例えば、サイト全体でタグが5個あったとします。これをページのメニューに自動的に加えて、 メニューからタグに分類されたページに遷移できるようにするには、「列挙機能」を使います。
例によって、小さい例から始めましょう。これはminimumに含まれているレイアウトです:
<p>Tags:{foreach tags} '{item}'{end}</p>
tags
キーワードは、タグのリストを示します(後述)
これは、{foreach tags}
と{end}
の間にある文書が、tags
の個数だけ繰り返し出力されるというものです。
「間にある文書」とは、ここでは '{item}'
の事です。スペースが含まれてることに注意してください。
同様に、改行やHTMLのタグなど、この間には何を含んでいても構いません。
では、以下のようなマークダウンを変換したとします:
---
title: Hello MarkTheRipper
tags: foo,bar,baz
---
(... 本文 ...)
すると、<p>Tags: 'foo' 'bar' 'baz'</p>
と出力されます。
tags
のfoo,bar,baz
が、スペースで区切られて展開されて、クオートされて出力されました。
{foreach tags}
と{end}
の間にある文書が繰り返し出力されるので、以下のように使う事も出来ます:
<ul>
{foreach tags}
<li>{item.index}/{item.count} {item}</li>
{end}
</ul>
結果:
<ul>
<li>0/3 foo</li>
<li>1/3 bar</li>
<li>2/3 baz</li>
</ul>
間に挿入されている{item}
は、繰り返される一つ一つの値を参照できるキーワードです。
また、{item.index}
と指定すると、0から始まって1,2,3... とカウントする数値が得られます。
{item.count}
は、繰り返しの個数です。上記ではタグが3個あるため、この値は常に3となります。
さらに、複数のキーワードをネストさせる事も出来ます。以下の例は、タグを2重に繰り返します:
<ul>
{foreach tags}
{foreach tags}
<li>{item.index} {item}</li>
{end}
{end}
</ul>
結果:
<ul>
<li>0 foo</li>
<li>1 bar</li>
<li>0 foo</li>
<li>1 bar</li>
</ul>
ところで、この場合のitem
は、2重にネストした内側のtags
の繰り返しを指している事に注意して下さい。
場合によっては、外側の繰り返しの値を使いたいと思うかもしれません。
その場合は、foreach
に「束縛名」を指定します:
<ul>
{foreach tags tag1}
{foreach tags tag2}
<li>{tag1.index}-{tag2.index} {tag1}/{tag2}</li>
{end}
{end}
</ul>
結果:
<ul>
<li>0-0 foo/foo</li>
<li>0-1 foo/bar</li>
<li>1-0 bar/foo</li>
<li>1-1 bar/bar</li>
</ul>
束縛名を省略した場合は、item
が使用されます。
これで、foreach
による繰り返しの使い方が把握できたと思います。
繰り返しの使い方さえ理解できれば、タグやカテゴリの操作は出来たも同然です。 MarkTheRipperは、コンテンツのすべてのタグとカテゴリの分類を、自動的に集約します。 タグについては、以下の特殊なキーワードで参照できます:
キーワード | 内容 |
---|---|
tags |
タグのリスト。各マークダウンのヘッダ部分に記述する |
tagList |
すべてのタグを集約したリスト |
まずはタグの一覧を作ってみましょう:
<ul>
{foreach tagList tag}
<li>{tag}</li>
{end}
</ul>
結果:
<ul>
<li>foo</li>
<li>bar</li>
<li>baz</li>
:
</ul>
前節では、個々のマークダウンに定義されたtags
を使って繰り返しましたが、ここではtagList
を使っています。
この違いは、1つのマークダウンファイルではなく、MarkTheRipperが処理する全てのマークダウンのタグを集計した結果を扱っている事です。
つまり、tagList
を使えば、タグによるメニュー項目やリンクリストを追加出来るようになります。
各タグの項目に、リンクを加えるにはどうすれば良いでしょうか?
タグだけでは、タグに紐づいたコンテンツ群は分かりませんが、実はタグはforeach
で列挙することが出来ます:
{foreach tagList tag}
<h1>{tag}</h1>
{foreach tag.entries entry}
<h2><a href="{entry.path}">{entry.title}</a></h2>
{end}
{end}
束縛名を指定して、何を列挙しようとしているのか分かりやすくしていることに注意してください。
entries
プロパティを列挙すると、対応するマークダウン群の情報にアクセスできます。
この例のようにpath
というプロパティを使用すると、コンテンツに対応するファイルへのパスが得られ、
title
を使用すれば、そのタイトル(マークダウンのヘッダに記述されたtitle
)が得られます。
path
は、マークダウンへのパスではなく、変換されたHTMLファイルへのパスが得られます。
ところで、このパスは、出力ディレクトリを基準とした相対パスです。
HTMLにパスを埋め込む場合、HTMLファイルが存在するディレクトリからの相対パスである必要があります。
この計算を行うには、MarkTheRipper内蔵の関数キーワード relative
を使います:
<h2><a href="{relative entry.path}">{entry.title}</a></h2>
relative
を使ってパスを計算すると、MarkTheRipperが出力したHTMLがどのサーバーにどのようにデプロイされたとしても、正しくリンクが機能するようになります。
ハードコーディングされたサイトの基準パスを使用するよりも、安全となるでしょう。
カテゴリについては、以下の特殊なキーワードで参照できます:
キーワード | 内容 |
---|---|
category |
カテゴリの階層リスト。各マークダウンのヘッダ部分に記述する |
rootCategory |
ルート(分類なし)となるカテゴリ |
タグとカテゴリが決定的に異なるのは、タグは並行して定義されるものであり、カテゴリは階層化を伴って定義されるものである事です。例えば:
(root) --+-- foo --+-- bar --+-- baz --+-- foobarbaz1.md
| | | +-- foobarbaz2.md
| | |
| | +-- foobar1.md
| |
| +--- foo1.md
| +--- foo2.md
| +--- foo3.md
|
+--- blog1.md
+--- blog2.md
上の例では、foobarbaz1.md
は、カテゴリfoo/bar/baz
に属しています。
また、blog1.md
は、どのカテゴリにも属していません。
MarkTheRipper内部では、無名の(root)
カテゴリに属することになっています。
これが、rootCategory
キーワードです。
タグの場合はtags
キーワードを使用して定義しましたが、カテゴリの場合はcategory
というキーワードを使用します。
上記のfoobarbaz1.md
に相当する定義は:
---
title: Hello MarkTheRipper
category: foo,bar,baz
---
(... 本文 ...)
のように、階層をリストで指定します。タグと異なり、このリストは階層を表していることに注意してください。 CMSやサイトジェネレーターではこのような階層構造を、しばしば「パンくずリスト(breadcrumb)」と呼ぶことがあります。
ところで、MarkTheRipperは、このようにいちいちcategory
キーワードでカテゴリを明示しなくても、コンテンツをカテゴリ分けされたサブディレクトリに配置するだけで、ディレクトリ名からカテゴリを決定出来ます。
従って、カテゴリによるコンテンツの分類は、サブディレクトリで区分けするだけで良いのです。
カテゴリの基本的な構造を把握出来たと思うので、実際にレイアウトを書いてみましょう。まずはルートカテゴリを列挙します:
<h1>{rootCategory.name}</h1>
<ul>
{foreach rootCategory.entries entry}
<li>{entry.path}</li>
{end}
</ul>
結果:
<h1>(root)</h1>
<ul>
<li>blog1.html</li>
<li>blog2.html</li>
</ul>
rootCategory
はルートカテゴリを表しているため、そのプロパティname
は(root)
になります。
この名称が表示にふさわしくない場合は、キーワードの再帰検索を使って置き換えるか、またはこの例ではルートなので、直接書いてしまっても良いと思います。
そして、タグの時と同様にentries
で列挙したそれぞれの要素から、各マークダウンのヘッダ情報を引き出すことが出来ます。
ここではpath
を指定して、コンテンツのパスを出力していますが、title
とすればタイトルが出力出来て、relative item.path
とすれば、現在のコンテンツからの相対パスが得られるので、これをそのままリンクのURLにすることで、リンクを実現できます。
カテゴリを列挙するには、children
プロパティを使います:
<h1>{rootCategory.name}</h1>
{foreach rootCategory.children child1}
<h2>{child1.name}</h2>
{foreach child1.children child2}
<h3>{child2.name}</h3>
{end}
{end}
列挙のネストを増やしていけば、深いカテゴリ構造を全て列挙することが出来ます。 残念ながら、カテゴリ構造を動的に列挙する、つまり存在する子孫カテゴリまでを自動的に再帰的に列挙させる事は出来ません。 これは設計上の制約で、MarkTheRipperには、関数と再帰関数を定義する能力が無いためです。 (そのような要求は、恐らくサイト全体の構造リストを出力する場合だけであり、必要性を感じなかったためです)
カテゴリ操作の最後に、パンくずリストを出力する例を示します。これは非常に簡単です:
<ul>
{foreach category.breadcrumbs}
<li>{item.name}</li>
{end}
</ul>
breadcrumbs
プロパティは、対象のカテゴリに至るカテゴリを、ルートから列挙出来る値を返します。
(但し、対象のカテゴリがルートの場合は、ルートカテゴリを含み、それ以外の場合は含みません)
列挙した個々の要素は、今まで説明してきたカテゴリと同様です。上記例ではname
プロパティでカテゴリ名を出力しています。
これまでに説明してきたキーワードの置き換えは、レイアウトファイルに対して行うというものでした。 このキーワード置き換え機能は、マークダウンファイルにも同様に適用されます。例えば:
---
title: hoehoe
tags: foo,bar,baz
---
Title: {title}
このようなマークダウンを記述すると、{title}
が同じようにキーワード置換されます。
もちろんこれまでに説明してきた、関数キーワードによる計算も可能です。
マークダウン上のキーワード置き換えは、コードブロックに対しては機能しません:
---
title: hoehoe
tags: foo,bar,baz
---
Title: `{title}`
```
{title}
```
上記のように、コードブロック内に配置された{...}
は、MarkTheRipperで解釈されずに、そのまま出力されます。
これまでに出てきた関数を含めた、組み込み関数の一覧を示します:
関数 | 内容 |
---|---|
format |
引数を文字列に整形します。 |
relative |
引数のパスを、相対パスに変換します。 |
lookup |
引数が示す結果を元に、メタデータ辞書を引きます。 |
older |
引数が示す記事よりも古い記事を列挙します。 |
newer |
引数が示す記事よりも新しい記事を列挙します。 |
take |
列挙可能数を制限します。 |
add |
引数を加算します。 |
sub |
引数を減算します。 |
mul |
引数を乗算します。 |
div |
引数を除算します。 |
mod |
引数の剰余を取得します。 |
embed |
oEmbedプロトコルなどを使用して、埋め込みコンテンツを生成します。 |
card |
OGPメタデータなどを使用して、カード形式のコンテンツを生成します。 |
引数を文字列に整形と言っても、format
を使わずとも、これまでも特に問題なく整形出来ていました。
この関数が特に有用な場合は、日時を扱う場合です。
まず、書式がどのように決まるのかを示します。このようなHTMLで、date
キーワードやgenerated
キーワードを使う場合:
<p>Date: {date}</p>
以下の書式で日時が出力されます:
---
date: 2022/1/23 12:34:56
lang: ja-jp
---
(... 本文 ...)
<p>Date: 2022/01/02 12:34:56 +09:00</p>
この書式は、lang
が示す言語によって変化します。ja-jp
ではなくen-us
の場合は:
<p>Date: 1/2/2022 12:34:56 PM +09:00</p>
のように、その言語の標準的な書式に従います。
また、文字列に整形する際に timezone
キーワードの値が参照され、タイムゾーン補正された日時が出力されます。
書式を固定したい場合は、以下のようにformat
関数を使用します:
<p>Date: {format date 'yyyy-MM-dd HH.mm.ss'}</p>
<p>Date: 2022-01-02 12.34.56</p>
format
関数の第一引数は、整形したい値を示す式、第二引数は書式指定文字列です。
書式指定文字列は、シングルクオートかダブルクオートで括ります。
以前に、「MarkTheRipperは.NETで書かれていますが、使用者は.NETの事を知らなくても問題ありません」と書きましたが、 この書式指定文字列だけは、.NETの慣例に従っています。 日時の正確な書式指定文字列の形式は、このドキュメントを参照してください。
実際には、日時に限らず、あらゆる値を書式に従って整形出来ます。
例えば、列挙中のindex
は数値ですが、以下のようにすれば:
<p>{format item.index 'D3'}</p>
数値を3桁にすることが出来ます。
<p>007</p>
様々な書式指定の形式も、上に挙げた.NETドキュメントの周辺に詳しく掲載されています。
既に解説した通り、relative
関数は現在の記事のパスからの相対パスを計算します。
<p>{relative item.path}</p>
関数の引数には、文字列を渡すこともできます。例えば:
<link rel="stylesheet" href="{relative 'github.css'}">
このようにすれば、スタイルシートが存在する正しい相対パスを指定出来ます。
引数に指定するパスは、サイトのルートディレクトリ、docs
からの相対位置です。
上記例では、docs/github.css
への相対パスとなります。
通常このような場合は、絶対パスを指定します。
しかし絶対パスを使った場合、サイトのデプロイ環境によっては、無効なパスとなってしまいます。
relative
関数を使用すれば、生成されたコンテンツのポータビリティが向上します。
lookup
関数は、引数に指定された値と同じ名称のキーワードを、再度メタデータ辞書から検索します。
典型的な使用例は、タグやカテゴリの名称を、メタデータ辞書から引き直すというものです:
<p>Tag: {lookup tag}</p>
{tag}
の場合は、タグの真の名称が出力されますが、lookup
関数を使った場合は、
メタデータ辞書からタグ名と同じキーワードの値を取得して出力します。
従って、metadata.json
に:
{
"diary": "今日起きたこと"
}
のようなキーワードを登録しておけば、実際に出力される文字列を入れ替えることが出来ます。 これまで見てきたように、関数の引数に直接値を指定することも出来るため、固定の文字列を出力したい場合でも:
<p>Tag: {lookup 'blog'}</p>
のようにすれば、置き換えが可能です。
older
やnewer
を使用すると、指定された記事よりも古い記事や、新しい記事を列挙することが出来ます。
例えば、現在の記事よりも新しい記事を列挙するには、以下の式を使います:
{foreach (newer self)}<p>{item.title}</p>{end}
ここで、self
は現在の記事(このレイアウトを使用中のマークダウン記事)を示し、foreach
が列挙する個々の要素が、新しい記事を示します。
従って、以下のようにプロパティを参照して、relative
関数を応用して、リンクを作ることが出来ます:
{foreach (newer self)}<p><a href="{relative item.path}">{item.title}</a></p>{end}
括弧の位置に注意しましょう。上記では、newer
関数にself
を引数として指定するために、括弧を使用しています。
括弧を指定しないと、foreach
にnewer
とself
を引数として指定したことになり、正しく動作しません。
列挙される記事は、その記事が属するカテゴリで、日付順に得られます。
そのため、MarkTheRipperでは、日付順を意識する「ブログ」のような記事を、
blog
のようなカテゴリに分類して、そのカテゴリ内でこの関数を使用してナビゲーションする事をお勧めします。
older
やnewer
の引数には、self
以外にも、記事を示す式を指定できます。
entries
プロパティを列挙した値が相当します。
take
は、列挙可能な値の列挙数を制限します。
例えば、tag.entries
は、そのタグを持つ全ての記事を列挙します:
{foreach tags tag}
<h2>{tag}</h2>
{foreach tag.entries}<p>{item.title}</p>{end}
{end}
しかし、これを数個に制限したい事もあるでしょう:
{foreach tags tag}
<h2>{tag}</h2>
{foreach (take tag.entries 3)}<p>{item.title}</p>{end}
{end}
take
関数は2つの引数を指定します。1個目は列挙対象、2個目は制限する列挙数です。上記の式では、最大3個に制限しています。
このtake
関数と、foreach
を使用した、ちょっとしたテクニックがあります。前節のnewer
関数と組み合わせて応用します:
{foreach (take (newer self) 1)}<a href="{relative item.path}">Newer: {item.title}</a>{end}
列挙する個数を1個に制限するという事は、もし0個の場合は全く列挙されないと言う事です。
つまり、列挙する記事が無い場合は、foreach
の中が出力されないため、表示させない事が出来ます。
それにより、現在の記事よりも新しい記事が存在しない場合は、リンクが表示されなくなります。
この式は、ほぼ同じものがサンプルのレイアウトにあります。
これらは計算を行う関数です。引数は1個以上必要で、3個以上の場合は、連続して計算を行います。例えば:
<p>1 + 2 + 3 = {add 1 2 3}</p>
のように、引数の数値を全て加算します。 複雑な計算を行う場合は、括弧を使います:
<p>(1 + 2) * 4 = {mul (add 1 2) 4}</p>
括弧はいくつでもネスト出来ます。括弧を応用して、format
関数を使って望ましい形に整形出来ます:
<p>1 + 3 = {format (add 1 3) 'D3'}</p>
結果:
<p>1 + 3 = 004</p>
引数が数値ではなくても、文字列が数値として見なすことが出来ればOKです:
<p>1 + 2 + 3 = {add 1 '2' 3}</p>
引数の中に、小数を含む数値があっても構いません。その場合は小数を含む計算として処理されます(浮動小数点演算と呼びます):
<p>1 + 2.1 + 3 = {add 1 2.1 3}</p>
小数を含む結果は、意図したとおりの表示にならない事があります。
そのような可能性を考えて、常にformat
関数を使った方が良いかもしれません。
小数を含む書式の指定方法は、ここを参照して下さい。
計算を使用する簡単な例を示します。列挙中のitem.index
は、0から開始される数値です。
一方、item.count
は列挙できる数ですが、これを並べると一般的な表記となりません:
<p>index/count = {item.index}/{item.count}</p>
結果:
<p>index/count = 0/3</p>
<p>index/count = 1/3</p>
<p>index/count = 2/3</p>
このような場合に、add
を使って:
<p>index/count = {add item.index 1}/{item.count}</p>
とすれば、1からcount
までの数値となり、自然な表現に近づける事が出来ます。
結果:
<p>index/count = 1/3</p>
<p>index/count = 2/3</p>
<p>index/count = 3/3</p>
embed
(と後述のcard
) 関数は特殊で、単純な計算を行うのではなく、
外部データを参照してコンテンツを生成する、強力な関数です。
あなたは、自分のブログにYouTubeの動画を埋め込みたいと考えたことはありませんか? あるいは、単なるリンクではなく、外部コンテンツへのカード形式のコンテンツを表示したいと考えたこともあるかも知れません。
embed
とcard
関数は、このような複雑なコンテンツ埋め込みを容易に実現します。
例えば、文書中に以下のように書きます:
## すばらしい動画を発見
いつか、自由に旅行できるようになったら、訪れてみたい...
{embed https://youtu.be/1La4QzGeaaQ}
すると、以下のように表示されます:
この画像は、単なるサムネイルではありません。実際にページ内で動画を再生することもできます。 マジック!ですか? これは、業界標準のoEmbedプロトコルを使って、 HTMLに埋め込むべきコンテンツを自動的に収集して実現しています。
embed
関数の引数には、そのコンテンツの「パーマリンク」、つまり共有すべきURLを指定するだけです。
YouTubeの他にも、数多くの有名コンテンツサイトが対応しているため:
## Today's DIVISION
{embed https://twitter.com/kekyo2/status/1467073038667882497}
このように、Twitterなど、他のコンテンツも簡単に埋め込めます。 どのコンテンツサイトが対応しているのかは、直接oEmbedのjsonメタデータを参照して確かめてみてください。 かなり多くのサイトが対応済みであることが分かります。
しかし、私たちが知りうる有用なサイトの一つであるAmazonは、何とoEmbedに対応していません! そのため、MarkTheRipperでは、Amazonの商品リンクを特別に認識して、同様に埋め込めるようにしています:
## USBホストを行う実験
{embed https://amzn.to/3V6lYlQ}
- このリンクは、Amazon associatesを有効化する事で、取得出来るようになります。 Amazon associatesは、Amazonのアカウントがあれば誰でも有効化出来ますが、ここでは詳細を省きます。
さて、この便利な関数を使用するには、少しだけ準備が必要です。
この埋め込みコンテンツを表示させるための、専用のレイアウトファイルlayouts/embed.html
を用意します。
内容は以下の通りです:
<div style="max-width:800px;margin:10px;">
{contentBody}
</div>
これまでのレイアウト解説と同様に、contentBody
には、実際に埋め込むべきoEmbedのコンテンツが埋め込まれます。
外側のdiv
タグは、この埋め込みコンテンツの領域を決めるものです。
上記では外周に若干空白を持たせて、本体は横が800pxとなるようにしています。
あなたのサイトのデザインに合わせて、調整すると良いでしょう。
もし、コンテンツサイトごとに異なるレイアウトが必要な場合は、layouts/embed-YouTube.html
のように、
oEmbedプロバイダ名を指定したファイル名とする事で、サイト固有のカスタマイズが出来ます。
ところで、oEmbedプロトコルで得られる情報には、埋め込みコンテンツが含まれていない場合があります。
そのような場合は、一緒に取得出来たoEmbedメタデータを使用して、次に紹介するcard
関数と同様のコンテンツを生成します。
embed
関数は、コンテンツプロバイダーが用意した埋め込みコンテンツを直接表示させるものでした。
このcard
関数は、コンテンツのメタデータを収集して、MarkTheRipper側で用意したビューで表示させます。
メタデータは、以下の方法で収集します:
- oEmbed: 付随するメタデータを使用(
embed
関数で埋め込みコンテンツが提供されなかった場合を含む) - OGP (Open Graph protocol): 対象のページをスクレイピングし、ページに含まれるOGPメタデータを収集。
- Amazon: Amazon associatesページから収集。
使い方はembed
関数と全く同じです:
## すばらしい動画を発見
いつか、自由に旅行できるようになったら、訪れてみたい...
{card https://youtu.be/1La4QzGeaaQ}
すると、以下のように表示されます:
embed
関数と異なり、付加情報をカード状にまとめた、コンテンツとリンクとして表示します。
同様に:
## USBホストを行う実験
{card https://amzn.to/3V6lYlQ}
様々なコンテンツを、同じカード形式で表示できます。 埋め込み形式とカード形式のどちらを使うかは、好みで使い分ければ良いでしょう。
card
関数も、embed
関数と同様に、専用のレイアウトファイルを用意する必要があります。
レイアウトファイルlayouts/card.html
も、以下のひな形を元に、自分のサイトに合わせていけば良いでしょう:
<div style="max-width:640px;margin:10px;">
<ul style="display:flex;padding:0;border:1px solid #e0e0e0;border-radius:5px;">
<li style="min-width:180px;max-width:180px;padding:0;list-style:none;">
<a href="{permaLink}" target="_blank" style="display:block;width:100%;height:auto;color:inherit;text-decoration:inherit;">
<img style="margin:10px;width:100%;height:auto;" src="{imageUrl}" alt="{title}">
</a>
</li>
<li style="flex-grow:1;margin:10px;list-style:none; ">
<a href="{permaLink}" target="_blank" style="display:block;width:100%;height:auto;color:inherit;text-decoration:inherit;">
<h5 style="font-weight:bold;">{title}</h5>
<p>{author}</p>
<p>{description}</p>
<p><small class="text-muted">{siteName}</small></p>
</a>
</li>
</ul>
</div>
このひな形は、完全に独立したHTMLになっています。もしBootstrapと併用したいのであれば、
mtr init standard
やmtr init rich
で生成されるサンプルレイアウトに含まれるファイルを参照して下さい。
card
関数でも、layouts/card-YouTube.html
のようなファイル名にする事で、サイト固有のカスタマイズが出来ます。
$ dotnet tool install -g MarkTheRipper --nuget-source http://nuget.kekyo.online:59103/repository/nuget/index.json
Apache-v2.
- 0.4.0:
- ページングナビゲーションができるようになりました。#20
- 0.3.0:
- oEmbed を使用できるようになりました。#18
- 0.2.0:
- キーワード展開がマークダウン文書そのものに適用できるようになりました。#3
- 0.1.0:
- マークダウン・ファイルに日付のメタデータを自動的に挿入するようにしました。#13
- 関数呼び出しができるようになりました。#2
- カテゴリーとタグをマークダウンされたコンテンツ全体から集計する事が出来るようになりました。#14
- ブラケット文字に対応しました。#6
- ジェネレータキーワードを使用できるようになりました。#5
- カテゴリの自動収集が可能になりました。#1