技術をかじる猫

適当に気になった技術や言語、思ったこと考えた事など。

Scala で markdown パースするだけではなく、出力を少しいじる

前回 knockoff なるライブラリを紹介したのですが、ちょこっと内容を変更してみます。 knockoff でパースした型というのは

  def knockoff( source : java.lang.CharSequence ) : Seq[Block] = {

なんて定義になっており、要は Block と呼ばれる解析データのシーケンスであると。 であれば、その型が分かれば必然的に出力結果を弄れるわけで、ソースを追いかける。

trait Block { def position : Position }

case class Paragraph( spans : Seq[Span], position : Position ) extends Block

case class Header( level : Int, spans : Seq[Span], position : Position )
extends Block

case class LinkDefinition( id : String, url : String, title : Option[String],
                           position : Position )
extends Block

case class Blockquote( children : Seq[Block], position : Position )
extends Block

case class CodeBlock( text : Text, position : Position ) extends Block

case class HorizontalRule( position : Position ) extends Block

case class OrderedItem( children : Seq[Block], position : Position )
extends Block

case class UnorderedItem( children : Seq[Block], position : Position )
extends Block

case class HTMLBlock( html: String, position: Position )
extends Block

case class OrderedList( items : Seq[OrderedItem] ) extends Block {
  lazy val position = if ( items.isEmpty ) NoPosition else items.head.position
}

case class UnorderedList( items : Seq[UnorderedItem] ) extends Block {
  lazy val position = if ( items.isEmpty ) NoPosition else items.head.position
}

おぅふ、そのままですね。 ということで、Header を事前に全て引っこ抜いて、索引を作りたいと思ったら

import java.io.PrintWriter
import scala.io.{Codec, Source}

import com.tristanhunt.knockoff.DefaultDiscounter._
import com.tristanhunt.knockoff._
import scala.util.parsing.input.{NoPosition, Position}

object Sample extends App {
  // ベースとなるデータ
  val source = Source.fromFile("sample.txt")(Codec.UTF8)
  var content = knockoff(source.getLines().mkString("\n"))

  // Header 定義をリンクに変換する
  var counter = 0
  def mkLi(num:Int, h:Header) = {
    val Header(l, s, p) = h
    """<li><a href="#%d">%s</a></li>""".format(num, s.head.asInstanceOf[Text].content)
  }

  // Header を抜き出して、リンクのリストに変換する
  val headers = content.filter(_.isInstanceOf[Header]).map(_.asInstanceOf[Header])
    .zipWithIndex.map { case (header, num) => mkLi(num + 1, header) }
  val headerList = HTMLBlock("""<ul>%s</ul>""".format(headers.mkString), NoPosition)

  // Header 内のテキストに id を付与する
  def convertLinkSpan(s:Span) = {
    s match {
      case Text(content) => {
        counter += 1
        HTMLSpan("""<span id="%d">%s</span>""".format(counter, content))
      }
      case s => s
    }
  }

  // ヘッダに id を振る
  content = content.map(v => {
    v match {
      case Header(l, s, p) => Header(l, s.map(convertLinkSpan), p)
      case v => v
    }
  })

  val converted = new PrintWriter("output.html")
  converted.println("<html><body>")

  // リンク
  converted.println(toXHTML(headerList +: content))
  converted.println("</body></html>")
  converted.flush()
  converted.close()
}

てな感じで書くと

<html><body>
<ul><li><a href="#1">PlayFramework をインストールする</a></li><li><a href="#2">Playプロジェクトを作成する</a></li></ul><h1><span id="1">PlayFramework をインストールする</span></h1><p>Playframework 2.1.1 をインストールする手順です
</p><ol><li>Java6 以上を入れます(この詳細はJavaの物を探してください)
</li><li><a href="http://www.playframework.com/:title">Play公式</a>から2.1.1 をダウンロードしてきます
</li><li>適当なディレクトリへ解凍し、パスを通します
</li></ol><p>基本的にはこれだけです。
</p><pre><code>wget http://downloads.typesafe.com/play/2.1.1/play-2.1.1.zip
unzip play-2.1.1.zip
sudo mv play-2.1.1 /usr/local/
... (以下略)