External Post

posted: 2019/03/05

Gatsbyでgatsby-remark-componentを使いつつrehypeのASTをいじくってnl2br的な事をする

GatsbyでMarkdownを記述すると、普通に改行しても反映されない。Markdownの仕様としてはそれで正しいのだが、気を使いたくないブログだとちょっと面倒さがある。

そんな時にgatsby-remark-componentを見つけた。

まず一般的なGatsbyのMarkdown利用の場合、gatsby-transformer-remarkを通して生のHTMLとして記事が取得出来る。そのHTMLを下記のようにdangerouslySetInnerHTMLで表示する事になる。

<div dangerouslySetInnerHTML={{ __html: post.html }} />

dangerouslySetInnerHTMLを使うのはまあまあ気持ち悪いのもあるし、拡張性も高くない。
そこでgatsby-remark-componentの出番になる。

使い方としては他のプラグインと同じでconfigに追記する

plugins: [
  {
    resolve: "gatsby-transformer-remark",
    options: {
      plugins: ["gatsby-remark-component"]
    }
  }
]

ここからちょっと特殊だ。
このpluginはあくまでrehypeのASTをhtmlAstとして返してくれるだけので、そこからは自分で変換する必要がある。
また取得のためにGraphQL側も変更が必要だ

import rehypeReact from "rehype-react"

// 変換用の関数。
const renderAst = new rehypeReact({
  createElement: React.createElement,
}).Compiler

const BlogPost = ({ data }) => {
  const { markdownRemark: post } = data
  const content = renderHtmlAST(post.htmlAst)
  // return <Blog><div dangerouslySetInnerHTML={{ __html: post.html }} /></Blog>
  return <Blog>
    {content}
  </Blog>
}

export const pageQuery = graphql`
  query BlogPostByID($id: String!) {
    markdownRemark(id: { eq: $id }) {
      // ...
      id
      html
      htmlAst // GraphQLも追加が必要
      // ...
    }
  }
`

nl2brの変換をする

ASTが返ってくればあとは\nを見つけて変換してやればいい。
今回はunist-util-visitを利用する形にした。

import highlight from "rehype-highlight"
import unified from "unified"

const nl2br = () => {
  const transformer = (tree) => {
    visit(tree, (node, index, parent) => {
      if (node.type !== "text") return node
      if (parent.tagName !== "p") return node
      const values = node.value.trim().split("\n")
      if (values.length < 1) {
        return
      }
      const children = values
        .map((v, i) => {
          return i == 0
            ? [{ type: "text", value: v }]
            : [{ type: "element", tagName: "br" }, { type: "text", value: v }]
        })
        .flat()
      // .reduce((a, b) => [...a, ...b], []) // TODO: Array.prototype.flat

      const newChildren = [
        ...parent.children.slice(0, index),
        ...children,
        ...parent.children.slice(index + 1)
      ]
      parent.children = newChildren
    })
    return tree
  }
  return transformer
}

const renderAst = new rehypeReact({
  createElement: React.createElement
}).Compiler

export const renderHtmlAST = htmlAst => {
  // せっかくなのでunifiedに習ったやり方にした
  const tree = unified()
    .use(highlight)
    .use(nl2br)
    .runSync(htmlAst)
  // console.log(tree)
  return renderAst(tree)
}

Arrayのspreadでの合成はReduxの公式ドキュメントを参考にした。

この記事の修正をする