😺

日本語Markdownの「なんか読みづらい」が CSS 3行で直った話

に公開

ずっと違和感があった

villar を毎日使っていて、ずっと言語化できない違和感がありました。

英語の Markdown を読むと「いい感じ」なのに、日本語の Markdown を読むと「うーん」となる。同じレイアウト、同じフォント、同じ行間。でも何かが詰まっていて、何かが余っている。

長い間「日本語は読みにくいフォントが多いからしょうがない」と思っていました。実際フォントの問題もあります。が、半年使い続けて気づいたのはフォントよりも**約物(やくもの)**の周辺、つまり句読点・カッコ・記号類の扱いがほとんど何もチューニングされていないことでした。

ブラウザがデフォルトでやっている日本語組版は、控えめに言ってお粗末です。Word や InDesign が当然のようにやっている処理を、ブラウザは標準ではやってくれません。仕方ないとも思います。Webは英語中心の言語環境で育ってきたので。

v0.4.3 で、その違和感をCSSの新しい仕様3つで一気に解決しました。この記事はその話です。

https://tyler0702.github.io/villar/

違和感の正体は3つあった

並べてみると、こうです。

1. カッコや句読点の前後が空きすぎる

これは「villarの設計」についての文章です。

普通に書くと「villarの設計」の左カッコの前と右カッコの後に、なんだか余白が見える気がする。これは見間違いではなくて、フォント側に約物用の空きが組み込まれていて、それが文章中で出てきたときに前後に詰まずにそのまま空くからです。

紙の本だと、左カッコは行頭でない限り左側の空きを詰めます。右カッコは行末でない限り右側を詰めます。Webは詰めません。

2. 行頭・行末の禁則処理が緩い

ブラウザの日本語禁則は標準で「ゆるい」設定です。line-break: normal。これで何が起きるかというと、行末に句点が単独で来てしまったり、行頭に小さい「ゃゅょぁぃぅ」や長音「ー」が来てしまったりする。

「読みづらい」という意識的な感覚にはならないものの、目が行末・行頭で一瞬詰まる。脳の処理が一段ずれる。これが続くと「なんか読みづらい」という総合的な疲労になる。

3. 右端が句読点で揃わない

これは長い文章で、最後に句読点が来ます。
そして次の段落も、こんな感じで終わります。

右端を見ると、句読点と平仮名と漢字が入り混じって、揃ったり揃わなかったりする。紙の本だと、行末の句読点は行幅の外に「ぶら下げる」ことで右端をきれいに揃えます。これがぶら下げ組(hanging punctuation)

CSS の新しい仕様で全部解決した

ここ数年でCSS仕様が一気に進化していました。半年前は「ブラウザ対応がまだ」と思っていた機能が、Tauri が使っている Chromium で動くようになっていたんです。

.reading-root.jp-typography-on {
  text-spacing-trim: trim-start;
  line-break: strict;
  word-break: normal;
  overflow-wrap: anywhere;
  hanging-punctuation: allow-end;
}

5プロパティ、しかも中身を理解すれば3つの問題に1対1で対応しています。

text-spacing-trim: trim-start

CSS Text Module Level 4 の比較的新しい仕様で、約物の前後の空きを自動で詰めてくれます。trim-start は「行頭側の空きを詰める」モード。これで「カッコの前の空きすぎ」が消えます。

適用前: 「villarの設計」を読んでいた
適用後:「villarの設計」を読んでいた

ぱっと見の違いは小さいですが、長文を読み続けると効きます。

line-break: strict

ブラウザの禁則ルールを「ゆるい」から「厳しい」に変えます。これで小書き仮名(ゃゅょ)、長音符(ー)、繰り返し記号(々ゝゞ)などが行頭に来なくなります。

仕様のリスト上では数十文字の差ですが、これがあるだけで「読みやすさ」の体感が変わります。normal の状態で行頭に「ょ」が来ている瞬間が、いかに読み手の目を止めていたか、strict にしてみるとよく分かります。

hanging-punctuation: allow-end

行末の句読点をぶら下げ表示にします。これでようやく右端が揃う。

適用前:
これは長い文章で、最後に句読点が来ます。
そして次の段落も、こんな感じで終わります。

適用後:
これは長い文章で、最後に句読点が来ます。
そして次の段落も、こんな感じで終わります。

ぱっと見では分からないかもしれませんが、 が行幅から微妙にはみ出して、その分本文の右端がぴったり揃っている。本のページを開いた時の安心感、というか。

word-break: normal + overflow-wrap: anywhere

これは保険です。line-break: strict を入れると禁則処理が厳しくなる分、長いURLや英単語が行をはみ出すケースが出てくる。overflow-wrap: anywhere で「どうしてもはみ出すならどこで折ってもいい」と指示しています。日本語の組版ルールと、英語の長い単語の現実、両方に折り合いをつけるための1行です。

コードブロックには適用しない

これが地味に大事でした。

.reading-root.jp-typography-on pre,
.reading-root.jp-typography-on code {
  text-spacing-trim: normal;
  line-break: auto;
  hanging-punctuation: none;
}

コードブロックに text-spacing-trim を効かせると、ソースコード中の ( , などの後の空白を「約物の空き」と認識して詰めてしまうことがあります。def foo(a, b):def foo(a,b): になる、までは行きませんが、見た目の安定感が崩れます。

ソースコードは「書かれた通りに見える」ことが最優先です。日本語タイポグラフィの恩恵はあくまで地の文だけに留める。Markdown のテーブルや引用ブロックは適用、<pre><code> は除外。

CSS のセレクタを書くのは面倒ですが、これがないと「日本語が綺麗になった」けど「コードが微妙にずれた」になります。

なぜ「オプション」にしたのか

このセクションは技術というより設計の話です。

text-spacing-trimline-break: stricthanging-punctuation は、英語中心のドキュメントには副作用がほぼゼロです。だったらデフォルトで有効でいいじゃないか、と最初は思いました。

設定 → Reading の中にトグルを置いた理由は3つあります。

1. ブラウザ実装の差

villar は Tauri なのでユーザーの環境は基本 Chromium ベース(WebView2 / WebKit)です。が、macOS だと WKWebView も使われる。text-spacing-trim は Safari 17+、hanging-punctuation は Safari でも限定的、と微妙にバージョン差があります。「自分の環境では動かない」というユーザーが出てきたとき、オフにできる経路が必要でした。

2. 「等幅っぽさ」を好む人がいる

意外と「組版が綺麗になりすぎている」のが好みじゃない人もいます。プログラマーの文化的に、テキストは等幅で並んでいる方が安心する、という感覚があります。hanging-punctuation で右端が揃うのを「不自然」と感じる人もいる。好みなのでオフにできた方がよい。

3. デバッグのため

新しいCSS機能なので、「あれ、なんか表示崩れた」というときに最初に疑いたいのがこの3行です。トグル一発で切れるようにしておくと、原因切り分けが10秒で済みます。

デフォルトはONにした

japaneseTypography: true をデフォルトにしました。villar のユーザー層を考えると AI 出力の日本語ドキュメントを読む人が多いはずなので、初期状態で「綺麗な日本語組版」が見えている方が「おっ」となるはず、という判断です。

オフにしている人を見ていません。たぶん全員ONのまま使っている、はずです。表示崩れもまだ報告されていません。デフォルトを変える勇気を持てたのは、約物関連のCSSがここまで安定したからです。

おまけ:同じリリースで踏んだ小さなバグ

v0.4.3 では、もう1つ小さな UX バグも直しました。書いておきます。

villar には「カードをクリックするとそのカードがアクティブになる」というUIがあります。これ自体は問題ないのですが、画面の下に半分見えているカードを間違ってクリックしたとき、そのカードがビューポート上端まで一気にスクロールしてしまう挙動になっていました。

「カード単位でジャンプして読む人」には自然な動きです。が、「スクロールでなだらかに読み下す人」にとっては突然画面が飛ぶことになる。ありがとう、と思いつつクリックを諦めてスクロールに戻る、という二重の被害を生んでいました。

修正は、判定式を cardTop >= containerTop から「カードの矩形がコンテナの矩形と縦方向で1px でも重なっていたらスクロールしない」に変えただけです。

export function isCardVerticallyVisible(card, container): boolean {
  return card.bottom > container.top && card.top < container.bottom;
}

論理的にはほぼ等価に見えますが、「カードの上端が見えなくなった瞬間にスクロール対象」だった旧ロジックでは、上が見えなくなっても下が見えているカードはスクロール対象でした。新ロジックでは「画面に少しでも映っているなら触らない」になります。

ピュア関数に切り出して13個のユニットテストを追加しました。1pxの境界、コンテナの上下とぴったり重なるケース、カードがコンテナより大きいケース。テストを書いている間に「これは本当にスクロールしないべきか?」を何度も確かめられて、本体の修正より理解が深まりました。

小さなバグですが、報告してくれたユーザーが「villar はスクロール中心の読み方も尊重してほしい」という言い方をしていたのが印象的でした。アプリの設計思想は、ユーザーがその言葉で語ってくれるまで自分では言語化できないものです。

数行のCSSが、地味だけど効く

v0.4.3 の典型的な特徴は「ほんの数行で世界の見え方が変わる」でした。

  • 日本語タイポグラフィ:CSS 7〜10行
  • カードクリック修正:13行のヘルパー + テスト
  • 別件のマルチウィンドウ設定分離:25行ほど(前回の記事

3つ合わせても 100 行に届かない変更で、毎日使っているアプリの体感が一段上がりました。Mermaid パーサーに何日もかけたあの時間と、text-spacing-trim: trim-start 1行の効果を秤にかけると、何が「効く」かは本当にやってみないと分かりません。

CSS仕様の世界はここ数年で日本語まわりが急速に整っています。text-wrap: prettytext-wrap: balance も対応が進んできています。次は見出しの折り返しに text-wrap: balance を入れる予定です。たぶんまた、たった1行で何かが少し綺麗になります。


シリーズ一覧:

  1. AIが出力する4000行のMarkdownが読めなくて、Tauri v2でリーダーアプリを作った
  2. H1で分割して大失敗した — villarのMarkdownパイプラインを全部見せる
  3. 46テーマ作って半分後悔した — villar開発 2ヶ月の振り返り
  4. 「カード表示いらないからMarkdown見せて」と言われて作ったRaw View — villar v0.4.0
  5. ウィンドウを2つ開いたら設定が混ざった — villar マルチウィンドウ事件簿
  6. 日本語Markdownの「なんか読みづらい」が CSS 3行で直った話(この記事)

https://tyler0702.github.io/villar/

Discussion