hakyllでOrgのPROPERTIESドロワーの情報を使う方法
解決方法が知りたい場合 *解決方法まで飛ばして下さい。
私のこのブログでも使っているので参考までに: Cj-bc/blog
Hakyllの提供している
Metadata
は使わない よPostを処理する
Rules
内でpandocを用いて取り出し、saveSnapshot
するよ
Hakyllのメタデータとは
Hakyllでは、作者やタイトル・タグ等のデータを "メタデータ"として特別なフォーマットを用いて扱います。
デフォルトでは
Yamlヘッダー形式
対応する
.metadata
ファイルからの読み込み
の2種類の方法により収集され、テンプレートの中で例えば
<h1>$title$</h1>
<h2>Author: $author$</h2>
のようにして使用されることになります。
問題点
Orgファイル(他のファイル形式でも)では、元々メタデータ用の 構文を持っています。
#+TITLE: タイトルのメタデータです
とする形式(この記事では扱いません!!!)と
* 見出し
:PROPERTIES:
:AUTHOR: Cj.bc-sd
:END:
とするプロパティを用いる形式の二つです。
特にOrg文書はOrg-modeとの連携が大事になり、出来ることならそれ自体の 形式を使いたいわけです。
しかし、残念ながら 後から Metadata
を更新する方法はなさそう です。
そのため、 上記の情報等を Org propertiesから読み込むことができません。
余談: 書き換えられない証拠
Metadata
を扱える型クラス MonadMetadata
の Compiler
インスタンスを見てみると、
compilerGetMetadata
> resourceMetadata
> load
(Compiler
の loadとは別)
> loadMetadata
> の中で、色々な関数において直接
ファイルを読み込んでパースしているのがわかります。
解決方法
orgファイルを読み込む時にPandocのデータ型から取り出し、 Snapshot
に仕舞っておくという手法を取ります。
1. 投稿の内容を Pandoc
型として取得する
pandocCompiler
を使うと String
型に変換されてしまいメタデータが消えるため、
readPandoc
若しくは readPandocWith
を使います。(オプションを変更したい場合は
readPandocWith
の方を使います。)
getResourceBody
を用いることで、今処理しているファイルの中身が取れるのでそれを
readPandoc
に渡します。
match "posts/*.org" $ do
route idRoute
compiler $ do
-- ↓ここの行のこと
postPandoc <- getResourceBody >>= readPandoc
2. Pandoc
からProperties drawerの情報を取り出す
PROPERTIES drawerに入っている情報は、
該当のheadlineのAttributeとして保存されます。
Pandoc
の Meta
ではない ので注意してください。
Org文章でいうHeadlineは、Pandocでは Block
の Header
に
あたります。この中の Attr
にkey-value Pairとして格納されています。
以下は実装からの引用(一部抜粋):
-- | Block element.
data Block
= ...
-- | Header - level (integer) and text (inlines)
| Header Int Attr [Inline]
-- | Attributes: identifier, classes, key-value pairs
type Attr = (Text, [Text], [(Text, Text)])
具体的には、先程の postPandoc
の中身を調べていくことになります。
私は、各投稿の最初には必ずLevel1のheadlineが来るようにしているので
Level1の Header
が来ることを期待して取り出す処理をかきます。
様々な状況に対応させたい場合、ここはもう少し丁寧にやった方が良いと思います。
match "posts/*.org" $ do
route idRoute
compiler $ do
(Pandoc _ blocks) <- getResourceBody >>= readPandoc
let properties = case blocks of
(Header 1 (_:_:kv) _) -> kv
_ -> []
3. お目当ての情報を取り出す
先程作った properties
変数(properties :: [(Text, Text)]
)は
(プロパティ名, プロパティの値)
という構造になっているので、
必要なプロパティを取り出します。
今回は例として、 AUTHOR
を取り出すことにします。
match "posts/*.org" $ do
route idRoute
compiler $ do
(Pandoc _ blocks) <- getResourceBody >>= readPandoc
let properties = case blocks of
(Header 1 (_:_:kv) _) -> kv
_ -> []
authorProperty = fromMaybe "著者不明" $ lookup "AUTHOR" properties
4. Snapshotに仕舞っておく
Snapshot
に仕舞っておくことで、
後に他の Compiler
や Context
の中から使用することができます。
match "posts/*.org" $ do
route idRoute
compiler $ do
(Pandoc _ blocks) <- getResourceBody >>= readPandoc
let properties = case blocks of
(Header 1 (_:_:kv) _) -> kv
_ -> []
authorProperty = fromMaybe "著者不明" $ lookup "AUTHOR" properties
saveSnapshot "title" =<< makeItem authorProperty
5. 他の場所から使う(別の Compiler
編)
上記のステップの後、別のCompilerモナドからtitleプロパティの値を使うことができるようになりました。
loadSnapshotBody "" "title"
6. 他の場所から使う(Context
編)
実はこれが一番やりたかったことでした。
Contextの中から loadSnapshotBody
で読み込むことで
Identifier
毎に違う値を取り出すことができるため、
listField
を使う際に各要素毎に違う値を持たせることができます。
そのためには field
関数を直接使います。
titleField' :: String -> Context String
titleField' key = field key $ \item -> loadSnapshotBody (itemIdentifier item) "title"
これで、与えられたキーに対して"title"というSnapshotの値を入れこむことができるようになりました。
listField
で使う例はこんな感じです(このブログのコードだったりします):
-- | Common Contexts for pages that holds post list
postListCtx :: [Item String] -> Context b
postListCtx posts = listField "posts" (titleField' "title" <> postCtx) (return posts)
参考情報
実は、メタデータの収集元については他にも困っている人がそれなりに いるようで、Githubにissueがいくつか立っています。