🧊

PrettierのembeddedLanguageFormattingについて

これまでも個別に調べたりしてるけど、総まとめ的なものを書いておく。

バージョンは3.8.3時点。(ちなみにもうすぐ3.9.0が出るらしい)

xxx-in-yyy

JSの中にCSSが埋め込まれてるケースを、CSS-in-JSと呼ぶ。

それぞれ言語別に見ていく。

JS / TS

  • scss-in-js / scss-in-ts
    • styled-components, css/styled, <style jsx>, css prop, @Component({ styles })
  • graphql-in-js / graphql-in-ts
    • gql/graphql tag, graphql.experimental tag, graphql() call, /* GraphQL */ comment
  • html-in-js / html-in-ts
    • html tag, /* HTML */ comment
  • angular-in-js / angular-in-ts
    • @Component({ template })
  • markdown-in-js / markdown-in-ts
    • md/markdown tag(${}があると対象外)

ちなみに/* XXX */系のコメントはスペース込みの完全一致で判定される。

JSON / JSONC / JSON5 / JSONStringify

  • (no op)

GraphQL

  • (no op)

CSS / LESS / SCSS

  • yaml-in-css / yaml-in-less / yaml-in-scss
    • front matter
  • toml-in-css / toml-in-less / toml-in-scss
    • front matter

誰がそんな使い方するんやって思ったら、Jekyll系か・・・。

---で囲われてたらYAML、+++で囲われてたらTOMLになる。 ただ、TOMLはデフォルトでサポートされていないので、そのまま出力される。(TOMLプラグインがあったりして、inferできたらフォーマットされる)

ちなみに、このfront matterの挙動は、言語ごとに実装されてるというより、いくつかの言語向けに根本で入ってる。

YAML

  • json-in-yaml
    • .prettierrc / .stylelintrc / .lintstagedrcのみ

そもそもYAMLって、JSONそのまま書いても合法って知らんかった。

この3ファイルだけは特別なフローで処理される。

  • まずは、ファイル名からYAMLとして分類されてパースされる
  • しかし、print時にembedの仕組みで全文をparser:jsonでフォーマットし直そうとする
  • JSONとして解釈できたらJSON、できなかったら、parser:yamlの結果として出力される

なぜならこのファイルたちは、JSON/YAMLどっちで書いてもいいとされてる厄介なやつだから・・・。

Markdown

  • ANYTHING-in-markdown
    • fenced code block
  • yaml-in-markdown / toml-in-markdown
    • front matter

Markdownも、inferできるものならコードブロックになんでも書ける。

front matterの挙動はCSSに書いた先述のとおり。

MDX

  • ANYTHING-in-mdx
    • fenced code block
  • yaml-in-mdx / toml-in-mdx
    • front matter
  • js-in-mdx
    • import / export statement
  • js-in-mdx
    • JSX expr

基本はMarkdownと同じで、import/exportがありJSXのコンポーネントがあるくらい。

HTML

  • css-in-html
    • <style> / style="" attr
  • less-in-html / scss-in-html
    • <style lang=...>
  • js-in-html
    • <script>src属性があると対象外), on*="" event handler attr(既知のイベント属性のみ)
  • ts-in-html
    • <script lang=ts> / type=application/x-typescript
  • markdown-in-html
    • <script type=text/markdown>
  • json-in-html
    • <script type=*json | importmap | speculationrules>
  • glimmer-in-html
    • <script type=text/x-handlebars-template>
  • html-in-html
    • <script type=text/html>
  • ANYTHING-in-html
    • <script lang> / <style lang>
  • yaml-in-html / toml-in-html
    • front matter

わけがわからん。

なお、このHTML系の埋め込み(<script>のtype/lang、<style>のlang、front matter)は、html / vue / angular / lwc / mjml の5パーサーで実装が共有されてる。なので、ここに挙げたものは以降のHTML系でも全部使える。

ちなみに、言語の埋め込みではないけど、同じembedの仕組みでこれらもフォーマットされる。

  • <img srcset>: 行ごとに分割して右揃え
  • class属性: 不要な空白を削る
  • <iframe allow>Permissions Policyを;区切りで整形

ただembed全体はembeddedLanguageFormatting: "off"で一括無効になるので、これらの属性整形やfront matterのフォーマットも一蓮托生になってる。

Angular

  • ANYTHING-in-angular

基本的にHTMLと同上だが、expr部で使われる独自のやつがある。

  • __ng_interpolation
    • {{ expr }}
  • __ng_action
    • (click)="" / on-*=""
  • __ng_binding
    • [target]="" / bind-*="" / [(target)]="" / bindon-*=""
    • AngularJS 1.x風のng-if/ng-show/ng-hide/ng-class/ng-style@let RHS
  • __ng_directive
    • *directive=""
    • 新しめのcontrol flow block(@if / @else if / @for / @switch / @case)のパラメータ部も

微調整されたJS/TSって感じ。

あとi18n属性は、言語じゃなくただの長文テキストとして整形される。

Vue

  • ANYTHING-in-vue
    • HTMLと同上
    • 加えてSFCトップレベルのcustom blockの<xxx lang>
  • js-in-vue / ts-in-vue
    • v-for LHS, v-slot bindings, <script setup> attr, <style vars> attr
  • ts-in-vue
    • <script generic=""> attr

一部のexpr(v-for leftとか)は、function _(...) {}みたいなプレースホルダーに埋めて通常のJS/TSパイプラインでフォーマットされるが、それとは別の独自のやつもある。

  • __vue_expression / __vue_ts_expression
    • :prop="" / .prop="" / v-bind:prop=""{{ expr }}
  • __vue_event_binding / __vue_ts_event_binding
    • @click="" / v-on:click=""(まず普通のexprとして試して、構文エラーならこっちにフォールバック)
  • __js_expression / __ts_expression
    • その他のv-*ディレクティブ、v-forのright

JS/TSどっちのパーサー系統になるかは、SFC内に<script lang="ts">があるかどうかで一括で決まる。

LWC

  • ANYTHING-in-lwc
    • HTMLと同上

MJML

  • ANYTHING-in-mjml
    • HTMLと同上
  • css-in-mjml
    • <mj-style>

使ってる人いるんか?

Glimmer (Handlebars)

  • css-in-glimmer
    • <style> tag, lang=css/empty
  • yaml-in-glimmer / toml-in-glimmer
    • front matter

xxx-in-yyy-in-zzz

無段階にネストできるが、実用的には数段階で歯止めがかかるはず。 エスケープしまくってまで書きたくないと思うし。

ただ明示的にガードが入ってるものもある。 たとえばcss-in-html-in-jsは、<style>はフォーマットされるけど、style=""はフォーマットされない。

実装的には、style="" / on*="" / class / iframeのallowといった属性系には、!options.parentParserならっていうガードが入ってる。つまりHTML属性系は、親パーサーがいないトップレベルの時(xxx-in-htmlまで)だけフォーマットされる。 あと、これらの属性は値に{{を含む場合もスキップされる。テンプレートエンジンのinterpolationを壊さないためっぽい。

まとめ

つまり、Prettier互換ですって言うのはすごく大変ってこと。