データファイル
このページの内容
データファイル
再利用可能な変数 は、company や
supportEmail のようなフラットで一度きりの事実には最適ですが、チームの名簿、
料金表、機能比較表のような、実際に「形」を持つものには扱いにくくなります。
データファイル はそのギャップを埋めます - プロジェクトに
docs/data/*.yaml/.yml/.json ファイルを置くだけで、その内容全体 - オブジェクト
でも配列でも、好きな形で構いません - が、どのページからでも data.<file> として、
variables/page がすでに使っているのと同じ {{ }} 構文で参照できるようになります。
規約
docs/data/ フォルダを追加します。各ファイルのベースネーム(拡張子を除いたもの)が、
data の下の1つのトップレベルキーになります:
docs/
├── index.md
└── data/
├── team.yaml
└── pricing.json
- name: Luis Majano
role: CEO
- name: Jon Clausen
role: CTO
{
"free": { "price": 0, "seats": 3 },
"pro": { "price": 29, "seats": 20 }
}
data.team はそのまま先ほどの配列となり、data.pricing.pro.price はネストした
その数値になります - ファイルのパース済みルートは、オブジェクトであれ配列であれ、
パースされたまま正確にそのまま使われ、従うべき固定の形はありません。
docs/data/ フォルダが一切ない場合は、単に data が存在しないだけです - これは
docs/functions.bxs/
docs/blog/authors.yml がすでに採用している、存在すればオプトインされる
のと同じ形です。
その内容は、通常の Markdown からドット区切りのパスで参照できます:
The Pro plan is **${{ data.pricing.pro.price }}/mo** for up to
{{ data.pricing.pro.seats }} seats.
ビルドすると次のようになります:
<p>The Pro plan is <strong>$29/mo</strong> for up to 20 seats.</p>
同じベースネームを持つファイルが拡張子違いで複数存在する場合(products.yaml と
products.json の両方がある場合)、.yaml が優先され、次に .yml、最後に
.json の順になります - 実運用では、この優先順位に頼るのではなく、ベースネーム
ごとに1つの形式を選ぶようにしてください。
データを利用する
スカラーの {{ data.x.y }} 参照は、{{ }} がすでに機能するあらゆる場所で動作
しますが、チームのグリッドや料金表のような実際のコンテンツでは、たいてい
data.* に対するループが必要になります。そのループがどこに属するかに応じて、
3つの方法があります:
テーマオーバーライドの中で
プロジェクトが theme/ オーバーライドを持つと(テーマ
を参照)、data は page/siteConfig がすでにそうなっているのと同じように、
裸のまま layout.bxm/page.bxm にバインドされます - {{ }} は不要で、本物の
BoxLang だけです:
<ul class="footer-sponsors">
<bx:loop array="#data.sponsors#" index="sponsor">
<li>#encodeForHTML( sponsor )#</li>
</bx:loop>
</ul>
これは、特定の1ページのコンテンツというより、すべての ページに属するデータ (フッターのスポンサー一覧、サイト全体のナビバッジなど)にとって自然な 置き場所です。
マジック関数から
マジック関数 も data を裸のまま
読み取れます(page/siteConfig などと同じ「サポート変数」の1つです)- 本物の
BoxLang でそれをループ/分岐させ、Markdown/HTML の断片を返せます:
function $team() {
var html = ""
for ( item, idx in data.team ) {
html &= "- **" & encodeForHTML( item.name ) & "** - " & encodeForHTML( item.role ) & char( 10 )
}
return html
}
## Our team
{{ $team() }}
これはサーバーサイドで、ビルド時にレンダリングされます - 下記の Alpine の レシピとは異なり、JavaScript なしで検索クローラーにも見えます。
Markdown 内で直接、::: for/::: if を使う
マジック関数をまったく必要としないループや単純な真偽チェックであれば、
::: for/::: if が
Markdown から直接使えます:
::: for member, idx in data.team
{{ idx }}. **{{ member.name }}** - {{ member.role }}
:::
::: for <item>, <index> in <dotted.path> は、<dotted.path> が何に解決される
かに応じて、BoxLang 自身のネイティブな2変数版 for ループのセマンティクスを
使って <item>/<index> をバインドします - 配列なら要素 + 1始まりのインデックス、
構造体ならキー + 値というように、どちらの場合もまったく同じ構文です(配列か
構造体かで自分で分岐を書く必要はありません):
::: for name, enabled in data.flags
- {{ name }}: {{ enabled }}
:::
::: if <dotted.path> は、解決された値が真の場合にのみ自身のコンテンツを
レンダリングします(空の配列/構造体/文字列、0、false はすべて偽として
扱われます):
::: if data.flags.betaBanner
このビルドではベータ機能が有効になっています。
:::
::: if の直後に ::: elseif <dotted.path>(何個でも)と、末尾に裸の
::: else を連ねることで、本物の if/elseif/else セマンティクスになります -
最初に真になった条件が採用され、::: else は残りすべてを引き受け、あとに続く
分岐自身の条件は、自分の番が来るまでは解決すらされません。連鎖全体は末尾の
1つの ::: で閉じます - ::: elseif/::: else 自体が直前の分岐の終わりを
示すため、それぞれの手前に ::: を書く必要はありません(そのように明示的に
書きたい場合でも、それでも動作はします):
::: if data.flags.darkModeDefault
ダークモードがデフォルトで有効になっています。
::: elseif data.flags.betaBanner
ベータ機能は有効になっていますが、ダークモードはデフォルトでは有効ではありません。
::: else
このビルドには特に変わったところはありません。
:::
どちらの本文にも、通常の Markdown や、ネストした ::: for/::: if を含む他の
コンテンツブロックを入れることができます。文法は意図的に狭く絞られており、
{{ }} 自身と同じです - この最初のバージョンでは、ドット区切りのパスのみで、
比較演算子(==、&&、...)はありません。本物の比較が必要な場合は、代わりに
(上記の)マジック関数を使ってください - そちらはすでに BoxLang をフルに使えます。
Alpine で、クライアントサイドから(x-data)
インタラクティビティ はすでに、生の x-data/x-for HTML を
Markdown に直接書き込む方法をカバーしています。手書きの JS 配列の代わりに
data.* からそれを供給するには、data.* を安全な HTML 属性値に変換するだけで
済みます。jsonSerialize() だけでは不十分です - その結果は、"..." で
クォートされた属性の中に安全に収まるために、さらに HTML 属性エンコードが
必要です(ColdBox 自身の attribute()/forAttribute() ヘルパーが使うのと
同じ2段階のレシピです)- そのため、自分自身の functions.bxs の中で、一度だけ
1行のヘルパーを定義してください:
function $jsonAttr( required any value ) {
return encodeForHtmlAttribute( jsonSerialize( arguments.value ) )
}
encodeForHtmlAttribute() は bx-esapi 由来で、これはすでにすべての bx-sites
プロジェクトの依存関係です - 新しい依存関係は不要で、このレシピを使うだけです。
続いて、Markdown では:
<div x-data="{ team: {{ $jsonAttr(data.team) }} }">
<template x-for="member in team" :key="member.name">
<li x-text="member.name + ' - ' + member.role"></li>
</template>
</div>
x-data の周りではプレーンな二重引用符がそのまま安全に使えます -
encodeForHtmlAttribute() がすでにその衝突を処理しているため、シングル
クォートによる回避策は不要です。これは、クライアントサイドだけでレンダリング
される唯一の経路です(JavaScript を無効にした読者や検索クローラーには何も
見えません)- コンテンツが JavaScript なしでも見える必要がある場合は、代わりに
マジック関数か ::: for を使ってください。
なぜ Markdown 内の BoxLang テンプレートではなく、データファイルなのか?
これを設計する過程で、関連するより大きな疑問が持ち上がりました: 狭い
::: for/::: if を追加し、それ以上のことはマジック関数に頼らせる代わりに、
なぜ Markdown 自体を本物の BoxLang テンプレート(ループ、条件分岐、任意の
ロジック)にしてしまわないのか? 理由は2つあります:
- 信頼境界。
docs/**.mdは、多数の/外部の/信頼度の低い貢献者によって 日常的に編集される唯一の成果物です(ドキュメント PR)。docs/functions.bxsは、プロジェクトオーナー が明示的に作成する唯一の成果物です。すべての.mdファイルを本物の BoxLang テンプレートとしてコンパイルしてしまうと、 その境界が崩れてしまいます - ドキュメント PR を開けるだけの貢献者が、 単なる Markdown テキストではなく、任意の BoxLang 実行(ファイル I/O、 環境アクセス)を手に入れることになってしまいます。 - 失敗モード。 今日、一致しない
{{ }}はリテラルテキストとしてそのまま 残ります - タイプミスがビルドを壊すことは決してありません。BoxLang テンプレートのコンパイルエラーはハードな失敗です。::: for/::: ifも 同じ寛容な形を保っています(解決できないパスは、静かに誤ってコンパイル されるのではなく、タイプミスをきちんと捕捉する明確なエラーを送出します - エラー を参照)。
データファイルは、どちらのトレードオフも払うことなく、実際のギャップ
(構造化されたコンテンツと、それに対するループ/条件分岐)を埋めます:
Markdown 自体は {{ }} で置換されるまで不活性なままであり、functions.bxs
は本物の BoxLang ロジックへの、明示的に信頼された唯一の抜け道であり続けます。
スコープ
docs/data/はプロジェクト全体のもので、一度だけ読み込まれます - これはfunctions.bxsがすでに持っているのと 同じ、単一読み込みのスコープです。すべてのバージョン/ロケールツリーが同一のdataを見ます。このバージョンでは、バージョンごと/ロケールごとの オーバーライドやマージはありません。docs/data/をdocs/versions/<name>/やdocs/i18n/<code>/に複製しないでください - そこからは読み込まれません。- フラットなディレクトリのみです - このバージョンでは
docs/data/への サブフォルダの再帰はありません。これはdocs/blog/authors.ymlがすでに持っている「ちょうど1ファイル」という形と同じです。 dataは、pageがすでにそうであるのと同じように、予約された{{ }}の 名前です(予約された名前 を 参照)- もしプロジェクトが何らかの形でbxsites.yamlのvariables.dataエントリを宣言していたとしても、それが優先されるのではなく、docs/data/自身の構造体によって覆い隠されます。同じ理由で、docs/functions.bxsもdataという名前の関数を宣言することはできません。
エラー
BxSites.InvalidDataFile-docs/data/*.yaml/.yml/.jsonファイルの パースに失敗した場合(YAML/JSON の構文エラー)。問題のファイル名が 示されます。BxSites.UnknownVariable-{{ data.x.y }}(または::: for/::: ifのパス)が、実際のdocs/data/の内容に対して解決できない場合。BxSites.InvalidForTarget-::: for自身のパスが、配列でも構造体でも ない何かに解決された場合(ループできません)。