# HTMLテンプレート 記述ガイド

`templates/` 配下の HTML テンプレートの書き方をまとめます。
構文の網羅サンプルは [04_template_syntax_showcase.md](04_template_syntax_showcase.md) を参照してください。

---

## 目次

1. [ディレクトリ規約](#1-ディレクトリ規約)
2. [インデント規約](#2-インデント規約)
3. [置換文字の一覧](#3-置換文字の一覧)
4. [トークン分解規則](#4-トークン分解規則)
5. [フォーム部品](#5-フォーム部品)
6. [プログラム変数](#6-プログラム変数)
7. [真偽値判定](#7-真偽値判定)
8. [ループ処理](#8-ループ処理)
9. [コメント](#9-コメント)
10. [部品(part)](#10-部品part)
11. [レイアウトと `{_contents}`](#11-レイアウトと-_contents)
12. [空白・改行の扱い](#12-空白改行の扱い)
13. [テンプレートに書いてはいけないこと](#13-テンプレートに書いてはいけないこと)
14. [実テンプレートの解説](#14-実テンプレートの解説)

---

## 1. ディレクトリ規約

テンプレートルートは `templates/`。その配下は `web/` のディレクトリ構造を踏襲します。

```
templates/
    view/
        form/                      ← web/form/ に対応
            layout.html            レイアウト
            _parts/                このアプリ専用の共通部品
                header.html
                footer.html
            error/
                404.html
            index.html
            confirm.html
            complete.html
        shop/                      ← web/shop/ に対応
            layout.html
            _parts/
                header.html
                footer.html
            error/
                404.html
            product/
                index.html
                detail.html
            cart/
                index.html
            order/
                index.html
                confirm.html
                complete.html
            admin/                 ← web/shop/admin/ に対応
                layout.html
                _parts/
                    header.html
                error/
                    404.html
                login.html
                dashboard.html
                product/
                    index.html
                    form.html
                order/
                    index.html
                    detail.html
                shipping/
                    index.html
                    carrier.html
                    fee.html
```

**規約**

| 規約 | 内容 |
|---|---|
| 拡張子 | `.html` 固定。テンプレート内のパス指定に拡張子は書かない |
| レイアウト | 各アプリの Viewディレクトリ直下に `layout.html` を置く |
| 共通部品 | `_parts/` に置く。`_parts/` 配下は Controller のルーティング対象にしない |
| エラーページ | `error/404.html` に置く。レイアウトなしで描画されるため単体で完結したHTMLにする |
| 画面テンプレート | `Model::render()` に渡すパスと一致させる。`render('product/detail', ...)` → `product/detail.html` |

---

## 2. インデント規約

**インデントはタブ1文字。** HTML の入れ子の深さに合わせて増やします。

```html
{# マスターレイアウト。Viewの描画結果が {_contents} に差し込まれる #}
<!DOCTYPE html>
<html lang="ja">
	<head>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, initial-scale=1">
		<title>{_title}</title>
		<link rel="stylesheet" href="{_baseUrl}/commons/css/style.css">
	</head>
	<body>
		{part:_parts/header}
		<main class="l-container">
		{_contents}
		</main>
		{part:_parts/footer}
	</body>
</html>
```

- `<head>` / `<body>` は1段
- その子要素は2段
- `{part:...}` `{_contents}` も、その位置の階層に合わせる
- 制御タグ(`{_x:y}` `{_x:start}` など)は、囲む対象と同じ階層に置く

制御タグが単独で1行を構成している場合、その行はインデントごと出力から除去されるため、
**インデントを付けても出力HTMLに空白が堆積しません。**

---

## 3. 置換文字の一覧

| 項目 | 置換文字 |
|---|---|
| テキストボックス | `{full_name}` |
| ラジオボタン | `{gender:male:checked}` |
| チェックボックス(配列) | `{source:web:checked}` |
| プルダウン | `{age:20s:selected}` |
| テキストエリア(改行変換なし) | `{policy}` |
| テキストエリア(改行を `<br />` へ) | `{etc:br}` |
| プログラム変数 | `{_title}` `{_token}` |
| コンテンツ差し込み(レイアウト用) | `{_contents}` |
| 真偽値(単体判定) | `{_user:admin:y}` 〜 `{/_user:admin:y}` |
| 真偽値(否定) | `{_user:admin:n}` 〜 `{/_user:admin:n}` |
| 真偽値(OR判定) | `{_user:admin,master,leader:y}` 〜 `{/_user:admin,master,leader:y}` |
| 真偽値(AND判定) | `{_user:admin:y}{_user:leader:y}` 〜 `{/_user:leader:y}{/_user:admin:y}` |
| 真偽値(判定値なし = 空でないとき) | `{_message:y}` 〜 `{/_message:y}` |
| ループ | `{_products:start}` 〜 `{/_products:end}` |
| ループ(0件のとき) | `{_products:n}` 〜 `{/_products:n}` |
| ループ(1件以上のとき) | `{_products:y}` 〜 `{/_products:y}` |
| ループの件数 | `{_products:count}` |
| ループの連番(1始まり) | `{_products:index}` |
| ループの最初の1件 | `{_products:first}` 〜 `{/_products:first}` |
| ループの最後の1件 | `{_products:last}` 〜 `{/_products:last}` |
| コメント | `{# 出力に含まれないメモ #}` |
| 部品の取り込み(相対) | `{part:_parts/header}` |
| 部品の取り込み(ルート起点) | `{part:/_parts/site_nav}` |

### 3.1 予約語(修飾子)

修飾子は以下の **閉じた集合** です。これ以外を末尾に書くとコンパイル時エラーになります。

```
checked  selected  br  start  end  y  n  count  index  first  last
```

---

## 4. トークン分解規則

区切り文字 `:` が判定値の中に現れても破綻しないよう、**右端アンカー方式** で分解します。

> **変数名は最初の `:` まで。修飾子は最後の `:` 以降。その間はすべて判定値(`:` を含んでよい)。**

| 記述 | 変数名 | 判定値 | 修飾子 |
|---|---|---|---|
| `{full_name}` | full_name | — | — |
| `{etc:br}` | etc | — | br |
| `{_products:start}` | _products | — | start |
| `{gender:male:checked}` | gender | male | checked |
| `{time:10:00〜12:00:selected}` | time | `10:00〜12:00` | selected |
| `{sel:A:checked:selected}` | sel | `A:checked` | selected |

### 4.1 エスケープシーケンス

| 記述 | 意味 |
|---|---|
| `\,` | 判定値中のリテラルなカンマ(OR判定の区切りと区別する) |
| `\}` | 判定値中のリテラルな `}` |
| `\\` | リテラルなバックスラッシュ |

### 4.2 変数名に使える文字

英数字・アンダースコア・角括弧(`user[name]` 形式のフォーム名)。**`:` は使えません。**

---

## 5. フォーム部品

`_` を **付けない** 名前はフォーム値を参照します(Model が `setForm()` で渡した配列)。
`name` 属性の値と置換文字の変数名を一致させます。

### 5.1 テキスト / メール / 数値

```html
<input type="text" id="name" name="name" class="c-form__control" maxlength="50" value="{name}">
<input type="email" id="email" name="email" class="c-form__control" maxlength="255" value="{email}">
<input type="number" id="price" name="price" class="a-form__control" min="0" value="{price}">
```

### 5.2 テキストエリア

```html
<textarea id="body" name="body" class="c-form__control" rows="8">{body}</textarea>
```

確認画面など、改行を `<br />` にして表示したい場合は `:br` を付けます。

```html
<td class="c-table__data">{note:br}</td>
```

### 5.3 ラジオボタン

一致したときだけ ` checked="checked"` が出力されます。

```html
<div class="c-form__choices">
	<label class="c-form__choice"><input type="radio" name="gender" value="male"{gender:male:checked}>男性</label>
	<label class="c-form__choice"><input type="radio" name="gender" value="female"{gender:female:checked}>女性</label>
	<label class="c-form__choice"><input type="radio" name="gender" value="other"{gender:other:checked}>回答しない</label>
</div>
```

### 5.4 チェックボックス(配列)

フォーム値が配列の場合は「含まれているか」で判定します。

```html
<label class="c-form__choice"><input type="checkbox" name="source[]" value="web"{source:web:checked}>WEB広告</label>
<label class="c-form__choice"><input type="checkbox" name="source[]" value="paper"{source:paper:checked}>新聞広告</label>
```

### 5.5 セレクトボックス(選択肢が固定の場合)

```html
<select id="inquiryType" name="inquiryType" class="c-form__control">
	<option value="">選択してください</option>
	<option value="estimate"{inquiryType:estimate:selected}>お見積りについて</option>
	<option value="support"{inquiryType:support:selected}>サポートについて</option>
	<option value="other"{inquiryType:other:selected}>その他</option>
</select>
```

### 5.6 セレクトボックス(選択肢がDB由来の場合)

**判定値に変数は書けません。** Model 側で選択状態を判定した `_selected` を使います。

```html
<select id="prefectureCode" name="prefectureCode" class="c-form__control c-form__control--short">
	<option value="">選択してください</option>
	{# 選択肢はDB由来で可変のため、選択状態はモデルが判定した _selected で出し分ける #}
	{_prefectures:start}
	<option value="{_code}"{_selected:y} selected="selected"{/_selected:y}>{_name}</option>
	{/_prefectures:end}
</select>
```

---

## 6. プログラム変数

`_` を付けた名前はプログラム変数(Model が `set()` 等で渡した値、またはループ行のフィールド)を参照します。

```html
<h1 class="c-heading">{_heading}</h1>
<p class="c-lead">全{_total}</p>
<input type="hidden" name="_token" value="{_token}">
<a href="{_baseUrl}/cart">カート</a>
```

`Model::createViewData()` が既定で設定する変数。

| 変数 | 内容 |
|---|---|
| `{_baseUrl}` | フロントコントローラの設置ディレクトリまでのURL |
| `{_currentYear}` | 現在の年(フッターの著作権表示など) |

### 6.1 未定義の変数

**空文字が出力されます。**エラーにはなりません。

---

## 7. 真偽値判定

### 7.1 判定値なし(空でないとき)

偽とみなす値: `null` / `false` / `''` / `array()` / `0` / `'0'`

```html
{_messages:y}
<div class="c-notice">
	<ul class="c-notice__list">
		{_messages:start}
		<li>{_message}</li>
		{/_messages:end}
	</ul>
</div>
{/_messages:y}
```

この1つの定義により、**フラグ判定とループ0件判定が同一規則になります。**

### 7.2 否定(`:n`)

`else` に相当する記法はありません。`:n` が `:y` の否定として機能します。

```html
{_products:n}
<p class="c-empty">該当する商品がありません。</p>
{/_products:n}
```

### 7.3 判定値あり

| 変数の型 | 判定方法 |
|---|---|
| スカラー | `(string)$value === '判定値'` |
| 配列 | 配列に判定値が含まれるか |

```html
{_status:shipped:y}<span class="a-badge a-badge--ok">発送済</span>{/_status:shipped:y}
```

> 判定値は **英数字とアンダースコアのみ** で構成してください。
> 非ASCII を判定値にしたい場合は Model 側で `setFlag()` を使います。

### 7.4 OR判定

判定値をカンマ区切りで並べます。

```html
{_role:admin,master,leader:y}
<a href="{_navSettingUrl}">システム設定</a>
{/_role:admin,master,leader:y}
```

### 7.5 AND判定

真偽値ブロックを入れ子にします。新しい演算子は導入しません。

```html
{_role:admin:y}
	{_role:leader:y}
	<p>管理者かつリーダーのときだけ表示する内容</p>
	{/_role:leader:y}
{/_role:admin:y}
```

### 7.6 インラインでの使い方

属性値の一部を出し分ける用途にも使えます。

```html
<a href="{_url}" class="c-filter__item{_current:y} is-current{/_current:y}">{_name}</a>
```

```html
<td>{_published:y}<span class="a-badge a-badge--ok">公開</span>{/_published:y}{_published:n}<span class="a-badge">非公開</span>{/_published:n}</td>
```

---

## 8. ループ処理

### 8.1 基本形

```html
{_products:y}
<ul class="c-grid">
	{_products:start}
	<li class="c-card">
		<a href="{_url}" class="c-card__link">
			<p class="c-card__name">{_name}</p>
			<p class="c-card__price">{_price}</p>
		</a>
	</li>
	{/_products:end}
</ul>
{/_products:y}

{_products:n}
<p class="c-empty">該当する商品がありません。</p>
{/_products:n}
```

ループの内側では、ループ変数名を繰り返さず **`{_フィールド名}`** で参照します。

### 8.2 ネスト

```html
{_orders:start}
<section>
	<h2>{_orderNo}</h2>
	{_items:start}
	<p>{_name} × {_quantity}</p>
	{/_items:end}
	{_items:n}
	<p>明細がありません</p>
	{/_items:n}
</section>
{/_orders:end}
```

内側のループ(`_items`)は、外側のループ行が持つ `_items` キーの配列を対象とします。
フィールド名が親子で重複する場合は **一番内側の値が優先** されます。

### 8.3 メタ情報

| 記法 | 意味 |
|---|---|
| `{_products:count}` | 件数(整数)。ループの外でも使える |
| `{_products:index}` | ループ内の連番(**1始まり**) |
| `{_products:first}` 〜 `{/_products:first}` | 最初の1件のときだけ出力 |
| `{_products:last}` 〜 `{/_products:last}` | 最後の1件のときだけ出力 |

```html
{_products:y}<span class="c-badge">{_products:count}件</span>{/_products:y}
{_products:start}
	<tr class="row-{_products:index}">
		<td>{_name}</td>
	</tr>
{/_products:end}
```

---

## 9. コメント

`{# ... #}` で記述します。出力されるHTMLには **一切含まれません**。**ネストも可能です。**

```html
{# 商品一覧。カテゴリ絞り込みとページ送りに対応する #}
```

```html
{# 一時的に無効化
   {# 内側のコメント #}
   ここも出力されない
#}
```

コメント除去は part 展開より前に行われるため、以下も正しく機能します。

```html
{# 一時的に無効化 {part:old_header} #}
```

**テンプレート先頭には、そのテンプレートの役割を1行コメントで書く運用にしています。**

---

## 10. 部品(part)

### 10.1 基本

`{part:ファイル名}` は、**データを流し込む前のテンプレートソースの段階** で、
指定したファイルの中身をその場に貼り付けます。

```html
<body>
	{part:_parts/header}
	<main class="l-container">
	{_contents}
	</main>
	{part:_parts/footer}
</body>
```

### 10.2 スコープ

part は **貼り付け先と同じスコープ・同じ変数の集合** の中でレンダリングされます。
値を渡すための特別な記法は不要です。

```html
<!-- view/shop/_parts/footer.html -->
{# 共通フッター #}
<footer class="l-footer">
	<div class="l-container">
		<p class="l-footer__copyright">&copy; {_currentYear} Sample Shop</p>
	</div>
</footer>
```

ループの内側で part を使った場合も、ループスコープのルールがそのまま適用されます。

```html
{_products:start}
	{part:_parts/item_card}
{/_products:end}
```

### 10.3 パス解決

| 記述 | 解決基準 |
|---|---|
| `{part:header}` | **呼び出し元ファイルと同じディレクトリ** |
| `{part:_parts/item_card}` | 呼び出し元ファイルと同じディレクトリ |
| `{part:/_parts/site_nav}` | **テンプレートルート起点**(`templates/_parts/site_nav.html`) |

- 拡張子 `.html` は自動付与される
- `..` を含むパスは無条件でエラー
- part 名は **静的リテラルのみ**。`{part:{_theme}/header}` は許可されない
- ネストは許可。**循環参照はエラー**(参照経路つきで報告される)

> 本プロジェクトではアプリごとに `_parts/` を持たせているため、
> 相対指定 `{part:_parts/header}` を使います。

---

## 11. レイアウトと `{_contents}`

`{_contents}` は **1スロットのみ**。名前付きブロック(複数スロット)やレイアウトの入れ子はありません。

```html
{# マスターレイアウト。Viewの描画結果が {_contents} に差し込まれる #}
<!DOCTYPE html>
<html lang="ja">
	<head>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, initial-scale=1">
		<title>{_title}</title>
		<link rel="stylesheet" href="{_baseUrl}/commons/css/style.css">
	</head>
	<body>
		{part:_parts/header}
		<main class="l-container">
		{_contents}
		</main>
		{part:_parts/footer}
	</body>
</html>
```

- レイアウトと View は **同じ `ViewData` を共有する**。`{_title}` や `{_baseUrl}` はどちらからも参照できる
- ページ固有の `<script>` や `<meta>` を差し込みたい場合は、Model が `setRaw()` で渡した変数を使う

---

## 12. 空白・改行の扱い

制御タグが単独で1行を構成している場合、**その行は行ごと出力から除去されます。**

> 行が `^[ \t]*` + 制御タグ1個 + `[ \t]*` + (`\r?\n` または EOF)にマッチする場合、その行全体を削除する。

対象:真偽値・ループ・`first`/`last` の開始/終了タグ、`{part:...}`。

そのため、以下のように書いても出力HTMLに空行やインデントは残りません。

```html
{_products:y}
<ul class="c-grid">
	{_products:start}
	<li class="c-card">{_name}</li>
	{/_products:end}
</ul>
{/_products:y}
```

逆に、**同一行に他の内容と一緒に書いた制御タグは除去されません。**
属性値の出し分けに使う場合はこの挙動を利用します。

```html
<a href="{_url}" class="c-filter__item{_current:y} is-current{/_current:y}">{_name}</a>
```

---

## 13. テンプレートに書いてはいけないこと

| やってはいけないこと | 代わりにすること |
|---|---|
| 金額のカンマ区切り、日付の書式変換 | Model で `number_format()` / `date()` して渡す |
| URL の組み立て(`{_baseUrl}/detail/` + ID の連結など) | Model で組み立てて `_url` として渡す |
| 判定値に変数を使う(`{categoryId:{_id}:selected}`) | Model で判定して `_selected` を渡す |
| 条件式・計算(`{_price * _quantity}` のような記法) | Model で計算して渡す |
| `{_x:raw}` のようなテンプレート側でのエスケープ解除 | Model の `setRaw()` に一本化する |
| `_parts/` 配下のファイルを Controller から直接描画する | `_parts/` は part 専用にする |
| ループ行のフィールドに `_` を付けない | すべて `_` プリフィックスを付ける |

**「テンプレートには判定と出力だけを残す」** が原則です。

---

## 14. 実テンプレートの解説

### 14.1 商品一覧(`view/shop/product/index.html`)

```html
{# 商品一覧。カテゴリ絞り込みとページ送りに対応する #}
<h1 class="c-heading">{_heading}</h1>
<p class="c-lead">全{_total}</p>

<nav class="c-filter">
	{_categories:start}
	<a href="{_url}" class="c-filter__item{_current:y} is-current{/_current:y}">{_name}<span class="c-filter__count">{_count}</span></a>
	{/_categories:end}
</nav>

{_products:n}
<p class="c-empty">該当する商品がありません。</p>
{/_products:n}

{_products:y}
<ul class="c-grid">
	{_products:start}
	<li class="c-card">
		<a href="{_url}" class="c-card__link">
			<p class="c-card__category">{_categoryName}</p>
			<p class="c-card__name">{_name}</p>
			<p class="c-card__price">{_price}</p>
			{_soldOut:y}
			<p class="c-card__soldout">在庫切れ</p>
			{/_soldOut:y}
		</a>
	</li>
	{/_products:end}
</ul>
{/_products:y}

{_pages:y}
<nav class="c-pager">
	{_pages:start}
	<a href="{_url}" class="c-pager__item{_current:y} is-current{/_current:y}">{_number}</a>
	{/_pages:end}
</nav>
{/_pages:y}
```

| 記述 | 解説 |
|---|---|
| `{_total}` | Model が `number_format($total).'件'` まで作って渡している |
| `{_current:y} is-current{/_current:y}` | 現在地の判定は Model が bool で持たせている。テンプレートは CSS クラスを足すだけ |
| `{_products:n}` / `{_products:y}` | 0件と1件以上の出し分け。同じ規則で書ける |
| `{_url}` | 商品詳細・カテゴリ絞り込み・ページャの URL はすべて Model が組み立て済み |
| `{_soldOut:y}` | 在庫の有無は Model が `($stock <= 0)` で判定済み |

### 14.2 一覧の行ごとに削除フォームを置く(`view/shop/admin/product/index.html`)

テーブルの中に `<form>` を入れ子にできないため、
**フォームをテーブルの外に置き、`form` 属性でボタンと紐づけます。**

```html
			<td>
				<a href="{_editUrl}" class="a-button a-button--small">編集</a>
				<button type="submit" form="delete-{_id}" class="a-button a-button--small a-button--danger">削除</button>
			</td>
		</tr>
		{/_products:end}
	</tbody>
</table>

{# 削除フォームはテーブルの外に置き、form属性でボタンと紐づける #}
{_products:start}
<form action="{_deleteUrl}" method="post" id="delete-{_id}">
	<input type="hidden" name="_token" value="{_token}">
	<input type="hidden" name="id" value="{_id}">
</form>
{/_products:end}
```

同じ配列を2回ループしている点に注目してください。ループは何度でも回せます。

### 14.3 グループ見出し付きの入力欄(`view/shop/admin/shipping/fee.html`)

47都道府県を地方区分ごとに区切って表示します。
**「見出しを出すかどうか」の判定も Model 側で `_regionHead` として持たせています。**

```html
	<div class="a-fee">
		{# 地方区分が変わる行にだけ見出しを出す(モデルが判定した _regionHead を使用する) #}
		{_fees:start}
		{_regionHead:y}<h3 class="a-fee__region">{_region}</h3>{/_regionHead:y}
		<div class="a-fee__item">
			<label class="a-fee__label" for="fee-{_code}">{_name}</label>
			<input type="number" id="fee-{_code}" name="fees[{_code}]" class="a-fee__input" min="0" value="{_fee}">
			<span class="a-fee__unit">円</span>
		</div>
		{/_fees:end}
	</div>
```

`name="fees[{_code}]"` のように、**`name` 属性を動的に組み立てる** こともできます。
受け側は `$this->request->getPost('fees', array())` で配列として受け取ります。

### 14.4 確定前と確定後で表示を変える(`view/shop/cart/index.html`)

送料は都道府県が決まらないと確定しません。
**「確定したか」を Model が `_shippingDecided` で持たせ、テンプレートは表示を切り替えるだけです。**

```html
<div class="c-summary">
	<dl class="c-summary__list">
		<dt>商品小計({_quantity}点)</dt>
		<dd>{_subtotal}</dd>
		<dt>送料</dt>
		{# 都道府県が未確定の間は送料を確定できないため、金額の代わりに案内を出す #}
		{_shippingDecided:y}
		<dd>{_shipping}</dd>
		{/_shippingDecided:y}
		{_shippingDecided:n}
		<dd>お届け先により決定</dd>
		{/_shippingDecided:n}
		<dt class="c-summary__total">合計</dt>
		{_shippingDecided:y}
		<dd class="c-summary__total">{_total}</dd>
		{/_shippingDecided:y}
		{_shippingDecided:n}
		<dd class="c-summary__total">{_subtotal} + 送料</dd>
		{/_shippingDecided:n}
	</dl>
	{# 送料無料の閾値が設定されている場合だけ案内文を出す #}
	{_hasFreeShipping:y}
	{_freeShipping:y}
	<p class="c-summary__note">送料無料の対象です。</p>
	{/_freeShipping:y}
	{_freeShipping:n}
	<p class="c-summary__note">商品小計{_freeShippingThreshold}以上で送料無料です。</p>
	{/_freeShipping:n}
	{/_hasFreeShipping:y}
</div>
```

`{_hasFreeShipping:y}` の中に `{_freeShipping:y}` / `{_freeShipping:n}` を入れ子にすることで、
**AND 条件を演算子なしで表現** しています。
