diff --git a/app/src/main/scala/app.scala b/app/src/main/scala/app.scala index aebb8d97..f05045e5 100644 --- a/app/src/main/scala/app.scala +++ b/app/src/main/scala/app.scala @@ -12,11 +12,11 @@ object Pamflet { def main(args: Array[String]) { System.exit(run(args)) } - private def storage(dir: File) = CachedFileStorage(dir) + private def storage(dir: File, ps: List[FencePlugin]) = CachedFileStorage(dir, ps) def run(args: Array[String]) = { args match { case Array(Dir(input), Dir(output)) => - Produce(storage(input).globalized, output) + Produce(storage(input, fencePlugins).globalized, output) println("Wrote pamflet to " + output) 0 case Array(Dir(dir)) => preview(dir) @@ -34,8 +34,9 @@ object Pamflet { 1 } } + def fencePlugins: List[FencePlugin] = Nil def preview(dir: File): Int = { - Preview(storage(dir).globalized).run { server => + Preview(storage(dir, fencePlugins).globalized).run { server => unfiltered.util.Browser.open( "http://127.0.0.1:%d/".format(server.portBindings.head.port) ) diff --git a/knockoff/src/main/scala/fenced.scala b/knockoff/src/main/scala/fenced.scala index aa437410..f6c6e066 100644 --- a/knockoff/src/main/scala/fenced.scala +++ b/knockoff/src/main/scala/fenced.scala @@ -2,19 +2,46 @@ package pamflet import com.tristanhunt.knockoff._ import scala.util.parsing.input.{ CharSequenceReader, Position, Reader } +import collection.mutable.ListBuffer trait FencedDiscounter extends Discounter { + /** List of FencePlugin */ + def fencePlugins: List[FencePlugin] + override def newChunkParser : ChunkParser = new ChunkParser with FencedChunkParser - override def blockToXHTML: Block => xml.Node = block => block match { - case FencedCodeBlock(text, _, language) => - fencedChunkToXHTML(text, language) - case _ => super.blockToXHTML(block) + override def blockToXHTML: Block => xml.Node = { + val fallback: PartialFunction[Block, xml.Node] = { case x => super.blockToXHTML(x) } + val fs: List[PartialFunction[Block, xml.Node]] = + (fencePlugins map {_.blockToXHTML}) ++ List(FencePlugin.Plain.blockToXHTML, fallback) + fs.reduceLeft(_ orElse _) + } + + def notifyBeginLanguage(): Unit = + fencePlugins foreach {_.onBeginLanguage()} + def notifyBeginPage(): Unit = + fencePlugins foreach {_.onBeginPage()} + + def fencedChunkToBlock(language: Option[String], content: String, position: Position, + list: ListBuffer[Block]): Block = { + val processors: List[PartialFunction[(Option[String], String, Position, ListBuffer[Block]), Block]] = + fencePlugins ++ List(FencePlugin.Plain) + val f = processors.reduceLeft(_ orElse _) + f((language, content, position, list)) } - def fencedChunkToXHTML(text: Text, language: Option[String]) = -
{ text.content }
+}
+
+trait MutableFencedDiscounter extends FencedDiscounter {
+ private[this] val fencePluginBuffer: ListBuffer[FencePlugin] = ListBuffer()
+ def registerFencedPlugin(p: FencePlugin): Unit = fencePluginBuffer.append(p)
+ def fencePlugins = fencePluginBuffer.toList
+ def clearFencePlugins(): Unit = fencePluginBuffer.clear()
+ def knockoffWithPlugins(source: java.lang.CharSequence, ps: List[FencePlugin]): Seq[Block] =
+ {
+ clearFencePlugins()
+ ps foreach registerFencedPlugin
+ super.knockoff(source)
+ }
}
trait FencedChunkParser extends ChunkParser {
@@ -46,11 +73,46 @@ trait FencedChunkParser extends ChunkParser {
case class FencedChunk(val content: String, language: Option[String])
extends Chunk {
- def appendNewBlock( list : collection.mutable.ListBuffer[Block],
+ def appendNewBlock( list : ListBuffer[Block],
remaining : List[ (Chunk, Seq[Span], Position) ],
spans : Seq[Span], position : Position,
- discounter : Discounter ) {
- list += FencedCodeBlock(Text(content), position, language)
+ discounter : Discounter ): Unit = discounter match {
+ case fd: FencedDiscounter => list += fd.fencedChunkToBlock(language, content, position, list)
+ case _ => sys.error("Expected FencedDiscounter")
+ }
+}
+
+/** A FencePlugin must implement the following methods:
+ * 1. def isDefinedAt(language: Option[String]): Boolean
+ * 2. def toBlock(language: Option[String], content: String, position: Position, list: ListBuffer[Block]): Block
+ * 3. def blockToXHTML: PartialFunction[Block, xml.Node]
+ *
+ * First, you have to declare what "language" your FencePlugin supports with `isDefinedAt`.
+ * Next, in `toBlock` evaluate the incoming content and store them in a custom case class that extends `Block`.
+ * Finally, in `blockToXHTML` turn your custom case class into an xml `Node`.
+ */
+trait FencePlugin extends PartialFunction[(Option[String], String, Position, ListBuffer[Block]), Block] {
+ def isDefinedAt(language: Option[String]): Boolean
+ def toBlock(language: Option[String], content: String, position: Position, list: ListBuffer[Block]): Block
+ def blockToXHTML: PartialFunction[Block, xml.Node]
+
+ override def isDefinedAt(x: (Option[String], String, Position, ListBuffer[Block])): Boolean = isDefinedAt(x._1)
+ override def apply(x: (Option[String], String, Position, ListBuffer[Block])): Block = toBlock(x._1, x._2, x._3, x._4)
+ def onBeginLanguage(): Unit = ()
+ def onBeginPage(): Unit = ()
+}
+object FencePlugin {
+ val Plain: FencePlugin = new FencePlugin {
+ override def isDefinedAt(language: Option[String]): Boolean = true
+ override def toBlock(language: Option[String], content: String, position: Position, list: ListBuffer[Block]): Block =
+ FencedCodeBlock(Text(content), position, language)
+ override def blockToXHTML = {
+ case FencedCodeBlock(text, _, language) => fencedChunkToXHTML(text, language)
+ }
+ def fencedChunkToXHTML(text: Text, language: Option[String]) =
+ { text.content }
}
}
diff --git a/knockoff/src/main/scala/parsers.scala b/knockoff/src/main/scala/parsers.scala
index f375548b..f67e5bed 100644
--- a/knockoff/src/main/scala/parsers.scala
+++ b/knockoff/src/main/scala/parsers.scala
@@ -5,7 +5,7 @@ import scala.util.parsing.input.{ CharSequenceReader, Position, Reader }
object PamfletDiscounter
extends Discounter
- with FencedDiscounter
+ with MutableFencedDiscounter
with SmartyDiscounter
with IdentifiedHeaders
with Html5Imgs
diff --git a/library/src/main/scala/knock.scala b/library/src/main/scala/knock.scala
index b736d416..a385e568 100644
--- a/library/src/main/scala/knock.scala
+++ b/library/src/main/scala/knock.scala
@@ -5,18 +5,17 @@ import com.tristanhunt.knockoff._
import collection.immutable.Map
object Knock {
- def knockEither(value: String, propFiles: Seq[File]): Either[Throwable, (String, Seq[Block], Template)] = {
- val frontin = Frontin(value)
- val template = StringTemplate(propFiles, frontin header, Map())
- val raw = template(frontin body)
- try {
- Right((raw.toString, PamfletDiscounter.knockoff(raw), template))
- } catch {
- case e: Throwable => Left(e)
- }
- }
+ lazy val discounter = PamfletDiscounter
+ def notifyBeginLanguage(): Unit = discounter.notifyBeginLanguage()
+ def notifyBeginPage(): Unit = discounter.notifyBeginPage()
+
+ def knockEither(value: String, propFiles: Seq[File], ps: List[FencePlugin]): Either[Throwable, (String, Seq[Block], Template)] =
+ knockEither(value, StringTemplate(propFiles, None, Map()), ps)
+
+ def knockEither(value: String, template0: Template): Either[Throwable, (String, Seq[Block], Template)] =
+ knockEither(value, template0, List())
- def knockEither(value: String, template0: Template): Either[Throwable, (String, Seq[Block], Template)] = {
+ def knockEither(value: String, template0: Template, ps: List[FencePlugin]): Either[Throwable, (String, Seq[Block], Template)] = {
val frontin = Frontin(value)
val template = frontin.header match {
case None => template0
@@ -24,7 +23,7 @@ object Knock {
}
val raw = template(frontin body)
try {
- Right((raw.toString, PamfletDiscounter.knockoff(raw), template))
+ Right((raw.toString, discounter.knockoffWithPlugins(raw, ps), template))
} catch {
case e: Throwable => Left(e)
}
diff --git a/library/src/main/scala/printer.scala b/library/src/main/scala/printer.scala
index d62c82c7..90a026cd 100644
--- a/library/src/main/scala/printer.scala
+++ b/library/src/main/scala/printer.scala
@@ -1,5 +1,5 @@
package pamflet
-import PamfletDiscounter.toXHTML
+import Knock.discounter.toXHTML
import collection.immutable.Map
object Printer {
@@ -160,6 +160,7 @@ case class Printer(contents: Contents, globalized: Globalized, manifest: Option[
case Right(x) => x
case Left(x) =>
Console.err.println("Error while processing " + x)
+ // x.printStackTrace()
throw x
}
toXHTML(blocks)
diff --git a/library/src/main/scala/storage.scala b/library/src/main/scala/storage.scala
index 8ec79dee..7cd7b95e 100644
--- a/library/src/main/scala/storage.scala
+++ b/library/src/main/scala/storage.scala
@@ -13,7 +13,7 @@ trait Storage {
/** Cache FileStorage based on the last modified time.
* This should make previewing much faster on large pamflets.
*/
-case class CachedFileStorage(base: File) extends Storage {
+case class CachedFileStorage(base: File, ps: List[FencePlugin]) extends Storage {
def allFiles(f0: File): Seq[File] =
f0.listFiles.toVector flatMap {
case dir if dir.isDirectory => allFiles(dir)
@@ -27,7 +27,7 @@ case class CachedFileStorage(base: File) extends Storage {
CachedFileStorage.cache.get(base) match {
case Some((lm0, gl0)) if lm == lm0 => gl0
case _ =>
- val st = FileStorage(base)
+ val st = FileStorage(base, ps)
val gl = st.globalized
CachedFileStorage.cache(base) = (lm, gl)
gl
@@ -39,7 +39,7 @@ object CachedFileStorage {
val cache: TrieMap[File, (Long, Globalized)] = TrieMap()
}
-case class FileStorage(base: File) extends Storage {
+case class FileStorage(base: File, ps: List[FencePlugin]) extends Storage {
def propFile(dir: File): Option[File] =
new File(dir, "template.properties") match {
case file if file.exists => Some(file)
@@ -69,6 +69,7 @@ case class FileStorage(base: File) extends Storage {
def isSpecialDir(dir: File): Boolean =
dir.isDirectory && ((dir.getName == "layouts") || (dir.getName == "files"))
def rootSection(dir: File, propFiles: Seq[File]): Section = {
+ Knock.notifyBeginLanguage()
def emptySection = Section("", "", Seq.empty, Nil, defaultTemplate)
if (dir.exists) section("", dir, propFiles).headOption getOrElse emptySection
else emptySection
@@ -98,10 +99,11 @@ case class FileStorage(base: File) extends Storage {
source.mkString("")
}
def knock(file: File, propFiles: Seq[File]): (String, Seq[Block], Template) =
- Knock.knockEither(read(file, defaultTemplate.defaultEncoding), propFiles) match {
+ Knock.knockEither(read(file, defaultTemplate.defaultEncoding), propFiles, ps) match {
case Right(x) => x
case Left(x) =>
Console.err.println("Error while processing " + file.toString)
+ // x.printStackTrace()
throw x
}
def isMarkdown(f: File) = {