自分好みのテキストレイアウトシステム作る

自分好みのテキストレイアウトシステムを作る

活字のようなマルチカラムの組版を目指して

このWebサイトは自作のCMSで構築しているのである程度は機能を好きに実装することができます。このレイアウトシステムも私が独自に設計したものです。まあまあいい感じになっているとは思うのですが、動作確認していないブラウザやOS環境もあるので、macOSSafari環境以外ではもしかしたら粗が目立つかもしれません。

Webシステムの互換性に関しては、ブラウザ対応もそうだったのですが、やはり組版の実装はそれなりに難しく、AIの手を借りても堂々巡りの実装を繰り返してしまいました。指示者が適切な知識や設計の勘所を有していないと曖昧な指示になってしまい、AIも変な処理を繰り返してしまうことがあります。

そこで、私は日本語組版に関していくつかの参考書籍にあたってみました。まずは『日本語組版入門』という本です。これは日本語における組版の基本的な知識、規則、考え方が網羅されているので、これ一冊で理解を深めることができます。アルゴリズムの解説になっているので、物書きをする方はもちろん、ソフトウェアで組版を設計するような人にもおすすめです。

そのほかでは文字の組方ルールブック』という小冊子も参考にしました。ハンドブックのようなもので非常に薄い本(本来の意味で)なのですが、サクッと組版ルールを探るにはちょうど良さそうです。ヨコ組編とタテ組編が別々にあります。

モリサワの「MORISAWA PASSPORT 英中韓組版ルールブック」も参考になります。

このノートでは、それらの知見やレイアウト設計の勘所についてを紹介します。特に後半は独自設計の構文についての解説になります。現時点でユーザーが私しかいないので(オープンソースにしているわけでもない)、ほとんどの読者にとってはあまり意味のない説明になってしまいますが、DSLの設計例という感じで読んでみてください。

二段組レイアウト

このWebサイトの各ノートページは、ご覧のように、二段組(2列)でレイアウトしています(デスクトップのみ)。Web上の多くの読み物が一段組(1列)で作られており、1行に文字を多く抱え、個人的にはそれらは長く読みづらいと感じていました。モバイルようなフォームファクタでは問題になりにくいのですが、デスクトップのように横に広いスクリーンの環境には一段組は適していないのではと感じています。

もちろん、一段組の方が読みやすいという方もいらっしゃると思います。Webでは見慣れないので視線が踊りがちになるとか、その感覚は私も理解できます。でも日常生活で一切触れたことがないというわけでもないでしょうし、活字的な印象もやっぱりいいよなあと私は思うので、自サイトでは自由にしてもいいだろうと、思い切ってみた次第でございます。

二段組と言っても、ただ2列にすれば良いわけではありません。2列を一つの「スプレッド」単位にして、スプレッドの最大高を決めておく必要があります。でないと列がひたすら長く間延びしてしまって、2列にする意味がありません。ある程度のところで改列させて2列目にコンテンツを流し込んでいかないといけません。視線移動としては「и」のような動きをひたすら縦に繰り返していく具合です。

2列で一つのスプレッド
2列で一つのスプレッド

ただ、これの実装は結構大変でした。詳細はまた別の機会に語ろうと思いますが、Webブラウザごとの挙動の違いや、特殊な条件下での挙動不審が多く起きてしまい、安定させるのに苦労しました。macOSSafariChromeでは一見うまく動いているように見えるので、多分大丈夫なんじゃないかなと思いますけれども。Windowsやその他OSは確認環境がないのでちょっとわからないですが。

字下げ

段落が始まる最初の行頭に全角1文字分のスペースを開けることです。一般的な日本人なら小学校の国語で習ったことがあると思います。懐かしいですね。Webメディアではすっかり字下げをしなくなってしまいましたが、このWebサイトでは字下げを自動的に施す仕組みを導入しました。Markdownテキストに文字として全角スペース「 」を入れるのではなく、段落と認識される部分の行頭に自動的に字下げが入ります。

後述する無効化マーカーindentサブタイプ指定により、任意の段落の字下げを無効化することもできます。

例えばこういった用途に使えます:

◆佐藤:字下げ無効化は、このように人の発言を引用するタイプの読み物に使えそうですね。

◆鈴木:発言ごとに改段落されるので、自動字下げが邪魔になることがあります。ここだけ字下げを無効化できるなら便利です。

なお、このレイアウトシステムでは段落間にも1行程度の余白を設けています。字下げは本来段落の開始点を視認するための手がかりなのでこの対応は必要ないのですが、仮に段落間余白を詰めてしまうと長大な読み物のような印象を抱きやすいため、Webメディアでの一般的な表現を踏襲しています。この辺りは紙とスクリーンの違いを考慮しました。


禁則処理

禁則処理にはいくつかの方策を組み合わせました。CSSの標準機能と、JavaScriptによる独自設計の機構、それからGoogleBudouXです。当初はCSSのみで実現していたのですが、Webブラウザによって解釈が異なる場合が見られることや、一部の約物を条件によって半角に詰めるなどの処理を施したかったので、結局独自設計の仕組みを構築することにしました。

全角文字の

導入した禁則処理の一つに、全角括弧や句読点類を条件次第で片側余白部分を削る処理「二分詰め」を行うことがあります。これらの文字は片側が空白に見えるようにデザインされているので、例えば、

(「『【{そのまま}】』、」。)

このように全角括弧類を並べて書くと、妙に文字の間が空いて見えてしまいます。これらを二分詰めすることで、

「『【{二分詰め}】』、」。)

このようにスペースを詰めることができます。これには全角の開き括弧類と閉じ括弧類、それから句読点類の定義をそれぞれ行って、それらが一定規則で連続する場合に二分詰めを施す禁則処理にしています。

また、行頭に開き括弧系が来た場合と、行末に閉じ括弧系や句読点が来た場合にも二分詰めを施すようにしています。これによって、行頭や行末が妙に開いてしまうことを防ぎ、後述するきんとうわりつけの効果を向上させます。

「←無効❌

←有効✅

❌無効→。

✅有効→。

Google Chromeではtext-spacing-trimが有効であるため、連続約物に対しては二分詰めが施されるようです。Safariでは現状デフォルトではありません。

きんとうわりつけ

均等割付とは、決められた枠(行)に対して、文字を均等に配置する処理です。異なる文字数のラベルを揃えたり、マルチカラムレイアウトで行を段の幅に揃えたりする時に使います。

一般的なWebメディアでは均等割付を施すことは少ないと思われますが、このWebサイトでは書籍や誌面に近い印象を目指したかったので、マルチカラムレイアウトに均等割付を採用しています。行の終わりの文字がすべて揃って見えるはずです。

分離禁止文字

次の文字が連続している場合には、改行・折り返し時に分離せずに同じ行内に留まります。一般的な日本語組版の禁則処理に倣っています。

  • 連続リーダ ……
  • 連続二倍ダッシュ(ダーシ) ――
  • くの字点 〳〵 〴〵
  • 連数字 01234 01234

行頭禁止文字

行頭に配置してはならない文字を定義しています。主に小文字の仮名、踊り字(「々」や「ゝ」系の文字)が含まれます。一般的にはCSSline-break: strictで自動対応できるはずですが、今回のマルチカラムレイアウトでは状況が特殊であることから、ブラウザによって表現が変わる可能性が考えられたため、念のために独自に処理を加えてあります。これでチャ/ック佐/々木さん学問のす/ゝめ」のような改行が起きにくくなるはずです。副作用としては、行内の文字数が少なくなる場合があり、均等割付の効能によって文字間隔が妙に開いてしまうことがあります。

?!?!:;ぁぃぅぇぉっゃゅょゎゕゖァィゥェォッャュョヮヵヶ々ゝゞヽヾ〻〃ー

以下は「々」と「ヶ」が行頭に来ないことを実験する文字列です。行頭禁止を優先した結果、均等割付の作用によって文字間隔が変化していることがわかります。

■■■■□■■■■□■■■■□■■阿良々々々々々木さん
■■■■□■■■■□■■■■□■■■■□阿良々々々々々木さん
■■■■□■■■■□■■■■□■■■■□■■戦場ヶ原さん

これらの挙動は、後述する無効化マーカーにより無効化が可能です。

ハイフネーションぶんてつ

欧文に対してはハイフネーションを適用しています。ハイフネーションとは、行の折り返し点(改行)に重なった単語をハイフン“-”を挟んで分割し、2行に跨って組版するルールです。“Typo-graphy”ような具合になります。

BudouXよる自然な分かち書き

このWebサイトでは、見出しなどの一部の要素でBudouXによる分かち書き処理を実現しています。単語の途中ではなく、適度な単語の境界で改行するように働くので、日本語の文章がより読みやすくなっています。


独自記法かくかっこうぶん」によるMarkdown拡張

このレイアウトシステムにはコンテンツを注入する際の記述言語として、独自のMarkdown構文を導入しています。 角括弧構文 かくかっここうぶんはさまざまなテキストスタイルやプリセットを簡単に呼び出せる拡張記法です。本Webサイトのみで実装されている、rehypeプラグインによるMarkdownの独自拡張です。MarkdownのベースはJohn Gruberのオリジナルを拡張したCommonMarkですが、GitHub Flavored Markdownも取り込んでいます。

Webサイトでは最終的には人間が自然に読める形でテキストをHTMLに出力することを目的としているため、Markdownそのものや、情報アーキテクチャとして記事コンテンツの記述構文やスキーマの互換性を深く考慮する必要がありません。強いて言えば、私がこの記事を編集しているCotEditorで十分にシンタックス対応しきれない問題はありましょうが、シンプルな構文であるためほとんど無視できます。

角括弧構文の記述形式

角括弧構文は次のような形式で表します。

// 一般形式
[TEXT(METHOD:SUBTYPES, ARGUMENTS)OPERATOR]

// 省略形1(TEXTに対しインラインに作用)
[TEXT(METHOD, ARGUMENTS)]

// 省略形2(自己完結型)
[(METHOD, ARGUMENTS)]

この[]で括られたひとつの範囲を「マーカー」と呼びます。マーカーには機能を示すメソッドが必ずあり、これで効能が変化します。メソッドによっては ひきすう を取ることがあり、任意の値をパラメータとして渡せることがあります。

例えばあるテキストを[]で括ってメソッド名にoblique、引数に25を与えると、そのテキストに対して25度の斜体のスタイル(オブリーク)を適用します。イタリックとは異なるので、イタリック体を含まないフォントにも斜体スタイルを適用可能です

[色は匂へど散りぬるを(oblique,25)]

結果

色は匂へど散りぬるを

けんてんぼうてん

圏点とは、こういう点々をつけることです。点にはいろいろな種類(5)があります。ちなみのこのゴマ点(9)は主に縦書き用(10です。横書き用にはビュレット(1)を使います。ビュレット(3)にもいくつかの(4)種類(8)がありますが、横書きでは主にこの点(1)を使います。

[けんてん(emphasize,1)]

結果

けんてん

圏点には次の種類があります。

番号Unicode
1U+2022
2U+25E6
3U+25CF
4U+25CB
5U+25CE
6U+25C9
7U+25B2
8U+25B3
9︅﹅U+FE45
10︆﹆U+FE46

ルビ

通常のHTML5仕様のルビは次のように多くのタグを入れ子に連ねる必要があり、そのソースコードは非常に書きづらく、可読性も悪いです。

HTMLでのルビ
<ruby>標<rt>ひょう</rt>準<rt>じゅん</rt>機<rt>き</rt>能<rt>のう</rt></ruby>

ひょうじゅんのう

ルビを扱う機会がそう多いわけでもありませんが、これを毎回手で入力するのは正気の沙汰ではないので、角括弧構文で専用のマーカーを作りました。というのも、角括弧構文を設計するきっかけは、七面倒なルビの記述方法をどうにかしたかったからなのです。

一時は青空文庫のルビ記法も検討しましたが、入力の手間を鑑みて、これはすぐに不採用となりました。他の記法との互換性などは重視していなかったため、結局、私のルビ記法はシンプルに次のような形になりました:

角括弧構文でのルビ
[標準機能(ruby,ひょう じゅん  のう)]
[標準機能(ruby,ひょうじゅんきのう)]
[超電磁砲(ruby,レールガン)]

ひょうじゅんのう 標準機能ひょうじゅんきのう 超電磁砲レールガン

記述としてはとてもシンプルです。構文の引数がルビ文字列で、それらを半角スペースで区切るとモノルビ的に文字単位で対応付けできます。半角スペースを除去すれば丸ごとグループルビになります。

テキストスタイル

オブリークの他にも基本的なテキストスタイルの構文を用意しました。よく使いそうなのは太字等幅でしょうが、文字スケールは自分でもほとんど使う場面がなさそうです。これを多用するとタレントさんのブログのような印象を再現できます。

記述結果
[TEXT (scale, 1.5)]文字スケール(1.5)
[TEXT (scale, 0.74)]文字スケール(0.74)
[TEXT (stroke)]打ち消し線
[TEXT (stroke,4)]打ち消し線(4px)
[TEXT (doublestroke,2)]二重打ち消し線(2px)
[TEXT (strong)]太字
[TEXT (weight,200)]ウェイト指定(200)
[TEXT (weight,900)]ウェイト指定(900)
[TEXT (oblique)]オブリーク(斜体)
[TEXT (oblique,30)]オブリーク角度指定(30)
[TEXT (mono)]mono type 0123
[TEXT (mono,800)]mono type 0123
[[TEXT (mono,500)] (stroke)]mono type 0123 + stroke同時適用

複数のスタイルを適用するには、構文を入れ子にします。その際に、既存のMarkdown構文の[[]]と解釈されないよう、閉じ角括弧と開きパーレンの間にスペースを入れます。

記述
[[テスト1(stroke)] (weight, 200)]
[[Test2(strong)] (oblique,30)]

結果

テスト1 Test2

インタラクション系シンボ

Webサイトの性質上、おそらくキーボード操作の説明をする機会が多くなると考えたので、そのユースケースに対応できるさまざまなキーシンボルを用意しました。Apple系を中心に基本的な修飾キーや特殊キーのほか、テキストの自由記入でそれをキーシンボルにしたり、マウスポインタのクリック操作を示すシンボルも用意しました。これで、いちいち「⌘」文字を入力しなくても、フォントに依存しないSVGで描画されるシンボル Eject を簡単にテキストの中に流し込むことができます Command Escape Return

任意のテキストに対してキーシンボル属性を付与するには、テキストをブラケットで囲います。

[Command(key)] [Tab(key)] [キー名(key)]

Command Tab キー名

キーシンボルの入力にはプリセット記法が便利です。

プリセット記法結果
[(key.command)]Command
[(key.delete,title)]Delete
[(key.delete,title="Backspace")]Backspace

titleもしくはalt属性が付いているものは、選択してコピーすると名前の文字列が含まれるようになり、マウスオーバーでもTooltipで名前が表示されます コマンド Touch ID。プリセット記法ではtitleの引数を追加するだけで、デフォルトの名前が表示されます。

キーシンボルのプリセット一覧

記述結果
[(key.control)]Control
[(key.option)]Option
[(key.shift)]Shift
[(key.command)]Command
[(key.alt)]Alt
[(key.caps-lock)Caps lock
[(key.globe)]Globe
[(key.function)]Function
[(key.delete)]Delete
[(key.forward-delete)]Forward delete
[(key.tab)]Tab
[(key.back-tab)]Back tab
[(key.return)]Return
[(key.enter)]Enter
[(key.escape)]Escape
[(key.space)]Space
[(key.home)]Home
[(key.end)]End
[(key.page-up)]Page down
[(key.page-down)]Page up
[(key.arrow-up)]Arrow up
[(key.arrow-down)]Arrow down
[(key.arrow-left)]Arrow left
[(key.arrow-righ)]Arrow right
[(key.menu)]Menu
[(key.power)]Power
[(key.eject)]Eject
[(key.touchid)]Touch ID
[(key.windows)]Windows

ポインターシンボルプリセット一覧

マウスポインターのいくつかの操作方法をプリセット記法で定義しています。

記述結果
[(pointer.click)]Click
[(pointer.press)]Press
[(pointer.drag)]Drag
[(pointer.click,title)]Click
[(pointer.drag,title="ドラッグ")]ドラッグ

組み合わせるとこのような使い方ができます:

Macユーザー:ファイルをクリックしてから、Optionを押しながらドラッグでコピーする。

Windowsユーザー:ファイルをクリックしてから、Altを押しながらドラッグでコピーする。

Touch IDキーを長押しして、Macを強制再起動。

スペーサー

スペーサーは縦後方に任意の幅の空白を挿入します。

// デフォルト: 1em(引数省略可)
[(spacer)]

// em, rem, px, pt, ch, %に対応
[(spacer,2em)]
[(spacer,8px)]
[(spacer,0.5em)]
[(spacer,2ch)]
[(spacer,50%)]

マルチカラムの場合、autoでその列の残りをスペースで埋めて、実質的に改列を行います。シングルカラムでは1emと同等の効能です。

// 改列
[(spacer,auto)]

区切り線

<hr/>相当の区切り線を挿入します。構造的には段落単位になるので、マーカーの前後には空行が必要です。

// 実線(サブタイプ指定を省略可)
[(divider)]
[(divider:solid)]

// 二重実線 
[(divider:doublesolid,gap=3px)]

// 点線
[(divider:dash,length=5px,gap=3px)]

// 斜線領域
[(divider:slash,height=1em,gap=10px)]
// 斜線領域(反転)
[(divider:slash,height=1em,gap=10px,reversed)]

実線:


二重実線:


点線:


斜線領域:



整列

マーカーを添えた段落を左揃え、中央揃え、右揃えにします。後述する範囲マーカーも使用可能です。サブタイプには3種類あります。

  • left
  • center
  • right
記述
段落[(alignment:center)]

[(alignment:right)..]
段落1
段落2
[..]

中央テスト

右テスト

無効化マーカー

任意の範囲を<div>または<span>タグで括り、それらのクラス名をdisableShorthandとすることで、その範囲への構文の適用を無効化します。しかしこれも入力が面倒なので、これ自体を角括弧構文で再定義しました。\[(disable:shorthand)\]を段落中のいずれかに置くと、その段落全体に対して角括弧構文を無効化するクラスを自動付与します。この説明文中でも多用しています。

// 囲った範囲で角括弧構文を無効化(ブロック単位)
<div class="disableShorthand">  </div>

// 囲った範囲で角括弧構文を無効化(span単位)
<span class="disableShorthand">  </span>

// これを置いた段落全体で角括弧構文を無効化
[(disable:shorthand)]

無効化マーカーのサブタイプにはshorthand角括弧構文の無効化), indent(段落インデント・字下げの無効化), kinsoku(禁則処理の無効化), justify(均等割付の無効化)があり、カンマ区切りで複数記述・同時適用も可能です。

範囲オペレータ

[(METHOD)..][(end)..]の範囲にある段落にメソッドの効果が作用します。[(end)..][..]と省略することができます。

角括弧構文のオペレータは現在のところ範囲オペレータのみが実装されており、他のオペレータは拡張予定ではあるものの未定義です。メソッドは次のものが実装されています。

columns範囲内をマルチカラムにする
,count列数
,maxheight理想的な最大列高(オプショナル、デフォルト800px)
disable範囲内でサブタイプについて無効化する
:shorthand角括弧構文を無効化
:indent行頭字下げを無効化
:justify均等割付を無効化
alignment範囲内でテキストを寄せる
:未指定左寄せ
:left左寄せ
:right右寄せ
:center中央寄せ
codeblock範囲内をコードブロックにする
,wrapped自動折り返し(オプショナル)
,lang=LANG言語指定(オプショナル)
imageblock範囲内を画像ブロックにする
,size=mini|small|regular範囲内の画像サイズを指定(オプショナル)

次の例はルビを適用するマーカーを設置したものです。

記述
月日は[百代(ruby,はくたい)]の[過客(ruby,くわかく)]にして、行かふ年も又旅人[也(ruby,なり)]。

出力

月日は百代はくたい過客くわかくにして、行かふ年も又旅人なり

この文章を [(disable:shorthand)..][..] で括ると、構文が適用されず、記述内容そのままに出力されます。

記述
[(disable:shorthand)..]
月日は[百代(ruby,はくたい)]の[過客(ruby,くわかく)]にして、行かふ年も又旅人[也(ruby,なり)]。
[..]

出力

月日は百代はくたい過客くわかくにして、行かふ年も又旅人なり

マーカーリテラル表示(エスケープ)

Markdownソースに \\[ \\]\2つ)を書くとマーカーのリテラルをそのまま本文に出力(無効化)できます。

記述(テキスト中)
\\[(disable:shorthand)\\] \\[(key.command)\\]

出力

[(disable:shorthand)] [(key.command)]

コードブロック内では \ 1つでエスケープ可能です:

記述(コードブロック内)
\[(disable:shorthand)\] \[(key.command)\]

出力

出力(コードブロック内)
[(disable:shorthand)] [(key.command)]

実は開き角括弧のエスケープ\[だけでもマーカーのリテラル表示が効くのですが、不安定なことがあるので閉じ角括弧にもエスケープを付けた方が安全です。

そのほかの拡張構文

OGイメージリンク

リンクをOGイメージ付き(OGP)にします。通常のMarkdownリンク構文のなかで、URLの後に半角スペースを空け、"ogp"と書きます。

// URLリンク(別ドメインなら自動的に別タブ)
[](https://github.com/usagimaru "ogp")

// サイト内相対パス
[](/path/to/page "ogp")

例:

インラインリンク別タブで開く

同一ドメイン(このWebサイト)内のインラインリンクでも、"external"と付ければ外部リンクにできます。外部リンクにはバッジが付きます。

[リンクテキスト](https://clickandmagic.com "external")

例:リンクテキスト

画像の挿入

画像はアセットとして組み込んだファイルを参照し、Astroがビルドする際にWebP圧縮をかける仕組みにしています。埋め込みカラープロファイルを維持するので、Display P3がsRGBに置き換えられるようなこともありません。

画像は次の構文で挿入します:

![キャプション](../relative/path/to/image.jpg "small")

Markdownの画像挿入記法を拡張していて、4種類のサイズを指定できます。いずれもRetina Display高解像度に自動対応します。キャプションを入れると画像の下に説明文が添えられます。

  • 未指定): コンテナ幅に合わせる
  • regular: 600px幅
  • small: 400px幅
  • mini: 250px幅
画像挿入テスト (mini)
画像挿入テスト (mini)

拡大可能画像の挿入/画像ブロッ

regular以上で拡大可能になります。

画像挿入テスト (regular)

範囲オペレータで範囲内の画像とテキストを一つのブロックとしてペアリングします。複数のペアを並べることもできます。テキストはキャプション扱いになります。キャプションにスタイルを当てやすいので、![]()構文単体よりも便利です。

imageblock範囲オペレータによる画像ブロック挿入
[(imageblock,size=regular)..]

![](../path/to/image.jpg "regular")

imageblock中で前後空行を挟んでキャプション文字列を入れる。

段落は改行で結合。

[リンク](https://ndlsearch.ndl.go.jp/books/R100000002-I034158992 "external")も可能。

[..]
imageblock中で前後空行を挟んでキャプション文字列を入れる。
段落は改行で結合。
リンクも可能。

Footnotes

  1. 記号系の文字類。句読点、括弧など。

  2. 縦書き用。

  3. Webサイトの表示書体は特定のフォントファミリーを指定しておらず、任意のシステムフォントが適用される設計です。Apple系なら「システムフォント」書体になります。Windowsでは游ゴシック系になると思われます。