docco.scala

package ru.circumflex
package docco

import core._, freemarker._
import java.io._
import org.apache.commons.io.filefilter.{TrueFileFilter, RegexFileFilter}
import java.util.{Collection => JCollection}
import collection.mutable.ListBuffer
import org.apache.commons.io.{FilenameUtils, IOUtils, FileUtils}

case class Section(private var _doc: String = "", private var _code: String = "") {

  var committed = false

  protected def trimNewLines(s: String) =
    s.replaceAll("^\\n+(.*)", "$1").replaceAll("(.*?)\\n+$", "$1")

  private var _md: String = null

  def addCode(s: String): this.type = {
    committed = true
    _code += s + "\n"
    this
  }
  def code = trimNewLines(_code)

  def addDoc(s: String): this.type = {
    _doc += s + "\n"
    _md = null
    this
  }
  def doc: String = {
    if (_md == null)
      _md = markeven.toHtml(_doc)
    _md
  }

  def isEmpty: Boolean = _doc == "" && _code == ""
}

Documenting Scala files with Docco

This utility generates a user-friendly HTML for specified Scala source file by placing documentation and corresponding code blocks side by side.

The usage is trivial:

Docco("my.scala").writeHtml("my.scala.html")

or shortcut of above:

Docco("my.scala").writeHtml

Docco uses FreeMarker to process pages, so you can provide your own FreeMarker Configuration and templates.

object Docco {
  def apply(sourceFile: String, stripScaladoc: Boolean = true): Docco =
    new Docco(new File(sourceFile), stripScaladoc)
}

class Docco(val file: File, val stripScaladoc: Boolean = true) {

  val pageTemplate: String = cx.get("docco.pageTemplate")
      .map(_.toString).getOrElse("/docco-single-page.html.ftl")
  val docSingleLine = "^\\s*/\\*!\\s*(.*?)\\*/".r
  val docBegin = "^(\\s*)/\\*!\\s*(.*)".r
  val docEnd = "(.*?)\\*/\\s*".r
  val scaladocBegin = "^(\\s*)/\\*\\*(.*)".r

  val sections = {
    var result = new ListBuffer[Section]
    var section = new Section()
    val reader = new BufferedReader(new FileReader(file))
    var insideDoc = false
    var insideScaladoc = false
    var indent = ""
    def flushSection() {
      if (!section.isEmpty)
        result ++= List(section)
      section = new Section()
    }
    try {
      var str = reader.readLine
      while (str != null) {
        str match {
          case docSingleLine(s) if (!insideScaladoc) =>
            if (section.committed)
              flushSection()
            section.addDoc(s)
            insideDoc = false
          case docBegin(i, s) if (!insideScaladoc) =>
            if (section.committed)
              flushSection()
            section.addDoc(s)
            indent = i
            insideDoc = true
          case scaladocBegin(i, s) if (!insideDoc) =>
            insideScaladoc = true
            if (!stripScaladoc) section.addCode(str)
          case docEnd(s) =>
            if (insideDoc)
              section.addDoc(s)
            if (insideScaladoc && !stripScaladoc)
              section.addCode(str)
            insideDoc = false
            insideScaladoc = false
          case s => {
            if (insideDoc) {
              section.addDoc(s.replaceAll("^" + indent, ""))
            } else {
              if (!insideScaladoc || !stripScaladoc)
                section.addCode(s)
            }
          }
        }
        str = reader.readLine
      }
      flushSection()
    } finally {
      reader.close()
    }
    result
  }

  def writeHtml(writer: Writer) {
    ftlConfig.getTemplate(pageTemplate)
        .process(Map[String, Any]("title" -> file.getName, "sections" -> sections), writer)
  }

  def writeHtml(file: File) {
    val fw = new FileWriter(file)
    try {
      writeHtml(fw)
    } finally {
      fw.close()
    }
  }

  def writeHtml(file: String) {
    writeHtml(new File(file))
  }

  def writeHtml() {
    writeHtml(file.getAbsolutePath + ".html")
  }

}

Batch processing

This utility generates Docco for specified docco.basePath configuration parameter. It is intended to build a documentation suite for arbitrary Maven project. The documentation is saved in location defined by docco.outputDirectory configuration parameter and contains:

  • index.html;
  • generated subfolders and documentation;
  • custom resources in .docco.
class DoccoBatch {
  // Base path for crawler
  val basePath: File = cx.get("docco.basePath") match {
    case Some(f: File) => f
    case Some(s: String) => new File(s)
    case _ => new File(".")
  }
  val outputPath = cx.get("docco.outputPath") match {
    case Some(f: File) => f
    case Some(s: String) => new File(s)
    case _ => new File("target/docco")
  }
  // Where should we store results of our work?
  // Templates
  val pageTemplate: String = cx.get("docco.pageTemplate")
      .map(_.toString).getOrElse("/docco-batch-page.html.ftl")
  val indexTemplate: String = cx.get("docco.indexTemplate")
      .map(_.toString).getOrElse("/docco-index.html.ftl")
  // Regex to filter sources
  val filenameRegex = cx.get("docco.filenameRegex").map(_.toString)
      .getOrElse(".*\\.scala$")
  // Custom resources
  var customResources: List[String] = Nil
  // Title for index
  val title: String = cx.get("docco.title").map(_.toString)
      .getOrElse(basePath.getCanonicalFile.getName + " index")
  // Should we strip Scaladoc ( `/** .. */` ) or not?
  val stripScaladoc: Boolean = cx.get("docco.stripScaladoc")
      .map(_.toString.toBoolean).getOrElse(true)
  // Should we ignore files with no docco?
  val skipEmpty: Boolean = cx.get("docco.skipEmpty")
      .map(_.toString.toBoolean).getOrElse(true)

  def addCustomResource(v: String) { customResources ++= List(v) }

  def prepareCustomResources() {
    if (customResources.size > 0) {
      val customResDir = new File(outputPath, ".docco")
      // create output directories if they do not already exist
      FileUtils.forceMkdir(customResDir)
      // copy resources
      customResources.foreach {
        r =>
          var f = new File(r)
          if (f.isDirectory) FileUtils.copyDirectory(f, customResDir)
          else if (f.isFile) FileUtils.copyFile(f, customResDir)
          else {
            // try to load the resource as stream
            val res = getClass.getResource(r)
            if (res != null) {
              val in = res.openStream
              val out = new FileOutputStream(new File(customResDir, FilenameUtils.getName(r)))
              try {
                IOUtils.copy(in, out)
              } finally {
                in.close()
                out.close()
              }
            }
          }
      }
    }
  }

  def generate() {
    prepareCustomResources()
    // crawl basePath for the sources
    val bp = basePath.getCanonicalPath
    val op = outputPath.getCanonicalPath
    val sources = new ListBuffer[File]
    val srcIt = FileUtils.listFiles(basePath,
      new RegexFileFilter(filenameRegex),
      TrueFileFilter.INSTANCE).asInstanceOf[JCollection[File]].iterator
    while (srcIt.hasNext)
      sources += srcIt.next
    // generate doccos
    val doccos = sources.flatMap { f =>
      val docco = new Docco(f, stripScaladoc)
      if (!skipEmpty || docco.sections.size > 1) {
        val fp = f.getCanonicalPath
        val relName = fp.substring(bp.length + 1) + ".html"
        val outFile = new File(outputPath, relName)
        FileUtils.forceMkdir(outFile.getParentFile)
        val out = new FileWriter(outFile)
        try {
          ftlConfig.getTemplate(pageTemplate).process(Map[String, Any](
            "title" -> f.getName,
            "sections" -> docco.sections,
            "depth" -> relName.toList.filter(c => c == File.separatorChar).length), out)
        } finally {
          out.close()
        }
        Some(outFile)
      } else None
    }
    // prepare index
    val indexMap = doccos
        .groupBy(f => f.getParentFile).map { p =>
      val dirName = p._1.getCanonicalPath.substring(op.length + 1)
      val filenames = p._2.map(f => f.getName.replaceAll("\\.html$", ""))
      (dirName -> filenames)
    }
    val dirs = indexMap.keys.toList.sortBy(_.toString)
    val out = new FileWriter(new File(outputPath, "index.html"))
    try {
      var data = Map[String, Any]("dirs" -> dirs, "index" -> indexMap, "title" -> title)
      ftlConfig.getTemplate(indexTemplate).process(data, out)
    } finally {
      out.close()
    }
  }

}