MarkdownでのAA入力を対応してみた 【ぼくのがんがえたさいきょうの掲示板】

個人で作っている掲示板サイトがある。最近日本語対応を実装しているが、途中であることに気づいてしまった。

>「せっかく和訳したところでAA貼れないなら意味ない！」

AAは日本語の掲示板の不可欠な文化である。しかし現代のパソコンやスマホではAAが正しく表示されない場合が多い。これはどうにか解決したい。

# やりたいこと

私の掲示板はMarkdown記法で書き込む形だ。AAをそのまま入力すると、太字として認識されたり自動的に改行が入ったりする。2chの専ブラみたいにいい感じにAAを表示したい。

```markdown
<aa>(´・ω・｀)</aa>
```

自動的にAAを認識するのは大変そうなので、AAタグを用意する。AAタグの中身をAA用のフォントに変えて、Markdown記法の`*bold*`などを無視する。

# 無理矢理AAタグを対応する

利用しているMarkdownライブラリは[russross/blackfriday](https://github.com/russross/blackfriday)だ。諸々があって今だにv1を使ってる。
このライブラリはHTMLの入力を対応しているが、中身をMarkdown記法としてパースしてしまう。

まず、フォークしよう。そして、`markdown.go`に`blockTags`というマップを見てみる。

```go:markdown.go
// blockTags is a set of tags that are recognized as HTML block tags.
// Any of these can be included in markdown text without special escaping.
var blockTags = map[string]struct{}{
	"blockquote": {},
	"del":        {},
	"div":        {},
	// ...
	"aa":         {}, // ← 追加した
}
```
`blockTags`に入っているタグはブロック（パラグラフ）になり、エスケープされないとコメントが丁寧に教えてくれた。よし、`"aa": {},`を追加する。これで`<aa>`の中身はMarkdownにならずそのまま出力される。

XSS対策として[microcosm-cc/bluemonday](https://github.com/microcosm-cc/bluemonday)を使っている。blackfridayでMarkdownをHTMLに変換した後に、bluemondayでsanitizeをかける。bluemondayにAAタグを許可する必要がある。

```go
var sanitizer *bluemonday.Policy

func init() {
	sanitizer = bluemonday.UGCPolicy() // よくある書式設定用のタグなどを許可
	sanitizer.AllowNoAttrs().OnElements("aa") // 属性なしの<aa>を許可
}
```

こうしないと`<aa>`が丸ごと消されてしまう。

しかし非公式のHTMLタグをそのまま出力するのは気持ち悪いので、`<aa>`を`<p class="aa">`に変えよう。[PuerkitoBio/goquery](https://github.com/PuerkitoBio/goquery)はHTMLをいじるのに便利だ。

上記の処理を組み合わせるとこうなる。

```go

func renderMarkdown(content string) template.HTML {
	// Markdownをrender
	renderer := blackfriday.HtmlRendererWithParameters(commonHtmlFlags, "", "", blackfriday.HtmlRendererParameters{
		// ...
	})
	md := blackfriday.MarkdownOptions([]byte(content), renderer, blackfriday.Options{
		// ...
	})
	// XSS対策としてsanitize
	sanitized := sanitizer.SanitizeBytes(md)

	// goqueryを使ってHTMLをいじる
	doc, err := goquery.NewDocumentFromReader(bytes.NewReader(sanitized))
	if err != nil {
		panic(err)
	}
	// <aa>を<p class="aa">に変換
	doc.Find("aa").Each(func(_ int, sel *goquery.Selection) {
		sel.SetAttr("class", "aa")
		sel.Nodes[0].Data = "p"
	})

	// <html><body>が勝手に追加されるので省略
	html, err := doc.Find("body").Html()
	if err != nil {
		panic(err)
	}
	return template.HTML(html)
}
```
※ 本当はpanicせずにerrorを返すべき

たぶん、こういう処理はmarkdownのライブラリの中でやるべきだが、blackfridayのコードはヤバイのでこっちの方が楽である。実際のコードでは`<aa>`の対応だけじゃなくてYouTubeのembedなどは似た手段で対応している。

# AAをいい感じに表示する

@scrpgilさんは素敵な[AAフォントのまとめ](https://fonts.aahub.org/)を提供してくれているので参考にした。めっちゃ助かる。ありがとうございます！

aahub_light[^1]というフォントは軽くて綺麗なので使うことにした。woffファイルをS3にアップてして、キャッシュするように`Cache-Control`ヘッダを`public, max-age=31536000, immutable`にした。AA用のCSSも作った。これも永遠にキャッシュかけたいのでメインのCSSと分けてCache-Controlを指定した。

```css:aa.css
@font-face {
  font-family: "aahub_light";
  src: url("[CDNのURL]/aahub_light.woff") format("woff");
  font-display: swap;
}

p.aa {
  font-family: "aahub_light";
  white-space: pre;
  overflow: scroll;
  word-break: keep-all;
  overflow-wrap: normal;
  /*font-size: 16px;*/
  /*line-height: 18px;*/
}
```

スマホなどで大きいAAがoverflowしがちなので`overflow: scroll`にした。もっとも正しい表示の仕方は`font-size`と`line-height`を指定する必要があるがスマホだと大きすぎるのでとりあえず消した。それでもなんとなく大丈夫だった。

# 結果
<img width="326" alt="Screen Shot 2020-01-12 at 22.34.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52098/43f44cf8-fad7-7c6a-d0c1-dd6711ec54db.png">

スマホでもAAはいい感じに表示される。やったぜ！

「ぼくのかんがえたさいきょうの掲示板」をシリーズとしてまた記事を書きたいと思うので次もよろしくお願いします！



[^1]: https://qiita.com/scrpgil/items/b8bde1257a135d173585