---
name: partial-views
description: "Scaffold partial-view PHP files from an indented-tree DSL notation. Generates structural skeletons with correct wrappers, printMe() glue, and early-return guards. Use when the user says /partial-views. TRIGGER when: a prompt or task description contains a ```partial-views or ```pv fenced code block."
argument-hint: "<tree-file | --family <name> --type <type>> [--dry-run]"
---

# partial-views — Scaffold View Trees from DSL

Generate partial-view `.php` files from a compact indented-tree notation.
The skill creates structural skeletons only — leaf content (attribute reads,
conditionals on fields) is left for per-prompt work.

## Argument syntax

```
/partial-views <tree-file>                      # read tree from file
/partial-views --family <name> --type <type>    # scaffold family default
/partial-views --dry-run <tree-file>            # preview without writing
/partial-views --dry-run --family <name> --type <type>
```

If `$ARGUMENTS` is empty **and** the invoking prompt contains a `` ```partial-views ``
or `` ```pv `` fenced code block, use **inline mode** (see Step 2c).

If `$ARGUMENTS` is empty and no fenced block is found, ask the user which input mode
they want.

Flags can appear in any order. `--dry-run` works with all modes.
Flags on the fence opening line (e.g. `` ```pv --dry-run ``) are also accepted.

## Steps

### 1 — Parse arguments

Extract from `$ARGUMENTS`:

- `--dry-run` flag (boolean)
- `--family <name>` and `--type <type>` (family mode)
- remaining argument as file path (file mode)

If both `--family` and a file path are present, report error and stop.
If `--family` is present but `--type` is missing (or vice versa), report error and stop.

### 2 — Obtain the tree source

#### 2a — Family mode (`--family <name> --type <type>`)

Read `.claude/conventions/model_families.yaml`.

If the file does not exist, report:
> `.claude/conventions/model_families.yaml` not found. Create it first — see the model-families convention doc.

Stop.

Look up `families.<name>`. If not found, list available family names and stop.

Extract the `tree` field. Replace every `{type}` with the `--type` value.
Use the result as the tree source.

#### 2b — File mode

Read the file at the given path. If it does not exist, report and stop.
Use its contents as the tree source.

#### 2c — Inline mode (fenced code block)

When the skill is triggered by a `` ```partial-views `` or `` ```pv `` fence in
the prompt (rather than an explicit `/partial-views` command):

1. Extract the content between the opening fence and the closing ` ``` `.
2. Parse any flags on the opening fence line (e.g. `` ```pv --dry-run ``).
3. If multiple fenced blocks are present, process them in order as a single
   concatenated tree source (separated by a blank line).
4. Use the extracted content as the tree source.

Both `partial-views` and `pv` are equivalent — `pv` is the short alias.

### 3 — Parse the tree

The tree source is a series of **blocks** separated by one or more blank lines.
Each block declares one partial view and its children.

#### 3.1 — Tokenize lines

For each non-blank line, compute its **indent level** (number of leading spaces ÷ 2).
Lines with odd leading spaces are a parse error — report the line number and stop.

Strip comments: everything from `#` to end-of-line (unless inside `{…}`).

#### 3.2 — Block structure

The first line of each block (indent 0) is the **partial-view declaration**.
It must match one of:

| Pattern | Meaning |
|---|---|
| `type/viewname` | Declares `public/local/views/<type>/<type>_<viewname>.php` |

If the first line has no `/`, it is a **continuation block** — reuse the type from
the previous block. If there is no previous block, report error and stop.

#### 3.3 — Child nodes

Subsequent indented lines are children. Parse each line as one of these node types
(try in order):

| Syntax | Node type |
|---|---|
| `if-attr:<attr1>,<attr2>,...` | early-return guard (attributes) |
| `if-empty: <expr>` | early-return guard (expression) |
| `if: <expr>` | scoped conditional |
| `> partialname {classes: "<tw>"}` | printMeArguments call |
| `> partialname` | printMe call |
| `> printAttr:<attr> {classes: "<tw>"}` | printAttrValue call |
| `> printAttr:<attr>` | printAttrValue call (no extra classes) |
| `__svg name {classes: "<tw>"}` | __svg() call |
| `__svg name` | __svg() call (no classes) |
| `tag#id.cls1.cls2` | DOM element with id and classes |
| `tag.cls1.cls2` | DOM element with classes |

**Guard placement rules:**
- `if-attr:` and `if-empty:` must be the **first child** (indent 1) of a partial-view declaration. If found elsewhere, report error and stop.
- `if:` can appear at any indent level; it wraps only its immediate subtree.

**DOM element parsing:**
Split on `.` — first segment is `tag` (default `div` if the line starts with `.`).
If the tag segment contains `#`, split on `#` to get tag and id.
Remaining segments after the first are CSS classes, joined with spaces.

When a DOM element node's class list contains `{type}` placeholders, substitute
them with the current block's type value (hyphens, not underscores — e.g. type
`dirokis` stays `dirokis`).

### 4 — Generate PHP

For each parsed block, generate one `.php` file.

#### 4.1 — File path

`public/local/views/<type>/<type>_<viewname>.php`

#### 4.2 — Wrapper class

The outermost element gets a class derived from the filename: replace underscores
with hyphens. Example: `dirokis_main_header` → `dirokis-main-header`.

This named class is always the **first** class on the wrapper element.

#### 4.3 — Early-return guard

If the block has an `if-attr:` child:

```php
<?php
$attr1 = $item->getAttr('attr1');
$attr2 = $item->getAttr('attr2');
if (!$attr1 || !$attr2) return;
?>
```

If the block has an `if-empty:` child:

```php
<?php
if (<expr>) return;
?>
```

The guard appears **before** the wrapper element — an empty view prints nothing.

#### 4.4 — Body generation (recursive)

Process children top-down. For each node:

**`> partialname`** →
```php
  <?php $item->printMe('partialname'); ?>
```

**`> partialname {classes: "<tw>"}`** →
```php
  <?php $item->printMeArguments('partialname', ['class' => '<tw>']); ?>
```

**`> printAttr:<attr> {classes: "<tw>"}`** →
```php
  <?= $item->printAttrValue('<attr>', '<tw>') ?>
```

**`> printAttr:<attr>`** (no classes) →
```php
  <?= $item->printAttrValue('<attr>') ?>
```

**`__svg name {classes: "<tw>"}`** →
```php
  <?php __svg('<name>', '<tw>'); ?>
```

**`__svg name`** →
```php
  <?php __svg('<name>'); ?>
```

**`tag.cls1.cls2`** → opens element, processes children, closes element:
```php
  <tag class="cls1 cls2">
    ...children...
  </tag>
```

**`tag#id.cls1.cls2`** → same with id:
```php
  <tag id="id" class="cls1 cls2">
    ...children...
  </tag>
```

**`if: <expr>`** → wraps its subtree:
```php
  <?php if (<expr>) { ?>
    ...children...
  <?php } ?>
```

#### 4.5 — Indentation

Use 2-space indentation in generated PHP. The wrapper `<div>` is at indent 0.
Each nesting level adds 2 spaces.

#### 4.6 — Wrapper element

The wrapper is always `<div class="named-class">`. All children are indented
inside it. The tree source should not repeat the wrapper as a child node —
the `type/viewname` declaration line is what creates the wrapper.

### 5 — Check existing files

Before writing each file, check if it already exists.

- **Exists** → add to skipped list, do **not** overwrite.
- **Missing** → add to write list.

If all files already exist, report that and stop.

### 6 — Write or preview

**If `--dry-run`:**
For each file in the write list, print the file path and generated content.
Do not write anything. Report skipped files too.

**Otherwise:**
Create the type subdirectory under `public/local/views/` if it doesn't exist.
Write each file. Report what was created and what was skipped.

### 7 — Report

Show the user:
- Files created (or would be created in dry-run)
- Files skipped (already existed)
- Any warnings

## DSL notation reference

### Node types

| Syntax | Meaning |
|---|---|
| `type/viewname` | Partial view declaration → `views/<type>/<type>_<viewname>.php` |
| `> partialname` | `$item->printMe('partialname')` |
| `> partialname {classes: "<tw>"}` | `$item->printMeArguments('partialname', ['class' => '<tw>'])` |
| `> printAttr:<attr> {classes: "<tw>"}` | `$item->printAttrValue('<attr>', '<tw>')` |
| `> printAttr:<attr>` | `$item->printAttrValue('<attr>')` |
| `__svg name {classes: "<tw>"}` | `__svg('<name>', '<tw>')` |
| `__svg name` | `__svg('<name>')` |
| `tag.cls1.cls2` | DOM element with classes |
| `tag#id.cls1` | DOM element with id and classes |
| `if-attr:<a>,<b>` | Early-return guard: return if any attr is empty |
| `if-empty: <expr>` | Early-return guard: return if expression is true |
| `if: <expr>` | Scoped conditional (wraps subtree only) |
| `# comment` | Comment (ignored) |

### Indentation

2 spaces per nesting level. Blank lines separate blocks.

### Placeholders

`{type}` is replaced with the current type value everywhere (family mode
substitutes it; in file mode the type comes from the `type/viewname` declaration).

## Worked examples

### Example 1 — Trivial partial

Input:
```
news/content_body
  > printAttr:content {classes: "prose"}
```

Output `public/local/views/news/news_content_body.php`:
```php
<div class="news-content-body">
  <?= $item->printAttrValue('content', 'prose') ?>
</div>
```

### Example 2 — Early-return guard with if-attr

Input:
```
news/box_descr
  if-attr:title,info
  div.flex.flex-col.gap-2
    > printAttr:title {classes: "text-20 font-bold uppercase leading-tight"}
    > printAttr:info {classes: "text-16"}
```

Output `public/local/views/news/news_box_descr.php`:
```php
<?php
$title = $item->getAttr('title');
$info = $item->getAttr('info');
if (!$title || !$info) return;
?>
<div class="news-box-descr">
  <div class="flex flex-col gap-2">
    <?= $item->printAttrValue('title', 'text-20 font-bold uppercase leading-tight') ?>
    <?= $item->printAttrValue('info', 'text-16') ?>
  </div>
</div>
```

### Example 3 — Family default

Command:
```
/partial-views --family article --type news
```

Uses `model_families.yaml` → `families.article.tree`, substitutes `{type}` → `news`.
Generates:

- `public/local/views/news/news_full.php`
- `public/local/views/news/news_main.php`
- `public/local/views/news/news_content.php`

`news_full.php`:
```php
<div class="news-full">
  <?php $item->printMe('full_header'); ?>
  <?php $item->printMe('main'); ?>
  <?php $item->printMe('full_footer'); ?>
</div>
```

`news_main.php`:
```php
<div class="news-main">
  <?php $item->printMe('main_header'); ?>
  <div class="container-limited">
    <?php $item->printMe('content'); ?>
  </div>
  <?php $item->printMe('main_footer'); ?>
</div>
```

`news_content.php`:
```php
<div class="news-content">
  <?php $item->printMe('content_header'); ?>
  <?php $item->printMe('content_body'); ?>
  <?php $item->printMe('content_footer'); ?>
</div>
```

## Conventions enforced

- `{ }` for control structures, never `if():` / `endif;`
- `<?= ?>` for output, `<?php ?>` for logic
- `printAttrValue()` for attribute output, never `getAttr()+htmlspecialchars()`
- `__svg()` for SVGs, never `printMeArguments`
- 2-space indentation
- Named class (filename → hyphens) is always first on the wrapper
- Guard before wrapper — empty content produces zero output
- No opening `<?php` block unless a guard exists

## Error handling

Report clearly and stop without producing partial output:

- Missing `model_families.yaml` when `--family` is used
- Unknown family name (list available families)
- Missing `--type` with `--family` (or vice versa)
- Odd indentation (not a multiple of 2 spaces)
- `if-attr:` / `if-empty:` not as first child of a block
- Unknown node syntax (line doesn't match any node type)
- File path not found (file mode)
