🖥️

[Handlebars]textareaやpreタグで改行後にスペースが入ってしまう[NestJS]

2024/07/26に公開

使用バージョン

  • nestjs@10.3.10
  • handlebars@4.7.7
  • hbs@4.2.0

こちらのリポジトリで動作を試すことができます。
https://github.com/hid3h/nestjs-examples/tree/main/handlebars

現象

controllerで改行入りの文字をviewに渡す。

src/app.controller.ts
import { Controller, Get, Render } from '@nestjs/common';

@Controller()
export class AppController {
  @Get()
  @Render('index')
  getHello() {
    const message = `あいうえお\n改行されました`;
    return { message };
  }
}

handlebarsのInline Partialsを使う。
https://handlebarsjs.com/guide/partials.html#inline-partials

views/layouts/layout.hbs
<div class="content">
  {{> content}}
</div>
views/index.hbs
{{#> layout}}
  {{#*inline "content"}}
    <div>
      二重括弧{{message}}
    </div>
    <div>
      三重括弧{{{message}}}
    </div>
    <div>
      <textarea>{{message}}</textarea>
    </div>
    <div>
      <textarea>{{{message}}}</textarea>
    </div>
  {{/inline}}
{{/layout}}

textarea内とpreタグ内の改行後に謎の半角スペースが2つ入りました。

原因

https://github.com/handlebars-lang/handlebars.js/issues/858 ここで色々書かれていますが、
Partials呼び出し部分のインデントが自動的にPartialsの出力にも反映されます。
それにより、textareaやpreタグが影響を受けるようです。

views/layouts/layout.hbs
<div class="content">
  以下のPartials呼び出し部分のインデントのこと
  {{> content}}
</div>

このインデントがHTMLでの表示時にtaxtareやpreタグ内でスペースとして反映されていたということ。

この自動インデントは以下によるとmustacheの仕様に合わせるために導入されたようです。
https://github.com/handlebars-lang/handlebars.js/issues/858#issuecomment-61107516

解決案

案1

preventIndentオプションを使う。
Handlebarsで自動インデントを無効にするオプションを用意してくれています。
https://handlebarsjs.com/api-reference/compilation.html

このオプションはHandlebarsのコンパイル時に適用され、Partialsの内容にインデントを追加しないようにします。
Handlebarasのコードを見てみると、
https://github.com/handlebars-lang/handlebars.js/blob/v4.7.7/lib/handlebars/compiler/compiler.js#L196
どうやらここでpreventIndentオプションをチェックして、インデントを調整しているのが伺えます。

ただ肝心の、NestJSでどうやってpreventIndentオプションを有効にするかは見つけられず。

NestJSのデフォルト設定では直接このオプションを指定する方法がなさそうなので、以下のようにHandlebarsのcompile()をカスタマイズする方法を考えました。

src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { join } from 'path';
import { NestExpressApplication } from '@nestjs/platform-express';
import * as hbs from 'hbs';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);

  app.setBaseViewsDir(join(__dirname, '..', 'views'));
  app.setViewEngine('hbs');
  hbs.registerPartials(join(__dirname, '..', 'views', 'layouts'));

  // 元の関数を保持
  const originalCompile = hbs.handlebars.compile;
  hbs.handlebars.compile = (input, options) => {
    const newOptions = options || {};
    // ここでpreventIndentを有効に
    newOptions.preventIndent = true;
    return originalCompile.call(this, input, newOptions);
  };

  await app.listen(3000);
}
bootstrap();

ここで、オーバーライドしてるcompile()とは以下のことです。
https://github.com/handlebars-lang/handlebars.js/blob/v4.7.7/lib/handlebars/compiler/compiler.js#L510

これにより、textareaやpreタグでの改行後のスペースが無くなりました。

ちなみに、
hbsでなくexpress-handlebarsを使っている場合は、以下のようにcompilerOptionspreventIndentオプションを設定できそうです。
https://github.com/express-handlebars/express-handlebars?tab=readme-ov-file#compileroptions

案2

Partials呼び出し部分にインデントを用いない。

views/layouts/layout.hbs
<div class="content">
// Partials呼び出し部分でインデントを用いない
{{> content}}
</div>

すると、半角スペースが消えました。

局所的に挙動を変えたい時はこちらでもいいかも...?
ただ、将来的にtextareaやpreタグを使用する箇所が増えたときに、スペースを反映させたくない所全てに適用する必要があり漏れが発生しそうだったり、
エディタによる自動整形でインデントが自動で入る場合に、対象のファイルだけ入らないようにする必要も出ててきたり...
グローバルに設定しておくほう安定かもですね。

おわり

この記事が少しでも参考になれば幸いです。
励みになりますのでもしよかったらいいねお願いします🙏

Discussion