A Day in the Life

はてな関連の HTML で書かれた記事を markdown にする

このサイトに持ってきたはてな関係の記事、すなわち古くははてなダイアリーから、はてなグループ、そしてはてなブログの記事は MovableType 形式で export したものを使っていて、本文は HTML で出力されている。

いつかは markdown にしたいなぁ、と思っていたのだけど、とりわけ困ってるわけでもなかったのでなかなか重い腰が上がらなかった。のだけど、サイトの構成をもうちょっといろいろしたくなってきて、そのためにも markdown のほうが融通がきくので変換した。

MovableType 形式のファイルの読み込みには mt-parser を使って簡単にできるのだけど、はてなの HTML から markdown が、古の時代(ダイアリーやグループ)の HTML が含まれるため、少し時間がかかってしまった。

具体的にはturndownを使って markdown に変換した。turndown はよく出来ていて、無視したいノード、お好きに変換したいノード、何もしないノード、などを自分の好きな感じにできるし、また TypeScript の型ファイルがあったのでそれも相成って書きやすかった。

ちょっとハマったのが、TurndownService.keep() は一つしか関数を登録できず、keep を複数回書いてしまって関数が上書きされてしまい意図した動作にならず、解決まで時間を食ってしまった。

実際はこんな感じのコードで、意図した感じの markdown に変換ができた。これで統一フォーマットでアレコレできるので、進捗を上げたい。

import TurndownService from "turndown"

const turndownService = new TurndownService({
  bulletListMarker: "-",
  headingStyle: "atx",
  hr: "---",
})

// はてなのキーワードリンクの削除
turndownService.addRule("remove hatena keywords", {
  filter: (node, options) => {
    return (
      node.nodeName === "A" &&
      (node.getAttribute("href")?.includes("/keyword/") ||
        node.getAttribute("href")?.includes(":keyword:"))
    )
  },
  replacement: (content, node, options) => {
    return content
  },
})

turndownService.addRule("pre to ```", {
  filter: (node, options) => {
    return node.nodeName === "PRE"
  },
  replacement: (content, node, options) => {
    return "\n```\n" + node.textContent.trim() + "\n```\n"
  },
})

// HTML のまま残す要素
turndownService.keep((node, options) => {
  if (
    node.nodeName === "IFRAME" &&
    !node.getAttribute("src")?.includes("facebook.com/")
  ) {
    // facebook の iframe 以外は残す
    return true
  }
  // 特定の div の class があるものは残す
  return node.nodeName === "DIV" && node.className?.includes("photos-hatena")
})