Pugのカスタムフィルタを作るのに、あまり情報がなかったのであちこちで情報を拾い集めてしんどかったので、備忘録としてメモします。
公式の情報は、公式ドキュメントのカスタムフィルタの項目を参照してください。
この記事は、Node.jsやPugといった環境を構築済みであること、基本的なPugの構文や実行(コンパイル)などの知識があることが前提です。また、カスタムフィルタはJavaScriptで書かれるので、その知識もある程度必要です。
Pugのコンパイルについては、基本的にpug-cliでPugを実行する前提ですが、GruntやGulpやWebpackといったタスクランナーを使う場合も基本的な部分は共通しているかと思います。Gruntについては少しだけ触れています。
Pugのフィルタ
公式ドキュメントによれば、プレーンなテキストを受け取って、そのテキストに処理を加えるものと読み取れます。
Filters let you use other languages in Pug templates. They take a block of plain text as an input.
フィルターを使用すると、Pug テンプレートで他の言語を使用できます。プレーンテキストのブロックを入力として受け取ります。
JSTransformer モジュールというものがPugのフィルタとして使えます。非常に強力なフィルタとして使えるため、Web上の多くの情報はこちらを使うためのノウハウです。しかし、たとえば「改行を<br>
タグに変換する」というような単純なものはカスタムフィルタを自作したほうがお手軽なのかなと思います。
基本的なカスタムフィルタ
作ったフィルタを置く場所
フィルタはNodeモジュールとして定義するので、程よいところに置いてあればどこでも大丈夫かと思います。今回はPugファイルと同じディレクトリに入れます。
project
├ src/
│ └ pug/
│ ├ index.pug
│ └ _filters/
│ └ filters.js
└ dest/
カスタムフィルタは:フィルタ名
で使用できます。下記の例ではmy-filter
という名前のカスタムフィルタをPugの構文の中で使っています。フィルタの処理の対象は「吾輩は猫である。名前はまだ無い。」というテキストになります。
doctype html
html
head
body
section
:my-filter
吾輩は猫である。名前はまだ無い。
フィルタはmodule.exports.filters
に追加します。フィルタの引数にはテキストが渡ってきます。この例では、受け取ったテキストの特定の文字を置換します。
(今回はCLIでコンパイルしますが、gruntやgulp、webpackなどの場合はそれぞれの環境に合った方法で追加してください。たぶんoptions.filters
に追加するのかな)
module.exports = {
filters: {
'my-filter': function(text) {
let sub = [['猫','ニャンコ'],['名前','ニャ前'],['無い','ニャい'],['は','ニャ']];
for (const [a, b] of sub) {
text = text.replace(new RegExp(a, 'g'), b);
}
return text;
}
}
}
CLIでのコンパイル時に、-O
オプションにフィルタのファイルのパスを指定します。
pug -P -O src/pug/_filters/filters.js src/pug -o dest
できあがるHTMLファイルは、テキストにフィルタが適用されたものになります。
<!DOCTYPE html>
<html>
<head></head>
<body>
<section>
吾輩ニャニャンコである。ニャ前ニャまだニャい。</section>
</body>
</html>
カスタムフィルタにパラメータを渡す
カスタムフィルタの2番めの引数にパラメータを渡すことができます。
パラメータはPugの通常の構文と同じく、カッコ内に:フィルタ名(キー=値)
のようにして渡します。この例では、置換のための文字のペアをsubというパラメータに配列で渡しています。
doctype html
html
head
body
section
:my-filter(sub=[['猫','犬'],['は','ワン'],['。','ワン。']])
吾輩は猫である。名前はまだ無い。
フィルタの2番めの引数で、Pugファイルで渡したパラメータを受け取ります。
module.exports = {
filters: {
'my-filter': function(text, options) {
let sub = options.sub;
if (sub && sub.length) {
for (const [a, b] of sub) {
text = text.replace(new RegExp(a, 'g'), b);
}
}
return text;
}
}
}
<!DOCTYPE html>
<html>
<head></head>
<body>
<section>
吾輩ワン犬であるワン。名前ワンまだ無いワン。</section>
</body>
</html>
Pugの構文を使えるようにする
工夫すれば、PugのAPIをつかって、最初の引数に渡ってきたテキストをHTML変換して返すようにできます。
FilterのスコープからPugへの参照ができないと思うので、参照できるところにPugをrequireしておきます。素直にフィルタ内でrequireします(何かもっといい方法があれば教えて下さい)。
(Gruntではフィルタ内からthis.pugで参照できるのであらためてのrequireは不要です。その他の環境はわからないので、自分で確かめてください)
pug.render(source, ?options, ?callback)を使います。
下層のテキストをPugの構文にします。
doctype html
html
head
body
section
:my-filter(sub=[['猫','犬'],['は','ワン'],['。','ワン。']])
p.dog 吾輩は#[strong 猫]である。名前はまだ無い。
冒頭でpugをrequireして、値を返すときに、pug.renderでPugの構文を変換します。
module.exports = {
filters: {
'my-filter': function(text, options) {
+ const pug = require('pug');
let sub = options.sub;
if (sub && sub.length) {
for (const [a, b] of sub) {
text = text.replace(new RegExp(a, 'g'), b);
}
}
- return text;
+ return pug.render(text);
}
}
}
<!DOCTYPE html>
<html>
<head></head>
<body>
<section>
<p class="dog">吾輩ワン<strong>犬</strong>であるワン。名前ワンまだ無いワン。</p></section>
</body>
</html>
当然ですが、その他のモジュールを使うこともできます。
フィルタを別ファイルにする
ファイルを分けられたほうが何かと具合がいいとおもうので、別のファイルにしてrequireで個別に読み込みます。
project
├ src/
│ └ pug/
│ ├ index.pug
│ └ _filters/
│ ├ my-filter.js
│ ├ autolink.js
│ └ filters.js
└ dest/
module.exports = {
filters: {
'my-filter': require('./my-filter.js'),
'autolink': require('./autolink.js')
}
};
module.exports = {
'my-filter': function(text, options) {
const pug = require('pug');
let sub = options.sub;
if (sub && sub.length) {
for (const [a, b] of sub) {
text = text.replace(new RegExp(a, 'g'), b);
}
}
return pug.render(text);
}
}
module.exports = {
autolink: function(text, options) {
//...何らかの処理
return text;
}
}
Gruntの場合
Gruntの場合は、pugのタスクのoptions.filters
に直接フィルタを書くか、require
でフィルタを読み込みます。
module.exports = function(grunt) {
grunt.initConfig({
pug: {
compile: {
options: {
pretty: true,
filters: require('./src/pug/_filters/pug-filters.js'),
},
files: [{
expand: true,
cwd: 'src/pug',
dest: 'dest',
src: [
'**/*.pug',
'!**/_*.pug'
],
ext: '.html'
}]
}
}
});
grunt.loadNpmTasks('grunt-contrib-pug');
grunt.registerTask('default', ['pug:compile']);
};
module.exports = {
'my-filter': require('./my-filter.js'),
'autolink': require('./autolink.js')
};
余談
たとえば、:responsive-img(src="img/example-img.jpg" alt="Example image")
みたいなフィルタで、
<img src="img/example-img.jpg" srcset="img/example-img.jpg 1x, img/example-img@2x.jpg 2x" alt="Example image">
とか、
<picture>
<source media="(min-width: 320px)" srcset="img/example-img.jpg 1x, img/example-img@2x.jpg 2x">
<source media="(min-width: 640px)" srcset="img/example-img-640.jpg 1x, img/example-img-640@2x.jpg 2x">
<img src="img/example-img.jpg" alt="Example image">
</picture>
とする、みたいなフィルタも作れるでしょう。