🍇

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

rehypeプラグインも作った

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

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

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

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

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

お、ここで紹介する法をとらずとも、公式で提供されているWeb Components使えば同様の表示を実現できる。ただしその場合、余分なJavaScript1バンドルに含める必要があるうえ、分かち書きされた要素の間に必ずゼロ幅スペース2挟まることになる。

これは一見問題なさそうだが、実はゼロ幅スペースはページ上でテキストを選択した際に文字として含まれてしまうため、あまり嬉しくない。代わりにゼロ幅スペースに相当するHTML要素<wbr>)を挿入するよう実装すると、この問題を回避できる。

準備

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

Terminal window
npm add -D budoux

READMEよると、JavaScript向けにParser.parse()HTMLProcessingParser.translateHTMLString()など、いくつかの処理方法が提供されている。今回は生のHTMLをパースしたいわけではないため、前者(をラップしているloadDefaultJapaneseParser().parse())を利用する3

お、BudouXで処理した要素には、word-break: keep-all; overflow-wrap: anywhere;いうCSSを当てる必要がある。今回はdata-budouxいう属性を付与しておき、あとからグローバルCSSで当てることにする。

本文(Markdown / MDX)

MarkdownやMDXで記述したコンテンツについては、rehypeプラグインで自動的に処理を行う。

src/lib/rehype-budoux.ts
import type { HTMLProcessingParser } from "budoux";
import { loadDefaultJapaneseParser } from "budoux";
import type { Root } from "hast";
import { h } from "hastscript";
import { visit } from "unist-util-visit";
const defaultExcludeTagNames: string[] = ["pre", "code"];
interface Options {
/**
* The list of tag names to exclude from processing.
* @default ["pre", "code"]
*/
excludeTagNames?: string[];
}
let parser: HTMLProcessingParser | null = null;
const rehypeBudoux = ({
excludeTagNames = defaultExcludeTagNames,
}: Options = {}) => {
return (tree: Root) => {
visit(tree, "text", (node, index, parent) => {
if (
index === undefined ||
!parent ||
node.value.trim().length <= 0 ||
parent.type !== "element" ||
excludeTagNames.includes(parent.tagName)
) {
return;
}
if (!parser) {
parser = loadDefaultJapaneseParser();
}
const parsed = parser
.parse(node.value)
.flatMap((value, i) => [
...(i > 0 ? [h("wbr")] : []),
{ type: "text" as const, value },
]);
parent.children.splice(index, 1, ...parsed);
if (parsed.length > 1) {
parent.properties = {
...parent.properties,
dataBudoux: true,
};
}
});
};
};
export default rehypeBudoux;

本文(上記以外)

それ以外の箇所では、同様の処理を行う<Budoux />コンポーネントを作成し、適宜インポートして利用する。

src/components/Budoux.astro
---
import { loadDefaultJapaneseParser } from "budoux";
interface Props {
text: string;
}
const { text } = Astro.props;
---
<>
{
loadDefaultJapaneseParser()
.parse(text)
.flatMap((value, index) => [
...(index > 0 ? [<wbr />] : []),
<>{value}</>,
])
}
</>
---
import Budoux from "@/components/Budoux.astro";
---
<p data-budoux><Budoux text={post.data.description} /></p>

CSS

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

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

OpenGraph画像

お、本文だけでなく、satori等を使用して生成している画像内の日本語についても、BudouXを使ってまっとうな改行をさせることができる。これに関しては他のサイトを参照せよ。

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

おわりに

BudouXの公式サイト曰く、

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

とのこと。


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

  2. 本来であれば、改行可能な位置にゼロ幅スペースU+200B)を挟むか、HTMLの<wbr>要素を挟むかを選択できるべきである。しかしWeb Componentsでは後者の選択肢が奪われてしまう

  3. ちなみに.translateHTMLString()実装を見てみると、デフォルトでは改行位置にゼロ幅スペースを挟むようになっていることがわかる。これもParser.parse()使う理由である