YAMLでHTMLを書いてみる

## 概要

タイトルからして「お前は何を言ってるんだ」って感じですが、HTMLに変換できるデータ構造をYAMLの仕様の範囲内で記述して[Haml](https://haml.info/)や[Pug](https://pugjs.org/)みたいなことができないかと考えてみました。

## 仕組み

JSON界隈には[JSONML](http://www.jsonml.org/)というXMLをJSONで表現する方法があります。厳密には違いますが、XMLを表現できるのならHTMLも表現できるのではないか、また、YAMLはJSONの(ほぼ)スーパーセットであり、JSONで表現できるならYAMLでも表現できるので、JSONMLをYAMLの仕様で書いてHTMLに変換できたら見やすく書けるんじゃないかという気がしたので試してみた感じです。

## JSONMLの仕様

JSONMLの仕様をざっくりと説明すると、以下のような感じです。

- 要素はリスト形式で表す
- リストの1つ目は必ずタグ名を表す文字列であり、省略不可。
- リストの2つ目は属性を表すオブジェクトである、ただしこれは省略可能。
- リストの2つ目(属性を省略した場合)あるいは3つ目(属性を設定した場合)以降にあるリストは子要素、リスト・オブジェクト以外のプリミティブ型はコンテンツを表す
- リストになっていない部分がある、空リストである、リストの1つ目が文字列ではなかったり空文字だったりする、リストの2つ目以外にオブジェクトがあるようなデータは不正

## YAMLで書いてみる

JSONMLをJSONMLやYAMLの仕様に反しない範囲でできるだけ見やすさ、書きやすさを重視して書くと以下のような感じになりました。

御託はいいので実際に動いているものをみたい場合は以下をどうぞ。

https://colab.research.google.com/github/uniras/MiscNotebook/blob/main/yamlml.ipynb

### 全体像

```yaml
- html
- {lang: ja}
- - head
  - [meta, {charset: utf-8}]
  - [title, JSON Markup Language]
- - body
  - [h1, JSON Markup Language]
  - - div
    - - p
      - >-
        JSON Markup Language (JSONML) is a lightweight markup language
        used to represent HTML or XML documents in JSON format.
      - - font
        - {color: red}
        - Hello, World!
      - [font, {color: blue}, "Goodbye, World!"]
      - Goodbye, World!
```

### ポイント

- `- html`（ハイフン・スペース・html）でhtml要素（ルート要素）を開始する。

```yaml
- html
```

- html要素（ルート要素）以外のタグ名は`- - head`のようにハイフンを2つ付ける。

```yaml
- html
- - head
```

- 親要素の2つ目のハイフンの位置から`- - div`のように書くことで子要素を追加できる。
- 属性やコンテンツを記述する`-`はタグ名の2つ目のハイフン(ルート要素の場合は1つ目)にインデントを合わせて書く。

```yaml
- html
- - body
  - - h1
    - "Hello World!"
```

- 属性は`- {lang: ja}`のようにフロー形式のオブジェクトで書くと読みやすい。

```yaml
- html
- {lang: ja}
- - head
  - - meta
    - {charset: utf-8}
```

- リストでもオブジェクトでもないデータはコンテンツとして扱われる。

```yaml
- html
- {lang: ja}
- - head
  - - title
    - "Hello World!"
```

- void要素や子要素のないシンプルな要素の場合、`- [font, {color: red}, "Hello World!"]`のようにフロー形式のリストで要素全体を書いたほうが読みやすい。その場合、`-`は1つにする。

```yaml
- html
- {lang: ja}
- - head
  - [meta, {charset: utf-8}]
  - [title, "Hello World!"]
```

- 同じインデントでリストを追記することで要素内にコンテンツと子要素を複数含めることができる。順序は保持される。

```yaml
- html
- {lang: ja}
- - body
  - - div
    - - p
      - "Hello World!"
      - - font
        - {color: red}
        - "Hello World2!"
      - "Hello World3!"
      - [font, {color: blue}, "Hello World4!"]
      - "Hello World5!"
```

- YAMLの仕様で複数行のコンテンツを書くことができる。改行を反映しない`- >-`がHTMLの仕様に近い。`<pre>`の場合は改行をすべて反映する`- |+`が適している。

```yaml
- html
- {lang: ja}
- - body
  - - div
    - - p
      - >-
        Hello World!
        Hello World2!
        Hello World3!
    - - pre
      - |+
        Hello World!
        Hello World2!
        Hello World3!
```

- コンテンツの中に直接HTMLタグを書くこともできる。`<br>`要素等はそのほうが読みやすい。

```yaml
- html
- {lang: ja}
- - body
  - - div
    - - p
      - >-
        Hello World!<br>
        Hello World2!<br>
        Hello World3!
```

## HTMLへの変換

残念ながらJSONMLはお世辞にも普及しているとは言えないので、JSONMLをHTMLに変換するライブラリは軽く探した感じ見当たりませんでした。

ライブラリがあるならそれを使えばいいと思いますが、とりあえずPythonでJSONMLをjson.loadsやyaml.loadで読み込んだPythonオブジェクトからHTMLを生成する関数を書いてみました。

```python

html_void_elements = ["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]

def jsonmlobj_to_html(obj, use_selfclose=False, void_elements=html_void_elements):
    """
    JSONMLオブジェクトをHTML文字列に変換する

    obj: JSONMLをjson.loadsやyaml.load等で読み込んだPythonオブジェクト
    use_selfclose: 自己終了タグに/を付けるかどうか
    void_elements: 自己終了タグ(void要素)のリスト
    """

    # 以下のいずれかに該当する場合はエラー
    # - objがリストでない
    # - objが空リスト
    # - objの最初の要素が文字列でない
    # - objの最初の要素が空文字列
    if not isinstance(obj, list) or len(obj) == 0 or not isinstance(obj[0], str) or obj[0] == "":
        raise Exception("Invalid JSONML object. The object must be a non-empty list with a string as the first element.")

    output = ""
    tag = ""
    attr = ""
    start = 1
    is_selfclose = False

    # タグ名を取得し、自己終了タグ(void要素)かどうかを判定
    tag = obj[0].lower()
    if tag in void_elements:
        is_selfclose = True
    elif tag.endswith("/"):
        tag = tag[:-1]
        is_selfclose = True
    output += f"<{tag}"

    # 属性を表すオブジェクトが存在する場合は属性を取得
    if len(obj) > 1 and isinstance(obj[1], dict):
        attr = " " + (" ".join(f'{k}="{v}"' for k, v in obj[1].items()))
        start = 2

    # 開始タグを出力
    output += f"{attr} />" if is_selfclose and use_selfclose else f"{attr}>"

    # (自己終了タグでない場合は)子要素及びコンテンツを取得して出力
    if not is_selfclose:
        for i in range(start, len(obj)):
            # リストの場合は子要素なので再帰呼び出し
            if isinstance(obj[i], list):
                output += jsonmlobj_to_html(obj[i], use_selfclose, void_elements)

            # 属性以外の場所でオブジェクトが存在する場合はエラー
            elif isinstance(obj[i], dict):
                raise Exception(f"Invalid JSONML object. Unexpected dictionary at position {i}. Dictionaries are only allowed as the second element to specify attributes.")

            # リストでもオブジェクトでもない場合はコンテンツとして出力
            else:
                output += str(obj[i])

        # 終了タグを出力
        output += f"</{tag}>"

    # 自己終了タグなのに子要素やコンテンツが指定されている場合はエラー
    elif len(obj) > start:
        raise Exception("Invalid JSONML object. Self-closing tags should not have children.")

    return output
```

## 感想

YAMLやJSONMLのHTML出力がPython標準ライブラリの範囲で使えたら面白い選択肢になりそうな気はするのですが、いずれも標準ライブラリになりそうな気配はまったくないのでまぁ趣味の範囲かなぁという感じです。