ティーポットは珈琲を淹れられない

ソフトウェアエンジニアK5のブログ

Pugの文法まとめ

2020年11月29日

Pug の文法について網羅的に説明された日本語のサイトが見つからなかったので、自分でまとめてみました。

英語でよければ公式にしっかりしたドキュメントがあります。 この記事はその内容の中でも特に文法に絞って、重要な順番で解説しています。

できるだけリファレンスを網羅している長い記事ですが、大事な順に書いているので、単に HTML を簡潔に書くためだけなら、最初の方の「HTML を簡潔に書くための基本文法」だけ読んでおけば大丈夫です。

Pug って何?

HTML を簡潔に書くことができるものです。HTML に変換されます。昔は Jade と呼ばれていましたが名前が変更されました。

単に HTML を簡潔に書けるだけでなく、テンプレートエンジン的な機能も持っています。

  • 他のファイルを include する
  • レイアウトを他のファイルに書いて extends して内容を埋め込む
  • 変数を定義する
  • ループしたり条件分岐したり
  • Markdown ファイルを HTML に変換して埋め込んだり、Sass を CSS に変換して埋め込んだり

といったことができます。

どういう時に使ってる?

動的 Web サイトや SPA のデザイン段階でまずは HTML + CSS だけでサンプルを作る時に使います。先に HTML + CSS だけでデザインを固めておくと、静的サイトジェネレータ使うにしろ、サーバーサイドレンダリングなフレームワーク使うにしろ、SPA フレームワーク使うにしろ、移植するのが楽です。

このブログは Gatsby という React ベースの静的サイトジェネレータで作られていますが、Pug を使って HTML + CSS だけである程度デザインしてから、React のコードを作成しました。

また勤め先の株式会社 creatoのホームページは WordPress で管理されていたものを Pug で静的サイトを生成するように変更中です。ちゃんとした静的サイトジェネレータ(Gatsby や Hugo など)を使うか迷ったんですが、エンジニアでない(が HTML/CSS は知ってる)人でも理解しやすく変更しやすいようにという配慮から Pug を簡易静的サイトジェネレータとして利用しています。1

自分は仕事で SPA や サーバーサイドのテンプレートエンジンに Pug を組み合わせることはしていません。 株式会社 creato では SPA 作るときは Angular が第一選択肢ですが、公式で Pug 記法をサポートしてくれてたら使いたくなるでしょうね。

HTML を簡潔に書くための基本文法

まずはテンプレートエンジン的な機能は置いておいて、HTML を簡潔に書く記法について解説します。

タグ

以下の HTML が Pug でどうなるか見てみましょう。

<main>
  <article>
    <header>
      <h1>記事タイトル</h1>
    </header>
    <section>
      <p>記事サンプルです。</p>
    </section>
  </article>
</main>

Pug は以下のようになります。

main
  article
    header
      h1 記事タイトル
    section
      p 記事サンプルです。

タグを閉じるのではなく、インデント(行の先頭から続く空白の数)でタグの入れ子構造を表します。

doctype は以下のように記述します。

doctype html
html
  head
  body

↓ HTML に変換すると…

<!DOCTYPE html>
<html>
  <head></head>
  <body></body>
</html>

属性

以下のように属性があるタグをどう書くかの基本を解説します。

基本

<a id="google-link" class="button small" href="//google.com">Google</a>

タグの属性は括弧の中に入れて表します。(文字列を囲うのはシングルクォーテーション、ダブルクォーテーションのどちらも使えます)

a(id='google-link' class='button small' href='//google.com') Google

属性と属性の間にコンマを入れても構いません。

a(id='google-link', class='button small', href='//google.com') Google

属性を複数行に分けて書くこともできます。

a(
  id='google-link'
  class='button small'
  href='//google.com'
) Google

id の # 記法

id は特別に # を使った記法が使えます。

a#google-link(class='button small' href='//google.com') Google

なお、div タグの場合はタグ名を省略できます。

#link
  a#google-link(class='button small' href='//google.com') Google

<div id="link">
  <a id="google-link" class="button small" href="//google.com">Google</a>
</div>

class の . 記法

クラスにも特別に . を使った記法が用意されています。複数つなげて指定できます。

a#google-link.button.small(href='//google.com') Google

こちらも id と同様に div を省略できます。

.link
  a#google-link.button.small(href='//google.com') Google

<div class="link">
  <a id="google-link" class="button small" href="//google.com">Google</a>
</div>

style 属性

タグに直接スタイルを指定するときは以下のように書けます。

a(style={color: 'red', background: 'green'})

Boolean 属性

真偽値を表す属性は truefalse を受け付けます。値を指定しなければ true だとみなされます。

input(type='checkbox' checked)
input(type='checkbox' checked=true)
input(type='checkbox' checked=false)

false を指定した場合、Pug はその属性を出力しません。 true の場合、Pug はその文書の doctype が HTML かどうかで適切に出力します。 出力の詳しい動作については後述する Boolean 属性の出力 を参照。

文字列(Plain Text)

タグの後に続けて中身の文字列を記述することができます。

p 記事サンプルです。

複数行に分けて書きたい場合は、パイプ( | ) を利用した記法があります。

p
  | これはサンプルです。
  | Pugのパイプを使って複数行に分けて記述しています。

これは以下の HTML になります。パイプを使って複数行に分けて書いたものは、まとめてタグの中身になります。

<p>これはサンプルです。Pugのパイプを使って複数行に分けて記述しています。</p>

パイプを使うと、文章中にインラインタグを埋め込むのは以下のように書けます(後述の もうちょっと高度な話 で説明する #[] を使って埋め込むこともできます。)。

p
  | 文章の中に
  span.keyword インラインタグ
  | を埋め込むことができます。
  br
  | 改行タグも埋め込めます。

<p>
  文章の中に<span class="keyword">インラインタグ</span>を埋め込むことができます。<br />
  改行タグも埋め込めます。
</p>

コメント

HTML として出力されるコメントは以下のように書きます。

// これはHTMLに出力されるコメントです。
p foo

以下のように HTML のコメントとして出力されます。

<!-- これはHTMLに出力されるコメントです。 -->
<p>foo</p>

HTML に出力して欲しくないコメントは以下のようにハイフンを追加して書きます。

//- これはHTMLに出力されないコメントです。
p foo

複数行にわたるブロックコメントも記載できます。

body
  //-
    これはHTMLには出力されないコメントです。
    複数行に渡るブロックコメントです。
  //
    これはHTMLに出力されるコメントです。
    複数行に渡るブロックコメントです。

テンプレート構文

単に HTML を Pug でシンプルに記述したいだけであれば、覚えることは今まで説明しただけで事足ります。

他のテンプレート構文と組み合わせて使うなら、これから説明する内容は覚えなくてもいい・・・というより、混ぜると訳がわからなくなるので Pug のテンプレート構文は使うべきではないでしょう。

他のテンプレート構文と組み合わせて使うというのは、例えば Laravel のテンプレートエンジン Blade と組み合わせる とか、React で利用する とか Vue.js で利用する とか、Angular で利用する とかを指しています 2。この場合は、後述の もうちょっと高度な話 のうち、HTML 記述に関係するところには目を通しておくと良いかと思います。

Pug の公式では変数の定義に var キーワードを使って説明されています。古いブラウザ(IE10 以下とか)でも動くようにという配慮からだと思いますが、もう var は使わず letconst を使うべきかと。少なくとも Node.js 上で Pug を HTML に変換するのであればブラウザの対応は関係ありません。ここでは letconst で説明します。

JavaScript コード

ここで説明するのは HTML に含めるブラウザで動作させる JavaScript のことではなく、Pug が変換時に実行する JavaScript のことです。 Pug では HTML への変換時に JavaScript を実行する機能が用意されています。とはいえ、Node.js パッケージを読み込んで利用するような高度なことはできません。

Unbuffered Code

頭に -(ハイフン)を付ける記述方法で、HTML タグとは無関係に JavaScript コードを実行します。 Pug では HTML として出力されるものを buffered, 出力されないものを unbuffered と呼んでいます。

最もよく利用するのは変数の定義でしょう。

- const list = ["Uno", "Dos", "Tres", "Cuatro", "Cinco", "Seis"]

このように - (ハイフン)を頭に付けると JavaScript コードと認識されます。

以下のように複数行に分けて記述することもできます。

-
  const list = ["Uno", "Dos", "Tres",
          "Cuatro", "Cinco", "Seis"]

Buffered Code

タグの中身に JavaScript コードの実行結果を埋め込みます。

//- イコール(=)を使ってタグの中身に JavaScript の結果を埋め込める
p
  = 'This code is' + ' <escaped>'
//- 1行で書くこともできる
p= 'This code is' + ' <escaped>'

タグの内容は HTML エスケープされます。

<p>This code is &lt;escaped&gt;!</p>
<p>This code is &lt;escaped&gt;!</p>

エスケープして欲しくない場合は = ではなく != を使います。次の「変数の埋め込み」で説明します。

変数の埋め込み

エスケープする場合

#{変数名} で変数の内容を文字列に埋め込むことができます。また先ほど説明した Buffered Code を使って h1= 変数名 とか p= 変数名 の形でタグの中に変数の内容を埋め込めます。内容が埋め込まれる前に HTML はエスケープされます。

- const title = "On Dogs: Man's Best Friend";
- const author = "enlore";
- const theGreat = "<span>escape!</span>";

h1= title
p Written with love by #{author}
p This will be safe: #{theGreat}

出力される HTML は以下になります。

<h1>On Dogs: Man's Best Friend</h1>
<p>Written with love by enlore</p>
<p>This will be safe: &lt;span&gt;escape!&lt;/span&gt;</p>

変数の埋め込みと紹介していますが、もちろん JavaScript のコードを実行して結果を埋め込むこともできます。

h1= title.toUpperCase()
p Written with love by #{author.toUpperCase()}

#{ 自体を文字列に含めたい場合は、\#{ のようにスラッシュでエスケープするか、#{} の中に入れて JavaScript の文字列として評価させる方法が取れます。

p Escaping works with \#{interpolation}
p Interpolation works with #{'#{interpolation}'} too!

以下の HTML に変換されます。

<p>Escaping works with #{interpolation}</p>
<p>Interpolation works with #{interpolation} too!</p>

エスケープしない場合

埋め込む文字列をエスケープしたくない場合は !=!{} を利用します。 当然ですがユーザーの入力をそのまま埋め込むのは危険です。

- const title = "On Dogs: <span class=\"subtitle\">Man's Best Friend</span>"
- const theGreat = "<span>escape!</span>";
title!= title
p This will be unsafe: !{theGreat}

以下のように変数内の HTML タグがそのまま出力されます。

<h1>On Dogs: <span class="subtitle">Man's Best Friend</span></h1>
<p>This will be safe: <span>escape!</span></p>

条件分岐

if

まずは if 文から。

- const user = {description: 'foo bar baz'}
- const authorised = false
#user
  if user.description
    h2.green Description
    p.description= user.description
  else if authorised
    h2.blue Description
    p.description.
      User has no description,
      why not add one...
  else
    h2.red Description
    p.description User has no description

unless

if の逆の「もし〜でなければ」を表す unless が用意されています。

unless user.isAnonymous
  p You're logged in as #{user.name}

これは if を使って以下を書いた場合と同じになります。

if !user.isAnonymous
  p You're logged in as #{user.name}

case

JavaScript では switch 文に相当するものですね。

- const friends = 10
case friends
  when 0
    p you have no friends
  when 1
    p you have a friend
  default
    p you have #{friends} friends

break は書かなくても勝手に下まで実行していったりしません。もし複数の条件で同じ内容を実行したい場合は以下のように記述できます。

- const friends = 0
case friends
  when 0
  when 1
    p you have very few friends
  default
    p you have #{friends} friends

ある条件のときには何もしないという場合は、明示的に break を記述する必要があります。

- const friends = 0
case friends
  when 0
    - break
  when 1
    p you have very few friends
  default
    p you have #{friends} friends

繰り返し処理

each

集合の中を順に処理する each が用意されています。ほとんどの場合、繰り返し処理で使うのはこれです。

- const values = [1, 2, 3, 4, 5]
ul
  each val in values
    li= val

<ul>
  <li>1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
  <li>5</li>
</ul>

values の部分に JavaScript のコードを直接書いても構いません。

ul
  each val in [1, 2, 3, 4, 5]
    li= val
ul
  each val in values.length ? values : ['There are no values']
    li= val

値だけでなくインデックスも受け取れます。

ul
  each val, index in ['zero', 'one', 'two']
    li= index + ': ' + val

オブジェクトをループさせることもできます。(注: each の値に指定する変数は 値, キー の順番

- const dict = {1: 'one', 2: 'two', 3: 'three'}
ul
  each val, key in dict
    li= key + ': ' + val

while

条件に一致する間ループを回す while 文も利用できます。

- let n = 0;
ul
  while n < 4
    li= n++

<ul>
  <li>0</li>
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>

他のファイルの埋め込み (inlucde)

他のファイルの内容を取り込みます。

例えば index.pug が以下の内容とします。

//- index.pug
html
  //- ここで includes/_head.pug を読み込み
  include includes/_head.pug
  body
    h1 My Site
    p Welcome to my super lame site.
    //- ここで includes/_foot.pug を読み込み
    include includes/_foot.pug

includes/_head.pug は以下の内容とします。

//- includes/_head.pug
head
  title My Site
  script(src='/javascripts/jquery.js')
  script(src='/javascripts/app.js')

includes/_foot.pug は以下の内容とします。

//- includes/_foot.pug
footer#footer
  p Copyright (c) foobar

index.pug を HTML に変換すると、inculdes/_head.pug と includes/_foot.pug の内容が埋め込まれて以下のようになります。

<!DOCTYPE html>
<html>
  <head>
    <title>My Site</title>
    <script src="/javascripts/jquery.js"></script>
    <script src="/javascripts/app.js"></script>
  </head>
  <body>
    <h1>My Site</h1>
    <p>Welcome to my super lame site.</p>
    <footer id="footer">
      <p>Copyright (c) foobar</p>
    </footer>
  </body>
</html>

平文テキストの include

Pug ではないファイルを取り込むこともできます。例えば CSS ファイルや JavaScript ファイルですね。

//- index.pug
doctype html
html
  head
    style
      include style.css
  body
    h1 My Site
    p Welcome to my super lame site.
    script
      include script.js
/* style.css */
h1 {
  color: red;
}
// script.js
console.log('You are awesome');

index.pug は以下の HTML に変換されます。

<!DOCTYPE html>
<html>
  <head>
    <style>
      /* style.css */
      h1 {
        color: red;
      }
    </style>
  </head>
  <body>
    <h1>My Site</h1>
    <p>Welcome to my super lame site.</p>
    <script>
      // script.js
      console.log('You are awesome');
    </script>
  </body>
</html>

Markdown ファイルの埋め込み (Filter)

Pug には 後述する Filter という機能 があります。これを使うと Markdown や SASS などの変換処理を行った上で埋め込むことができます。

//- index.pug
doctype html
html
  head
    title An Article
  body
    include:markdown-it article.md
# article.md

This is an article written in markdown.

index.pug を変換すると以下の HTML になります。

<!DOCTYPE html>
<html>
  <head>
    <title>An Article</title>
  </head>
  <body>
    <h1>article.md</h1>
    <p>This is an article written in markdown.</p>
  </body>
</html>

共通レイアウトの定義 (extends)

include では例えばヘッダやフッタなどの複数ページで利用する共通部品を分離することができました。 そうではなく、全体で共通に利用するレイアウトを用意して、その中にページごとに異なる部分を埋め込みたい時に extends が利用できます。

基本

レイアウトファイルを以下のように作成します。

//- _layout.pug

//- 外から block で変数(title)を渡せるようにする
block variables

html
  head
    //- title はページごとに異なるので外から与えられたものを埋め込む
    title My Site - #{title}
    //- ページによって利用する js ファイルが異なるので block で渡せるようにする
    block scripts
      //- もし block scripts が渡されなければ、デフォルト値として以下を利用
      script(src='/jquery.js')
  body
    //- ページのコンテンツは block で渡せるようにする
    block content
    //- ページによってはフッタが異なるので block で渡してカスタマイズできるようにする
    block foot
      //- もし block foot が渡されなければ、デフォルト値として以下を利用
      #footer
        p some footer content

これを利用するページは以下のように extends を使います。

//- page-a.pug
extends _layout.pug

//- タイトルを変数で渡す
block variables
  - const title = 'page A'

//- jquery 以外に pets.js というページ独自の js ファイルも利用するのでデフォルト値を上書き
block scripts
  script(src='/jquery.js')
  script(src='/pets.js')

//- ページのコンテンツを渡す
block content
  h1= title
  - var pets = ['cat', 'dog']
  each petName in pets
    p= petName

//- フッタはデフォルトのものを利用(block foot は渡さない)

↓ page-a.pug を HTML に変換すると

<html>
  <head>
    <title>My Site - page A</title>
    <script src="/jquery.js"></script>
    <script src="/pets.js"></script>
  </head>
  <body>
    <h1>page A</h1>
    <p>cat</p>
    <p>dog</p>
    <div id="footer">
      <p>some footer content</p>
    </div>
  </body>
</html>

append / prepend (デフォルト値の前後に追加)

今までの説明では block に指定するとデフォルト値を上書き(置き換え)していました。 そうではなく、デフォルト値にさらに追加するための方法が用意されています。

例えば先の例で page-a.pug は jquery.js の他に pet.js も利用するために

//- jquery 以外に pets.js というページ独自の js ファイルも利用するのでデフォルト値を上書き
block scripts
  script(src='/jquery.js')
  script(src='/pets.js')

と記述していました。 これは append を利用して以下のように記述できます。

//- デフォルト値である jquery.js の後に pets.js というページ独自の js ファイルの利用を追加
block append scripts
  script(src='/pets.js')

append でなく prepend を指定するとデフォルト値の前に追加されます。

レイアウトを継承してサブレイアウトを定義

サイト全体で利用する _layout.pug を作ったわけですが、幾つかのページではさらにコンテンツ部分の構造が同じなので共通化したいとします。

以下のように _layout.pug を継承した _sub-layout.pug を作成します。

//- _sub-layout.pug
extends _layout.pug

//- コンテンツ部分の構造をさらに共通化
block content
  .sidebar
    block sidebar
      p nothing
  .primary
    block primary
      p nothing

これで sidebar と primary というブロックが content の内部に作られました。

これを以下のように利用できます。

//- page-b.pug
extends _sub-layout.pug
block variables
  - const title = 'page B'
block sidebar
  p Sidebar
block primary
  p Primary

<html>
  <head>
    <title>My Site - page B</title>
    <script src="/jquery.js"></script>
  </head>
  <body>
    <div class="sidebar">
      <p>Sidebar</p>
    </div>
    <div class="primary">
      <p>Primary</p>
    </div>
    <div id="footer">
      <p>some footer content</p>
    </div>
  </body>
</html>

extends の継承については公式が注意書きをしているので、多用したり複雑な使い方をするのであれば extends 利用のよくある間違い には目を通しておいた方がいいでしょう。

Mixin

同じ部品を色々な箇所で利用したい場合、別ファイルにして include を利用する方法を既に説明しました。 でも1つのファイルでしか使わないので別ファイルにするほどでも・・・という場合もあります。 また小さい部品を幾つか集めて1つのファイルで管理したいということもあるでしょう。

Mixin はプログラム言語で言うところの関数みたいなものです。 あるいは独自タグを定義するような、SPA フレームワークでのコンポーネント的なものとも言えるかもしれません。

//- Mixinの宣言
mixin list
  ul
    li foo
    li bar
    li baz

//- Mixinを利用するときは頭に + を付ける
+list
+list

<ul>
  <li>foo</li>
  <li>bar</li>
  <li>baz</li>
</ul>
<ul>
  <li>foo</li>
  <li>bar</li>
  <li>baz</li>
</ul>

Mixin に引数を渡す

引数を渡すこともできます。

mixin pet(name)
  li.pet= name

ul
  +pet('cat')
  +pet('dog')
  +pet('pig')

引数のデフォルト値もサポートしています。

mixin article(title='Default Title')
  .article
    .article-wrapper
      h1= title

//- 引数省略してデフォルト値を利用
+article()

+article('Hello world')

可変長引数もサポートしています。

//- 最初の引数は id で、その後は items に配列として格納
mixin list(id, ...items)
  ul(id=id)
    //- items を順にループ
    each item in items
      li= item

//- 最初の 'my-list' は id、残りは items
+list('my-list', 1, 2, 3, 4)

<ul id="my-list">
  <li>1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
</ul>

Mxin にブロックを渡す

block を渡すこともできます。

mixin article(title)
  .article
    .article-wrapper
      h1= title
      if block
        //- ブロックが渡されてたらそれを埋め込む。
        block
      else
        //- ブロックが渡されてなかったら以下
        p No content provided

//- ブロックを渡さないで呼び出し
+article('Hello world')

//- ブロックを渡して呼び出し
+article('Hello world')
  p This is my
  p Amazing article

<div class="article">
  <div class="article-wrapper">
    <h1>Hello world</h1>
    <p>No content provided</p>
  </div>
</div>
<div class="article">
  <div class="article-wrapper">
    <h1>Hello world</h1>
    <p>This is my</p>
    <p>Amazing article</p>
  </div>
</div>

1つ目は “No content provided” が、2つ目は渡したブロックが埋め込まれていますね。

Mixin に属性を渡す

Mixin では渡された属性を attributes で参照できます。

mixin link(href, name)
  //- 属性として渡される class を、a タグの class に設定(!= を使ってるのでエスケープはしない)
  a(class!=attributes.class href=href)= name

//- 属性として class="btn" を渡している
+link('/foo', 'foo')(class="btn")

class!=attributes.class としてエスケープしないようにしているのは、Mixin を呼び出す際の class="btn" で既にエスケープされてるので、もう一度エスケープするのが無駄だからです。

渡された属性をそのまま全部展開したい場合は &attributes が利用できます。

mixin link(href, name)
  a(href=href)&attributes(attributes)= name

+link('/foo', 'foo')(class="btn")

渡された属性 class="btn" がそのまま link Mxin 内の属性として展開されます。

<a class="btn" href="/foo">foo</a>

引数なしの Mixin に属性を渡す場合、+link(class="btn") と書いてもそれが引数でなく属性だと認識されます。 ただ Pug 公式では +link()(class="btn") のように引数がないことを明示する記述をお勧めしています。

Filter

Pug に渡す前に変換処理を行う仕組みです。例えばマークダウンを HTML に変換するとか、SCSS を CSS に変換して埋め込むとかができます。

Filter は頭に : を付けて呼び出します。括弧の中にはフィルタに渡すオプションを指定できます。

:markdown-it(linkify langPrefix='highlight-')
  # Markdown

  Markdown document with http://links.com and


  ```js
  var codeBlocks;
  ```

<h1>Markdown</h1>
<p>Markdown document with <a href="http://links.com">http://links.com</a> and</p>
<pre><code class="highlight-js">var codeBlocks;
</code></pre>

この解説は文法の説明に絞っており、Pug の実行方法には触れません3が、markdown-it フィルタを利用するにはそのためのパッケージのインストールが必要です。

全ての JSTransformer モジュールがフィルターとして利用可能です。

注意点として、フィルタに Pug の処理中に JavaScript で生成した動的なものを渡すことはできません。 ちょっとわかりにくいので以下の例で説明します。

-
  const mds = [
    '# タイトル1',
    '# タイトル2',
  ]

each md in mds
  :markdown-it(linkify langPrefix='highlight-') md

上記で md の部分は変数の内容を渡したいのですが、このままでは単に “md” という文字列としてしか認識してくれません。

  :markdown-it(linkify langPrefix='highlight-') #{md}

のように #{md} で渡しても、フィルタの中身は単なる文字列としてしか認識されず、”#{md}” という文字列が渡されるだけです。 なおフィルタのオプションにも変数を渡したり JavaScript で動的に生成したものを渡したりできません。 JavaScirpt で生成したものをフィルタに渡す機能が一切提供されていないんですね。

高速に動作させるためとのことですが、Pug を静的サイトジェネレータとして利用しようとすると、これが大きな制限になります。

もうちょっと高度な話

タグ

文字列中へのタグの埋め込み

長い文字列の中にインラインタグを埋め込むときに #[] でタグを埋め込む構文を覚えておくと楽に書けます。 これを知らなくても |(パイプ)を使って書けるので基本としては紹介しませんでした。

p.
  This is a very long and boring paragraph that spans multiple lines.
  Suddenly there is a #[strong strongly worded phrase] that cannot be
  #[em ignored].
p.
  And here's an example of an interpolated tag with an attribute:
  #[q(lang="es") ¡Hola Mundo!]

以下の HTML に変換されます。

<p>
  This is a very long and boring paragraph that spans multiple lines. Suddenly there is a
  <strong>strongly worded phrase</strong> that cannot be <em>ignored</em>.
</p>
<p>
  And here's an example of an interpolated tag with an attribute:
  <q lang="es">¡Hola Mundo!</q>
</p>

入れ子を1行で表現

a
  img

を以下のように1行で記述する方法が用意されています。

a: img

case を短く書きたい場合にも使えます。

- const friends = 1
case friends
  when 0: p you have no friends
  when 1: p you have a friend
  default: p you have #{friends} friends

閉じタグのいらないタグ

img のような閉じタグのいらないタグを Pug は自動的に判別してそのように変換してくれます。

a
  img

これは以下の HTML になります。

<a><img /></a>

HTML 5 に規定されているタグなら勝手に判別してくれますが、コンポーネントなどの独自タグの場合はそうはいきません。以下のように閉じタグがないことを後ろにスラッシュを付けて伝える必要があります。

foo(bar='baz')/

以下の HTML に変換されます。

<foo bar="baz" />

属性

エスケープの抑止

Pug はデフォルトで全ての属性の値をエスケープします。エスケープして欲しくない場合は = ではなく != を使います。

div(escaped="<code>")
div(unescaped!="<code>")

上記が下記の HTML に変換されます。 ”<” が &lt; に、 ”>” が &gt; にエスケープされるかどうかが異なっています。

<div escaped="&lt;code&gt;"></div>
<div unescaped="<code>"></div>

特殊文字が含まれる場合

SPA フレームワークと組み合わせて利用すると、属性に [] とか () などの特殊文字が使われることがあります。そう Angular のことですね。そういう特殊文字が含まれる場合は ''"" で囲ってやってください。

button(class='button' '(click)'='play()')

Boolean 属性の出力

doctype html を指定していない場合、Pug は文書が HTML かどうか分からない(例えば XML かもしれない)ので、値として属性名を出力します。

input(type='checkbox' checked)
input(type='checkbox' checked=true)

↓ どちらも以下になります。値には属性名である “checked” が出力されます。

<input type="checkbox" checked="checked" />

false の場合、属性自体を出力しません。

input(type='checkbox' checked=false)

<input type="checkbox" />

文書に doctype html を指定している場合、Pug は HTML の真偽値の表し方で出力します。 つまり true だった場合に属性名を値として出力しません。

input(type='checkbox' checked=true)

↓ checked という属性に値は設定されません。

<input type="checkbox" checked />

なおこのルールはあくまで指定したのが真偽値だった場合のみ適用されます。 例えば以下の場合、値が true または false の真偽値ではなく、“true” という文字列なので、上記のルールは適用されずにそのまま文字列として出力されます。

input(type='checkbox' checked=true.toString())
input(type='checkbox' checked='true')
input(type='checkbox' checked=true && 'true')

↓ 上記3つはどれも以下のように値に文字列がそのまま出力されます。

<input type="checkbox" checked="true" />

&attributes

実はこれは既に「Mixin に属性を渡す」で紹介しています。

JavaScript のオブジェクトをそのまま属性として展開します。

div#foo(data-bar="foo")&attributes({'data-foo': 'bar'})

<div id="foo" data-bar="foo" data-foo="bar"></div>

もちろん変数に入れたオブジェクトも展開されます。

- const attributes = { class: 'baz' }
div#foo(data-bar="foo")&attributes(attributes)

<div class="baz" id="foo" data-bar="foo"></div>

extends 利用のよくある間違い

公式がわざわざ注意書きを書いているので、説明しておきます。

Pug のテンプレート継承機能は複雑なページ構造を小さくシンプルなファイルに分解する強力な機能だけれども、あまり多くのテンプレートを繋げてしまうと余計複雑になってしまいます4

extends を使う子供テンプレートのトップレベル(つまりインデントされていない部分)には block と Mixin 定義だけが現れるようにすべきです。 これは重要です!親のテンプレートはページ全体の構造を定義し、子供のテンプレートはただ特定のブロックのロジックやマークアップを append / pretend したり置き換えるだけにすべきです。

このことはマークアップを含むことのできる unbuffered code(頭が -(ハイフン)で始まる JavaScript コード)にも当てはまります。

同じ理由で // 始まりの HTML に出力されるコメントも子供テンプレートのトップレベルに現れてはいけません。 (//- 始まりの HTML に出力しない Pug コメントはどこに書いても構いません。)


  1. この解説ページは、社内に「Pug の文法はここ読んで」と言えるページがあるといいなという目的もあって作成しています。

  2. React をテンプレート構文と呼ぶのは違和感ありますが

  3. 別途記事を書ければとは思っています。

  4. 設計の基本ですね。多くの設計テクニックは、複雑さを追加することでもっと大きな複雑さを軽減します。ファイル分割は「ファイルが増えて全体を追いかけて理解するのが難しくなる」という複雑さを追加しますので、それによって軽減される複雑さと天秤にかける必要があります。


© 2016-2020 K5