10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

アスキーアート総合Advent Calendar 2019

Day 5

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

Last updated at Posted at 2020-01-12

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

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

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

やりたいこと

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

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

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

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

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

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

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を使っている。blackfridayでMarkdownをHTMLに変換した後に、bluemondayでsanitizeをかける。bluemondayにAAタグを許可する必要がある。

var sanitizer *bluemonday.Policy

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

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

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

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


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フォントのまとめを提供してくれているので参考にした。めっちゃ助かる。ありがとうございます!

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

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-sizeline-heightを指定する必要があるがスマホだと大きすぎるのでとりあえず消した。それでもなんとなく大丈夫だった。

結果

Screen Shot 2020-01-12 at 22.34.31.png

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

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

  1. https://qiita.com/scrpgil/items/b8bde1257a135d173585

10
4
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?