🍇

AstroでBudouXによる日本語の自然な折り返しを実装する

本文とOpenGraph画像の日本語をいい感じに折り返す

あなたとJAVA,
今すぐダウンロー

上のテキストを見てほしい。西洋中心主義者たちの無自覚なふるまいによってめちゃくちゃにされた、見るも無惨な日本語の姿である参考)。

もう二度とこうした悲劇が起きないよう、Googleは近年、BudouX 🍇 というライブラリを作った。これは軽量ながら、日本語・中国語・タイ語等の改行位置をうまいこと判定してくれるという優れものである。

GitHub - google/budoux
Contribute to google/budoux development by creating an account on GitHub.
github.com
GitHub - google/budoux

Chromium系(v119-)のブラウザにはすでにこの機能が組み込まれていてword-break: auto-phrase指定すると利用できる。しかしFirefoxやSafariではまだ使えない。そこで、Astro製のサイトにBudouXを組み込み、ブラウザを問わず綺麗な折り返しができるようにした。

お、ここで紹介する法をとらずとも、公式で提供されているWeb Components使えばもっと簡便に同じ表示を実現できる。ただしその場合、余分なJS1バンドルに含める必要があるうえ、せっかくサイトを静的生成しているのにクライアント側で処理を走らせる羽目になる。そのため、ここではビルド時にすべての処理を済ませてしまう方針をとる。

準備

まずはBudouXのJavaScriptモジュールをインストールしておく。

Terminal window
npm add -D budoux

READMEよると、JavaScript向けにParser.parse()HTMLProcessingParser.applyToElement()など、いくつかのメソッドが提供されている。今回は本文のHTMLを処理するため、HTMLProcessingParser.translateHTMLString()使う。

さて、.translateHTMLString()実装を見てみると、デフォルトでは改行可能な位置にU+200B(ゼロ幅スペース)を挿入するようになっているHTMLProcessorOptions.separator)。これは一見問題なさそうだが、実はゼロ幅スペースはページ上でテキストを選択した際に文字として含まれてしまうため、あまり嬉しくない。代わりに、ゼロ幅スペースに相当するHTML要素<wbr>)を挿入すると、この問題を回避できる2

また、デフォルトでは出力されるHTMLにword-break: keep-all; overflow-wrap: anywhere;いうスタイルがインラインで付加されるが、HTMLProcessorOptions.className指定しておくと、代わりに任意のクラスを付与できる。今回はbudouxいうクラスを付与しておき、あとからグローバルCSSで同様のスタイルを当てることにする。

以上を適当な関数にまとめると以下のようになる。

src/lib/budoux.ts
import { HTMLProcessingParser, jaModel } from "budoux";
import type { HTMLProcessingParser as HTMLProcessingParserType } from "budoux";
import { win } from "budoux/dist/win";
let cachedParser: HTMLProcessingParserType | null = null;
export function getBudouxParser() {
if (!cachedParser) {
const wbr = win.document.createElement("wbr");
cachedParser = new HTMLProcessingParser(jaModel, {
className: "budoux",
separator: wbr,
});
}
return cachedParser;
}
export const budouxProcess = (html: string) =>
getBudouxParser().translateHTMLString(html);

グローバルCSSも忘れずに書いておく。

src/styles/global.css
.budoux {
word-break: keep-all;
overflow-wrap: anywhere;
}

本文(Markdown / MDX)

MarkdownやMDXで記述したコンテンツについては、rehypeプラグインで自動的に処理を行う。対象にしたい要素<p><li>)をHTML化し、BudouXの処理を適用し、元の要素を置き換えるだけ3

src/lib/rehype-budoux.ts
import { fromHtml } from "hast-util-from-html";
import { toHtml } from "hast-util-to-html";
import { SKIP, visit } from "unist-util-visit";
import { budouxProcess } from "./budoux";
import type { Element, ElementContent, Root } from "hast";
import type { Plugin } from "unified";
const targetTagNames = ["p", "li", "h1", "h2", "h3", "h4", "h5", "h6"];
function isElement(node: Element | ElementContent): node is Element {
return node.type === "element";
}
function isTargetNode(node: Element | ElementContent): node is Element {
return isElement(node) ? targetTagNames.includes(node.tagName) : false;
}
const rehypeBudoux: Plugin<[], Root> = () => {
return (tree) => {
visit(tree, "element", (node, index, parent) => {
if (typeof index !== "number" || !isTargetNode(node)) {
return;
}
const newNode = fromHtml(budouxProcess(toHtml(node)), {
fragment: true,
}).children[0];
newNode && parent?.children.splice(index, 1, newNode);
return SKIP;
});
};
};
export default rehypeBudoux;

本文(上記以外)

それ以外の箇所については、<Budoux />コンポーネントを作成し、適宜インポートして利用する。<Budoux />コンポーネントは受け取った子要素をHTML化し4BudouXの処理を適用し、その出力を直接HTMLとしてセットしている。

src/components/Budoux.astro
---
import { budouxProcess } from "@/lib/budoux";
const slotContent = await Astro.slots.render("default");
const parsed = budouxProcess(slotContent);
---
<Fragment set:html={parsed} />
---
import Budoux from "@/components/Budoux.astro";
---
<Budoux><p>{post.data.description}</p></Budoux>

OpenGraph画像

本文だけでなく、satori等を使用して生成している画像内の日本語についても、BudouXを使ってまっとうな改行をさせることができる。このトピックについてはすでに数多書かれているのでそちらを参照した。

BudouXとSatoriを使ってタイトルが分かち書きされたOGP画像を出力する。 - return $lock;
Google Developers Japanのブログを読んでいたら、BudouXという分かち書き器が紹介されていました。以前からOGP画像でタイトルが変なところで改行されているのをどうにかしたいと考えていたので、BudouXを導入して問題を解決するまでの過程と結果を残しておきます。
retrorocket.biz
BudouXとSatoriを使ってタイトルが分かち書きされたOGP画像を出力する。 - return $lock;

おわりに

BudouXの公式サイト曰く、

Google の使命は、世界中の情報を整理し、世界中の人がアクセスできて使えるようにすることです。

とのこと。

脚注

  1. とはいえ実測で20KB(gzip前)程度だが……

  2. HTMLProcessorOptions.separatorにはstringまたはHTML要素Node)を指定できる。Node.jsなど非ブラウザ環境でHTML要素を作成するのは少々厄介だが、BudouXが内部で利用しているjsdomWindowを流用するとうまくいく。

  3. 構文木(hast)をHTML化して、またhastに戻しているのは明らかに非効率なので、本来はhastの状態のまま処理を行いたいところだが、そのためにはBudouXが行っている処理を(hastに即した形で)再実装する必要があり、やや面倒になる。誰か暇ならやってほしい。

  4. Astro.slots.render()使うと、子要素をHTMLとしてレンダリングできるというのがポイントかもしれない。