豪鬼メモ

一瞬千撃

Fakebook: ゼロから作るSNS その8 Word/Docsからコピペでブログ公開

SNSで記事を書く際に、コピ&ペーストで画像を貼り付けたり、リッチテキストをMarkdownとして貼り付けたりする機能を実装した。これによって、任意のアプリの画像を使った投稿が簡単にできるようになり、ワープロ派が一撃で構造化文書を作成して公開できるようになった。WordやGoogle Docsを開いて記事をコピーすると、HTML形式のリッチテキストがクリップボードに入る。それをSNS上にペーストすると勝手に構造化して上でMarkdownとして書き込まれるようにした。あとは、その記事を外部公開するだけで、構造化されたブログ文書が世界に発信できる。

リエータ層とAIに奉仕するSNS



前回の議論で、「クリエータ層とAIに奉仕するSNS」というSTGYのコンセプトが固まった。右の画像はトップページのもので、「An SNS for intellectual creators and AI agents」というモットーを掲げている。記事の外部公開機能でブログエンジンとしても動作するようになったので、ガチで運用できる基盤はほぼ整った。

Markdownなら、記法を全く知らなくても普通のプレーンテキストとして文章が書けるし、ヘッダやリスト程度の構造化は10秒で学べるし、テーブルやルビや数式なども含めた本格的な構造化文書を普通のWebフォームやテキストエディタの上で書くことができる。ゆえに、一般の人々に普通に使いやすいとともに、技術系ライターやソフトウェアエンジニアやジャーナリストや理系の研究者が原稿を書く際にも使える仕様になっている。写真や図などの画像をコピペで記事に埋め込めれば、大抵の内容の記事は表現できることだろう。Markdownになってさえいれば、任意のフォーマットに変換できるし、AIに読み込ませることも容易い。

しかし、それで全ての層に訴求できるわけではない。どうやら、小説家や社会系ライターや文系の研究者の多くは、Microsoft WordやGoogle Docsで原稿を書いているらしい。それらは、単体でバージョン管理もされるし、校正などの赤ペン入れもできるので、確かに彼らのワークフローに合っているツールだ。小説は縦書きで組版されるのが一般的だが、そこでの読者の感覚に合わせるために、縦書きモードにこだわる作家も多い。最終出力を固定してそれに最適化した執筆環境を目指すなら、WYSIWYGにも理がある。しかし、それは専用のワープロじゃないとできない芸当であり、中途半端なWebアプリの出る幕ではない。その現状を踏まえると、WordやDocsの上にあるデータを簡単にSTGY上に以降できる機能が求められる。それが、リッチテキストのコピペ機能だ。既存の文書をコピーして、STGY上のフォームにペーストすると、内容が勝手に構造化されて、Markdownが書かれる。あとは、「Save」すれば、STGY上での公開作業が完了する。さらに「Configure publication」を選べばブログ記事としても公開できる。

画像コピペ入稿機能

STGYには画像管理機能がある。S3上に画像を置いて、それをMarkdownの画像埋め込み記法で参照することで、記事に埋め込むのだ。そのワークフローを簡単にするために、投稿フォームの右上に画像系のメニューを付けてある。S3上の自分の画像をダイアログで選んで記事内に埋め込む「既存画像埋め込みボタン」であり、もう一つは、ローカルの画像ファイルを選んでS3にアップロードすると同時に記事内にその参照を埋め込む「新規画像アップロード埋め込みボタン」である。予め画像ファイルを用意するワークフローなら、この方式で充足する。

自分でDogfoodingをしていて思ったのは、予め画像ファイルを用意するのが面倒くさいということだ。Webで見かけた画像とか、適当なアプリで表示される画像とかを、いちいちファイルに保存してから使うというのは、だるい。コピペしたい。はてなブログでもGitHubでもその他のCMS系サイトでもサポートしている機能であるから、当然STGYでもサポートすべきだ。

で、実装してみたのだが、意外に簡単だった。ペーストすると発生するonPasteイベントを拾って、データ属性のMIMEタイプが画像だったら、それをファイルとみなして、既存のファイルアップロードのルーチンを呼び出せばいい。

クリップボード内の画像はPNG形式であることが多いが、従来の画像アップロード機能が様々な形式をサポートしているので、PNG以外でも大抵動く。そして、従来の画像ファイルアップロード機能と同様に、画像サイズやバイト数が大きすぎる場合には勝手にWeb最適化されたWebP形式の画像に変換してくれる。よって、スクリーンショットなどで巨大なPNGを生成した場合も、ユーザ自身で縮小等の作業をすることなく、画像の投稿が行える。

コピペで簡単に画像を貼れると、同じ画像を何度もアップロードする可能性が高まる。そうすると、サーバ側の負荷が無駄に高まるし、ユーザのクォータも使い尽くしてしまう。そこで、既にアップロードした画像と同じものをアップロードしようとすると、自動的に既存のものを再利用するようにした。S3上の直近200件くらいのオブジェクトのハッシュ値のリストを取得して、アップロードしようとしたものと一致するものがあれば、それを再利用する、というのが率直な実装だ。しかし、わざわざS3に問い合わせると転送量とそれに応じた遅延と金がかかるし、S3互換サービスを使ってハッシュ値の仕様が変わったりする懸念がある。そこで、ブラウザのlocalStorageにアップロード済みの画像のハッシュ値とS3上のオブジェクトIDのマップを保存することにした。既存画像を上書きさせない運用にしているので、ハッシュ値とオブジェクトIDの関係は不変だ。また、重複アップロードを心配しなきゃならないのは同一ブラウザ上の操作だけだ。LRU削除方式で200件も保持すれば、実用上ほとんどの重複アップロードを回避できるだろう。

リッチテキストコピペ入稿機能

Microsoft WordやGoogle Docsでテキストをコピーすると、クリップボードにはプレーンテキストとHTMLの両方が入れられる。Webブラウザのフォームにペーストする際に呼ばれるデフォルトのイベントトリガでは、プレーンテキストの方を選択して、フォームに文字列を入力してくれる。onPasteイベントを拾って自前のトリガを呼べば、HTMLを処理することができる。あとは、HTMLをMarkdownに変換してやればよい。これができると、任意のWebページを構造化文書としてSTGY上に複製できるようにもなる。

HTMLをMarkdownに変換するのは、一筋縄ではいかない。HTMLは構造化文書とは限らないからだ。正確に言うと、HTMLは構造化文書の記述を企図して策定された仕様だが、それを使って構造的でない文書を作る慣行が一般化してしまっているからだ。例えば、Google Docsの文書に以下のように書いて、それをコピーする。

abc

def

そうすると、以下のような情報がクリップボードに入れられる。

<meta charset='utf-8'><meta charset="utf-8"><b style="font-weight:normal;" id="docs-internal-guid-0df11aa8-7fff-81cc-3cce-958591186977"><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">abc</span></p><br /><p dir="ltr" style="line-height:1.38;margin-top:0pt;margin-bottom:0pt;"><span style="font-size:11pt;font-family:Arial,sans-serif;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">def</span></p></b><br class="Apple-interchange-newline">

不要なmetaや、ぐちゃぐちゃしたスタイルを外すと、こうなる。

<b>
<p><span>abc</span></p>
<br />
<p><span>def</span></p>
</b>

なぜか、インライン要素であるb要素がルートになっていて、その中にブロック要素であるp要素が入っている。インライン要素の中にブロック要素を入れるのは、構造化うんぬん以前に、HTMLの仕様違反だ。さらに、行をpで囲み、空行をbrで示すという構造になっている。Google Docsには、段落という概念が無いということだ。構造化文書で考えるなら、段落をpで囲み、段落内の改行をbrで示すのが率直だが、行を全てpにするというのは真逆の発想だ。バーナーズ=リー卿もこれを見たら落胆するだろう。

もう少し凝った、以下のスクリーンショットの例を見てみよう。タイトル、サブタイトル、ヘッダ1から4、リスト、番号付きリスト、表、段落風に見える地の文、その中での太字等の装飾、画像埋め込み、を全て表現した。

これをコピーして作られるHTMLを整理すると、以下のような構造になる。

<meta charset="utf-8">
<b id="docs-internal-guid-b1a47853-7fff-5909-dcfa-3db62191c5fd" style="font-weight:normal;">
  <p><span style="font-size:26pt;">title</span></p>
  <p><span style="font-size:15pt;color:#666666;">subtitle</span></p>
  <h1>head1</h1>
  <h2>head2</h2>
  <h3>head3</h3>
  <h4>head4</h4>
  <ul>
    <li>list item 1</li>
    <ul>
      <li>nested</li>
    </ul>
    <li>list item 2</li>
    <li>list item 3</li>
  </ul>
  <ol>
    <li>num list item 1</li>
    <ol>
      <li>nested</li>
    </ol>
    <li>num list item 2</li>
    <li>num list item 3</li>
  </ol>
  <br>
  <p>plain <span>hello</span> <span><span>world</span></span></p>
  <p>
    <b>bold</b>
    <i>italic</i>
    <span style="text-decoration:underline;">underline</span>
    <span style="text-decoration:line-through;">strike</span>
  </p>
  <br>
  <p>plain</p>
  <p>
    <span style="color:#ff0000;">red</span>
    <span style="color:#0000ff;">blue</span>
    <span style="background-color:#ffff33;">highlight</span>
    <span style="font-size:11pt;">big</span>
    <span style="font-size:8pt">small</span>
    <a href="https://example.com/">link</a>
  </p>
  <table>
    <tbody>
      <tr>
        <td>r1c1</td>
        <td>r1c2</td>
      </tr>
      <tr>
        <td>r2c1</td>
        <td>r2c2</td>
      </tr>
    </tbody>
  </table>
  <p>
    <span><img src="data:image/png;base64,...."></span>
  </p>
</b>

ここから学べることは、いくつかある。

  • ルートのbはfont-weight:normalでスタイルが打ち消されていて、本当に意味がないし、HTML仕様違反である。おそらくコンテナとして使われているのだが、なぜdivじゃないのかは謎だ。
  • 「タイトル」「サブタイトル」のスタイルは単なるpの下のspanにfont-sizeのスタイルがついたものである。
  • ヘッダ1から4までは、ちゃんとh1からh4の構造化要素が割り当てられている。
  • リストと番号付きリストはulやolを使っているが、ネストされたulやolが直前のliの中ではなく親のulやolの直下にあり、HTML仕様違反である。
  • 表はtable要素でちゃんと構造化されている。
  • 段落の概念はないので、pの外側のbr要素を段落区切りとみなすしかない。
  • 装飾で、太字と斜体はb要素とi要素を使うが、それ以外はspanとスタイルで表現している。
  • 埋め込み画像は、img要素で表現され、画像データはdata書式のURLの中にBase64として埋め込まれている。

以上を踏まえて、具体的な設計に入る。まずは、ちゃんと構造化されたHTMLをASTに変換できるようにするのが基本となる。

  • bodyがある場合、最初に見つかったbodyの中身のみを対象とする。bodyがない場合、全体を対象とする。
  • head, meta, script, link、canvasなど、HTML標準で非表示の要素は、無視する。
  • h1からh6までをMarkdownのヘッダに割り当てる。
  • pを地の文にする。
  • b、i、s、uなどのインライン装飾要素はMarkdownのインライン装飾として拾う。
  • span要素でfont-weight:boldなどの装飾も、ベストエフォートで、Markdownのインライン装飾として拾う。
  • ruby要素はMarkdownのインライン装飾として拾う。
  • ul要素とol要素はMarkdownのリストとして拾う。
  • table要素はMarkdownのテーブルとして拾う。
  • img要素のsrc属性にdata URL形式で画像が埋め込まれている場合、それを既存の画像アップロード機能で処理する。
  • 上記のルールに適合しない要素は、中身のテキストを全て地の文として拾う。

WordやDocs等から来るであろう、非構造化文書の構造化を行うにあたって、HTMLのDOMレベルで前処理を行う。前処理なのが重要で、そうするとASTの実装を汚さずに済む。

  • ステージ1: ブロック要素を含むインライン要素は、中身を取り出してから消滅させる。
    • 入力DOM:<b><p>123<p><br><p>456</p><br><p>789</p><b>
    • 出力DOM:<p>123<p><br><p>456</p><br><p>789</p>
  • ステージ2: トップレベルで、文書の冒頭付近(5番目まで)にp要素であり子の唯一のspanがfont-size:20pt以上であるものがある場合、それをh1要素として扱う。それが発動した場合、元々のh1要素があれば、全てh2要素に格下げする。元々のh2要素以下も同様。h6要素はそのまま。発動しなかった場合、全てがそのまま。
    • 入力DOM:<p><span style="fint-size:20px">title</span></p><h1>BODY-H1</h1><p>PPP</p><h2>BODY-H2></h2>
    • 出力DOM:<h1><span style="fint-size:20px">title</span></h1><h2>BODY-H1</h2><p>PPP</p><h3>BODY-H2></h3>
  • ステージ3: ある階層で、ブロック要素とbr要素が並列の兄弟であった場合、隣接するp要素を単一のp要素にまとめ、元来のpの内容をbr要素で区切る。元来のbrは消滅させる。
    • 入力DOM:<p>123<p><p>456</p><br><p>789</p><p>abc</p><br><p>def</p><p>ghi</p>
    • 出力DOM:<p>123<br>456</p><p>789<br>abc</p><p>def<br>ghi</p>
  • ステージ4:ulやolの子供にulやolを発見した場合、子のulやolの直前にliがあれば、その子供として付け替える。
    • 入力DOM:<ul><li>1</li><ul><li>1-1</li></ul><li>2</li></ul>
    • 出力DOM:<ul><li>1<ul><li>1-1</li></ul></li><li>2</li></ul>

ASTをMarkdownに書き出す実装も必要だ。今まで必要がなかったので用意していなかったが、今回書き下ろした。結果として、以下のようなMarkdownが得られる。

# title

subtitle

## head1
### head2
#### head3
##### head4

- list item 1
  - nested
- list item 2
- list item 3

-+ list item 1
  -+ nested
-+ list item 2
-+ list item 3

plain hello world
**bold** ::italic:: __underline__ ~~strike~~

plain
red blue @@highlight@@ big %%small%% [link](https://example.com)

|r1c1|r1c2|
|r2c1|r2c2|

![](/images/0001000000000011/masters/797488/8f83b5d618a7bf7a.webp)

実際の処理結果は以下のようになる。ちゃんとMarkdownに変換されていて、それを再びHTMLに直した際の見た目は元文書のものとだいたい同じになる。

デモサイトで利用できるようにしているので、ユーザ登録(無料)の上、任意のWebページやらGoogle DocsやらMicrosoft Wordやらからコピペして遊んでみて欲しい。既存のワープロ文書が、ゴミHTMLの塊であるにも関わらず、コピペするだけで面白いように簡単に構造化文書になる。もちろんヘッダ等をちゃんと配して構造を作っていればの話だが。Docsの例はこちら:例1例2

そして、投稿した後に、投稿につけられたメニューの「configure external publication」でチェックを入れれば、ブログ記事として公開ができる。

実用上不可欠な工夫がある。ブラウザ上でWebページ上のテキストをコピーすると、太字などのスタイルが付いているかどうかに関わらず、常にリッチテキストがクリップボードに入れられる。それをペーストすると、意図せずMarkdownの装飾記法が入る可能性がある。例えば、ヘッダの文字列だけをコピペする場合、それを記事内でヘッダにしたいとは限らないのに、ヘッダになってしまう。スタイルがない地の文でも、全体をMarkdownとして再解釈してペーストすると、それは段落になるので、改行付きの文字列が挿入されてしまう。これは、地味に鬱陶しい。そこで、2つのルールを導入した。第一に、コピーされたHTMLのDOMに要素が一つしかない場合、それはリッチテキストとしては解釈せず、プレーンテキストとして処理する。第二に、コピーされたHTMLのDOMにブロック要素が含まれない場合、それはインライン要素なので、改行を削って挿入する。これらによって、多くの場合で、意図しない装飾記法の混入は避けられる。それでも意図せず装飾記法が発動した場合には、Shiftを押しながらペーストしてもらうことになる。

HTMLとASTの間およびASTとMarkdownの間の相互変換は、packages/markdown というサブパッケージとして公開している。HTMLのDOMをASTに変換する際に、Markdownの構造化文書で表現できる積集合的な構造に読み替えるのが肝だ。また、前処理として、WYSIWYG経由の非構造化HTMLをヒューリスティックで構造化HTMLに修正するユーティリティも入れてある。このパッケージを使えば、STGY以外の任意のプラットフォームで似たようなワークフローが構築できるはずだ。このパッケージを公開していることで、データのインターオペラビリティがより強固になる。Markdownである時点で、HTMLや独自XMLよりはだいぶマシなのだが、ASTを介してJSONや任意のフォーマットに変換できると工数が大幅に削減できる。

余談だが、素のHTMLでもGoogle DocsでもGitHubのissueでも、文書内の最上位ヘッダをh1で始めるべきか、h2で始めるべきか、悩ましいことがある。例えばブログの場合、ブログ名や連載名がh1の位置づけなのだから、それぞれの記事のヘッダはh2から始めるという人がいる一方で、各記事は独立した文書なのだからh1から始めるべきという人もいる。Google Docsでは、タイトル装飾の要素がh1的な位置づけ(実際はpだが)なので、文書内のヘッダはh2から始めるべきと考える人がいる一方で、やはりh1から厳格に振る人もいる。私は断然h1派だ。今回のコピペ機能では、両者を満足させる方策を取っている。タイトル装飾の要素がh1に昇格した場合、既存のヘッダでh1がある場合にのみ、ヘッダで降格処理がなされる。つまり、ヘッダをh1で始めていても、h2で始めていても、文書内のトップはh1になり、h2、h3と続くことになる。タイトル装飾がない場合でヘッダをh2で始めている文書は、そのままh2がトップのMarkdownが生成されるが。文書の一部分をコピペする可能性があるので、h2がトップだからってh1に自動昇格させることはしない。なお、h2がトップの文書が外部公開される場合には、ちゃんと最初のh2を文書の題名として拾うようになっている。

Google SpreadsheetやMicrosoft Excelからのコピペも、HTMLからのコピペであることは変わらないので、問題なく動く。colspan、rowspan、右寄せなどのスタイル情報もベストエフォートで移植してくれる。提案書、報告書、論文など、表計算の結果をコピペしたくなることは多いので、この機能かなり重宝するだろう。


コピペによるフィードバック

ワープロ側からHTMLでコピペ入稿できるようにして、STGY上で編集もできるようにした。しかし、STGYはCMSではない。STGY上にあるデータは原本ではなく、公開用の複製という位置づけだ。複製に対する編集は原本にフィードバックしたくなるだろう。原本側で更新作業を二重に行うというのが最も原始的な対応になるが、そのような屈辱的な作業でクリエータの貴重時間と精神力を使いたくはない。記事全体をコピペして入稿したのであれば、STGY上の記事全体をコピーしてワープロ側にペーストできるようにしたい。その際に文書構造や装飾が維持されるのであれば、コピペ一撃で同期作業が完了する。

STGYの内部UIでの投稿詳細画面や外部公開の画面では投稿全文がHTMLとしてレンダリングされるので、それをコピペすれば、文書構造と装飾がそのままワープロ側に移される。しかし、内部UIや外部公開で使われているスタイルシートのスタイルまでコピーされてしまうのがうざい。そこで、スタイルを排除した素のHTMLだけをクリップボードに入れる機能を用意した。投稿の表示欄にあるコピーアイコンのメニューで「Copy content HTML」を選ぶと、それが成される。HTMLの文字列を単にクリップボードに入れるのではなく、リッチテキストとして入れるのが重要だ。ついでに、「Copy content Markdown」「Copy content plaintext」も付けた。

記事全体をコピーしたいとは限らない。そこで、「View content HTML」という操作も付けている。これを選ぶと、素のHTMLが描写されたウィンドウが開かれる。その中で必要な部分をコピーすれば良い。ワープロに取り込んで提出論文用に二段組のレイアウトにするような作業も、素のHTMLから始めた方がやりやすい。

スタイルなしのHTMLをペーストすると、当然ワープロ側の素のスタイルが適用された状態になる。H1は大見出し風に、H2は中見出し風に、H3は小見出し風になる。b要素は太字に、i要素は斜体に、u要素は下線付きになる。作家やジャーナリストが原稿としてワープロを使っている場合、敢えてスタイルを変えることに意味を見出していないだろうから、素のスタイルで十分なことが多いだろう。スタイルを変えている場合にも、既存の文書の下に追加でコピペしてから、既存の文書部分の見出し等のコンテキストメニューで「見出し1をカーソル位置のスタイルに更新」とかいった機能を使うと一括してスタイルを整えることができる。

コピペしたリッチテキスト内の画像を埋め込んだ記事を作ると、コピペの度に画像がアップロードされてしまう。しかし、画像コピペ機能を実装する際に重複画像再利用機能を入れてあるので、その問題が解決される。さらに、一つの記事に埋め込める画像数を20個に制限することで、更新処理の負荷と表示時の負荷に制御している。具体的な制限値は運用しながら調整することになるだろうが、SNSである以上、無制限というわけにはいかない。

はてな、Note、Zennとの比較

ここで、先駆者たちの入稿UIを比較してみよう。まずは、本ブログでもおなじみの、はてなブログから。WYSIWYG方式である「見たままモード」と、マークアップ方式である「はてな記法モード」「Markdownモード」の3種類ある親切設計だ。

私が慣れているというのもあるが、普通に使いやすい。WYSIWYG派もマークアップ派も満足させるというのは、汎用ブログエンジンとして妥当な選択だろう。見たままモードはHTMLをデータとして保持するらしく、HTMLタグを直接編集することができる。HTML直書き世代の設計を引き継いでいる感がある。既存データのモードを切り替えることができないのは、原本をモード別に持つ方式だからだろう。モード毎の表現力が違うので、ASTを介しても完全な相互変換は不可能だ。モード別にデータを持つとDB側の実装が複雑化するが、不完全な相互変換を提供してメンテし続けるのは地獄なので、これまた妥当な設計だと思う。

はてなブログの履歴管理機能は、かなり強力だ。保存した全てのバージョンを保存していて、任意のバージョンに戻って編集を開始することができる。バージョン間のdiffの表示もできる。画像の管理も記事とは個別にできる。承認ワークフロー管理などはないが、個人のブログ用のCMSとしては十分な機能を持っている。

次に、Noteを見てみよう。こちらはWYSIWYG方式一択だ。はてなとは対象的に、機能と表現の幅を限定する代わりに、「シンプルでファンシーでモダン」なブログを書きやすくなっている。

実際に使ってみるとわかるが、「+」ボタンで次のブロックのデータ型を指定して追加するという操作が、絶望的に使いづらい。ペーストすると単一ブロックに全てのテキストが入ってしまうので、予め用意した構造化データを一撃で流し込むこともできない。FacebookのLexicalエディタも同じ方式だが、長文を書くには明らかに不便なUIだ。当然、設計者もそれは分かっていて、多くのユーザは構造なんて気にしないと割り切っているのだろう。実際、このUIで多くのシェアを取っているのだから、それは正しいのだ。

Noteの履歴管理機能は、限定的ながらも、存在する。公開時点でのバージョンと、最新のバージョンの2つだけが保存されている。バージョン管理の発想があまりないユーザならば、最新バージョンの他は公開時点でのバージョンだけがあれば十分と踏んでいるのだろう。

次に、Zennを見てみよう。Markdownでありつつ、WYSIWYG風にヘッダや太字が編集フォーム内でも装飾されるという折衷的UIだ。

Zennは明確にソフトウェアエンジニアを主な対象にしているサービスなので、Markdownを採用しているのは自然なことだろう。Markdownを知っている人には普通に使いやすい。それでいて、Note風味のファンシーさが表現できているので、絶妙なバランスが取れている。ただし、プレビュー以外の入力支援機能が全く無いので、Markdownを知らないと構造も修飾も全く表現できないことになる。ボタンを増やしてUIをごちゃごちゃさせたくないということだろう。「Markdown程度も使いこなせない輩はむしろ記事を書かなくてよろしい」というくらいの潔さがある。

Zennの履歴管理機能は、自身にはない。その代わり、GitHubの任意のリポジトリと連携できるようになっていて、該当リポジトリ内の文書が自動的にZenn上にデプロイされるようになっている。これまた、エンジニア向けと割り切った潔い選択だ。

最後に、STGYを見てみよう。Markdownでありつつプレーンテキストとしても書け、Twitter風にタイムラインの上で短文を書くのが基本のUIになっている。それでいて、プレビューボタンを押すとサイドバイサイド方式のMarkdownエディタで本格的な編集作業ができるという二階建ての構成だ。

STGYは、Twitterの皮を被ったEmacsのようなUIである。Twitterとして使う分には学習コストが低いが、真面目に構造化文書を書こうと思うと一定の学習コストがかかり、しかしそれを払うなら高い生産性が得られる。一般ユーザとクリエータの双方に最適化したつもりだが、両者の間のギャップをいかに乗り越えてもらうかが課題となる。今回実装したコピペ機能はその課題への対策だ。履歴管理機能が皆無なのはZennと同じだが、GitHub連携ツールは比較的簡単に作れるようになっているし、ワープロ連携もできるようになったので、インターオペラビリティは遜色ないはずだ。

カジュアルユーザ向けに最適化したNoteと、エンジニア向けながらとっつきやすくしたZenn。それらの設計は対象ユーザ層を明確に定めた結果として定められていて、対象ユーザが想定ユースケースに沿って動く範囲では学習コストが低い。UIも当世風で、レンダリング結果も洒落ている。一方で、老舗であるはてなブログは、いかにも建て増しした結果という感じの機能とUIで、学習コストは高めで、UIに平成の香りがする。高機能で便利なのは明らかにはてなだが、スイスアーミーナイフを使いこなせる人間は多くない。私の精神構造もはてなの開発陣に近いものがあるのか、STGYも高機能寄りの建付けになっている。ただし、STGY本体は「Markdown文書を持ち寄って共有する場所」としての機能に特化し、それ以外の機能は外部システム連携で済ませるという、UNIX哲学(Do One Thing and Do It Well)を実践しているつもりではある。あとは、「Markdown」「リッチテキスト」「外部システム連携」みたいな言葉や概念をユーザが意識しなくてもワークフローを提供できるかというUXの命題を追求するだけだ。

改めて考えてみると、「リッチテキストをコピペして構造化文書として入稿する」という機能は尋常ではない。使ってみれば非常に効率的なのだが、やってみようという気がまず起きないだろう。そのようなワークフローを持っている人は居ないだろうから、ヘルプ等で利便性を伝えるのが難しい。動画とか漫画とかでワークフローを分かりやすく提案する資料を容易した方がよさそうだ。

Google Fontsの導入

コピペ機能の拡充で外部公開の需要が増えると仮定すると、見た目の制御の重要性が増してくる。文書に重要なのは情報であり構造なので、クリエータにはそれらの向上に注力してほしい。逆説的に、サービス側で、デフォルトで読みやすい見た目になるように制御する必要が出てくる。デザインテーマで色使いはかなり柔軟に制御できるようになったが、フォントについてはまだ手を付けていなかったので、ここらで真面目に考えてみる。Steve JobsMacintoshを開発した背景に、彼が学生時代に学んだタイポグラフィの美しさを再現できる環境を作りたかったというのがあるらしい。美しいフォントで文章が読めるということは、人によっては、それだけ大事なことなのだ。

従来、STGYでは、フォントは主にサンセリフ体(装飾控えめ書体)を使い、font-family: sans-serifとだけ指定していた。pre要素とcode要素は等幅フォントである必要があるので、それはfont-family: monospaceとだけ指定していた。そうすると、ユーザがブラウザに指定しているフォントがそれぞれに利用される。サンセリフ体に関しては、Macではヒラギノ角ゴシックが使われ、Windowsでは游ゴシックUIが使われるのがデフォルトだ。この方法だと、ユーザが任意のフォントに変えることができる柔軟性がある反面、サービス側としては文字幅が予測しづらいのでデザイン崩れのリスクがあり、著者としてはフォントが制御できないというもどかしさがある。Webの大原則としてUA側で表示スタイルを自由に上書きできるので、情報提供側がフォントを完全に制御するというのは現実的ではないが、デフォルトが固定されていないという状況には問題が多い。

そこで、Google Fontsを導入して、フォントを固定することにした。link要素でGoogleのCDAからフォントをダウンロードするように仕込めば、ユーザがローカルにインストールしているフォントに依存せずに、こちらで指定したフォントを使わせることができる。私はMacヒラギノ角ゴシックに慣れていて、それが読みやすいと思っている。Google Fontsでは、それに近いもので人気のあるものをいくつか比較してみよう。

ヒラギノ角ゴシック:

Noto Sans JP:

IBM Plex Sans JP:

別タブで開いて切り替えてみると違いが分かりやすい。Noto Sans JPは、太くて角ばっていて玄人的で賢そうな印象がある。IBM Plex Sans JPは、丸っこくて素人的で優しい印象がある。ヒラギノ角ゴはその中間的でバランスが良い。ヒラギノ角ゴがGoogle Fontsにあればいいのにと思うが、無いので、Noto Sans JPかIBM Plex Sans JPのどちらかを選ぶ必要がある。結論としては、IBM Plex Sans JPの方を選んだ。長文を読んでいて楽に感じたからだ。

追記:他の人気があるゴシックフォントも比較してみた。Zen Kaku Gothicは、全体的に読みやすい。Sawarabi GothicとM Plug Gothicは、平仮名は読みやすいが、漢字が読みづらい。Kosugiは、看板っぽくて視認性は高いが、なぜか長文だと読みづらい。

Zen Kaku Gothic:

Sawarabi Gothic:

M Plus Gothic:

Kosugi:

Zen Kaku GothicはIBM Plex Sans JPと同じくらい良いので迷うところだ。IBM Plex Sans JPはタイプライター活字が出発点で、Zen Kaku Gothicは手書き文字が出発点らしい。それでいて、両者とも現代的な(ヒラギノ角ゴ的な)方向に収斂進化しているから、どちらも読みやすい。視認性が高いのはタイプライターの方で、美しいのは手書きの方だから、どちらが良いとは一概に言えない。しかし、長文を疲れずに読むということを目標にするならば、視認性が高い方を選ぶべきだろう。

等幅フォントに関しては、MacではデフォルトでMenlo、WindowsではデフォルトでConsolasが使われる。Google Fontsにある等幅フォントで人気があるのはSource Code ProとInconsolataだ。ここでは、Menlo、Source Code Pro、Inconsolataを比較する。

Menlo:

Source Code Pro:

Inconsolata:

これまた、別タブで開いて切り替えてみると違いが分かりやすい。Source Code Proは、文字幅が広くなって、視認性がとても良い。Inconsolataは、文字幅が狭くて、狭い領域に多くの文字が入る。Menloはその中間だ。私は普段のプログラミングではSource Code Proを使っているのだが、ここでは敢えてInconsolataを採用することにした。SNSやブログに乗せるコードは、じっくり読むというよりはチラ見するだけなので、画面内にたくさん表示できる方が得だ。

デザインテーマのwhiteboardとblackboardでは、手書き風のフォントであるKlee Oneを採用した。Google Fontsにある手書き風フォントの中ではこれが最も読みやすい。会社で会議をしたり大学で授業を受けたりしている雰囲気がありつつも、読みやすさが落ちていない、絶妙なデザインテーマになった。

なお、Klee Oneは太さが400(regular)と600(semi bold)しかなく、しかも600も見た目の太さがほとんど変わらないので、CSSで無理やり太くするハックを入れた。たとえ太くなくても文字色で区別できるようにしてはいるが、デザインテーマによって太かったりそうでなかったりするのは一貫性に欠けるし、色覚異常でも識別できるようにしたいので、ここで対応しておく。太さの変化が無かったり少なかったりするフォントは結構多いので、これを知っておくと重宝する。

.pub-theme-whiteboard .markdown-body strong {
  color: #e10;
  text-shadow: 0 0.01em 0 currentColor, 0 -0.01em 0 currentColor, 0.01em 0 0 currentColor, -0.01em 0 0 currentColor;
}

JP系のフォントは日本語で使う文字にしか対応していない。対応外の文字は別のフォントにフォールバックされる。また、結構な割合の漢字が日本語と中国語で形が違う。さらに、日本語のフォントでは、ギリシャ文字キリル文字を記号として全角で扱うものが多くあり、それらはギリシャ語やロシア語の文章には不適だ。よって、ユーザ毎や記事毎の言語によってフォントを出し分けることが望ましい。これを予期してユーザ毎と投稿毎にロケールが設定できるようにしてあり、それはユーザプロファイルや投稿記事のarticle要素のlang属性として反映される。ゆえに、CSSセレクタでlang属性に紐づけてフォントを出し分けることも容易だ。世界展開した暁には、それぞれの言語の設定を書き足していくことになるだろう。

まとめ

画像をコピペしてMarkdown記事に埋め込む機能と、リッチテキストをコピペしてMarkdown記事を作る機能を実装した。WYSIWYG方式であるワープロソフトが吐き出すリッチテキストは明示的な構造がなく、HTMLとしても破綻しているが、ヒューリスティックなルールで構造を見出すことで、構造化文書の自動生成が実現した。

リエータ達が持っている良質の文書を100秒以内にSNSおよびネット全体に配信できるプラットフォームを目指すと前回書いたが、今回の実装でそれが実現できたと言えそうだ。Markdown中間言語(lingua franca)として知識を集める知識ベースが実現できたとも言える。これによって人間にとってもAIにとってもアクセシビリティが最大化される。その上でどのようなコミュニティを作っていくかは、また別の課題ではあるが。