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) = {