diff --git a/build.sbt b/build.sbt index d753e6d813..a0d1372458 100644 --- a/build.sbt +++ b/build.sbt @@ -2,7 +2,7 @@ import Dependencies._ organization in ThisBuild := "net.liftweb" -version in ThisBuild := "2.6-SNAPSHOT" +version in ThisBuild := "3.0-SNAPSHOT" homepage in ThisBuild := Some(url("http://www.liftweb.net")) @@ -12,7 +12,9 @@ startYear in ThisBuild := Some(2006) organizationName in ThisBuild := "WorldWide Conferencing, LLC" -crossScalaVersions in ThisBuild := Seq("2.10.0", "2.9.2", "2.9.1-1", "2.9.1") +scalaVersion in ThisBuild := "2.10.0" + +crossScalaVersions in ThisBuild := Seq("2.10.0") libraryDependencies in ThisBuild <++= scalaVersion {sv => Seq(specs2(sv), scalacheck) } diff --git a/contributors.md b/contributors.md index 98447ec409..37e75e67da 100644 --- a/contributors.md +++ b/contributors.md @@ -168,3 +168,15 @@ Will Palmeri ### Email: ### wpalmeri at gmail dot com +### Name: ### +David Barri + +### Email: ### +japgolly @@ gmail .. com + +### Name: ### +Viktor Hedefalk + +### Email: ### +hedefalk @@ gmail .. com + diff --git a/core/actor/src/main/scala/net/liftweb/actor/LAFuture.scala b/core/actor/src/main/scala/net/liftweb/actor/LAFuture.scala index 1599280b63..397357dfb5 100644 --- a/core/actor/src/main/scala/net/liftweb/actor/LAFuture.scala +++ b/core/actor/src/main/scala/net/liftweb/actor/LAFuture.scala @@ -19,15 +19,25 @@ package actor import common._ + /** * A container that contains a calculated value * or may contain one in the future */ -class LAFuture[T] /*extends Future[T]*/ { +class LAFuture[T](val scheduler: LAScheduler) { private var item: T = _ + private var failure: Box[Nothing] = Empty private var satisfied = false private var aborted = false private var toDo: List[T => Unit] = Nil + private var onFailure: List[Box[Nothing] => Unit] = Nil + private var onComplete: List[Box[T] => Unit] = Nil + + def this() { + this(LAScheduler) + } + + LAFuture.notifyObservers(this) /** * Satify the future... perform the calculation @@ -39,24 +49,42 @@ class LAFuture[T] /*extends Future[T]*/ { if (!satisfied && !aborted) { item = value satisfied = true - toDo + val ret = toDo + toDo = Nil + onFailure = Nil + onComplete.foreach(f => LAFuture.executeWithObservers(scheduler, () => f(Full(value)))) + onComplete = Nil + ret } else Nil } finally { notifyAll() } } - funcs.foreach(f => LAScheduler.execute(() => f(value))) + funcs.foreach(f => LAFuture.executeWithObservers(scheduler, () => f(value))) + } + + /** + * Complete the Future... with a Box... useful from Helpers.tryo + * @param value + */ + def complete(value: Box[T]): Unit = { + value match { + case Full(v) => satisfy(v) + case x: EmptyBox => fail(x) + } } /** * Get the future value */ - def get: T = synchronized { + @scala.annotation.tailrec + final def get: T = synchronized { if (satisfied) item + else if (aborted) throw new AbortedFutureException(failure) else { this.wait() if (satisfied) item - else if (aborted) throw new AbortedFutureException() + else if (aborted) throw new AbortedFutureException(failure) else get } } @@ -67,19 +95,38 @@ class LAFuture[T] /*extends Future[T]*/ { * when the value is satified */ def foreach(f: T => Unit) { - val todo = synchronized { - if (satisfied) { - val v = item - () => f(v) - } else { - toDo ::= f - () => () - } - } + onSuccess(f) + } - todo() + /** + * Map the future over a function + * @param f the function to apply to the future + * @tparam A the type that the function returns + * @return a Future that represents the function applied to the value of the future + */ + def map[A](f: T => A): LAFuture[A] = { + val ret = new LAFuture[A](scheduler) + onComplete(v => ret.complete(v.map(f))) + ret + } + + def flatMap[A](f: T => LAFuture[A]): LAFuture[A] = { + val ret = new LAFuture[A](scheduler) + onComplete(v => v match { + case Full(v) => f(v).onComplete(v2 => ret.complete(v2)) + case e: EmptyBox => ret.complete(e) + }) + ret + } + + def filter(f: T => Boolean): LAFuture[T] = { + val ret = new LAFuture[T](scheduler) + onComplete(v => ret.complete(v.filter(f))) + ret } + def withFilter(f: T => Boolean): LAFuture[T] = filter(f) + /** * Get the future value or if the value is not * satisfied after the timeout period, return an @@ -87,7 +134,7 @@ class LAFuture[T] /*extends Future[T]*/ { */ def get(timeout: Long): Box[T] = synchronized { if (satisfied) Full(item) - else if (aborted) Empty + else if (aborted) failure else { try { wait(timeout) @@ -113,21 +160,170 @@ class LAFuture[T] /*extends Future[T]*/ { * Abort the future. It can never be satified */ def abort() { + fail(Empty) + } + + /** + * Execute the function on success of the future + * + * @param f the function to execute on success. + */ + def onSuccess(f: T => Unit) { + synchronized { + if (satisfied) {LAFuture.executeWithObservers(scheduler, () => f(item))} else + if (!aborted) { + toDo ::= f + } + } + } + + /** + * Execute a function on failure + * + * @param f the function to execute. Will receive a Box[Nothing] which may be a Failure if there's exception data + */ + def onFail(f: Box[Nothing] => Unit) { + synchronized { + if (aborted) LAFuture.executeWithObservers(scheduler, () => f(failure)) else + if (!satisfied) { + onFailure ::= f + } + } + } + + /** + * A function to execute on completion of the Future, success or failure + * + * @param f the function to execute on completion of the Future + */ + def onComplete(f: Box[T] => Unit) { + synchronized { + if (satisfied) {LAFuture.executeWithObservers(scheduler, () => f(Full(item)))} else + if (aborted) {LAFuture.executeWithObservers(scheduler, () => f(failure))} else + onComplete ::= f + } + } + + /** + * If the execution fails, do this + * @param e + */ + def fail(e: Exception) { + fail(Failure(e.getMessage, Full(e), Empty)) + } + + /** + * If the execution fails as a Box[Nothing], do this + * @param e + */ + def fail(e: Box[Nothing]) { synchronized { if (!satisfied && !aborted) { aborted = true + failure = e + onFailure.foreach(f => LAFuture.executeWithObservers(scheduler, () => f(e))) + onComplete.foreach(f => LAFuture.executeWithObservers(scheduler, () => f(e))) + onComplete = Nil + onFailure = Nil + toDo = Nil + notifyAll() } } } + + /** + * Has the future completed? + */ + def complete_? : Boolean = synchronized(satisfied || aborted) } /** * Thrown if an LAFuture is aborted during a get */ -final class AbortedFutureException() extends Exception("Aborted Future") +final class AbortedFutureException(why: Box[Nothing]) extends Exception("Aborted Future") object LAFuture { + /** + * Create an LAFuture from a function that + * will be applied on a separate thread. The LAFuture + * is returned immediately and the value may be obtained + * by calling `get` + * + * @param f the function that computes the value of the future + * @tparam T the type + * @return an LAFuture that will yield its value when the value has been computed + */ + def apply[T](f: () => T, scheduler: LAScheduler = LAScheduler): LAFuture[T] = { + val ret = new LAFuture[T](scheduler) + scheduler.execute(() => { + try { + ret.satisfy(f()) + } catch { + case e: Exception => ret.fail(e) + } + }) + ret + } + + /** + * Build a new future with a call-by-name value that returns a type T + * @param f the call-by-name code the defines the future + * @tparam T the type that + * @return + */ + def build[T](f: => T, scheduler: LAScheduler = LAScheduler): LAFuture[T] = { + this.apply(() => f, scheduler) + } + + private val threadInfo = new ThreadLocal[List[LAFuture[_] => Unit]] + + /** + * Notify all the observers that we created a future. + * + * @param future + */ + private def notifyObservers(future: LAFuture[_]) { + val observers = threadInfo.get() + if (null eq observers) {} else { + observers.foreach(_(future)) + } + } + + private def executeWithObservers(scheduler: LAScheduler, f: () => Unit) { + val cur = threadInfo.get() + scheduler.execute(() => { + val old = threadInfo.get() + threadInfo.set(cur) + try { + f() + } finally { + threadInfo.set(old) + } + }) + } + + /** + * Do something when a future is created on this thread. This can be used + * to see if there's any Future activity on a thread and if there is, + * we can do smart things on an observing thread. + * + * @param observation the function to execute on Future creation + * @param toDo the action call-by-name code to execute whi + * @tparam T the type of the value returned by toDo + * @return the value computed by toDo + */ + def observeCreation[T](observation: LAFuture[_] => Unit)(toDo: => T): T = { + val old = threadInfo.get() + threadInfo.set(if (null eq old) List(observation) else observation :: old) + try { + toDo + } finally { + threadInfo.set(old) + } + } + + /** * Collect all the future values into the aggregate future * The returned future will be satisfied when all the @@ -136,7 +332,9 @@ object LAFuture { def collect[T](future: LAFuture[T]*): LAFuture[List[T]] = { val sync = new Object val len = future.length - val vals = new collection.mutable.ArrayBuffer[T](len) + val vals = new collection.mutable.ArrayBuffer[Box[T]](len) + // pad array so inserts at random places are possible + for (i <- 0 to len) { vals.insert(i, Empty) } var gotCnt = 0 val ret = new LAFuture[List[T]] @@ -144,10 +342,10 @@ object LAFuture { case (f, idx) => f.foreach { v => sync.synchronized { - vals.insert(idx, v) + vals.insert(idx, Full(v)) gotCnt += 1 if (gotCnt >= len) { - ret.satisfy(vals.toList) + ret.satisfy(vals.toList.flatten) } } } @@ -166,7 +364,9 @@ object LAFuture { def collectAll[T](future: LAFuture[Box[T]]*): LAFuture[Box[List[T]]] = { val sync = new Object val len = future.length - val vals = new collection.mutable.ArrayBuffer[T](len) + val vals = new collection.mutable.ArrayBuffer[Box[T]](len) + // pad array so inserts at random places are possible + for (i <- 0 to len) { vals.insert(i, Empty) } var gotCnt = 0 val ret = new LAFuture[Box[List[T]]] @@ -176,10 +376,10 @@ object LAFuture { vb => sync.synchronized { vb match { case Full(v) => { - vals.insert(idx, v) + vals.insert(idx, Full(v)) gotCnt += 1 if (gotCnt >= len) { - ret.satisfy(Full(vals.toList)) + ret.satisfy(Full(vals.toList.flatten)) } } diff --git a/core/actor/src/main/scala/net/liftweb/actor/LiftActor.scala b/core/actor/src/main/scala/net/liftweb/actor/LiftActor.scala index 4055a560c9..753c564cf0 100644 --- a/core/actor/src/main/scala/net/liftweb/actor/LiftActor.scala +++ b/core/actor/src/main/scala/net/liftweb/actor/LiftActor.scala @@ -24,7 +24,19 @@ trait ILAExecute { def shutdown(): Unit } -object LAScheduler extends Loggable { +/** + * The definition of a scheduler + */ +trait LAScheduler { + /** + * Execute some code on another thread + * + * @param f the function to execute on another thread + */ + def execute(f: () => Unit): Unit +} + +object LAScheduler extends LAScheduler with Loggable { @volatile var onSameThread = false @@ -76,6 +88,11 @@ object LAScheduler extends Loggable { @volatile var exec: ILAExecute = _ + /** + * Execute some code on another thread + * + * @param f the function to execute on another thread + */ def execute(f: () => Unit) { synchronized { if (exec eq null) { @@ -328,6 +345,51 @@ trait SpecializedLiftActor[T] extends SimpleActor[T] { } } +/** + * A SpecializedLiftActor designed for use in unit testing of other components. + * + * Messages sent to an actor extending this interface are not processed, but are instead + * recorded in a List. The intent is that when you are testing some other component (say, a snippet) + * that should send a message to an actor, the test for that snippet should simply test that + * the actor received the message, not what the actor does with that message. If an actor + * implementing this trait is injected into the component you're testing (in place of the + * real actor) you gain the ability to run these kinds of tests. +**/ +class MockSpecializedLiftActor[T] extends SpecializedLiftActor[T] { + private[this] var messagesReceived: List[T] = Nil + + /** + * Send a message to the mock actor, which will be recorded and not processed by the + * message handler. + **/ + override def !(msg: T): Unit = { + messagesReceived.synchronized { + messagesReceived ::= msg + } + } + + // We aren't required to implement a real message handler for the Mock actor + // since the message handler never runs. + override def messageHandler: PartialFunction[T, Unit] = { + case _ => + } + + /** + * Test to see if this actor has received a particular message. + **/ + def hasReceivedMessage_?(msg: T): Boolean = messagesReceived.contains(msg) + + /** + * Returns the list of messages the mock actor has received. + **/ + def messages: List[T] = messagesReceived + + /** + * Return the number of messages this mock actor has received. + **/ + def messageCount: Int = messagesReceived.size +} + object ActorLogger extends Logger { } @@ -451,6 +513,17 @@ with ForwardableActor[Any, Any] { } } +/** + * A MockLiftActor for use in testing other compnents that talk to actors. + * + * Much like MockSpecializedLiftActor, this class is intended to be injected into other + * components, such as snippets, during testing. Whereas these components would normally + * talk to a real actor that would process their message, this mock actor simply + * records them and exposes methods the unit test can use to investigate what messages + * have been received by the actor. +**/ +class MockLiftActor extends MockSpecializedLiftActor[Any] with LiftActor + import java.lang.reflect._ object LiftActorJ { diff --git a/core/actor/src/test/scala/net/liftweb/actor/ActorSpec.scala b/core/actor/src/test/scala/net/liftweb/actor/ActorSpec.scala index 4957e4aa49..a0ca287c84 100644 --- a/core/actor/src/test/scala/net/liftweb/actor/ActorSpec.scala +++ b/core/actor/src/test/scala/net/liftweb/actor/ActorSpec.scala @@ -39,6 +39,7 @@ class ActorSpec extends Specification { } private def commonFeatures(actor: LiftActor) = { + sequential "allow setting and getting of a value" in { val a = actor @@ -57,7 +58,7 @@ class ActorSpec extends Specification { "allow adding of a value" in { val a = actor a ! Set(33) - (a !< Add(44)).get(50) must be_==(Full(Answer(77))).eventually(900, 100.milliseconds) + (a !< Add(44)).get(500) must be_==(Full(Answer(77))) } "allow subtracting of a value" in { diff --git a/core/actor/src/test/scala/net/liftweb/actor/MockLiftActorSpec.scala b/core/actor/src/test/scala/net/liftweb/actor/MockLiftActorSpec.scala new file mode 100644 index 0000000000..f72f4fabe1 --- /dev/null +++ b/core/actor/src/test/scala/net/liftweb/actor/MockLiftActorSpec.scala @@ -0,0 +1,117 @@ +/* + * Copyright 2011 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package actor + +import org.specs2.mutable.Specification + +class MockLiftActorSpec extends Specification { + "Mock Actor Specification".title + + sealed trait MockSpecActorMessage + case object MockSpecActorMessage1 extends MockSpecActorMessage + case object MockSpecActorMessage2 extends MockSpecActorMessage + case object MockSpecActorMessage3 extends MockSpecActorMessage + + "A MockSpecializedLiftActor" should { + "correctly indicate when it has received a message" in { + val mockActor = new MockSpecializedLiftActor[MockSpecActorMessage] + + mockActor ! MockSpecActorMessage1 + mockActor ! MockSpecActorMessage2 + + mockActor.hasReceivedMessage_?(MockSpecActorMessage1) must beTrue + } + + "correctly indicate when it has not received a message" in { + val mockActor = new MockSpecializedLiftActor[MockSpecActorMessage] + + mockActor ! MockSpecActorMessage1 + mockActor ! MockSpecActorMessage2 + + mockActor.hasReceivedMessage_?(MockSpecActorMessage3) must beFalse + } + + "correctly indicate the number of messages it has received" in { + val mockActor = new MockSpecializedLiftActor[MockSpecActorMessage] + + mockActor ! MockSpecActorMessage1 + mockActor ! MockSpecActorMessage2 + mockActor ! MockSpecActorMessage3 + + mockActor.messageCount must_== 3 + } + + "correctly list the messages it has received" in { + val mockActor = new MockSpecializedLiftActor[MockSpecActorMessage] + + mockActor ! MockSpecActorMessage1 + mockActor ! MockSpecActorMessage2 + mockActor ! MockSpecActorMessage3 + + mockActor.messages must_== List( + MockSpecActorMessage3, + MockSpecActorMessage2, + MockSpecActorMessage1 + ) + } + } + + "A MockLiftActor" should { + "correctly indicate when it has received a message" in { + val mockActor = new MockLiftActor + + mockActor ! MockSpecActorMessage1 + mockActor ! MockSpecActorMessage2 + + mockActor.hasReceivedMessage_?(MockSpecActorMessage1) must beTrue + } + + "correctly indicate when it has not received a message" in { + val mockActor = new MockLiftActor + + mockActor ! MockSpecActorMessage1 + mockActor ! MockSpecActorMessage2 + + mockActor.hasReceivedMessage_?(MockSpecActorMessage3) must beFalse + } + + "correctly indicate the number of messages it has received" in { + val mockActor = new MockLiftActor + + mockActor ! MockSpecActorMessage1 + mockActor ! MockSpecActorMessage2 + mockActor ! MockSpecActorMessage3 + + mockActor.messageCount must_== 3 + } + + "correctly list the messages it has received" in { + val mockActor = new MockLiftActor + + mockActor ! MockSpecActorMessage1 + mockActor ! MockSpecActorMessage2 + mockActor ! MockSpecActorMessage3 + + mockActor.messages must_== List( + MockSpecActorMessage3, + MockSpecActorMessage2, + MockSpecActorMessage1 + ) + } + } +} diff --git a/core/common/src/main/scala/net/liftweb/common/Box.scala b/core/common/src/main/scala/net/liftweb/common/Box.scala index 0e6af67052..ff562b35cf 100644 --- a/core/common/src/main/scala/net/liftweb/common/Box.scala +++ b/core/common/src/main/scala/net/liftweb/common/Box.scala @@ -98,17 +98,6 @@ sealed trait BoxTrait { case _ => Empty } - /** - * This method allows one to encapsulate any object in a Box in a null-safe manner, - * treating null values to Empty. This is a parallel method to - * the Scala Option's apply method. Note that the apply method is overloaded - * and it's much, much better to use legacyNullTest in this case. - * - * @return Full(in) if in is not null; Empty otherwise - */ - @deprecated("Use legacyNullTest", "2.5") - def apply[T](in: T): Box[T] = legacyNullTest(in) - /** * Apply the specified PartialFunction to the specified value and return the result * in a Full Box; if the pf is undefined at that point return Empty. @@ -234,38 +223,6 @@ sealed abstract class Box[+A] extends Product with Serializable{ */ def openOrThrowException(justification: String): A - /** - * Return the value contained in this Box if it is Full; - * throw an exception otherwise. - * - * Using open_! in an example posted to the Lift mailing list - * may disqualify you for a helpful response. - * - * The method has a '!' in its name. This means "don't use it unless - * you are 100% sure that the Box is Full and you should probably - * comment your code with the explanation of the guaranty." - * The better case for extracting the value out of a Box can - * be found at http://lift.la/scala-option-lift-box-and-how-to-make-your-co - * - * @return the value contained in this Box if it is full; throw an exception otherwise - */ - @deprecated("use openOrThrowException, or better yet, do the right thing with your code and use map, flatMap or foreach", "2.4") - final def open_! : A = openOrThrowException("Legacy method implementation") - - /** - * Return the value contained in this Box if it is Full; - * throw an exception otherwise. - * This means "don't use it unless - * you are 100% sure that the Box is Full and you should probably - * comment your code with the explanation of the guaranty. - * The better case for extracting the value out of a Box can - * be found at http://lift.la/scala-option-lift-box-and-how-to-make-your-co - * - * @return the value contained in this Box if it is full; throw an exception otherwise - */ - @deprecated("use openOrThrowException, or better yet, do the right thing with your code and use map, flatMap or foreach", "2.4") - final def openTheBox: A = openOrThrowException("Legacy method implementation") - /** * Return the value contained in this Box if it is full; otherwise return the specified default * @return the value contained in this Box if it is full; otherwise return the specified default @@ -382,6 +339,8 @@ sealed abstract class Box[+A] extends Product with Serializable{ */ def ?~(msg: => String): Box[A] = this + + /** * Transform an Empty to a ParamFailure with the specified typesafe * parameter. @@ -475,6 +434,18 @@ sealed abstract class Box[+A] extends Product with Serializable{ */ def dmap[B](dflt: => B)(f: A => B): B = dflt + + /** + * If the Box is Full, apply the transform function on the + * value, otherwise just return the value untransformed + * + * @param v the value + * @param f the transformation function + * @tparam T the type of the value + * @return the value or the transformed value is the Box is Full + */ + def fullXform[T](v: T)(f: T => A => T): T = v + /** * An Either that is a Left with the given argument * left if this is empty, or a Right if this @@ -500,6 +471,7 @@ sealed abstract class Box[+A] extends Product with Serializable{ if (pf.isDefinedAt(value)) Full(pf(value)) else Empty) } + } /** @@ -562,6 +534,18 @@ final case class Full[+A](value: A) extends Box[A]{ override def run[T](in: => T)(f: (T, A) => T) = f(in, value) + /** + * If the Box is Full, apply the transform function on the + * value, otherwise just return the value untransformed + * + * @param v the value + * @param f the transformation function + * @tparam T the type of the value + * @return the value or the transformed value is the Box is Full + */ + override def fullXform[T](v: T)(f: T => A => T): T = f(v)(value) + + /** * An Either that is a Left with the given argument * left if this is empty, or a Right if this diff --git a/core/common/src/main/scala/net/liftweb/common/ParseDouble.scala b/core/common/src/main/scala/net/liftweb/common/ParseDouble.scala deleted file mode 100644 index de11ec9f7c..0000000000 --- a/core/common/src/main/scala/net/liftweb/common/ParseDouble.scala +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2011 WorldWide Conferencing, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.liftweb -package common - -/** - * Safely parse a String into a Double, avoiding the JVM bug - * that causes the thread to hang if the String is - * 2.2250738585072012e-308 - * - * This wonkaround is not recommended anymore. Instead consider using a - * newer version of JVM which has the necessary fix. - * - * @see http://blogs.oracle.com/security/entry/security_alert_for_cve-2010-44 - */ -@deprecated("Use a newer or patched JVM instead.", "2.5") -object ParseDouble { - private val BrokenDouble = BigDecimal("2.2250738585072012e-308") - - /** - * Parse a String to a Double avoiding the - * JVM parsing bug. May throw NumberFormatException - * if the String is not properly formatted - */ - def apply(str: String): Double = { - val d = BigDecimal(str) - if (d == BrokenDouble) error("Error parsing 2.2250738585072012e-308") - else d.doubleValue - } - - /** - * A handy dandy extractor - */ - def unapply(in: String): Option[Double] = try { - Some(apply(in)) - } catch { - case e: Exception => None - } -} diff --git a/core/json/src/main/scala/net/liftweb/json/JsonAST.scala b/core/json/src/main/scala/net/liftweb/json/JsonAST.scala index 2ce5c9dd76..bb385655ad 100644 --- a/core/json/src/main/scala/net/liftweb/json/JsonAST.scala +++ b/core/json/src/main/scala/net/liftweb/json/JsonAST.scala @@ -246,6 +246,18 @@ object JsonAST { def filter(p: JValue => Boolean): List[JValue] = fold(List[JValue]())((acc, e) => if (p(e)) e :: acc else acc).reverse + /** + * To make 2.10 happy + */ + def withFilter(p: JValue => Boolean) = new WithFilter(this, p) + + final class WithFilter(self: JValue, p: JValue => Boolean) { + def map[A](f: JValue => A): List[A] = self filter p map f + def flatMap[A](f: JValue => List[A]) = self filter p flatMap f + def withFilter(q: JValue => Boolean): WithFilter = new WithFilter(self, x => p(x) && q(x)) + def foreach[U](f: JValue => U): Unit = self filter p foreach f + } + /** Concatenate with another JSON. * This is a concatenation monoid: (JValue, ++, JNothing) *

diff --git a/core/markdown/README.md b/core/markdown/README.md new file mode 100644 index 0000000000..943deca0cd --- /dev/null +++ b/core/markdown/README.md @@ -0,0 +1,61 @@ +##About Actuarius## +Actuarius is a Markdown Processor written in Scala using parser combinators. + +The project homepage can be found on github: https://github.com/chenkelmann/actuarius + +For detailled information, please consult the Actuarius Wiki: https://github.com/chenkelmann/actuarius/wiki + +To browse the scaladoc online, go to: http://doc.henkelmann.eu/actuarius/index.html + +To try Actuarius out, you can use the web dingus on my home page: http://henkelmann.eu/projects/actuarius/dingus +(The web preview is currently broken but will be fixed (hopefully) soon.) + +To get Actuarius, you can either check out the source from github, use maven/sbt to add it as a dependency (see below) or download the binary jar, javadoc jar or source jar directly from my maven repo at http://maven.henkelmann.eu/eu/henkelmann/ + +##License## +Actuarius is licensed under the 3-clause BSD license. For details see the `LICENSE` file that comes with the source. + +##Compatibility## +Actuarius tries to stay as close to the original Markdown syntax definition as possible. There were however some quirks in the original Markdown I did not like. I wrote Actuarius as a Markdown processor for my homebrew blog engine, so I took the liberty to diverge slightly from the way the original Markdown works. The details are explained [in the respective article in the Actuarius Wiki](https://github.com/chenkelmann/actuarius/wiki/Differences-Between-Actuarius-And-Standard-Markdown) + +##Maven## +The group id is `eu.henkelmann`, the artifact id is `actuarius_[scala-version]`, e.g.`actuarius_2.10.0`. The current stable version is 0.2.6. The current development version is 0.2.7-SNAPSHOT. +Actuarius is available from the [Sonatype OSS repository](https://oss.sonatype.org), so you should not have to add any repository definitions. + +Starting with version 0.2.5 there are builds for Scala 2.9.2 and 2.10.0. These versions are also available from maven central. +(How I hate Scala's binary incompatibilities…) + +##sbt## +To add the lib to your project, add the following to your `.sbt` file if you are using scala 2.10.x: + + libraryDependencies += "eu.henkelmann" % "actuarius_2.10.0" % "0.2.6" + +or, for 2.9.x compatibility: + + libraryDependencies += "eu.henkelmann" % "actuarius_2.9.2" % "0.2.6" + + +Currently, Actuarius itself is built using sbt 0.11.x + +##Version History## + +### 0.2.6 +* fixed bug in html / xml element parsing: attributes surrounded by ticks (`'`) are now also parsed as well as attributes surrounded by quotes (`"`) +* fixed bug in unordered list item handling, items can now be also started by `-` and `+` as well as `*` + +### 0.2.5 +* added support for scala 2.10.0 +* dropped support for every all older scala versions except 2.9.2 + +### 0.2.4 +* artifacts are published via Sonatype OSS repository +* added support for scala 2.9.2 +* switched to sbt 11.x as build system (thanks to David Pollack for the build file) +* added initial support for fenced code blocks (hint for programming language to format in is parsed but ignored) + +### 0.2.3 + +* moved project to github +* added support for scala 2.9.1 +* fixed bug that caused crashes when parsing windows line endings (CR LF) + diff --git a/core/markdown/TODO b/core/markdown/TODO new file mode 100644 index 0000000000..a475c2e64f --- /dev/null +++ b/core/markdown/TODO @@ -0,0 +1,7 @@ +- Decorator for whole output (default:

) +- fast links () evaluated too eagerly +- simple java wrapper +- speedup of br, em and strong +- speedup by referencing (start, stop) indices when tokenizing lines + (CharSequence based on index pairs) +- if xml is disabled, turn elements into plain text instead of turning them into links \ No newline at end of file diff --git a/core/markdown/src/main/scala/net/liftweb/markdown/BaseParsers.scala b/core/markdown/src/main/scala/net/liftweb/markdown/BaseParsers.scala new file mode 100644 index 0000000000..f97278b3d2 --- /dev/null +++ b/core/markdown/src/main/scala/net/liftweb/markdown/BaseParsers.scala @@ -0,0 +1,273 @@ +package net.liftweb.markdown + +/* + * Copyright 2013 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Based on https://github.com/chenkelmann/actuarius originally developed by + * Christoph Henkelmann http://henkelmann.eu/ + */ + +import util.parsing.json.Parser +import util.parsing.combinator.RegexParsers +import collection.SortedMap + + +/** + * Basic parsers for Markdown Source. + * Provides general, small parsers that are used by other parsers. + * Also contains parsers for XML elements. + */ + +trait BaseParsers extends RegexParsers { + + ///////////////////////////////////////// + // Basic parsers used by other parsers // + ///////////////////////////////////////// + + /** + * Whitespace is sometimes important in markdown parsing, + * we handle it manually. So this returns false. + */ + override def skipWhitespace = false + + /** accepts one or more spaces or tabs + * returns the matched whitespace + */ + def ws:Parser[String] = """( |\t|\v)+""".r + + /** accepts zero or more spaces or tabs + * returns the matched whitespace + */ + def ows:Parser[String] = Parser{in => + if (in.atEnd) Success("test", in) + else { + var i = in.offset + val s = in.source + val end = s.length + //process chars as long as it is whitespace + while (i + if (in.atEnd) { + Success("", in) + } else { + val source = in.source + val offset = in.offset + Success(source.subSequence(offset, source.length).toString, in.drop(source.length-offset)) + } + } + + /** + * Matches exactly one char, no matter which. + * This differs from "elem" as it returns a string consisting of that char. + */ + def any:Parser[String] = Parser{ in => + if (in.atEnd) Failure("End of input reached", in) + else Success(in.first.toString, in.rest) + } + + /** + * Matches one of the chars in the given set. + * Returns a string with the matched char. + */ + def oneOf(lookup:Set[Char]):Parser[String] = Parser{ in => + if (lookup.contains(in.first)) Success(in.first.toString, in.rest) + else Failure("Expected one of " + lookup + " but found '" + in.first + "'", in) + } + + /** + * Matches one of the given char keys in the map. + * Returns the string value for the matched char in the given map. + */ + def oneOf(lookup:Map[Char,String]):Parser[String] = Parser{ in => + if (lookup.contains(in.first)) Success(lookup(in.first), in.rest) + else Failure("Expected one of " + lookup.keys + " but found '" + in.first + "'", in) + } + + /** + * Looks if the preceding char was one of the given chars. + * Never consumes any input. + */ + def lookbehind(cs:Set[Char]):Parser[Unit] = Parser{ in => + val source = in.source + val offset = in.offset + if (offset == 0) { + Failure("No chars before current char, cannot look behind.", in) + } else if (!cs.contains(source.charAt(offset-1))) { + Failure("Previous char was '" + source.charAt(offset-1) + "' expected one of " + cs, in) + } else { + Success((), in) + } + } + + /** + * Returns a verbose description of a char (printed char & hex code). + * Used for debugging. + */ + def verboseString(c:Char) = "'" + c + "'(\\u" + Integer.toHexString(c) + ")" + + /** + * Matches one char in the given range, returns the matched char. + */ + def range(begin:Char, end:Char):Parser[Char] = Parser{ in => + val c = in.first + if (begin <= c && c <= end) Success(c, in.rest) + else Failure(verboseString(c) + " not in range " + + verboseString(begin) + " - " + verboseString(end), + in) + } + + def ranges(rs:SortedMap[Char, Char]):Parser[Char] = Parser{ in => + if (in.atEnd) Failure("End of input.", in) + else { + val c = in.first + val lower:SortedMap[Char,Char] = rs.to(c) + val (begin:Char, end:Char) = if (lower.isEmpty) ('\u0001', '\u0000') //this invalid pair always causes failure + else lower.last + + if (begin <= c && c <= end) Success(c, in.rest) + else Failure(verboseString(c) + " not in range " + + verboseString(begin) + " - " + verboseString(end), + in) + } + } + + /** + * Succeeds if the given parsers succeeds and the given function is defined at the parse result. + * Returns the result of the method applied to the given parsers result. + */ + def acceptMatch[S,T](f:PartialFunction[S,T])(p:Parser[S]):Parser[T] = Parser { in => + p(in) match { + case Success(result, next) if (f.isDefinedAt(result)) => Success(f(result), next) + case Success(result, _) => Failure("Function not defined at " + result, in) + case Failure(msg, _) => Failure(msg, in) + case Error(msg, _) => Error(msg, in) + } + } + + + ///////////////////////////////////////////////////////////////////////// + // parsers for basic markdown entities like letters, xml fragments ... // + ///////////////////////////////////////////////////////////////////////// + /** a mapping of all chars that have to be escaped in xml and the resulting escape sequence + * The array solution is very ugly, but cuts down block parsing time by 25% + */ + private val escapedXmlChars = new Array[String](128) + escapedXmlChars('<') = "<" + escapedXmlChars('>') = ">" + escapedXmlChars('"') = """ + escapedXmlChars('\'') = "'" + escapedXmlChars('&') = "&" + + /** + * Escapes the given char for XML. Returns Either the + * necessary XML escape Sequence or the same char in a String. + */ + def escapeForXml(c:Char):String = { + //looks horrible but massively faster than using a proper map and Option[String] + val escaped:String = escapeFastForXml(c) + if (escaped == null) Character.toString(c) + else escaped + } + + /** + * Either returns the XML escape sequence for the given char or null. + * This does not return Option[String] on purpose. While Option[String] + * would be a much cleaner solution, this is actually called so often + * that it is a noticeable difference if we use Option here. + */ + def escapeFastForXml(c:Char) = if (c < escapedXmlChars.length) escapedXmlChars(c) + else null + + /* A single char. If it is one of the chars that have to be escaped in XML it is returned as the xml escape code + * i.e. parsing '<' returns "<" + */ + def aChar = Parser{ in => + if (in.atEnd) { + Failure("End of input reached.", in) + } else { + Success(escapeForXml(in.first), in.rest) + } + } + + val xmlNameStartCharRanges:SortedMap[Char,Char] = + SortedMap(':' -> ':', 'A' -> 'Z', '_' -> '_', 'a' -> 'z', '\u00C0' -> '\u00D6', + '\u00D8' -> '\u00F6', '\u00F8' -> '\u02FF', '\u0370' -> '\u037D', '\u037F' -> '\u1FFF', + '\u200C' -> '\u200D', '\u2070' -> '\u218F', '\u2C00' -> '\u2FEF', '\u3001' -> '\uD7FF', + '\uF900' -> '\uFDCF', '\uFDF0' -> '\uFFFD')//'\u10000' -> '\uEFFFF' + + val xmlNameCharRanges:SortedMap[Char,Char] = + xmlNameStartCharRanges ++ SortedMap('-' -> '-', '.' -> '.', '0'->'9', + '\u00b7'->'\u00b7', '\u0300' -> '\u0369', '\u203F' -> '\u2040') + + /**Parser for one char that starts an XML name. + * According to W3C specs except that range #x10000 to #xEFFFF + * is excluded (cannot be expressed by char literals) + */ + def xmlNameStartChar:Parser[Char] = ranges(xmlNameStartCharRanges) + /** Parses an XML name char according to W3C spec except that range #x10000 to #xEFFFF is excluded + */ + def xmlNameChar:Parser[Char] = ranges(xmlNameCharRanges) + /** Parses an XML name (tag or attribute name) + */ + def xmlName:Parser[String] = xmlNameStartChar ~ (xmlNameChar*) ^^ {case c ~ cs => c + cs.mkString} + /** Parses a Simplified xml attribute: everything between quotes ("foo") + * everything between the quotes is run through the escape handling + * That way you can omit xml escaping when writing inline XML in markdown. + */ + def xmlAttrVal:Parser[String] = + ('"' ~> ((not('"') ~> aChar)*) <~ '"' ^^ {'"' + _.mkString + '"' }) | + ('\'' ~> ((not('\'') ~> aChar)*) <~ '\'' ^^ {'\'' + _.mkString + '\''}) + /** Parses an XML Attribute with simplified value handling like xmlAttrVal. + */ + def xmlAttr:Parser[String] = ws ~ xmlName ~ '=' ~ xmlAttrVal ^^ { + case w ~ name ~ _ ~ value => w + name + '=' + value + } + /** Parses an xml start or empty tag, attribute values are escaped. + */ + def xmlStartOrEmptyTag:Parser[String] = '<' ~> xmlName ~ (xmlAttr*) ~ ows ~ (">" | "/>") ^^ { + case name ~ attrs ~ w ~ e => '<' + name + attrs.mkString + w + e + } + + /** Parses closing xml tags. + */ + def xmlEndTag:Parser[String] = " xmlName <~ ">" ^^ {""} + + + /** Runs the given parser on the given input. + * Expects the parser to succeed and consume all input. + * Throws an IllegalArgumentException if parsing failed. + */ + def apply[T](p:Parser[T], in:String):T = { + parseAll(p, in) match { + case Success(t, _) => t + case e: NoSuccess => throw new IllegalArgumentException("Could not parse '" + in + "': " + e) + } + } +} \ No newline at end of file diff --git a/core/markdown/src/main/scala/net/liftweb/markdown/BlockParsers.scala b/core/markdown/src/main/scala/net/liftweb/markdown/BlockParsers.scala new file mode 100644 index 0000000000..d4c14f5138 --- /dev/null +++ b/core/markdown/src/main/scala/net/liftweb/markdown/BlockParsers.scala @@ -0,0 +1,477 @@ +package net.liftweb.markdown + +/* + * Copyright 2013 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Based on https://github.com/chenkelmann/actuarius originally developed by + * Christoph Henkelmann http://henkelmann.eu/ + */ + +import collection.immutable.StringOps +import collection.mutable.ListBuffer +import xml.{Group, Node, Text, NodeSeq, Elem => XmlElem, TopScope, XML} +import xml.parsing.XhtmlParser +import util.parsing.combinator.{Parsers, RegexParsers} + +/** + * A parser for the markdown language. + * Works on pre-parsed lines that can be created by a LineParser. + */ + +trait BlockParsers extends Parsers { + type Elem = MarkdownLine + //why does this not allow us to access the lookup map in the lookup parser? + //override type Input = MarkdownLineReader + //hmm, compiler does not accept this, though MarkdownLineReader extends Reader[MarkdownLine]... + + /** + * Used to define the output format of parsed blocks and whether verbatim xml blocks are allowed. + */ + def deco():Decorator = Decorator + + /** + * returns the current indentation string repeated the given number of levels + */ + def indent(level:Int):String = deco.indentation * level + + private val tokenizer = new LineTokenizer() + + /** A markdown block element. + */ + sealed abstract class MarkdownBlock extends InlineParsers{ + override def deco = BlockParsers.this.deco + + /** adds the resulting xhtml snippet to the given string builder + */ + def addResult(level:Int, out:StringBuilder):Unit + /** returns the resulting xhtml snippet as a string + */ + def result():String = { + val sb = new StringBuilder + addResult(0, sb) + sb.toString + } + } + + + + ////////////////////////// + // non-recursive blocks // + ////////////////////////// + + /**:? + * Represents a block of verbatim xml + */ + class VerbatimXml(line:XmlChunk) extends MarkdownBlock { + def addResult(level:Int, out:StringBuilder) {out.append(line.content)} + } + + /** + * Represents a horizontal ruler + */ + object Ruler extends MarkdownBlock { + def addResult(level:Int, out:StringBuilder) {out.append(indent(level)).append(deco.decorateRuler)} + } + + /** + * Represents a header + */ + case class Header(content:String, headerLevel:Int, lookup:Map[String, LinkDefinition]) extends MarkdownBlock{ + def addResult(level:Int, out:StringBuilder) { + out.append(indent(level)).append(deco.decorateHeaderOpen(headerLevel)) + .append(applyInline(content, lookup)) + .append(indent(level)).append(deco.decorateHeaderClose(headerLevel)) + } + } + + /** + * Represents a block of verbatim qouted code + */ + class CodeBlock(lines:List[MarkdownLine]) extends MarkdownBlock{ + def addResult(level:Int, out:StringBuilder) { + out.append(indent(level)).append(deco.decorateCodeBlockOpen) + for (line <- lines) { + val escaped = escapeXml(line.payload) + out.append(escaped).append('\n') + //out.append(line.content) + } + out.append(indent(level)).append(deco.decorateCodeBlockClose) + } + } + + class FencedCodeBlock(language:String, lines:List[MarkdownLine]) extends MarkdownBlock{ + def addResult(level:Int, out:StringBuilder) { + out.append(indent(level)).append(deco.decorateCodeBlockOpen) + for (line <- lines) { + val escaped = escapeXml(line.fullLine) + out.append(escaped).append('\n') + //out.append(line.content) + } + out.append(indent(level)).append(deco.decorateCodeBlockClose) + } + } + + /** + * Represents a paragraph of text + */ + class Paragraph(lines:List[MarkdownLine], lookup:Map[String, LinkDefinition]) + extends MarkdownBlock{ + + def addResult(level:Int, out:StringBuilder) { + out.append(indent(level)).append(deco.decorateParagraphOpen) + addResultPlain(level, out) + out.append(indent(level)).append(deco.decorateParagraphClose) + } + + /** + * Adds the result without any decoration, (no wrapping tags) + * Used for building list items that don't have their content wrappend in paragraphs + */ + def addResultPlain(level:Int, out:StringBuilder) { + + val temp = new StringBuilder() + lines.foreach(line => temp.append(indent(level)).append(line.payload).append('\n')) + val result = applyInline(temp.toString, lookup) + out.append(result) + + //lines.foreach(line => out.append(indent(level)).append(escapeXml(line.content))) + + //drop last newline so paragraph closing tag ends the line + if (!out.isEmpty && out.charAt(out.length-1) == '\n') out.deleteCharAt(out.length-1) + } + } + + ////////////////////// + // recursive blocks // + ////////////////////// + + /** + * Represents a quoted text block. Text in the block is recursively evaluated. + */ + class Blockquote(lines:List[MarkdownLine], lookup:Map[String, LinkDefinition]) + extends MarkdownBlock { + def addResult(level:Int, out:StringBuilder) { + //the block parser needs to recurse: + val innerLines = lines.map(line => line.payload) + val reader = BlockParsers.this.tokenizer.innerTokenize(innerLines, lookup) + //now apply the normal markdown parser to the new content + val innerBlocks = BlockParsers.this.applyBlocks(reader) + //wrap the resulting blocks in blockquote tags + out.append(indent(level)).append(deco.decorateBlockQuoteOpen) + innerBlocks.foreach(block => block.addResult(level+1, out)) + out.append(indent(level)).append(deco.decorateBlockQuoteClose) + } + } + + /** + * Helper class to build lists. Allows easy checking if an item ends with empty lines and + * recursively builds the content of an item. + */ + class ListItem(val lines:List[MarkdownLine], lookup:Map[String, LinkDefinition]) extends LineParsers { + def endsWithNewline = lines.size > 1 && (lines.last.isInstanceOf[EmptyLine]) + + def addResult(level:Int, out:StringBuilder, paragraph_? : Boolean) { + out.append(indent(level)).append(deco.decorateItemOpen) + //the block parser needs to recurse: + val innerLines = lines.map(line => line.payload) + val reader = BlockParsers.this.tokenizer.innerTokenize(innerLines, lookup) + //now apply the normal markdown parser to the new content + val innerBlocks = BlockParsers.this.applyBlocks(reader) + innerBlocks match { + case (p:Paragraph) :: Nil if (!paragraph_?) => p.addResultPlain(level+1, out) + case _ => innerBlocks.foreach(block => block.addResult(level+1, out)) + } + out.append(indent(level)).append(deco.decorateItemClose) + } + } + + /** + * Base class for ordered and unordered lists, allows for correct handling of paragraphs in lists. + */ + abstract class ListBlock (items:List[ListItem]) extends MarkdownBlock { + /** + * This method recursively goes through the given list and adds the items contents. + * It checks the previous item if it ends with empty lines. If it does, it signals the + * current item to create paragraphs. In order for this method to work it has to be + * called with the first item prepended twice in front of the list. So if the list is + * a::b::c, call this method with a::a::b::c + */ + protected def addResult(level:Int, out:StringBuilder, list:List[ListItem]):Unit = list match{ + case last::current::rest => { + current.addResult(level + 1, out, last.endsWithNewline) + addResult(level, out, current::rest) + } + case _ => {}//end of recursion, list with one item or less + } + + /** + * calls recursive handling of nested items + */ + def addResult(level:Int, out:StringBuilder) { + addResult(level, out, items.head::items) + } + } + + /** + * An ordered (i.e. numbered) list of items. + */ + class OList (items:List[ListItem]) extends ListBlock(items) { + override def addResult(level:Int, out:StringBuilder) { + out.append(indent(level)).append(deco.decorateOListOpen) + super.addResult(level, out) + out.append(indent(level)).append(deco.decorateOListClose) + } + } + + /** + * An unordered list of items. + */ + class UList (items:List[ListItem]) extends ListBlock(items) { + override def addResult(level:Int, out:StringBuilder) { + out.append(indent(level)).append(deco.decorateUListOpen) + super.addResult(level, out) + out.append(indent(level)).append(deco.decorateUListClose) + } + } + + + ///////////////////////////////////////////////////////////// + //////////////// helpers ///////////////////// + ///////////////////////////////////////////////////////////// + + /** + * Parses a line of the given type T + */ + def line[T](c:Class[T]):Parser[T] = Parser {in => + if (in.first.getClass == c) Success(in.first.asInstanceOf[T], in.rest) + else Failure("Not a fitting line.", in) + } + + /** + * Parses a line of any type *but* T + */ + def notLine[T](c:Class[T]):Parser[MarkdownLine] = Parser {in => + if (in.atEnd) Failure("At end of input.", in) + else if (in.first.getClass == c) Failure("Not a fitting line.", in) + else Success(in.first, in.rest) + } + + /** + * Parses any line. + */ + def anyLine:Parser[MarkdownLine] = Parser {in => + if (in.atEnd) Failure("End of input reached.", in) + else Success(in.first, in.rest) + } + + def emptyLine:Parser[EmptyLine] = line(classOf[EmptyLine]) + + /**accepts zero or more empty lines + */ + def optEmptyLines:Parser[List[MarkdownLine]] = emptyLine* + + /** accepts one or more empty lines + */ + def emptyLines:Parser[List[MarkdownLine]] = emptyLine+ + + /** returns the current link lookup from the reader + * always succeeds, never consumes input + */ + def lookup:Parser[Map[String, LinkDefinition]] = Parser { in => + //why is the instanceof necessary? re-declaring type Input above does not change anything :( + Success(in.asInstanceOf[MarkdownLineReader].lookup, in) + } + + /////////////////// + // Block parsers // + /////////////////// + + def atxHeader:Parser[Header] = line(classOf[AtxHeaderLine]) ~ lookup ^^ { + case l ~ lu => new Header(l.trimHashes, l.headerLevel, lu) + } + + def setExtHeader:Parser[Header] = + not(emptyLine) ~> anyLine ~ line(classOf[SetExtHeaderLine]) ~ lookup ^^ + {case l ~ setext ~ lu => new Header(l.fullLine.trim, setext.headerLevel, lu)} + + /** parses a horizontal ruler + */ + def ruler:Parser[MarkdownBlock] = (line(classOf[RulerLine]) | line(classOf[SetExtHeaderLine])) ^^^ {Ruler} + + /** parses a verbatim xml block + */ + def verbatimXml:Parser[VerbatimXml] = line(classOf[XmlChunk]) ^^ {new VerbatimXml(_)} + + /** parses a code block + */ + def codeBlock:Parser[CodeBlock] = line(classOf[CodeLine]) ~ ((optEmptyLines ~ line(classOf[CodeLine]))*) ^^ { + case l ~ pairs => new CodeBlock( l :: pairs.map({case (a~b) => a++List(b)}).flatten ) + } + + /** + * Parses a fenced code block: a line starting a fenced code block with + * "```", followed by any lines that do not stop it, optionally followed + * by the ending line. Optionally parsing the stopping line causes the + * code block to extend to the end of the document. (This is the github + * behavior, where omitting the line closing the code block causes the + * block to extend to the end of the document as well) + */ + def fencedCodeBlock:Parser[FencedCodeBlock] = + (line(classOf[ExtendedFencedCode])|line(classOf[FencedCode])) ~ + (notLine(classOf[FencedCode])*) ~ + opt(line(classOf[FencedCode]))^^ { + case (start:ExtendedFencedCode) ~ lines ~ _ => new FencedCodeBlock(start.languageFormat, lines) + case _ ~ lines ~ _ => new FencedCodeBlock("", lines) + } + + //line(classOf[FencedCodeStart]) ~ + //((not(line(classOf[FencedCodeEnd]))*) ~ + //opt(line(classOf[FencedCodeEnd])) ^^ { + // case start ~ lines ~ end => new CodeBlock(lines.map(_.fullLine)) + //} + + + /** a consecutive block of paragraph lines + * returns the content of the matched block wrapped in

tags + */ + def paragraph:Parser[Paragraph] = lookup ~ (line(classOf[OtherLine])+) ^^ {case lu ~ ls => new Paragraph(ls, lu)} + + /** + * Parses a blockquote fragment: a block starting with a blockquote line followed + * by more blockquote or paragraph lines, ends optionally with empty lines + */ + def blockquoteFragment:Parser[List[MarkdownLine]] = + line(classOf[BlockQuoteLine]) ~ ((line(classOf[BlockQuoteLine]) | line(classOf[OtherLine]))*) ~ (optEmptyLines) ^^ { + case l ~ ls ~ e => (l :: ls ++ e) + } + + /** + * Parses a quoted block. A quoted block starts with a line starting with "> " + * followed by more blockquote lines, paragraph lines following blockqoute lines + * and may be interspersed with empty lines + */ + def blockquote:Parser[Blockquote] = lookup ~ (blockquoteFragment+) ^^ { + case lu ~ fs => new Blockquote(fs.flatten, lu) + } + + + /** + * parses a list of lines that may make up the body of a list item + */ + def itemLines:Parser[List[MarkdownLine]] = ((line(classOf[CodeLine])|line(classOf[OtherLine]))*) + + /** + * The continuation of a list item: + * A line indented by four spaces or a tab (a continuation line), followed by more continuation or paragraph + * lines followed by empty lines + */ + def itemContinuation:Parser[List[MarkdownLine]] = + optEmptyLines ~ line(classOf[CodeLine]) ~ itemLines ^^ { + case e ~ c ~ cs => e ++ (c :: cs) + } + + /**parses an item in an unsorted list + */ + def uItem:Parser[ListItem] = lookup ~ line(classOf[UItemStartLine]) ~ itemLines ~ (itemContinuation*) ~ optEmptyLines ^^ { + case lu ~ s ~ ls ~ cs ~ e => new ListItem(s :: ls ++ cs.flatten ++ e, lu) + } + + /**parses an item in a sorted list + */ + def oItem:Parser[ListItem] = lookup ~ line(classOf[OItemStartLine]) ~ itemLines ~ (itemContinuation*) ~ optEmptyLines ^^ { + case lu ~ s ~ ls ~ cs ~ e => new ListItem(s :: ls ++ cs.flatten ++ e, lu) + } + + /** parses an unordered list + */ + def uList:Parser[UList] = (uItem+) ^^ {new UList(_)} + + /** parses an ordered list + */ + def oList:Parser[OList] = (oItem+) ^^ {new OList(_)} + + + /////////////////////////////////////////////////////////////// + /////////////////// high level processing ///////////////////// + /////////////////////////////////////////////////////////////// + + /** + * parses first level blocks (all blocks, including xml) + */ + def outerBlock:Parser[MarkdownBlock] = (verbatimXml <~ optEmptyLines) | innerBlock + + /** + * speed up block processing by looking ahead + */ + def fastBlock:Parser[MarkdownBlock] = Parser { in => + if (in.atEnd) { + Failure("End of Input.", in) + } else { + in.first match { + case l:AtxHeaderLine => atxHeader(in) + case l:RulerLine => ruler(in) + //setext headers have been processed before we are called, so this is safe + case l:SetExtHeaderLine => ruler(in) + case l:CodeLine => codeBlock(in) + case l:ExtendedFencedCode => fencedCodeBlock(in) + case l:FencedCode => fencedCodeBlock(in) + case l:BlockQuoteLine => blockquote(in) + case l:OItemStartLine => oList(in) + case l:UItemStartLine => uList(in) + case _ => paragraph(in) + } + } + } + + /** + * parses inner blocks (everything excluding xml) + */ + def innerBlock:Parser[MarkdownBlock] = (setExtHeader | fastBlock) <~ optEmptyLines + + /** + * a markdown parser + */ + def markdown:Parser[List[MarkdownBlock]] = optEmptyLines ~> (outerBlock*) + + /** Generic apply method to run one of our pasers on the given input. + */ + def apply[T](p:Parser[T], in:MarkdownLineReader):T = { + phrase(p)(in) match { + case Success(t, _) => t + case e: NoSuccess => throw new IllegalArgumentException("Could not parse '" + in + "': " + e) + } + } + + /** parses all blocks from the given reader + */ + def applyBlocks(in:MarkdownLineReader):List[MarkdownBlock] = apply((optEmptyLines ~> (innerBlock*)), in) + + /** Generic apply method to test a single parser + */ + def apply[T](p:Parser[T], list:List[MarkdownLine]):T = apply(p, new MarkdownLineReader(list)) + + /** Parses the given input as a markdown document and returns the string result + */ + def apply(in:MarkdownLineReader):String = { + phrase(markdown)(in) match { + case Success(bs, _) => { + val builder = new StringBuilder() + bs.foreach(block => block.addResult(0, builder)) + builder.toString + } + case e: NoSuccess => throw new IllegalArgumentException("Could not parse " + in + ": " + e) + } + } +} diff --git a/core/markdown/src/main/scala/net/liftweb/markdown/Decorator.scala b/core/markdown/src/main/scala/net/liftweb/markdown/Decorator.scala new file mode 100644 index 0000000000..3f5a4c58e5 --- /dev/null +++ b/core/markdown/src/main/scala/net/liftweb/markdown/Decorator.scala @@ -0,0 +1,108 @@ +package net.liftweb.markdown + +/* + * Copyright 2013 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Based on https://github.com/chenkelmann/actuarius originally developed by + * Christoph Henkelmann http://henkelmann.eu/ + */ + +/** + * This trait influences the behavior of the Markdown output of inline and block parsers + * and the complete transformer. + * Mix in this trait and override methods to change the behavior and override the "deco()" method + * in the respective parser/transformer to return + * your modified instances to change the output they create. + * + * Inline element decoration methods always get passed the spanned text, so you have to + * prepend and append the opening/closing tags. For block elements there is always a method + * for the opening and closing tags. This is to make block + * processing more efficient to prevent unnecessary String building of whole blocks just to + * add tags. (The block building uses a StringBuilder internally and just appends the returned tags) + * + * If you want line breaks after opening/closing block level tags, you have to add the newline yourself. + */ + +trait Decorator { + /** + * The string used to ident one level. Defaults to the empty string + */ + def indentation() = "" + /** + * If true, inline xml tags and verbatim xml blocks are allowed, + * otherwise they are escaped and included as plain text + */ + def allowVerbatimXml():Boolean = true + /** used to print out manual line breaks (default:
) + */ + def decorateBreak():String = "
" + /** used to print out inline code (default: ...) + */ + def decorateCode(code:String):String = "" + code + "" + /** used to print out emphasized text (default ...) + */ + def decorateEmphasis(text:String):String = "" + text + "" + /** Used to print out strong text (default: ... + */ + def decorateStrong(text:String):String = "" + text + "" + /** Used to print link elements (default: "" + text + "" + case Some(t) => "" + text + "" + } + /** Used to print image elements (default: "\""" + case Some(t) => "\""" + } + /**used to print a horizontal ruler defaults to "


\n" */ + def decorateRuler():String = "
\n" + /** used to print the beginning of a header, defaults to "" */ + def decorateHeaderOpen(headerNo:Int):String = "" + /** used to print the end of a header, defaults to "" */ + def decorateHeaderClose(headerNo:Int):String = "\n" + /** used to print the beginning of a code block, defaults to "
"*/
+    def decorateCodeBlockOpen():String = "
"
+    /** used to print the end of a code block, defaults to "
\n" */ + def decorateCodeBlockClose():String = "
\n" + /** used to print the beginning of a paragraph, defaults to "

" */ + def decorateParagraphOpen():String = "

" + /** used to print the end of a paragraph, defaults to "

\n" */ + def decorateParagraphClose():String = "

\n" + /** used to print the beginning of a blockquote, defaults to "
" */ + def decorateBlockQuoteOpen():String = "
" + /** used to print the end of a blockquote, defaults to "
\n" */ + def decorateBlockQuoteClose():String = "
\n" + /** used to print the beginning of a list item, defaults to "
  • " */ + def decorateItemOpen():String = "
  • " + /** used to print the end of a list item, defaults to "
  • " */ + def decorateItemClose():String = "\n" + /** used to print the beginning of an unordered list, defaults to "
      \n" */ + def decorateUListOpen():String = "
        \n" + /** used to print the end of an unordered list, defaults to "
      \n" */ + def decorateUListClose():String = "
    \n" + /** used to print the beginning of an ordered list, defaults to
      \n */ + def decorateOListOpen():String = "
        \n" + /** used to print the end of an ordered list, defaults to
      \n */ + def decorateOListClose():String = "
    \n" +} + +/** + * Default instance of Decorator with the standard Markdown behavior + */ +object Decorator extends Decorator { +} \ No newline at end of file diff --git a/core/markdown/src/main/scala/net/liftweb/markdown/InlineParsers.scala b/core/markdown/src/main/scala/net/liftweb/markdown/InlineParsers.scala new file mode 100644 index 0000000000..726c854a9a --- /dev/null +++ b/core/markdown/src/main/scala/net/liftweb/markdown/InlineParsers.scala @@ -0,0 +1,437 @@ +package net.liftweb.markdown + +/* + * Copyright 2013 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Based on https://github.com/chenkelmann/actuarius originally developed by + * Christoph Henkelmann http://henkelmann.eu/ + */ + +/** + * A parser for inline markdown, markdown escapes and XML escapes. + * This is used by the result classes of the block parsers to handle + * Markdown within a block. + */ + +trait InlineParsers extends BaseParsers { + + /** + * Defines how the output is formatted and whether inline xml elements are allowed. + */ + def deco():Decorator = Decorator + + ///////////////////////////////////// + // Types we use for inline parsing // + ///////////////////////////////////// + + /** + * Defines a lookup map for link definitions. + */ + type LinkMap = Map[String, LinkDefinition] + + /** + * A set of tags we have already created. Used to prevent nesting a link in a link or an emphasis in an emphasis. + */ + type VisitedTags = Set[String] + + /** + * Keeps track of visited tags and provides a lookup for link ids. + */ + case class InlineContext(val map:LinkMap, val tags:VisitedTags) { + def this(m:LinkMap) = this(m, Set()) + def this() = this(Map()) + def addTag(tag:String) = new InlineContext(map, tags + tag) + } + + /** This array is used as a lookup for mapping markdown escapes + * to the resulting char (if necessary already escaped for XML) + * Disgusting, I know, but this is such an often called operation + * that this is the fastest way to do it, even in the year 2010. + */ + private val escapableMarkdownChars = new Array[String](127) + escapableMarkdownChars('\\') = "\\" + escapableMarkdownChars('`') = "`" + escapableMarkdownChars('*') = "*" + escapableMarkdownChars('_') = "_" + escapableMarkdownChars('{') = "{" + escapableMarkdownChars('}') = "}" + escapableMarkdownChars('[') = "[" + escapableMarkdownChars(']') = "]" + escapableMarkdownChars('(') = "(" + escapableMarkdownChars(')') = ")" + escapableMarkdownChars('#') = "#" + escapableMarkdownChars('+') = "+" + escapableMarkdownChars('-') = "-" + escapableMarkdownChars('=') = "=" + escapableMarkdownChars('>') = ">" + escapableMarkdownChars('.') = "." + escapableMarkdownChars('!') = "!" + + /** + * Parses markdown text up to any of the chars defined in the given map. + * used to quickly escape any text between special inline markdown like + * emphasis. + */ + def markdownText(special:Set[Char], markdownEscapes:Boolean) = Parser{ in => + if (in.atEnd) { + Failure("End of input.", in) + } else { + var start = in.offset + var i = in.offset + val s = in.source + val end = s.length + val result = new StringBuffer() + //process chars until we hit a special char or the end + while (i + //markdownEscape | br | code | xmlTag | //simple inline + //a(ctx) | strong(ctx) | em(ctx) | fastA(ctx) | refA(ctx) | img(ctx) //recursive inline + + //} + /* explicit match is faster than the map lookup + private val elementParserLookup:Map[Char,(InlineContext=>Parser[String])] = Map( + '\\' -> (ctx => aChar), ' ' -> (ctx => br), '`' -> (ctx => code), '<' -> (ctx => xmlTag | fastA(ctx)), + '[' -> (ctx => a(ctx) | refA(ctx)), '*' -> (ctx => spanAsterisk(ctx)), '_' -> (ctx => spanUnderscore(ctx)), + '!' -> (ctx => img(ctx)) + ) + */ + + //TODO:better handling of " \n" here. Stopping at every space costs us 20% time! + /** Chars that may indicate the start of a special Markdown inline sequence. + */ + val specialInlineChars = Set(' ', '`', '<', '[', '*', '_', '!') + /** Chars that may indicate the start of a special markdown inline sequence or the end of a link text. + */ + val specialLinkInlineChars = specialInlineChars + ']' + + /** Hand rolled parser that parses a chunk of special inline markdown (like links or emphasis) + * based on a one char lookahead. + */ + def elementParsers(ctx:InlineContext) = Parser{ in => + if (in.atEnd) { + Failure("End of Input Reached", in) + } else { + in.first match { + case ' ' => br(in) + case '`' => code(in) + case '<' => (xmlTag | fastLink(ctx))(in) + case '[' => link(ctx)(in) + case '*' => spanAsterisk(ctx)(in) + case '_' => spanUnderscore(ctx)(in) + case '!' => img(ctx)(in) + case _ => Failure("Lookahead does not start inline element.", in) + } + } + } + + /** Parses a single inline token. Either a span element or a chunk of text. + */ + def oneInline(ctx:InlineContext):Parser[String] = + markdownText(specialInlineChars, true) | elementParsers(ctx) | aChar + + /** Parser for inline markdown, always consumes all input, returns the resulting HTML. + */ + def inline(m:LinkMap):Parser[String] = (oneInline(new InlineContext(m))*) ^^ {_.mkString} + + + + /////////////////////////////////////////////////////////// + // Inline Elements: // + // br,code,xml tag,fast link,link,image,emphasis,strong, text chunk // + /////////////////////////////////////////////////////////// + + /** Parses two spaces at the end of a line to a manual break (
    ) + */ + val br:Parser[String] = (" \n") ^^^ {deco.decorateBreak() + "\n"} + + + /** Parses an inline code element. + * An inline code element is surrounded by single backticks ("`") + * or double backticks ("``"). + */ + val code:Parser[String] = ((("``" ~> ((not("``")~> aChar)+) <~ "``")^^{_.mkString}) | + ('`' ~> markdownText(Set('`'), false) <~ '`') ) ^^ { + c => deco.decorateCode(c.mkString) + } + + + /** Parses any xml tag and escapes attribute values. + */ + val xmlTag:Parser[String] = if (deco.allowVerbatimXml) (xmlEndTag | xmlStartOrEmptyTag) + else failure("Inline XML processing disabled.") + + + /** A shortcut markdown link of the form + */ + def fastLink(ctx:InlineContext):Parser[String] = + if (ctx.tags.contains("a")){ + failure("Cannot nest a link in a link.") + } else { + elem('<') ~> markdownText(Set('>',' ', '<', '\n'), true) <~ '>' ^^ { u => deco.decorateLink(u, u, None) } + } + + /** A link started by square brackets, either a reference or a a link with the full URL. + */ + def link(ctx:InlineContext):Parser[String] = fullLink(ctx) | referenceLink(ctx) + + /** A markdown link with the full url given. + */ + def fullLink(ctx:InlineContext):Parser[String] = + if (ctx.tags.contains("a")){ + failure("Cannot nest a link in a link.") + } else { + '[' ~> linkInline(ctx.addTag("a")) ~ ("](" ~ ows) ~ url ~ ows ~ title <~ (ows ~ ')') ^^ { + case txt ~ _ ~ u ~ _ ~ ttl => deco.decorateLink(txt, u, ttl) + } + } + + /** A markdown link which references an url by id. + */ + def referenceLink(ctx:InlineContext):Parser[String] = + if (ctx.tags.contains("a")){ + failure("Cannot nest a link in a link.") + } else { + ref(ctx.addTag("a")) ^^ { + case (LinkDefinition(_, u, ttl), txt) => deco.decorateLink(txt, u, ttl) + } + } + + /** Inline markdown in a link. Like normal inline stuff but stops when it reaches a closing square bracket. + */ + def linkInline(ctx:InlineContext):Parser[String] = //( (not(']') ~> oneInline(ctx.addTag("a")))* ) ^^ {_.mkString} + ((markdownText(specialLinkInlineChars, true) | elementParsers(ctx) | ((not(']') ~> aChar)))*) ^^ {_.mkString} + + /** We parse everything as a link/img url until we hit whitespace or a closing brace. + */ + val url:Parser[String] = markdownText(Set(')', ' ', '\t'), true) + + /** A title is everything in quotation marks. We allow even quotation marks in quotation marks. + * We look ahead if we hit the closing brace after the quotation marks to detect if the title + * ends or not. + */ + val title:Parser[Option[String]] = opt('"' ~> ((markdownText(Set('"'),true) ~ opt(not('"'~ows~')') ~> aChar))*) <~ '"') ^^ { + case None => None + case Some(chunks) => { + val result = new StringBuilder() + for (chunk <- chunks) { chunk match { + case (text) ~ None => result.append(text) + case (text) ~ Some(s) => result.append(text).append(s) + } } + Some(result.toString) + } + } + + /** Plaintext variant to refInline. Escapable text until a square bracket is hit. + */ + val refText:Parser[String] = markdownText(Set(']'), true) + + /** Parses an id reference. (Any text that is not a square bracket) + * Succeeds only if the parsed id is found in the given lookup. + * Returns the found link definition and the matched text. + */ + def idReference(ctx:InlineContext):Parser[(String, LinkDefinition)] = + guard(acceptMatch(ctx.map)(refText ^^ (_.trim.toLowerCase))) ~ refText ^^ {case ld ~ t => (t, ld)} + /** + * A markdown reference of the form [text][id], [idText][] or [idText] + * Parser returns a tuple with the link definition first and the text to display second. + */ + def ref(ctx:InlineContext):Parser[(LinkDefinition, String)] = + ('[' ~> linkInline(ctx) ~ (']' ~ opt(' ') ~ '[') ~ idReference(ctx) <~ ']' ^^ { + case t ~ dummy ~ pair => (pair._2, t)} ) | + ('[' ~> idReference(ctx) <~ (']' ~ opt(opt(' ') ~ '[' ~ ows ~ ']')) ^^ { + case (t, ld) => (ld, t)} ) + + /** + * Parses either a referenced or a directly defined image. + */ + def img(ctx:InlineContext):Parser[String] = elem('!') ~> (directImg | refImg(ctx)) + + /** An image with an explicit path. + */ + val directImg:Parser[String] = + elem('[') ~> refText ~ ("](" ~ ows) ~ url ~ ows ~ title <~ (ows ~ ')') ^^ { + case altText ~ _ ~ path ~ _ ~ ttl => deco.decorateImg(altText, path, ttl) + } + /** + * Parses a referenced image. + */ + def refImg(ctx:InlineContext):Parser[String] = ref(ctx) ^^ { + case (LinkDefinition(_, u, ttl), alt) => deco.decorateImg(alt, u, ttl) + } + + /** Parses inline in a span element like bold or emphasis or link up until the given end marker + */ + def spanInline(end:Parser[Any], ctx:InlineContext):Parser[String] = + (markdownText(specialInlineChars, true) | elementParsers(ctx) | (not(end) ~> aChar)) ^^ {_.mkString} + + /** Parses a span element like __foo__ or *bar* + */ + def span(limiter:String, ctx:InlineContext):Parser[String] = + (limiter~not(ws))~> + (spanInline( (not(lookbehind(Set(' ', '\t', '\n'))) ~ limiter), ctx)+) <~ + limiter ^^ { + _.mkString + } + + /** Either an emphasis or a strong text wrapped in asterisks. + */ + def spanAsterisk (ctx:InlineContext) = strongAsterisk(ctx) | emAsterisk(ctx) + + /** Either an emphasis or strong text wrapped in underscores. + */ + def spanUnderscore(ctx:InlineContext) = strongUnderscore(ctx) | emUnderscore(ctx) + + /**Parses emphasized text wrapped in asterisks: *foo* + */ + def emAsterisk(ctx:InlineContext):Parser[String] = + if (ctx.tags.contains("em")) { + failure("Cannot nest emphasis.") + } else { + span("*", ctx.addTag("em")) ^^ { deco.decorateEmphasis(_) } + } + + + /**Parses emphasized text wrapped in underscores: _foo_ + */ + def emUnderscore(ctx:InlineContext):Parser[String] = + if (ctx.tags.contains("em")) { + failure("Cannot nest emphasis.") + } else { + span("_", ctx.addTag("em")) ^^ { deco.decorateEmphasis(_) } + } + + /**Parses strong text in asterisks: **foo** + */ + def strongAsterisk(ctx:InlineContext):Parser[String] = + if (ctx.tags.contains("strong")) { + failure("Cannot nest strong text.") + } else { + span("**", ctx.addTag("strong")) ^^ { deco.decorateStrong(_) } + } + + /**Parses strong text in underscores: __foo__ + */ + def strongUnderscore(ctx:InlineContext):Parser[String] = + if (ctx.tags.contains("strong")) { + failure("Cannot nest strong text.") + } else { + span("__", ctx.addTag("strong")) ^^ { deco.decorateStrong(_) } + } + + + /** + * Runs the inline parser on the given input and returns the result + */ + def applyInline(s:String, m:LinkMap) = apply(inline(m), s) + + /** + * Escapes the given string so it it can be embedded in xml. + * Markdown escapes are not processed. + */ + def escapeXml(s:String) = { + var i = 0 + val end = s.length + val result = new StringBuffer() + //process chars until we hit a special char or the end + while (i i + 1 => + return validEntitySet.contains(sb.toString) + case c if c == '#' || Character.isLetter(c) || Character.isDigit(c) => sb.append(c) + case _ => return false + } + pos += 1 + } + + false + } +} \ No newline at end of file diff --git a/core/markdown/src/main/scala/net/liftweb/markdown/LineParsers.scala b/core/markdown/src/main/scala/net/liftweb/markdown/LineParsers.scala new file mode 100644 index 0000000000..05a2a5570d --- /dev/null +++ b/core/markdown/src/main/scala/net/liftweb/markdown/LineParsers.scala @@ -0,0 +1,281 @@ +package net.liftweb.markdown + +/* + * Copyright 2013 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Based on https://github.com/chenkelmann/actuarius originally developed by + * Christoph Henkelmann http://henkelmann.eu/ + */ + +import scala.util.parsing.input.{Position, Reader} +import java.util.StringTokenizer +import scala.collection.mutable.{HashMap, ArrayBuffer, ListBuffer} + + +/** + * Represents a line of markdown. + * The prefix is the beginning of the line that indicates the line type, + * the payload is the actual content after the prefix. + */ +sealed abstract class MarkdownLine(val prefix:String, val payload:String){ + /** + * Constructs a MarkdownLine where the prefix is the empty String and the + * payload is the whole line. + */ + def this(c:String) = this ("", c) + + /** + * Returns the full line as it was originally, i.e. prefix+payload. + */ + def fullLine = prefix + payload +} + +/**Represents lines of verbatim xml. + * Actually this class is a little cheat, as it represents multiple lines. + * But it is a token that is created when "parsing with a line scope", so it is not too bad. + */ +case class XmlChunk(content:String) extends MarkdownLine(content) +/** Represents the underline for a setext style header + */ +case class SetExtHeaderLine(content:String, headerLevel:Int) extends MarkdownLine(content) + +/** + * An atx style header line. + * Trims hashes automatically and determines the header level from them. + */ +case class AtxHeaderLine(pre:String, pay:String) extends MarkdownLine(pre, pay) { + /** removes all whitespace, nl and trailing hashes from the payload + * " foo ## \n" => "foo" + */ + def trimHashes() = { + val s = payload.trim + var idx = s.length - 1 + while (idx >= 0 && s.charAt(idx) == '#') idx -= 1 + s.substring(0,idx+1).trim + } + + def headerLevel = prefix.length +} +/** A line consisting only of whitespace. + */ +case class EmptyLine(content:String) extends MarkdownLine(content) +/** A horizontal ruler line. + */ +case class RulerLine(content:String) extends MarkdownLine(content) +/** A line indicating a block quote (starts with "> ") + */ +case class BlockQuoteLine(pre:String, pay:String) extends MarkdownLine(pre, pay) +/** A line indicating the start of an unordered list item (starts with " * ") + */ +case class UItemStartLine(pre:String, pay:String) extends MarkdownLine(pre, pay) +/** A line indicating the start of an ordered list item (starts with " [NUMBER]. ") + */ +case class OItemStartLine(pre:String, pay:String) extends MarkdownLine(pre, pay) +/** A line in verbatim code or the continuation of a list item + */ +case class CodeLine(pre:String, pay:String) extends MarkdownLine(pre, pay) +/** Starting line of a fenced code block: three backticks followed by an optional + * language token + */ +case class ExtendedFencedCode(pre:String, pay:String) extends MarkdownLine(pre, pay) { + def languageFormat = pay.trim() +} +/** Ending line of a fenced code block: three backticks followed by optional whitespace + */ +case class FencedCode(pre:String) extends MarkdownLine(pre) +/** Any other line. + */ +case class OtherLine(content:String) extends MarkdownLine(content) + + +/** Definition of a link or url that can be referenced by id. + */ +case class LinkDefinition(id:String, url:String, title:Option[String]) + +/** Stub class that is an intermediate result when parsing link definitions. + */ +case class LinkDefinitionStart(id:String, url:String) { + def toLinkDefinition(title:Option[String]) = new LinkDefinition(id, url, title) +} + +/** + * This class allows us to reference a map with link definitions resulting from the line parsing during block parsing. + * It extends a Reader for MarkdownLines and allows us to add the said map to the parsing context. + * This is basically a modification of the parser monad's state. + */ +case class MarkdownLineReader private (val lines:Seq[MarkdownLine], + val lookup:Map[String, LinkDefinition], + val lineCount:Int) + extends Reader[MarkdownLine] { + /** Not existing line that signals EOF. + * This object cannot be referenced by any other code so it will fail all line parsers. + */ + private object EofLine extends MarkdownLine("\nEOF\n") + + + def this(ls:Seq[MarkdownLine], lu:Map[String, LinkDefinition]) = this(ls, lu, 1) + def this(ls:Seq[MarkdownLine]) = this (ls, Map()) + def first = if (lines.isEmpty) EofLine else lines.head + def rest = if (lines.isEmpty) this else new MarkdownLineReader(lines.tail, lookup, lineCount + 1) + def atEnd = lines.isEmpty + def pos = new Position { + def line = lineCount + def column = 1 + protected def lineContents = first.fullLine + } +} + +/** + * Parses single lines into tokens. + * Markdown lines are differentiated by their beginning. + * These lines are then organized in blocks by the BlockParsers. + */ +trait LineParsers extends InlineParsers { + + ///////////////////////////////// + // Link definition pre-parsing // + ///////////////////////////////// + + /** The Start of a link definition: the id in square brackets, optionally indented by three spaces + */ + def linkDefinitionId:Parser[String] = + """ {0,3}\[""".r ~> markdownText(Set(']'), true) <~ ("]:" ~ ows) ^^ {_.trim.toLowerCase} + /** The link url in a link definition. + */ + def linkDefinitionUrl:Parser[String] = + (elem('<') ~> markdownText(Set('>'), true) <~ '>' ^^ {_.mkString.trim}) | + (markdownText(Set(' ','\t'), true) ^^ {_.mkString}) + /** The title in a link definition. + */ + def linkDefinitionTitle:Parser[String] = + ows ~> ("""\"[^\n]*["]""".r | + """\'[^\n]*\'""".r | + """\([^\n]*\)""".r) <~ ows ^^ { s => s.substring(1,s.length-1) } + + /** A link definition that later gets stripped from the output. + * Either a link definition on one line or the first line of a two line link definition. + */ + def linkDefinitionStart:Parser[(LinkDefinitionStart, Option[String])] = + linkDefinitionId ~ linkDefinitionUrl ~ opt(linkDefinitionTitle) ^^ {case i ~ u ~ t => (new LinkDefinitionStart(i, u), t)} + + + ////////////////////////////////////////// + // Lines for XML Block tokenizing // + ////////////////////////////////////////// + + /** A line that starts an xml block: an opening xml element fragment. + */ + def xmlBlockStartLine:Parser[String] = guard('<' ~ xmlName) ~> rest + /** A line that ends an xml block: a line starting with an xml end tag + */ + def xmlBlockEndLine:Parser[String] = guard(xmlEndTag) ~> rest + /** A line not starting with an xml end tag + */ + def notXmlBlockEndLine:Parser[String] = not(xmlEndTag) ~> rest + + + ////////////////////////////// + // Markdown line tokenizing // + ////////////////////////////// + + /** Parses the line under a setext style level 1 header: ===== + */ + val setextHeader1:Parser[SetExtHeaderLine] = """=+([ \t]*)$""".r ^^ {new SetExtHeaderLine(_, 1)} + + /** Parses the line under a setext style level 2 header: ----- + */ + val setextHeader2:Parser[SetExtHeaderLine] = """((\-)+)([ \t]*)$""".r ^^ {new SetExtHeaderLine(_, 2)} + + /** Parses headers of the form: ### header ### + */ + val atxHeader:Parser[AtxHeaderLine] = """#+""".r ~ rest ^^ { + case prefix ~ payload => new AtxHeaderLine(prefix, payload) + } + + /** Parses a horizontal rule. + */ + val ruler:Parser[MarkdownLine] = """ {0,3}(((-[ \t]*){3,})|((\*[ \t]*){3,}))$""".r ^^ { new RulerLine(_) } + + /** Matches a line starting with up to three spaces, a '>' and an optional whitespace. + * (i.e.: the start or continuation of a block quote.) + */ + val blockquoteLine:Parser[BlockQuoteLine] = """ {0,3}\>( )?""".r ~ rest ^^ { + case prefix ~ payload => new BlockQuoteLine(prefix,payload) + } + + + /** A line that starts an unordered list item. + * Matches a line starting with up to three spaces followed by an asterisk, a space, and any whitespace. + */ + val uItemStartLine:Parser[UItemStartLine] = (""" {0,3}[\*\+-] [\t\v ]*""".r) ~ rest ^^ { + case prefix ~ payload => new UItemStartLine(prefix, payload) + } + + + /** A line that starts an ordered list item. + * Matches a line starting with up to three spaces followed by a number, a dot and a space, and any whitespace + */ + val oItemStartLine:Parser[OItemStartLine] = (""" {0,3}[0-9]+\. [\t\v ]*""".r) ~ rest ^^ { + case prefix ~ payload => new OItemStartLine(prefix, payload) + } + + /** Accepts an empty line. (A line that consists only of optional whitespace or the empty string.) + */ + val emptyLine:Parser[MarkdownLine] = """([ \t]*)$""".r ^^ {new EmptyLine(_)} + + /** Matches a code example line: any line starting with four spaces or a tab. + */ + val codeLine:Parser[CodeLine] = (" " | "\t") ~ rest ^^ { + case prefix ~ payload => new CodeLine(prefix, payload) + } + + /** + * A fenced code line. Can be the start or the end of a fenced code block + */ + val fencedCodeLine:Parser[FencedCode] = """ {0,3}\`{3,}[\t\v ]*""".r ^^ { + case prefix => new FencedCode(prefix) + } + + /** Matches the start of a fenced code block with additional language token: + * up to three spaces, three or more backticks, whitespace, an optional + * language token, optional whitespace + */ + val extendedFencedCodeLine:Parser[ExtendedFencedCode] = fencedCodeLine ~ """\w+[\t\v ]*""".r ^^ { + case prefix ~ languageToken => new ExtendedFencedCode(prefix.fullLine, languageToken) + } + + /** Matches any line. Only called when all other line parsers have failed. + * Makes sure line tokenizing does not fail and we do not loose any lines on the way. + */ + val otherLine:Parser[OtherLine] = rest ^^ {new OtherLine(_)} + + /////////////////////////////////////////////////////////////// + // combined parsers for faster tokenizing based on lookahead // + /////////////////////////////////////////////////////////////// + /** First tries for a setext header level 2, then for a ruler. + */ + val setext2OrRulerOrUItem:Parser[MarkdownLine] = setextHeader2 | ruler | uItemStartLine + /** First tries for a ruler, then for an unordered list item start. + */ + val rulerOrUItem:Parser[MarkdownLine] = ruler | uItemStartLine + /** First tries if the line is empty, if not tries for a code line. + */ + val emptyOrCode:Parser[MarkdownLine] = emptyLine | codeLine + + /** Parses one of the fenced code lines + */ + val fencedCodeStartOrEnd:Parser[MarkdownLine] = extendedFencedCodeLine | fencedCodeLine +} + diff --git a/core/markdown/src/main/scala/net/liftweb/markdown/LineTokenizer.scala b/core/markdown/src/main/scala/net/liftweb/markdown/LineTokenizer.scala new file mode 100644 index 0000000000..9ab46d5444 --- /dev/null +++ b/core/markdown/src/main/scala/net/liftweb/markdown/LineTokenizer.scala @@ -0,0 +1,248 @@ +package net.liftweb.markdown + +/* + * Copyright 2013 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Based on https://github.com/chenkelmann/actuarius originally developed by + * Christoph Henkelmann http://henkelmann.eu/ + */ + +import scala.util.parsing.combinator.Parsers +import scala.collection.mutable.{HashMap, ArrayBuffer} +import scala.util.parsing.input.{Position, Reader} +import scala.xml + +/** + * A Reader for reading whole Strings as tokens. + * Used by the Tokenizer to parse whole lines as one Element. + */ +case class LineReader private (val lines:Seq[String], + val lineCount:Int) + extends Reader[String] { + /**should never be used anywhere, just a string that should stick out for better debugging*/ + private def eofLine = "EOF" + def this(ls:Seq[String]) = this(ls, 1) + def first = if (lines.isEmpty) eofLine else lines.head + def rest = if (lines.isEmpty) this else new LineReader(lines.tail, lineCount + 1) + def atEnd = lines.isEmpty + def pos = new Position { + def line = lineCount + def column = 1 + protected def lineContents = first + } +} + +/** + * Chops the input into lines and turns those lines into line tokens. + * Also takes care of preprocessing link definitions and xml blocks. + */ +class LineTokenizer() extends Parsers { + object lineParsers extends LineParsers + + /**we munch whole lines (OM NOM NOM) + */ + type Elem = String + + /** Determines if xml blocks may be included verbatim. + * If true, they are passed through, else they are escaped and turned into paragraphs + */ + def allowXmlBlocks = true + + /** + * Returns a parser based on the given line parser. + * The resulting parser succeeds if the given line parser consumes the whole String. + */ + def p[T](parser:lineParsers.Parser[T]):Parser[T] = Parser{in => + if (in.atEnd) { + Failure("End of Input.", in) + } else { + lineParsers.parseAll(parser, in.first) match { + case lineParsers.Success(t, _) => Success(t, in.rest) + case n:lineParsers.NoSuccess => Failure(n.msg, in) + } + } + } + + /** Returns the first char in the given string or a newline if the string is empty. + * This is done to speed up header parsing. Used to speed up line tokenizing substantially + * by using the first char in a line as lookahead for which parsers to even try. + */ + def firstChar(line:String):Char = { + if (line.length == 0) '\n' else line.charAt(0) + } + + /**Finds the char in the given line that is the best indication of what kind of markdown line this is. + * The “special” Markdown lines all start with up to three spaces. Those are skipped if present. + * The first char after those (up to)three spaces or a newline is returned. + */ + def indicatorChar(line:String):Char = { + var i = 0 + //skip the first three spaces, if present + while (i < 3 && i < line.length && line.charAt(i) == ' ') i += 1 + //return the next char after the spaces or a newline if there are no more + if (i==line.length) '\n' + else line.charAt(i) + } + + //////////////////////// + // Link definitions // + //////////////////////// + + /** Tries to parse an URL from the next line if necessary. + * The passed tuple is the result from a previous parser and used to decide how to continue parsing. + */ + def maybeUrlInNextLine(prev:(LinkDefinitionStart, Option[String])):Parser[LinkDefinition] = prev match { + case (lds, Some(title)) => success(lds.toLinkDefinition(Some(title))) + case (lds, None) => Parser {in => + if (in.atEnd) { + Success(lds.toLinkDefinition(None), in) + } else { + lineParsers.parseAll(lineParsers.linkDefinitionTitle, in.first) match { + case lineParsers.Success(title, _) => Success(lds.toLinkDefinition(Some(title)), in.rest) + case _ => Success(lds.toLinkDefinition(None), in) + } + } + } + } + + /** + * Parses a link definition. + */ + def linkDefinition:Parser[LinkDefinition] = p(lineParsers.linkDefinitionStart) into(maybeUrlInNextLine) + + ///////////////// + // XML blocks // + ///////////////// + /** The start of a verbatim XML chunk: any line starting directly with an XML element + */ + def xmlChunkStart = p(lineParsers.xmlBlockStartLine) + + /** Parses any line that does not start with a closing XML element. + */ + def notXmlChunkEnd = p(lineParsers.notXmlBlockEndLine) + + /** Parses a line beginning with a closing XML tag. + */ + def xmlChunkEnd = p(lineParsers.xmlBlockEndLine) + + /** Very dumb parser for XML chunks. + */ + def xmlChunk = xmlChunkStart ~ (notXmlChunkEnd*) ~ xmlChunkEnd ^^ { + case s ~ ms ~ e => new XmlChunk(s + "\n" + ms.mkString("\n") + "\n" + e + "\n") + } + + /** Parses Markdown Lines. Always succeeds. + */ + def lineToken = Parser{ in => + if (in.atEnd) { + Failure("End of Input.", in) + } else { + val line = in.first + (firstChar(line), indicatorChar(line)) match { + case ('=', _) => p(lineParsers.setextHeader1)(in) + case ('-', _) => p(lineParsers.setext2OrRulerOrUItem)(in) + case ('#', _) => p(lineParsers.atxHeader)(in) + case (_, '-') => p(lineParsers.rulerOrUItem)(in) + case (_, '*') => p(lineParsers.rulerOrUItem)(in) + case (_, '+') => p(lineParsers.uItemStartLine)(in) + case (_, '>') => p(lineParsers.blockquoteLine)(in) + case (_, n) if (n >= '0' && n <= '9') => p(lineParsers.oItemStartLine)(in) + case (_, ' ') => p(lineParsers.emptyOrCode)(in) + case (_, '\t')=> p(lineParsers.emptyOrCode)(in) + case (_, '\n')=> p(lineParsers.emptyLine)(in) + case (_, '`') => p(lineParsers.fencedCodeStartOrEnd)(in) + case _ => p(lineParsers.otherLine)(in) + } + } + } | p(lineParsers.otherLine) //this makes sure every line is consumed, even if our guess was no good + + /** Parses link definitions and verbatim xml blocks + */ + def preprocessToken = Parser{ in => + if (in.atEnd) { + Failure("End of Input.", in) + } else { + val line = in.first + (firstChar(line), indicatorChar(line)) match { + //link definitions have absolute precedence + case (_, '[') => linkDefinition(in) + //then filter out xml blocks if allowed + case ('<', _) if (allowXmlBlocks) => xmlChunk(in) + //no token for preprocessing + case _ => Failure("No preprocessing token.", in) + } + } + } + + /** Parses tokens that may occur inside a block. Works like the normal token parser except that + * it does not check for link definitions and verbatim XML. + */ + def innerTokens(lookup:Map[String, LinkDefinition]):Parser[MarkdownLineReader] = phrase(lineToken *) ^^ { + case ts => new MarkdownLineReader(ts, lookup) + } + + /** Parses first level line tokens, i.e. Markdown lines, XML chunks and link definitions. + */ + def tokens:Parser[MarkdownLineReader] = phrase((preprocessToken | lineToken) *) ^^ { case ts => + val lines = new ArrayBuffer[MarkdownLine]() + val lookup = new HashMap[String, LinkDefinition]() + for (t <- ts) { t match { + case ld:LinkDefinition => lookup(ld.id) = ld + case ml:MarkdownLine => lines.append(ml) + } } + new MarkdownLineReader(lines.toList, lookup.toMap) + } + + /** Simple preprocessing: split the input at each newline. These whole lines are then fed to + * the actual Tokenizer. + */ + def splitLines(s:String):List[String] = { + def chopWindoze(line:String) = { + if (line.endsWith("\r")) { + line.substring(0, line.length-1) + } else { + line + } + } + + s.split('\n').map(chopWindoze(_)).toList + } + + /** Turns a list of inner lines (the payloads of the lines making up the block) + * into line tokens. Does not check for XML chunks or link definitions. + */ + def innerTokenize(lines:List[String], lookup:Map[String, LinkDefinition])= + innerTokens(lookup)(new LineReader(lines)) match { + case Success(reader, _) => reader + case n:NoSuccess => + throw new IllegalStateException("Inner line Tokenizing failed. This is a bug. Message was: " + n.msg) + } + + /** Tokenizes a whole Markdown document. + */ + def tokenize(s:String):MarkdownLineReader = tokenize(splitLines(s)) + + /** Tokenizes a preprocessed Markdown document. + */ + def tokenize(lines:List[String]):MarkdownLineReader = tokenize(new LineReader(lines)) + + /** Tokenizes preprocessed lines read from a line reader. + */ + def tokenize(lines:Reader[String]):MarkdownLineReader = tokens(lines) match { + case Success(reader, _) => reader + case n:NoSuccess => + throw new IllegalStateException("Tokenizing failed. This is a bug. Message was: " + n.msg) + } +} \ No newline at end of file diff --git a/core/markdown/src/main/scala/net/liftweb/markdown/TimeTest.scala b/core/markdown/src/main/scala/net/liftweb/markdown/TimeTest.scala new file mode 100644 index 0000000000..153dc806ac --- /dev/null +++ b/core/markdown/src/main/scala/net/liftweb/markdown/TimeTest.scala @@ -0,0 +1,137 @@ +package net.liftweb.markdown + +/* + * Copyright 2013 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Based on https://github.com/chenkelmann/actuarius originally developed by + * Christoph Henkelmann http://henkelmann.eu/ + */ + +import java.io.{InputStreamReader, FileInputStream, StringWriter} + +/** + * Quick and dirty test for measuring the time of this Parser. + * Contains hardcoded file paths, just ignore this, it will be removed soon. + */ + +trait TimedTransformer { + + /** + * Overwrite this method to return a custom decorator if you want modified output. + */ + def deco():Decorator = Decorator + + private object lineTokenizer extends LineTokenizer { + override def allowXmlBlocks() = TimedTransformer.this.deco().allowVerbatimXml() + } + private object blockParser extends BlockParsers { + override def deco() = TimedTransformer.this.deco() + } + + /** + * This is the method that turns markdown source into xhtml. + */ + def apply(s:String) = { + + //first, run the input through the line parser + val (ms1,lineReader:MarkdownLineReader) = TimeTest.executionTime(()=>lineTokenizer.tokenize(s)) + + //then, run it through the block parser + val (ms2, result) = TimeTest.executionTime(()=>blockParser(lineReader)) + println("lines=" + ms1 + ", blocks=" + ms2) + result + } +} + + + +object TimeTest { + private object actuariusProcessor extends TimedTransformer() + + private def readFile(path:String):String = { + //read from system input stream + val reader = new InputStreamReader(new FileInputStream(path)) + val writer = new StringWriter() + val buffer = new Array[Char](1024) + var read = reader.read(buffer) + while (read != -1) { + writer.write(buffer, 0, read) + read = reader.read(buffer) + } + //turn read input into a string + writer.toString + } + + def executionTime[T](f:(()=>T)):(Long, T) = { + val start = System.currentTimeMillis + val t = f() + val end = System.currentTimeMillis + (end - start, t) + } + + private def runActuarius(markdown:String, iterations:Int) { + for (i <- 0 until iterations) actuariusProcessor(markdown) + } + + + def testRun(markdown:String, iterations:Int) { + println("Running Actuarius " + iterations + " times...") + println("... took " + (executionTime(() => runActuarius(markdown, iterations)))._1 + "ms") + } + + object testParser extends BaseParsers { + //def ws1:Parser[String] = """( |\t|\v)+""".r + def ws2:Parser[String] = rep1(elem(' ') | elem('\t') | elem('\u000B')) ^^ {_.mkString} + + def runParser(s:String, p:Parser[String], iterations:Int) { + for (i <- 0 until iterations) { + apply(p, s) + } + } + } + + def runActuarius = { + val markdown = readFile("/home/chris/sbt_projects/markdown_race/test.txt").mkString*100 + val iterations = 10 + println("==== First run to warm up the VM: ====") + testRun(markdown, iterations) + println("==== Second run, JIT compiler should be done now: ====") + testRun(markdown, iterations) + } + + def runWs = { + val wsString = " " * 1000 + val iterations = 100000 + println("Running ws...") + println("...took " + executionTime (() => testParser.runParser(wsString, testParser.ws, iterations))._1 + "ms") + //println("Running ws1...") + //println("...took " + executionTime (() => testParser.runParser(wsString, testParser.ws, iterations))) + println("Running ws2...") + println("...took " + executionTime (() => testParser.runParser(wsString, testParser.ws2, iterations))._1 + "ms") + + } + + def main(args:Array[String]) { + /* + val markdown = readFile("/home/chris/sbt_projects/markdown_race/test.txt").mkString*100 + val iterations = 10 + println("==== First run to warm up the VM: ====") + testRun(markdown, iterations) + println("==== Second run, JIT compiler should be done now: ====") + testRun(markdown, iterations)*/ + //runWs + runActuarius + } +} \ No newline at end of file diff --git a/core/markdown/src/main/scala/net/liftweb/markdown/Transformer.scala b/core/markdown/src/main/scala/net/liftweb/markdown/Transformer.scala new file mode 100644 index 0000000000..49086ef81b --- /dev/null +++ b/core/markdown/src/main/scala/net/liftweb/markdown/Transformer.scala @@ -0,0 +1,73 @@ +package net.liftweb.markdown + +/* + * Copyright 2013 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Based on https://github.com/chenkelmann/actuarius originally developed by + * Christoph Henkelmann http://henkelmann.eu/ + */ + +import java.io.{InputStreamReader, StringWriter} + +/** + * This is the Transformer that uses the other parsers to transform markdown into xhtml. + * Mix this trait in if you want more control over the output (like switching verbatim xml on/off or using + * different opening/closing tags for the output). + */ +trait Transformer { + + /** + * Overwrite this method to return a custom decorator if you want modified output. + */ + def deco():Decorator = Decorator + + private object lineTokenizer extends LineTokenizer { + override def allowXmlBlocks() = Transformer.this.deco().allowVerbatimXml() + } + private object blockParser extends BlockParsers { + override def deco() = Transformer.this.deco() + } + + /** + * This is the method that turns markdown source into xhtml. + */ + def apply(s:String) = { + //first, run the input through the line tokenizer + val lineReader = lineTokenizer.tokenize(s) + //then, run it through the block parser + blockParser(lineReader) + } +} + +class SingleThreadedTransformer extends Transformer + +/** + * Simple Standalone Markdown transformer. + * Use this if you simply want to transform a block of markdown without any special options. + * val input:String = ... + * val xhtml:String = new ActuariusTransformer()(input) + * + * Note that this markdown parser isn't inherantly thread-safe, as Scala Parser Combinators isn't, so this + * class instantiates a SingleThreadedTransformer for each thread. + * You'll need to write your own pooled implementation if this isn't efficient for your usage. + */ +class ThreadLocalTransformer extends Transformer { + + private[this] val threadLocalTransformer = new ThreadLocal[SingleThreadedTransformer] { + override def initialValue = new SingleThreadedTransformer + } + + override def apply(s: String) = threadLocalTransformer.get()(s) +} diff --git a/core/markdown/src/test/scala/net/liftweb/markdown/BaseParsersTest.scala b/core/markdown/src/test/scala/net/liftweb/markdown/BaseParsersTest.scala new file mode 100644 index 0000000000..6bcbb66fcc --- /dev/null +++ b/core/markdown/src/test/scala/net/liftweb/markdown/BaseParsersTest.scala @@ -0,0 +1,74 @@ +package net.liftweb.markdown + +/* + * Copyright 2013 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Based on https://github.com/chenkelmann/actuarius originally developed by + * Christoph Henkelmann http://henkelmann.eu/ + */ + +import org.scalatest.junit.JUnitRunner +import org.scalatest.FlatSpec +import org.scalatest.matchers.ShouldMatchers +import collection.SortedMap +import org.junit.runner.RunWith + +/** + * Tests basic parsers that are used by the more complex parsing steps. + */ + +@RunWith(classOf[JUnitRunner]) +class BaseParsersTest extends FlatSpec with ShouldMatchers with BaseParsers{ + + "The BaseParsers" should "parse a newline" in { + val p = nl + apply(p, "\n") should equal ("\n") + evaluating(apply(p, "\r\n")) should produce[IllegalArgumentException] + evaluating(apply(p, " \n")) should produce[IllegalArgumentException] + } + + it should "parse whitespace" in { + val p = ws + apply(p, " ") should equal (" ") + apply(p, "\t") should equal ("\t") + apply(p, " ") should equal (" ") + apply(p, "\t\t") should equal ("\t\t") + apply(p, " \t \t ") should equal (" \t \t ") + //we want newlines to be treated diferrently from other ws + evaluating (apply(p, "\n")) should produce[IllegalArgumentException] + } + + it should "be able to look behind" in { + apply (((elem('a') ~ lookbehind(Set('a')) ~ elem('b'))^^{case a~lb~b=>a+""+b}), "ab") should equal ("ab") + evaluating {apply (((elem('a') ~ lookbehind(Set('b')) ~ elem('b'))^^{case a~b=>a+""+b}), "ab")} should produce[IllegalArgumentException] + + apply( (elem('a') ~ not(lookbehind(Set(' ', '\t', '\n'))) ~ '*' ), "a*" ) + + } + + it should "parse chars in ranges" in { + val p = ranges(SortedMap('A' -> 'Z', '0' -> '9')) + apply(p, "B") should equal ('B') + apply(p, "A") should equal ('A') + apply(p, "Z") should equal ('Z') + apply(p, "5") should equal ('5') + apply(p, "0") should equal ('0') + apply(p, "9") should equal ('9') + evaluating (apply(p, "a")) should produce[IllegalArgumentException] + evaluating (apply(p, "z")) should produce[IllegalArgumentException] + evaluating (apply(p, "<")) should produce[IllegalArgumentException] + } + +} \ No newline at end of file diff --git a/core/markdown/src/test/scala/net/liftweb/markdown/BlockParsersTest.scala b/core/markdown/src/test/scala/net/liftweb/markdown/BlockParsersTest.scala new file mode 100644 index 0000000000..e0fe7637d7 --- /dev/null +++ b/core/markdown/src/test/scala/net/liftweb/markdown/BlockParsersTest.scala @@ -0,0 +1,55 @@ +package net.liftweb.markdown + +/* + * Copyright 2013 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Based on https://github.com/chenkelmann/actuarius originally developed by + * Christoph Henkelmann http://henkelmann.eu/ + */ + +import org.junit.runner.RunWith +import org.scalatest.junit.JUnitRunner +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.FlatSpec +import xml.{Group, NodeSeq} + +/** + * Tests the parsing on block level. + */ +@RunWith(classOf[JUnitRunner]) +class BlockParsersTest extends FlatSpec with ShouldMatchers with BlockParsers{ + + "The BlockParsers" should "parse optional empty lines" in { + val p = optEmptyLines + val el = new EmptyLine(" \n") + apply(p, Nil) should equal (Nil) + apply(p, List(el)) should equal (List(el)) + apply(p, List(el, el)) should equal (List(el, el)) + } + + it should "accept empty documents" in { + val p = markdown + val el = new EmptyLine(" \n") + apply(p, Nil) should equal (Nil) + apply(p, List(el)) should equal (Nil) + apply(p, List(el, el)) should equal (Nil) + } + + it should "detect line types" in { + val p = line(classOf[CodeLine]) + apply(p, List(new CodeLine(" ", "code"))) should equal (new CodeLine(" ", "code")) + evaluating(apply(p, List(new OtherLine("foo")))) should produce[IllegalArgumentException] + } +} \ No newline at end of file diff --git a/core/markdown/src/test/scala/net/liftweb/markdown/InlineParsersTest.scala b/core/markdown/src/test/scala/net/liftweb/markdown/InlineParsersTest.scala new file mode 100644 index 0000000000..294ea0a628 --- /dev/null +++ b/core/markdown/src/test/scala/net/liftweb/markdown/InlineParsersTest.scala @@ -0,0 +1,283 @@ +package net.liftweb.markdown + +/* + * Copyright 2013 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Based on https://github.com/chenkelmann/actuarius originally developed by + * Christoph Henkelmann http://henkelmann.eu/ + */ + +import org.scalatest.FlatSpec +import org.scalatest.matchers.ShouldMatchers +import org.junit.runner.RunWith +import org.scalatest.junit.JUnitRunner + +/** + * Tests Inline Parsing, i.e. emphasis , strong text, links, escapes etc. + */ +@RunWith(classOf[JUnitRunner]) +class InlineParsersTest extends FlatSpec with ShouldMatchers with InlineParsers{ + + /////////////////////////////////////////////////////////////// + // Inline parsing Tests // + /////////////////////////////////////////////////////////////// + def runSucceedingParsingTests(p:Parser[String], l:List[(String, String)]) { + for ((a, b) <- l) { + try { + apply(p, a) should equal (b) + } catch { + case e:Throwable => println("Input causing the failure was: '" + a + "'."); throw e; + } + } + } + + def runExceptionParsingTests(p:Parser[String], l:List[String]) { + for (s <- l) evaluating{apply(p, s)} should produce[IllegalArgumentException] + } + + val italicTests:List[(String, String)] = List( + ("*italic*", "italic"), + ("*italic * italic*", "italic * italic"), + ("_italic_", "italic")) + + val boldTests = List( + ("**bold**", "bold"), + ("**bold * bold**", "bold * bold"), + ("__bold__", "bold")) + + val codeTests = List( + ("`code`", "code"), + ("``code``", "code"), + ("` *italic* `", " *italic* "), + ("`code\ncode`", "code\ncode"), + ("``code ` code``", "code ` code") + ) + + val entityTest = List("Hello   World" -> "Hello   World", + "Hello & World" -> "Hello & World", + ("test è text" ->"test è text"), + "test è text" ->"test è text", + "testè text" -> "testè text", + "test è" -> "test è", + "test ω" -> "test ω", + "test &unknown;" -> "test &unknown;", + "AT&T" -> "AT&T", + " "<ATT", + "test hello" -> "test hello", + "test   hello ;" -> "test   hello ;") + + + val linkTests = List( + ("""[link text](http://example.com "link title")""", + """link text"""), + ("""[link text](http://example.com )""", + """link text"""), + ("""[link text]( http://example.com "link title" )""", + """link text"""), + ("""[link text]( http://example.com "li)nk" title" )""", + """link text""") + ) + + val fastLinkTests = List( + ("""""", + """http://www.example.com?foo=a&bar=b*""") + ) + + + val imageTests = List( + ("""![alt text](/src/img.png "img title")""", + """alt text"""), + ("""![alt text](/src/img.png )""", + """alt text"""), + ("""![alt text]( /src/img.png "img title" )""", + """alt text"""), + ("""![alt text]( /src/img.png "i)mg" title" )""", + """alt text""") + ) + + val brTests = List( + (" \n", "
    \n") + ) + + val xmlNameTests = List( + ("foo", "foo"), + ("foo_bar", "foo_bar"), + ("a", "a") + ) + + val xmlNameExTests = List( + "", + "foo/bar", + "foobar", + "foo\"bar", + "foo\\bar", + "foo bar" + ) + + val xmlStartTagTests = List( + ("", ""), + ("""""", """"""), + ("""""", """"""), + ("""""", """"""), + ("""""", """"""), + ("""""", """"""), + ("""""", """""") + ) + + val xmlEndTagTests = List( + ("", ""), + ("", "") + ) + + val xmlInlineTests = List( + ("""hallo *italic* ballo""", + """hallo italic ballo"""), + ("""hallo *italic* ballo""", + """hallo italic ballo"""), + ("""hallo *italic* ballo""", + """hallo italic ballo"""), + ("""hallo *italic* ballo""", + """hallo italic ballo""") + ) + + val mixedTests = List( + ("*italic* **bold** *italic*", "italic bold italic"), + ("*italic***bold***italic*", "italic*bolditalic*"), + ("***foo***", "foo") + ) + + + /** + * These should pass the inline replacement unchanged and can be used to be put between "real tests" to simualate + * intermediate text. + */ + val dummyTests = List( + ("lorem ipsum ", "lorem ipsum "), + (" lorem ipsum", " lorem ipsum"), + (" lorem \n ipsum ", " lorem \n ipsum ") + ) + + + val allInlineTests = italicTests ++ boldTests ++ entityTest ++ + codeTests ++ linkTests ++ fastLinkTests ++ imageTests ++ brTests ++ + xmlStartTagTests ++ xmlEndTagTests ++ xmlInlineTests ++ dummyTests + + it should "create italic text" in { + runSucceedingParsingTests(emAsterisk(new InlineContext())|emUnderscore(new InlineContext()) , italicTests) + } + + it should "create bold text" in { + runSucceedingParsingTests(strongAsterisk(new InlineContext())|strongUnderscore(new InlineContext()), boldTests) + } + + it should "create inline code" in { + runSucceedingParsingTests(code, codeTests) + } + + it should "create links" in { + runSucceedingParsingTests(link(new InlineContext()), linkTests) + } + + it should "create fast links" in { + runSucceedingParsingTests(fastLink(new InlineContext()), fastLinkTests) + val p = fastLink(new InlineContext()) + evaluating(apply(p, "")) should produce[IllegalArgumentException] + + } + + it should "create images" in { + runSucceedingParsingTests((elem('!')~>directImg), imageTests) + } + + it should "create line breaks" in { + runSucceedingParsingTests(br, brTests) + } + + it should "parse simplified xml identifiers" in { + runSucceedingParsingTests(xmlName, xmlNameTests) + runExceptionParsingTests(xmlName, xmlNameExTests) + } + + it should "parse opening xml tags and escape their attribute vals" in { + runSucceedingParsingTests(xmlStartOrEmptyTag, xmlStartTagTests) + } + + it should "parse closing xml tags" in { + runSucceedingParsingTests(xmlEndTag, xmlEndTagTests) + } + + it should "allow inline xml and escape its parameters" in { + runSucceedingParsingTests(inline(Map()), xmlInlineTests) + } + + it should "parse mixed inline cases" in { + runSucceedingParsingTests(inline(Map()), mixedTests) + } + + val ld1 = new LinkDefinition("id", "http://www.example.com", Some("Title")) + val ld2 = new LinkDefinition("id 2", "http://other.example.com", Some("Title 2")) + val ld3 = new LinkDefinition("id 3", "http://none.example.com", None) + val map = Map(ld1.id -> ld1, ld2.id -> ld2, ld3.id -> ld3) + val ctx = new InlineContext(map) + + it should "resolve references" in { + val p = ref(ctx) + apply(p, "[text][id]") should equal ((ld1, "text")) + apply(p, "[text] [id]") should equal ((ld1, "text")) + apply(p, "[id][]") should equal ((ld1, "id")) + apply(p, "[id] []") should equal ((ld1, "id")) + apply(p, "[id]") should equal ((ld1, "id")) + apply(p, "[Id]") should equal ((ld1, "Id")) + } + + it should "resolve reference links" in { + val p = inline(map) + apply(p, "[text][id]") should equal ("""text""") + apply(p, "[text] [id]") should equal ("""text""") + apply(p, "[id][]") should equal ("""id""") + apply(p, "[id] []") should equal ("""id""") + apply(p, "[id]") should equal ("""id""") + apply(p, "[Id]") should equal ("""Id""") + + apply(p, "[id] [Id 2]") should equal ("""id""") + apply(p, "[id 3]") should equal ("""id 3""") + apply(p, "[foo \"bar\"][id 3]") should equal ("""foo "bar"""") + } + + it should "resolve reference images" in { + val p = inline(map) + apply(p, "![text][id]") should equal ("""text""") + apply(p, "![text] [id]") should equal ("""text""") + apply(p, "![id][]") should equal ("""id""") + apply(p, "![id] []") should equal ("""id""") + apply(p, "![id]") should equal ("""id""") + apply(p, "![Id]") should equal ("""Id""") + + apply(p, "![id] [Id 2]") should equal ("""id""") + apply(p, "![id 3]") should equal ("""id 3""") + apply(p, "![foo \"bar\"][id 3]") should equal ("""foo "bar"""") + } + + it should "handle all inline cases with the inline replacer" in { + runSucceedingParsingTests(inline(Map()), allInlineTests) + val concatTests = for ( + (a1, a2) <- allInlineTests; + (b1, b2) <- allInlineTests; + (c1, c2) <- allInlineTests) yield (a1+ " " + b1 + " " + c1, a2 + " " + b2 + " " +c2); + + runSucceedingParsingTests(inline(Map()), concatTests) + } +} diff --git a/core/markdown/src/test/scala/net/liftweb/markdown/LineParsersTest.scala b/core/markdown/src/test/scala/net/liftweb/markdown/LineParsersTest.scala new file mode 100644 index 0000000000..f7b8303fee --- /dev/null +++ b/core/markdown/src/test/scala/net/liftweb/markdown/LineParsersTest.scala @@ -0,0 +1,157 @@ +package net.liftweb.markdown + +/* + * Copyright 2013 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Based on https://github.com/chenkelmann/actuarius originally developed by + * Christoph Henkelmann http://henkelmann.eu/ + */ + +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.FlatSpec +import org.junit.runner.RunWith +import org.scalatest.junit.JUnitRunner + +/** + * tests parsing of individual lines + */ +@RunWith(classOf[JUnitRunner]) +class LineParsersTest extends FlatSpec with ShouldMatchers with LineParsers{ + + "The LineParsers" should "parse horizontal rulers" in { + val p = ruler + apply(p, "---") should equal (new RulerLine("---")) + apply(p, "***") should equal (new RulerLine("***")) + apply(p, "---------") should equal (new RulerLine("---------")) + apply(p, "*********") should equal (new RulerLine("*********")) + apply(p, "- - -") should equal (new RulerLine("- - -")) + apply(p, "* * *") should equal (new RulerLine("* * *")) + apply(p, " ---") should equal (new RulerLine(" ---")) + apply(p, " ***") should equal (new RulerLine(" ***")) + apply(p, " - - - ----") should equal (new RulerLine(" - - - ----")) + apply(p, " * * * ****") should equal (new RulerLine(" * * * ****")) + } + + it should "parse a ruler that starts like a setext header line" in { + val p = setext2OrRulerOrUItem + apply(p, "- - -") should equal (new RulerLine("- - -")) + } + + it should "parse a setext style level 1 header underline" in { + val p = setextHeader1 + apply(p, "=") should equal (new SetExtHeaderLine("=", 1)) + apply(p, "== ") should equal (new SetExtHeaderLine("== ", 1)) + apply(p, "========== \t ") should equal (new SetExtHeaderLine("========== \t ", 1)) + } + + it should "parse a setext style level 2 header underline" in { + val p = setextHeader2 + apply(p, "-") should equal (new SetExtHeaderLine("-", 2)) + apply(p, "-- ") should equal (new SetExtHeaderLine("-- ", 2)) + apply(p, "---------- \t ") should equal (new SetExtHeaderLine("---------- \t ", 2)) + } + + it should "parse an atx header line" in { + val p = atxHeader + apply(p, "#foo") should equal (new AtxHeaderLine("#", "foo")) + apply(p, "## #foo##") should equal (new AtxHeaderLine("##", " #foo##")) + } + + it should "parse an empty line" in { + val p = emptyLine + apply (p, "") should equal (new EmptyLine("")) + apply (p, " \t ") should equal (new EmptyLine(" \t ")) + evaluating (apply (p, " not empty ")) should produce[IllegalArgumentException] + } + + it should "parse arbitrary lines as OtherLine tokens" in { + val p = otherLine + apply(p, "a line") should equal (new OtherLine("a line")) + } + + it should "parse quoted block lines" in { + val p = blockquoteLine + apply(p, "> quote") should equal (new BlockQuoteLine("> ", "quote")) + apply(p, "> codequote") should equal (new BlockQuoteLine("> ", " codequote")) + apply(p, " > codequote") should equal (new BlockQuoteLine(" > ", " codequote")) + evaluating(apply(p, "not a quote")) should produce[IllegalArgumentException] + } + + it should "parse unordered item start lines" in { + val p = uItemStartLine + apply(p, "* foo") should equal (new UItemStartLine("* ", "foo")) + apply(p, " * foo") should equal (new UItemStartLine(" * ", "foo")) + apply(p, " * foo") should equal (new UItemStartLine(" * ", "foo")) + apply(p, " * foo") should equal (new UItemStartLine(" * ", "foo")) + apply(p, " * foo") should equal (new UItemStartLine(" * ", "foo")) + apply(p, " * \t foo") should equal (new UItemStartLine(" * \t ", "foo")) + apply(p, " * \t foo ") should equal (new UItemStartLine(" * \t ", "foo ")) + + evaluating(apply(p, "*foo")) should produce[IllegalArgumentException] + evaluating(apply(p, " * foo")) should produce[IllegalArgumentException] + evaluating(apply(p, "1. foo")) should produce[IllegalArgumentException] + + apply(p, "* foo") should equal (new UItemStartLine("* ", "foo")) + apply(p, "+ foo") should equal (new UItemStartLine("+ ", "foo")) + apply(p, "- foo") should equal (new UItemStartLine("- ", "foo")) + } + + + it should "parse ordered item start lines" in { + val p = oItemStartLine + apply(p, "1. foo") should equal (OItemStartLine("1. ", "foo")) + apply(p, " 12. foo") should equal (OItemStartLine(" 12. ", "foo")) + apply(p, " 0. foo") should equal (OItemStartLine(" 0. ", "foo")) + apply(p, " 44444444. foo") should equal (OItemStartLine(" 44444444. ", "foo")) + apply(p, " 465789. foo") should equal (OItemStartLine(" 465789. ", "foo")) + apply(p, " 4455. \t foo") should equal (OItemStartLine(" 4455. \t ", "foo")) + apply(p, " 9. \t foo ") should equal (OItemStartLine(" 9. \t ", "foo ")) + + evaluating(apply(p, "1.foo")) should produce[IllegalArgumentException] + evaluating(apply(p, " 1. foo")) should produce[IllegalArgumentException] + evaluating(apply(p, "* foo")) should produce[IllegalArgumentException] + } + + it should "parse link definitions" in { + val p = linkDefinitionStart + apply(p, "[foo]: http://example.com/ \"Optional Title Here\"") should equal ( + new LinkDefinitionStart("foo", "http://example.com/"), Some("Optional Title Here")) + apply(p, "[foo]: http://example.com/") should equal ( + new LinkDefinitionStart("foo", "http://example.com/"), None) + apply(p, "[Foo]: http://example.com/ 'Optional Title Here'") should equal ( + new LinkDefinitionStart("foo", "http://example.com/"), Some("Optional Title Here")) + apply(p, "[Foo]: http://example.com/?bla=<> (Optional Title Here)") should equal ( + new LinkDefinitionStart("foo", "http://example.com/?bla=<>"), Some("Optional Title Here")) + apply(p, "[Foo]: http://example.com/?bla=<> (Optional Title Here)") should equal ( + new LinkDefinitionStart("foo", "http://example.com/?bla=<>"), Some("Optional Title Here")) + } + + it should "parse link titles" in { + val p = linkDefinitionTitle + apply(p, " (Optional Title Here) ") should equal ("Optional Title Here") + } + + it should "parse openings of fenced code blocks" in { + val p = fencedCodeStartOrEnd + apply(p, "```") should equal ( + new FencedCode("```")) + apply(p, " ```\t") should equal ( + new FencedCode(" ```\t")) + apply(p, " ``` \t ") should equal ( + new FencedCode(" ``` \t ")) + apply(p, " ``` \t java \t ") should equal ( + new ExtendedFencedCode(" ``` \t ", "java \t ")) + } +} \ No newline at end of file diff --git a/core/markdown/src/test/scala/net/liftweb/markdown/LineTokenizerTest.scala b/core/markdown/src/test/scala/net/liftweb/markdown/LineTokenizerTest.scala new file mode 100644 index 0000000000..67a45c654b --- /dev/null +++ b/core/markdown/src/test/scala/net/liftweb/markdown/LineTokenizerTest.scala @@ -0,0 +1,87 @@ +package net.liftweb.markdown + +/* + * Copyright 2013 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Based on https://github.com/chenkelmann/actuarius originally developed by + * Christoph Henkelmann http://henkelmann.eu/ + */ + +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.FlatSpec +import org.junit.runner.RunWith +import org.scalatest.junit.JUnitRunner + +/** + * Tests the Line Tokenizer that prepares input for parsing. + */ +@RunWith(classOf[JUnitRunner]) +class LineTokenizerTest extends LineTokenizer with FlatSpec with ShouldMatchers{ + + "The LineTokenizer" should "split input lines correctly" in { + splitLines("line1\nline2\n") should equal (List("line1", "line2")) + splitLines("line1\nline2 no nl") should equal (List("line1", "line2 no nl")) + splitLines("test1\n\ntest2\n") should equal (List("test1", "", "test2")) + splitLines("test1\n\ntest2\n\n") should equal (List("test1", "", "test2")) + splitLines("\n\n") should equal (Nil) + splitLines("\n") should equal (Nil) + splitLines("") should equal (List("")) + } + + it should "preprocess the input correctly" in { + tokenize("[foo]: http://example.com/ \"Optional Title Here\"") should equal( + (new MarkdownLineReader(List(), Map( "foo"->new LinkDefinition("foo", "http://example.com/", Some("Optional Title Here")) )) ) ) + + tokenize( +"""[Baz]: http://foo.bar +'Title next line' +some text +> bla + +[fOo]: http://www.example.com "A Title" +more text +[BAR]: (Also a title)""" + ) should equal ( new MarkdownLineReader( List( +new OtherLine("some text"), +new BlockQuoteLine("> ", "bla"), +new EmptyLine(""), +new OtherLine("more text") + ), Map ( +"bar"->new LinkDefinition("bar", "http://www.example.com/bla", Some("Also a title")), +"baz"->new LinkDefinition("baz", "http://foo.bar", Some("Title next line")), +"foo"->new LinkDefinition("foo", "http://www.example.com", Some("A Title")) + ))) + + } + + it should "parse different line types" in { + def p(line:String) = { + lineToken(new LineReader(Seq(line))) match { + case Success(result, _) => result + } + } + p("a line") should equal (new OtherLine("a line")) + p(" a code line") should equal (new CodeLine(" ", "a code line")) + p("#a header#") should equal (new AtxHeaderLine("#", "a header#")) + p("> a quote") should equal (new BlockQuoteLine("> ", "a quote")) + p(" \t ") should equal (new EmptyLine(" \t ")) + p("* an item") should equal (new UItemStartLine("* ", "an item")) + p("- an item") should equal (new UItemStartLine("- ", "an item")) + p("+ an item") should equal (new UItemStartLine("+ ", "an item")) + p("===") should equal (new SetExtHeaderLine("===", 1)) + p("--- ") should equal (new SetExtHeaderLine("--- ", 2)) + p("- - -") should equal (new RulerLine("- - -")) + } +} \ No newline at end of file diff --git a/core/markdown/src/test/scala/net/liftweb/markdown/TransformerTest.scala b/core/markdown/src/test/scala/net/liftweb/markdown/TransformerTest.scala new file mode 100644 index 0000000000..5d3edbabe6 --- /dev/null +++ b/core/markdown/src/test/scala/net/liftweb/markdown/TransformerTest.scala @@ -0,0 +1,356 @@ +package net.liftweb.markdown + +/* + * Copyright 2013 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Based on https://github.com/chenkelmann/actuarius originally developed by + * Christoph Henkelmann http://henkelmann.eu/ + */ + +import org.scalatest.matchers.ShouldMatchers +import org.scalatest.FlatSpec +import org.junit.runner.RunWith +import org.scalatest.junit.JUnitRunner + +/** + * Tests the behavior of the complete parser, i.e. all parsing steps together. + */ +@RunWith(classOf[JUnitRunner]) +class TransformerTest extends FlatSpec with ShouldMatchers with Transformer { + + "The Transformer" should "create xhtml fragments from markdown" in { + apply("") should equal ("") + apply("\n") should equal ("") + apply("Paragraph1\n") should equal ( + "

    Paragraph1

    \n") + apply("Paragraph1\n\nParagraph2\n") should equal ( + "

    Paragraph1

    \n

    Paragraph2

    \n") + apply("Paragraph1 *italic*\n") should equal ( + "

    Paragraph1 italic

    \n") + apply("\n\nParagraph1\n") should equal ( + "

    Paragraph1

    \n") + } + + it should "parse code blocks" in { + apply(" foo\n") should equal ("
    foo\n
    \n") + apply("\tfoo\n") should equal ("
    foo\n
    \n") + apply(" foo\n bar\n") should equal ("
    foo\nbar\n
    \n") + apply(" foo\n \n bar\n") should equal ("
    foo\n  \nbar\n
    \n") + apply(" foo\n\tbaz\n \n bar\n") should equal ("
    foo\nbaz\n  \nbar\n
    \n") + apply(" public static void main(String[] args)\n") should equal ("
    public static void main(String[] args)\n
    \n") + } + + it should "parse paragraphs" in { + apply( +"""Lorem ipsum dolor sit amet, +consetetur sadipscing elitr, +sed diam nonumy eirmod tempor invidunt ut +""") should equal ( +"""

    Lorem ipsum dolor sit amet, +consetetur sadipscing elitr, +sed diam nonumy eirmod tempor invidunt ut

    +""") + } + + it should "parse multiple paragraphs" in { + apply("test1\n\ntest2\n") should equal ("

    test1

    \n

    test2

    \n") +apply( +"""test + +test + +test""" +) should equal ( +"""

    test

    +

    test

    +

    test

    +""" +) + } + + it should "parse block quotes" in { + apply("> quote\n> quote2\n") should equal("

    quote\nquote2

    \n
    \n") + } + + + + it should "parse ordered and unordered lists" in { + apply("* foo\n* bar\n* baz\n") should equal ( +"""
      +
    • foo
    • +
    • bar
    • +
    • baz
    • +
    +""" + ) + apply("+ foo\n+ bar\n+ baz\n") should equal ( +"""
      +
    • foo
    • +
    • bar
    • +
    • baz
    • +
    +""" +) + apply("- foo\n- bar\n- baz\n") should equal ( +"""
      +
    • foo
    • +
    • bar
    • +
    • baz
    • +
    +""" +) + apply("- foo\n+ bar\n* baz\n") should equal ( +"""
      +
    • foo
    • +
    • bar
    • +
    • baz
    • +
    +""" +) + apply("1. foo\n22. bar\n10. baz\n") should equal ( +"""
      +
    1. foo
    2. +
    3. bar
    4. +
    5. baz
    6. +
    +""" + ) + apply("* foo\n\n* bar\n\n* baz\n\n") should equal ( +"""
      +
    • foo

      +
    • +
    • bar

      +
    • +
    • baz

      +
    • +
    +""" + ) + apply("* foo\n\n* bar\n* baz\n") should equal ( +"""
      +
    • foo

      +
    • +
    • bar

      +
    • +
    • baz
    • +
    +""" + ) + apply("""* foo + +* bar +* baz + +* bam +""") should equal ( +"""
      +
    • foo

      +
    • +
    • bar

      +
    • +
    • baz
    • +
    • bam

      +
    • +
    +""" + ) + apply("""* foo + ++ bar +- baz + +* bam +""") should equal ( +"""
      +
    • foo

      +
    • +
    • bar

      +
    • +
    • baz
    • +
    • bam

      +
    • +
    +""" + ) + } + + it should "stop a list after an empty line" in { +apply("""1. a +2. b + +paragraph""" + ) should equal ( +"""
      +
    1. a
    2. +
    3. b
    4. +
    +

    paragraph

    +""" +) + + } + + it should "recursively evaluate quotes" in { + apply("> foo\n> > bar\n> \n> baz\n") should equal ( +"""

    foo

    +

    bar

    +
    +

    baz

    +
    +""" + ) + } + + it should "handle corner cases for bold and italic in paragraphs" in { + apply("*foo * bar *\n") should equal ("

    *foo * bar *

    \n") + apply("*foo * bar*\n") should equal ("

    foo * bar

    \n") + apply("*foo* bar*\n") should equal ("

    foo bar*

    \n") + apply("**foo* bar*\n") should equal ("

    *foo bar*

    \n") + apply("**foo* bar**\n") should equal ("

    foo* bar

    \n") + apply("** foo* bar **\n") should equal ("

    ** foo* bar **

    \n") + } + + it should "resolve referenced links" in { + apply("""An [example][id]. Then, anywhere +else in the doc, define the link: + + [id]: http://example.com/ "Title" +""") should equal ("""

    An example. Then, anywhere +else in the doc, define the link:

    +""") + } + + it should "parse atx style headings" in { + apply("#A Header\n") should equal ("

    A Header

    \n") + apply("###A Header\n") should equal ("

    A Header

    \n") + apply("### A Header \n") should equal ("

    A Header

    \n") + apply("### A Header##\n") should equal ("

    A Header

    \n") + apply("### A Header## \n") should equal ("

    A Header

    \n") + apply("### A Header ## \n") should equal ("

    A Header

    \n") + apply("### A Header ## foo ## \n") should equal ("

    A Header ## foo

    \n") + } + + it should "parse setext style level 1 headings" in { + apply("A Header\n===\n") should equal ("

    A Header

    \n") + apply("A Header\n=\n") should equal ("

    A Header

    \n") + apply(" A Header \n========\n") should equal ("

    A Header

    \n") + apply(" A Header \n=== \n") should equal ("

    A Header

    \n") + apply(" ==A Header== \n======\n") should equal ("

    ==A Header==

    \n") + apply("##Header 1==\n= \n") should equal ("

    ##Header 1==

    \n") + } + + it should "parse setext style level 2 headings" in { + apply("A Header\n---\n") should equal ("

    A Header

    \n") + apply("A Header\n-\n") should equal ("

    A Header

    \n") + apply(" A Header \n--------\n") should equal ("

    A Header

    \n") + apply(" A Header \n--- \n") should equal ("

    A Header

    \n") + apply(" --A Header-- \n------\n") should equal ("

    --A Header--

    \n") + } + + + it should "parse xml-like blocks as is" in { + apply(" bla\nblub hallo\n\n") should equal ( + " bla\nblub hallo\n\n") + } + + it should "parse fenced code blocks" in { +apply( +"""``` foobar +System.out.println("Hello World!"); + + verbatim xml + + <-not a space-style code line + 1. not a + 2. list + +## not a header +``` gotcha: not the end +----------- +but this is: +``` +""" +) should equal ( +"""
    System.out.println("Hello World!");
    +    
    +<some> verbatim xml </some>
    +    
    +    <-not a space-style code line
    + 1. not a
    + 2. list
    +    
    +## not a header
    +``` gotcha: not the end
    +-----------
    +but this is:
    +
    +""" +) + +apply( +"""``` +System.out.println("Hello World!"); +``` +And now to something completely different. + old style code +""" +) should equal ( +"""
    System.out.println("Hello World!");
    +
    +

    And now to something completely different.

    +
    old style code
    +
    +""" +) + +apply( +"""``` +System.out.println("Hello World!"); +No need to end blocks + +And now to something completely different. + old style code +""" +) should equal ( +"""
    System.out.println("Hello World!");
    +No need to end blocks
    +
    +And now to something completely different.
    +    old style code
    +
    +""" +) + +apply( +"""Some text first +``` +System.out.println("Hello World!"); +No need to end blocks + +And now to something completely different. + old style code +""" +) should equal ( +"""

    Some text first

    +
    System.out.println("Hello World!");
    +No need to end blocks
    +
    +And now to something completely different.
    +    old style code
    +
    +""" +) + } +} \ No newline at end of file diff --git a/core/util/src/main/scala/net/liftweb/util/AnyVar.scala b/core/util/src/main/scala/net/liftweb/util/AnyVar.scala index 2b1269bea7..17b5053216 100644 --- a/core/util/src/main/scala/net/liftweb/util/AnyVar.scala +++ b/core/util/src/main/scala/net/liftweb/util/AnyVar.scala @@ -103,11 +103,49 @@ trait AnyVarTrait[T, MyType <: AnyVarTrait[T, MyType]] extends PSettableValueHol protected def findFunc(name: String): Box[T] protected def setFunc(name: String, value: T): Unit protected def clearFunc(name: String): Unit + + private def _setFunc(name: String, value: T) { + setFunc(name, value) + + val sd = settingDefault_? + changeFuncs.foreach(f => Helpers.tryo(f(Full(value), sd))) + } + + private def _clearFunc(name: String) { + clearFunc(name) + changeFuncs.foreach(f => Helpers.tryo(f(Empty, false))) + } + protected def wasInitialized(name: String): Boolean + private var changeFuncs: List[FuncType] = Nil + /** + * The function takes a `Box[T]` (Full if the Var is being set, Empty if it's being cleared) and + * a Boolean indicating that the set function is setting to the default value. + * + */ + type FuncType = (Box[T], Boolean) => Unit protected def calcDefaultValue: T + + /** + * On any change to this Var, invoke the function. Changes are setting the value, clearing the value. + * There may not be a call if the Var goes out of scope (e.g., a RequestVar at the end of the Request). + * + * The function takes a `Box[T]` (Full if the Var is being set, Empty if it's being cleared) and + * a Boolean indicating that the set function is setting to the default value. + * + * The function should execute *very* quickly (e.g., Schedule a function to be executed on a different thread). + * + * The function should generally be set in Boot or when a singleton is created. + * + * @param f the function to execute on change + */ + def onChange(f: FuncType) { + changeFuncs ::= f + } + /** * A non-side-effecting test if the value was initialized */ @@ -173,13 +211,19 @@ trait AnyVarTrait[T, MyType <: AnyVarTrait[T, MyType]] extends PSettableValueHol /** * Set the Var if it has not been calculated */ - def setIsUnset(value: => T): T = doSync { + def setIfUnset(value: => T): T = doSync { if (!set_?) { set(value) } this.is } + /** + * Set the Var if it has not been calculated + */ + @deprecated("use setIfUnset") + def setIsUnset(value: => T): T = setIfUnset(value) + /** * Set the session variable * @@ -187,7 +231,8 @@ trait AnyVarTrait[T, MyType <: AnyVarTrait[T, MyType]] extends PSettableValueHol */ def apply(what: T): T = { testInitialized - setFunc(name, what) + _setFunc(name, what) + what } @@ -202,7 +247,10 @@ trait AnyVarTrait[T, MyType <: AnyVarTrait[T, MyType]] extends PSettableValueHol is } - def remove(): Unit = clearFunc(name) + def remove(): Unit = { + _clearFunc(name) + + } //def cleanupFunc: Box[() => Unit] = Empty @@ -228,13 +276,13 @@ trait AnyVarTrait[T, MyType <: AnyVarTrait[T, MyType]] extends PSettableValueHol */ def doWith[F](newVal: T)(f: => F): F = { val old = findFunc(name) - setFunc(name, newVal) + _setFunc(name, newVal) try { f } finally { old match { - case Full(t) => setFunc(name, t) - case _ => clearFunc(name) + case Full(t) => _setFunc(name, t) + case _ => _clearFunc(name) } } } diff --git a/core/util/src/main/scala/net/liftweb/util/BindHelpers.scala b/core/util/src/main/scala/net/liftweb/util/BindHelpers.scala index b902197f35..a2e4349bc0 100644 --- a/core/util/src/main/scala/net/liftweb/util/BindHelpers.scala +++ b/core/util/src/main/scala/net/liftweb/util/BindHelpers.scala @@ -147,6 +147,23 @@ trait BindHelpers { case _ => elem } + + /** + * Remove an attribute from the element + * + * @param name the name of the attribute to remove + * @param elem the element + * @return the element sans the named attribute + */ + def removeAttribute(name: String, elem: Elem): Elem = { + val a = elem.attributes.filter{ + case up: UnprefixedAttribute => up.key != name + case _ => true + } + + elem.copy(attributes = a) + } + /** * Adds a css class to the existing class tag of an Elem or create * the class attribute @@ -1255,7 +1272,7 @@ trait BindHelpers { new Elem(e.prefix, e.label, new UnprefixedAttribute("id", id, meta), - e.scope, e.child :_*) + e.scope, e.minimizeEmpty, e.child :_*) } case x => x diff --git a/core/util/src/main/scala/net/liftweb/util/ClassHelpers.scala b/core/util/src/main/scala/net/liftweb/util/ClassHelpers.scala index 538d9d333c..5a737b226d 100644 --- a/core/util/src/main/scala/net/liftweb/util/ClassHelpers.scala +++ b/core/util/src/main/scala/net/liftweb/util/ClassHelpers.scala @@ -151,12 +151,6 @@ trait ClassHelpers { self: ControlHelpers => def findClass(where: List[(String, List[String])]): Box[Class[AnyRef]] = findType[AnyRef](where) - @deprecated("Use StringHelpers.camelify", "2.3") - def camelCase(name : String): String = StringHelpers.camelify(name) - @deprecated("Use StringHelpers.camelifyMethod", "2.3") - def camelCaseMethod(name: String): String = StringHelpers.camelifyMethod(name) - @deprecated("Use StringHelpers.snakify", "2.3") - def unCamelCase(name : String) = StringHelpers.snakify(name) /** * @return true if the method is public and has no parameters diff --git a/core/util/src/main/scala/net/liftweb/util/CssSel.scala b/core/util/src/main/scala/net/liftweb/util/CssSel.scala index 9d68384263..a13f494868 100644 --- a/core/util/src/main/scala/net/liftweb/util/CssSel.scala +++ b/core/util/src/main/scala/net/liftweb/util/CssSel.scala @@ -136,7 +136,7 @@ private class SelectorMap(binds: List[CssBind]) extends Function1[NodeSeq, NodeS elemMap += (id -> sortBinds(i :: elemMap.getOrElse(id, Nil))) - case i@CssBind(StarSelector(_)) => starFunc = Full(sortBinds(i :: starFunc.openOr(Nil))) + case i@CssBind(StarSelector(_, _)) => starFunc = Full(sortBinds(i :: starFunc.openOr(Nil))) case i@CssBind(NameSelector(name, _)) => nameMap += (name -> sortBinds(i :: nameMap.getOrElse(name, Nil))) @@ -175,18 +175,18 @@ private class SelectorMap(binds: List[CssBind]) extends Function1[NodeSeq, NodeS case _ => false } - final def applyRule(bindList: List[CssBind], realE: Elem, onlySelThis: Boolean): NodeSeq = + final def applyRule(bindList: List[CssBind], realE: Elem, onlySelThis: Boolean, depth: Int): NodeSeq = bindList match { case Nil => realE // ignore selectThis commands outside the // select context case bind :: xs - if onlySelThis && isSelThis(bind) => applyRule(xs, realE, onlySelThis) + if onlySelThis && isSelThis(bind) => applyRule(xs, realE, onlySelThis, depth) case bind :: xs => { - applyRule(bind, realE) flatMap { - case e: Elem => applyRule(xs, e, onlySelThis) + applyRule(bind, realE, depth) flatMap { + case e: Elem => applyRule(xs, e, onlySelThis, depth + 1) case x => x } } @@ -210,9 +210,7 @@ private class SelectorMap(binds: List[CssBind]) extends Function1[NodeSeq, NodeS new UnprefixedAttribute(attr, flat, filtered) } - new Elem(elem.prefix, - elem.label, newAttr, - elem.scope, elem.child: _*) + elem.copy(attributes = newAttr) } case (elem, (bind, AttrAppendSubNode(attr))) => { @@ -290,7 +288,7 @@ private class SelectorMap(binds: List[CssBind]) extends Function1[NodeSeq, NodeS // This is where the rules are applied - final def applyRule(bind: CssBind, realE: Elem): NodeSeq = { + final def applyRule(bind: CssBind, realE: Elem, depth: Int): NodeSeq = { def uniqueClasses(cv: String*): String = { import Helpers._ @@ -460,10 +458,14 @@ private class SelectorMap(binds: List[CssBind]) extends Function1[NodeSeq, NodeS } buff ++= bind } - final def forStar(buff: ListBuffer[CssBind]) { + final def forStar(buff: ListBuffer[CssBind], depth: Int) { for { - bind <- starFunc - } buff ++= bind + binds <- starFunc + bind <- binds if (bind match { + case CssBind(StarSelector(_, topOnly)) => !topOnly || (depth == 0) + case _ => true + }) + } buff += bind } final def forName(in: Elem, buff: ListBuffer[CssBind]) { @@ -542,7 +544,7 @@ private class SelectorMap(binds: List[CssBind]) extends Function1[NodeSeq, NodeS } } - final private def treatElem(e: Elem, onlySel: Boolean): NodeSeq = { + final private def treatElem(e: Elem, onlySel: Boolean, depth: Int): NodeSeq = { val slurp = slurpAttrs(e.attributes) val lb = new ListBuffer[CssBind] @@ -551,12 +553,12 @@ private class SelectorMap(binds: List[CssBind]) extends Function1[NodeSeq, NodeS slurp.forClass(e, lb) slurp.forElem(e, lb) slurp.forAttr(e, lb) - slurp.forStar(lb) + slurp.forStar(lb, depth) if (onlySel) { lb.toList.filter(_.selectThis_?) match { case Nil => { - run(e.child, onlySel) + run(e.child, onlySel, depth + 1) NodeSeq.Empty } @@ -568,21 +570,21 @@ private class SelectorMap(binds: List[CssBind]) extends Function1[NodeSeq, NodeS } else { lb.toList.filterNot(_.selectThis_?) match { case Nil => new Elem(e.prefix, e.label, - e.attributes, e.scope, run(e.child, onlySel): _*) + e.attributes, e.scope, run(e.child, onlySel, depth + 1): _*) case csb => // do attributes first, then the body csb.partition(_.attrSel_?) match { - case (Nil, rules) => slurp.applyRule(rules, e, onlySel) + case (Nil, rules) => slurp.applyRule(rules, e, onlySel, depth) case (attrs, Nil) => { val elem = slurp.applyAttributeRules(attrs, e) new Elem(elem.prefix, elem.label, - elem.attributes, elem.scope, run(elem.child, onlySel): _*) + elem.attributes, elem.scope, run(elem.child, onlySel, depth + 1): _*) } case (attrs, rules) => { slurp.applyRule(rules, slurp.applyAttributeRules(attrs, e), - onlySel) + onlySel, depth) } } // slurp.applyRule(csb, e, onlySel) @@ -593,20 +595,20 @@ private class SelectorMap(binds: List[CssBind]) extends Function1[NodeSeq, NodeS final def apply(in: NodeSeq): NodeSeq = selectThis match { case Full(_) => { try { - run(in, true) + run(in, true, 0) } catch { case RetryWithException(newElem) => - run(newElem, false) + run(newElem, false, 0) } } - case _ => run(in, false) + case _ => run(in, false, 0) } - final private def run(in: NodeSeq, onlyRunSel: Boolean): NodeSeq = + final private def run(in: NodeSeq, onlyRunSel: Boolean, depth: Int): NodeSeq = in flatMap { - case Group(g) => run(g, onlyRunSel) - case e: Elem => treatElem(e, onlyRunSel) + case Group(g) => run(g, onlyRunSel, depth) + case e: Elem => treatElem(e, onlyRunSel, depth) case x => x } } diff --git a/core/util/src/main/scala/net/liftweb/util/CssSelector.scala b/core/util/src/main/scala/net/liftweb/util/CssSelector.scala index e34f960a4b..a7eb25974f 100644 --- a/core/util/src/main/scala/net/liftweb/util/CssSelector.scala +++ b/core/util/src/main/scala/net/liftweb/util/CssSelector.scala @@ -32,7 +32,7 @@ final case class ElemSelector(elem: String, subNodes: Box[SubNode]) extends def withSubnode(sn: SubNode): CssSelector = this.copy(subNodes = Full(sn)) } -final case class StarSelector(subNodes: Box[SubNode]) extends CssSelector { +final case class StarSelector(subNodes: Box[SubNode], singleDepth: Boolean) extends CssSelector { def withSubnode(sn: SubNode): CssSelector = this.copy(subNodes = Full(sn)) } @@ -157,16 +157,6 @@ object CssSelectorParser extends PackratParsers with ImplicitConversions { private implicit def str2chars(s: String): List[Char] = new scala.collection.immutable.WrappedString(s).toList - private lazy val _topParser: Parser[CssSelector] = { - phrase(idMatch | - nameMatch | - classMatch | - attrMatch | - elemMatch | - starMatch | - colonMatch) - } - private def fixAll(all: List[CssSelector], sn: Option[SubNode]): CssSelector = { (all, sn) match { // case (Nil, Some()) @@ -179,10 +169,10 @@ object CssSelectorParser extends PackratParsers with ImplicitConversions { private val atEnd = Parser { in => if(in.atEnd) Success(CharSequenceReader.EofCh, in) else Failure("", in)} private lazy val topParser: Parser[CssSelector] = - phrase(rep1((_idMatch | _nameMatch | _classMatch | _attrMatch | _elemMatch | + phrase(rep1((_idMatch | _dataNameMatch | _nameMatch | _classMatch | _attrMatch | _elemMatch | _colonMatch | _starMatch) <~ (rep1(' ') | atEnd)) ~ opt(subNode)) ^^ { case (one :: Nil) ~ sn => fixAll(List(one), sn) - case all ~ None if all.takeRight(1).head == StarSelector(Empty) => + case all ~ None if all.takeRight(1).head == StarSelector(Empty, false) => fixAll(all.dropRight(1), Some(KidsSubNode())) case all ~ sn => fixAll(all, sn) } @@ -198,63 +188,31 @@ object CssSelectorParser extends PackratParsers with ImplicitConversions { case "submit" => AttrSelector("type", "submit", Empty) case "text" => AttrSelector("type", "text", Empty) } - - private lazy val colonMatch: Parser[CssSelector] = - ':' ~> id ~ opt(subNode) ^? { - case "button" ~ sn => AttrSelector("type", "button", sn) - case "checkbox" ~ sn => AttrSelector("type", "checkbox", sn) - case "file" ~ sn => AttrSelector("type", "file", sn) - case "password" ~ sn => AttrSelector("type", "password", sn) - case "radio" ~ sn => AttrSelector("type", "radio", sn) - case "reset" ~ sn => AttrSelector("type", "reset", sn) - case "submit" ~ sn => AttrSelector("type", "submit", sn) - case "text" ~ sn => AttrSelector("type", "text", sn) - } - - - private lazy val idMatch: Parser[CssSelector] = '#' ~> id ~ opt(subNode) ^^ { - case id ~ sn => IdSelector(id, sn) - } private lazy val _idMatch: Parser[CssSelector] = '#' ~> id ^^ { case id => IdSelector(id, Empty) } - private lazy val nameMatch: Parser[CssSelector] = '@' ~> id ~ opt(subNode) ^^ { - case name ~ sn => NameSelector(name, sn) - } - private lazy val _nameMatch: Parser[CssSelector] = '@' ~> id ^^ { case name => NameSelector(name, Empty) } - private lazy val elemMatch: Parser[CssSelector] = id ~ opt(subNode) ^^ { - case elem ~ sn => ElemSelector(elem, sn) - } - private lazy val _elemMatch: Parser[CssSelector] = id ^^ { case elem => ElemSelector(elem, Empty) } - private lazy val starMatch: Parser[CssSelector] = '*' ~> opt(subNode) ^^ { - case sn => StarSelector(sn) - } + private lazy val _starMatch: Parser[CssSelector] = ('*' ^^ { + case sn => StarSelector(Empty, false) + }) | ( + '^' ^^ { + case sn => StarSelector(Empty, true) + } + ) - private lazy val _starMatch: Parser[CssSelector] = '*' ^^ { - case sn => StarSelector(Empty) + private lazy val _dataNameMatch: Parser[CssSelector] = ';' ~> id ^^ { + case name => AttrSelector("data-name", name, Empty) } - private lazy val classMatch: Parser[CssSelector] = - '.' ~> attrName ~ opt(subNode) ^^ { - case cls ~ sn => ClassSelector(cls, sn) - } - - private lazy val attrMatch: Parser[CssSelector] = - attrName ~ '=' ~ attrConst ~ opt(subNode) ^^ { - case "id" ~ _ ~ const ~ sn => IdSelector(const, sn) - case "name" ~ _ ~ const ~ sn => NameSelector(const, sn) - case n ~ _ ~ v ~ sn => AttrSelector(n, v, sn) - } private lazy val _classMatch: Parser[CssSelector] = '.' ~> attrName ^^ { diff --git a/core/util/src/main/scala/net/liftweb/util/HtmlParser.scala b/core/util/src/main/scala/net/liftweb/util/HtmlParser.scala index ff6eabb6c9..ddd81a5190 100644 --- a/core/util/src/main/scala/net/liftweb/util/HtmlParser.scala +++ b/core/util/src/main/scala/net/liftweb/util/HtmlParser.scala @@ -52,6 +52,7 @@ trait Html5Writer { str.charAt(pos) match { case '"' => writer.append(""") case '<' => writer.append("<") + case '&' if str.indexOf(';', pos) >= 0 => writer.append("&") case c if c >= ' ' && c.toInt <= 127 => writer.append(c) case c if c == '\u0085' => case c => { @@ -86,6 +87,7 @@ trait Html5Writer { str.charAt(pos) match { case '"' => writer.append(""") case '<' => writer.append("<") + case '&' if str.indexOf(';', pos) >= 0 => writer.append("&") case c if c >= ' ' && c.toInt <= 127 => writer.append(c) case c if c == '\u0085' => case c => { diff --git a/core/util/src/main/scala/net/liftweb/util/IoHelpers.scala b/core/util/src/main/scala/net/liftweb/util/IoHelpers.scala index 1481abfce6..bda7bc4f0f 100644 --- a/core/util/src/main/scala/net/liftweb/util/IoHelpers.scala +++ b/core/util/src/main/scala/net/liftweb/util/IoHelpers.scala @@ -118,3 +118,23 @@ trait IoHelpers { } } } + +/** + * A trait that defines an Automatic Resource Manager. The ARM + * allocates a resource (connection to a DB, etc.) when the `exec` + * method is invoked and releases the resource before the exec method terminates + * + * @tparam ResourceType the type of resource allocated + */ +trait AutoResourceManager[ResourceType] { + + /** + * Execute a block of code with the allocated resource + * + * @param f the function that takes the resource and returns a value + * @tparam T the type of the value returned by the function + *@return the value returned from the function + * + */ + def exec[T](f: ResourceType => T): T +} diff --git a/core/util/src/main/scala/net/liftweb/util/JodaHelpers.scala b/core/util/src/main/scala/net/liftweb/util/JodaHelpers.scala new file mode 100644 index 0000000000..4e482dceec --- /dev/null +++ b/core/util/src/main/scala/net/liftweb/util/JodaHelpers.scala @@ -0,0 +1,51 @@ +/* + * Copyright 2013 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package util + +import common._ +import Helpers.{asInt, tryo} + +import java.util.Date + +import org.joda.time._ +import org.joda.time.format._ + +object JodaHelpers extends JodaHelpers + +trait JodaHelpers { + def dateTimeFormatter: DateTimeFormatter = ISODateTimeFormat.dateTime + def toDateTime(in: Any): Box[DateTime] = { + try { + in match { + case null => Empty + case d: Date => Full(new DateTime(d)) + case d: DateTime => Full(d) + case lng: Long => Full(new DateTime(lng)) + case lng: Number => Full(new DateTime(lng.longValue)) + case Nil | Empty | None | Failure(_, _, _) => Empty + case Full(v) => toDateTime(v) + case Some(v) => toDateTime(v) + case v :: vs => toDateTime(v) + case s: String => tryo(DateTime.parse(s, dateTimeFormatter)) + case o => toDateTime(o.toString) + } + } catch { + case e: Exception => Failure("Bad date: "+in, Full(e), Empty) + } + } +} diff --git a/core/util/src/main/scala/net/liftweb/util/Mailer.scala b/core/util/src/main/scala/net/liftweb/util/Mailer.scala index cc7b6f637b..04e974e0df 100644 --- a/core/util/src/main/scala/net/liftweb/util/Mailer.scala +++ b/core/util/src/main/scala/net/liftweb/util/Mailer.scala @@ -24,19 +24,13 @@ import java.util.Properties import common._ import actor._ import xml.{Text, Elem, Node, NodeSeq} +import Mailer._ /** * Utilities for sending email. */ -object Mailer extends Mailer +object Mailer extends Mailer { -/** - * This trait implmenets the mail sending. You can create subclasses of this class/trait and - * implement your own mailer functionality - */ -trait Mailer extends SimpleInjector { - private val logger = Logger(classOf[Mailer]) - sealed abstract class MailTypes /** * Add message headers to outgoing messages @@ -73,9 +67,17 @@ trait Mailer extends SimpleInjector { final case class BCC(address: String, name: Box[String] = Empty) extends AddressType final case class ReplyTo(address: String, name: Box[String] = Empty) extends AddressType - implicit def xmlToMailBodyType(html: NodeSeq): MailBodyType = XHTMLMailBodyType(html) - final case class MessageInfo(from: From, subject: Subject, info: List[MailTypes]) +} + +/** + * This trait implmenets the mail sending. You can create subclasses of this class/trait and + * implement your own mailer functionality + */ +trait Mailer extends SimpleInjector { + private val logger = Logger(classOf[Mailer]) + + implicit def xmlToMailBodyType(html: NodeSeq): MailBodyType = XHTMLMailBodyType(html) implicit def addressToAddress(in: AddressType): Address = { val ret = new InternetAddress(in.address) diff --git a/core/util/src/main/scala/net/liftweb/util/MarkdownParser.scala b/core/util/src/main/scala/net/liftweb/util/MarkdownParser.scala new file mode 100644 index 0000000000..4320f7ee40 --- /dev/null +++ b/core/util/src/main/scala/net/liftweb/util/MarkdownParser.scala @@ -0,0 +1,41 @@ +package net.liftweb.util + +import xml.{Elem, NodeSeq} +import net.liftweb.common.Box +import net.liftweb.markdown.ThreadLocalTransformer + +object MarkdownParser { + lazy val matchMetadata = """(?m)\A(:?[ \t]*\n)?(?:-{3,}+\n)?(^([a-zA-Z0-9 _\-]+)[=:]([^\n]*)\n+(:?[ \t]*\n)?)+(:?-{3,}+\n)?""".r + + lazy val topMetadata = """(?m)^([^:]+):[ \t]*(.*)$""".r + + lazy val lineSplit = """(?m)^(.*)$""".r + + lazy val linkDefs = """(?m)^\p{Space}{0,3}\[([^:]+)[=:](?:[ ]*)(.+)\]:""".r + + lazy val hasYaml = """(?s)(?m)^[yY][aA][mM][lL][ \t]*\{[ \t]*$(.*?)^\}[ \t]*[yY][Aa][mM][Ll][ \t]*$""".r + lazy val htmlHasYaml = """(?s)(?m)\A(:?[ \t]*\n)*^[yY][aA][mM][lL][ \t]*\{[ \t]*$(.*?)^\}[ \t]*[yY][Aa][mM][Ll][ \t]*$""".r + + def childrenOfBody(in: NodeSeq): NodeSeq = { + (in \ "body").toList match { + case Nil => in + case xs => xs.collect { + case e: Elem => e + }.flatMap(_.child) + } + } + + private lazy val threadLocalTransformer = new ThreadLocalTransformer + + def parse(in: String): Box[NodeSeq] = { + for { + str <- Helpers.tryo(threadLocalTransformer.apply(in)) + res = Html5.parse("I eat yaks" + str + "") + info <- res.map { + res => (res \ "body").collect { + case e: Elem => e + }.flatMap(_.child) + } + } yield info + } +} diff --git a/core/util/src/main/scala/net/liftweb/util/PCDataMarkupParser.scala b/core/util/src/main/scala/net/liftweb/util/PCDataMarkupParser.scala index 56270ab6c9..b4730f8f35 100644 --- a/core/util/src/main/scala/net/liftweb/util/PCDataMarkupParser.scala +++ b/core/util/src/main/scala/net/liftweb/util/PCDataMarkupParser.scala @@ -190,7 +190,7 @@ object PCDataXmlParser { private def apply(source: Source): Box[NodeSeq] = { for { p <- tryo{new PCDataXmlParser(source)} - val _ = while (p.ch != '<' && p.curInput.hasNext) p.nextch + _ = while (p.ch != '<' && p.curInput.hasNext) p.nextch // side effects, baby bd <- tryo(p.document) doc <- Box !! bd } yield (doc.children: NodeSeq) diff --git a/core/util/src/main/scala/net/liftweb/util/Props.scala b/core/util/src/main/scala/net/liftweb/util/Props.scala index ca7abc5dfc..e0975ce059 100644 --- a/core/util/src/main/scala/net/liftweb/util/Props.scala +++ b/core/util/src/main/scala/net/liftweb/util/Props.scala @@ -27,7 +27,7 @@ import common._ * Configuration management utilities. * * If you want to provide a configuration file for a subset of your application - * or for a specifig environment, Lift expects configuration files to be named + * or for a specific environment, Lift expects configuration files to be named * in a manner relating to the context in which they are being used. The standard * name format is: * @@ -107,7 +107,8 @@ object Props extends Logger { * Recognized modes are "development", "test", "profile", "pilot", "staging" and "production" * with the default run mode being development. */ - lazy val mode = { + lazy val mode: RunModes.Value = { + runModeInitialised = true Box.legacyNullTest((System.getProperty("run.mode"))).map(_.toLowerCase) match { case Full("test") => Test case Full("production") => Production @@ -115,23 +116,82 @@ object Props extends Logger { case Full("pilot") => Pilot case Full("profile") => Profile case Full("development") => Development - case _ => { - val st = Thread.currentThread.getStackTrace - val names = List( - "org.apache.maven.surefire.booter.SurefireBooter", - "sbt.TestRunner", - "org.specs2.runner.TestInterfaceRunner", // sometimes specs2 runs tests on another thread - "org.specs2.runner.TestInterfaceConsoleReporter", - "org.specs2.specification.FragmentExecution" - ) - if(st.exists(e => names.exists(e.getClassName.startsWith))) - Test - else - Development + case _ => (autoDetectRunModeFn.get)() + } + } + + @volatile private[util] var runModeInitialised: Boolean = false + + /** + * Exposes a property affecting run-mode determination, for customisation. If the property is modified + * after the run-mode is realised, then it will have no effect and will instead log a warning indicating thus. + * + * @param name The property name (used to make logging messages clearer, no functional impact). + */ + class RunModeProperty[T](name: String, initialValue: T) extends Logger { + @volatile private[this] var value = initialValue + + def get = value + + /** + * Attempts to set the property to a new value. + * + * @return Whether the new property was installed. `false` means modification is no longer allowed. + */ + def set(newValue: T): Boolean = + if (allowModification) { + value = newValue + true + } else { + onModificationProhibited() + false } + + def allowModification = !runModeInitialised + + def onModificationProhibited() { + warn("Setting property " + name + " has no effect. Run mode already initialised to " + mode + ".") } } + /** + * The default run-mode auto-detection routine uses this function to infer whether Lift is being run in a test. + * + * This routine can be customised by calling `set` before the run-mode is referenced. (An attempt to customise this + * after the run-mode is realised will have no effect and will instead log a warning.) + */ + val doesStackTraceContainKnownTestRunner = new RunModeProperty[Array[StackTraceElement] => Boolean]("doesStackTraceContainKnownTestRunner", + (st: Array[StackTraceElement]) => { + val names = List( + "org.apache.maven.surefire.booter.SurefireBooter", + "sbt.TestRunner", + "org.jetbrains.plugins.scala.testingSupport.scalaTest.ScalaTestRunner", + "org.scalatest.tools.Runner", + "org.scalatest.tools.ScalaTestFramework$ScalaTestRunner", + "org.scalatools.testing.Runner", + "org.scalatools.testing.Runner2", + "org.specs2.runner.TestInterfaceRunner", // sometimes specs2 runs tests on another thread + "org.specs2.runner.TestInterfaceConsoleReporter", + "org.specs2.specification.FragmentExecution" + ) + st.exists(e => names.exists(e.getClassName.startsWith)) + }) + + /** + * When the `run.mode` environment variable isn't set or recognised, this function is invoked to determine the + * appropriate mode to use. + * + * This logic can be customised by calling `set` before the run-mode is referenced. (An attempt to customise this + * after the run-mode is realised will have no effect and will instead log a warning.) + */ + val autoDetectRunModeFn = new RunModeProperty[() => RunModes.Value]("autoDetectRunModeFn", () => { + val st = Thread.currentThread.getStackTrace + if ((doesStackTraceContainKnownTestRunner.get)(st)) + Test + else + Development + }) + /** * Is the system running in production mode (apply full optimizations) */ @@ -183,7 +243,7 @@ object Props extends Logger { /** * The resource path segment corresponding to the system hostname. */ - lazy val hostName: String = (if (inGAE) "GAE" else InetAddress.getLocalHost.getHostName) + lazy val hostName: String = (if (inGAE) "GAE" else Helpers.tryo(InetAddress.getLocalHost.getHostName).openOr("localhost")) private lazy val _hostName = dotLen(hostName) diff --git a/core/util/src/main/scala/net/liftweb/util/SourceInfo.scala b/core/util/src/main/scala/net/liftweb/util/SourceInfo.scala new file mode 100644 index 0000000000..f007f5c923 --- /dev/null +++ b/core/util/src/main/scala/net/liftweb/util/SourceInfo.scala @@ -0,0 +1,132 @@ +package net.liftweb.util + +import net.liftweb.common.Box +import xml.NodeSeq +import util.parsing.json.JSONArray +import net.liftweb.json.JsonAST.JValue +import scala.reflect.runtime.universe._ + +/** + * A trait that allows an object to tell you about itself + * rather than using reflection + */ +trait SourceInfo { + /** + * Given a name, look up the field + * @param name the name of the field + * @return the metadata + */ + def findSourceField(name: String): Box[SourceFieldInfo] + + /** + * Get a list of all the fields + * @return a list of all the fields + */ + def allFieldNames(): Seq[(String, SourceFieldMetadata)] +} + +case class SourceFieldMetadataRep[A](name: String, manifest: TypeTag[A], converter: FieldConverter{type T = A}) extends SourceFieldMetadata { + type ST = A +} + +/** + * Metadata about a specific field + */ +trait SourceFieldMetadata { + /** + * The field's type + */ + type ST + + /** + * The fields name + * @return the field's name + */ + def name: String + + /** + * The field's manifest + * @return the field's manifest + */ + def manifest: TypeTag[ST] + + /** + * Something that will convert the field into known types like String and NodeSeq + * @return + */ + def converter: FieldConverter{ type T = ST} +} + +/** + * An inplementation of SourceFieldInfo + * + * @param value the value + * @param metaData the metadata + * @tparam A the type + */ +case class SourceFieldInfoRep[A](value: A, metaData: SourceFieldMetadata{type ST = A}) extends SourceFieldInfo { + type T = A +} + +/** + * Value and metadata for a field + */ +trait SourceFieldInfo{ + + /** + * The type of the field + */ + type T + + /** + * The field's value + * @return + */ + def value: T + + /** + * Metadata about the field + * @return + */ + def metaData: SourceFieldMetadata {type ST = T} +} + + +/** + * Convert the field into other representations + */ +trait FieldConverter { + /** + * The type of the field + */ + type T + + /** + * Convert the field to a String + * @param v the field value + * @return the string representation of the field value + */ + def asString(v: T): String + + /** + * Convert the field into NodeSeq, if possible + * @param v the field value + * @return a NodeSeq if the field can be represented as one + */ + def asNodeSeq(v: T): Box[NodeSeq] + + /** + * Convert the field into a JSON value + * @param v the field value + * @return the JSON representation of the field + */ + def asJson(v: T): Box[JValue] + + /** + * If the field can represent a sequence of SourceFields, + * get that + * @param v the field value + * @return the field as a sequence of SourceFields + */ + def asSeq(v: T): Box[Seq[SourceFieldInfo]] +} \ No newline at end of file diff --git a/core/util/src/main/scala/net/liftweb/util/StringHelpers.scala b/core/util/src/main/scala/net/liftweb/util/StringHelpers.scala index b223fee77d..d83a152fb0 100644 --- a/core/util/src/main/scala/net/liftweb/util/StringHelpers.scala +++ b/core/util/src/main/scala/net/liftweb/util/StringHelpers.scala @@ -381,9 +381,6 @@ trait StringHelpers { /** @return a SuperString with more available methods such as roboSplit or commafy */ implicit def listStringToSuper(in: List[String]): SuperListString = new SuperListString(in) - @deprecated("Use blankForNull instead", "2.3") - def emptyForNull(s: String) = blankForNull(s) - /** * Test for null and return either the given String if not null or the blank String. */ diff --git a/core/util/src/main/scala/net/liftweb/util/TimeHelpers.scala b/core/util/src/main/scala/net/liftweb/util/TimeHelpers.scala index 39fbfdd8ed..9f622b1c79 100644 --- a/core/util/src/main/scala/net/liftweb/util/TimeHelpers.scala +++ b/core/util/src/main/scala/net/liftweb/util/TimeHelpers.scala @@ -70,16 +70,6 @@ trait TimeHelpers { self: ControlHelpers => def year = years } - /* - /** - * transforms a TimeSpan to a date by converting the TimeSpan expressed as millis and creating - * a Date lasting that number of millis from the Epoch time (see the documentation for java.util.Date) - */ - implicit def timeSpanToDate(in: TimeSpan): Date = in.date - - /** transforms a TimeSpan to its long value as millis */ - implicit def timeSpanToLong(in: TimeSpan): Long = in.millis - */ /** * The TimeSpan class represents an amount of time. @@ -294,17 +284,6 @@ trait TimeHelpers { self: ControlHelpers => /** @return the current year */ def currentYear: Int = Calendar.getInstance.get(Calendar.YEAR) - /** - * @return the current time as a Date object - */ - @deprecated("use now instead", "2.4") - def timeNow = new Date - - /** - * @deprecated use today instead - * @return the current Day as a Date object - */ - def dayNow: Date = 0.seconds.later.noTime /** alias for new Date(millis) */ def time(when: Long) = new Date(when) diff --git a/core/util/src/main/scala/net/liftweb/util/ValueHolder.scala b/core/util/src/main/scala/net/liftweb/util/ValueHolder.scala index 6daa8c80d3..8e084ea442 100644 --- a/core/util/src/main/scala/net/liftweb/util/ValueHolder.scala +++ b/core/util/src/main/scala/net/liftweb/util/ValueHolder.scala @@ -66,12 +66,12 @@ trait PValueHolder[T] extends ValueHolder { } object PValueHolder { - implicit def tToVHT[T](in: T): PValueHolder[T] = new PValueHolder[T] {def is = in; def get = is} + implicit def tToVHT[T](in: T): PValueHolder[T] = new PValueHolder[T] {def get = in; def is = get} def apply[T](in: T) = tToVHT(in) } object ValueHolder { - implicit def tToVHT[T](in: T): ValueHolder = new PValueHolder[T] {def is = in; def get = is} + implicit def tToVHT[T](in: T): ValueHolder = new PValueHolder[T] {def get = in; def is = get} def apply[T](in: T) = tToVHT(in) } diff --git a/core/util/src/main/scala/net/liftweb/util/package.scala b/core/util/src/main/scala/net/liftweb/util/package.scala index 48d4d85e2b..2f3311ef83 100644 --- a/core/util/src/main/scala/net/liftweb/util/package.scala +++ b/core/util/src/main/scala/net/liftweb/util/package.scala @@ -26,11 +26,6 @@ import xml.NodeSeq package object util { type CssBindFunc = CssSel - /** - * Changed the name ActorPing to Schedule - */ - @deprecated("Use Schedule", "2.3") - val ActorPing = Schedule /** * Wrap a function and make sure it's a NodeSeq => NodeSeq. Much easier diff --git a/core/util/src/test/scala/net/liftweb/util/CssSelectorSpec.scala b/core/util/src/test/scala/net/liftweb/util/CssSelectorSpec.scala index 0e0a96647e..5b3f4cec09 100644 --- a/core/util/src/test/scala/net/liftweb/util/CssSelectorSpec.scala +++ b/core/util/src/test/scala/net/liftweb/util/CssSelectorSpec.scala @@ -306,6 +306,12 @@ object CssBindHelpersSpec extends Specification { success } + "data-name selector works" in { + val xf = ";frog" #> hi + + xf(
    Moose
    ) must ==/ (
    hi
    ) + } + "support modifying attributes along with body" in { val org = foo val func = "a [href]" #> "dog" & "a *" #> "bar" @@ -333,6 +339,13 @@ object CssBindHelpersSpec extends Specification { } + "Only apply to the top elem" in { + val xf = "^ [href]" #> "wombat" + + xf(stuff) must ==/ (stuff) + } + + "Select a node" in { ("#foo ^^" #> "hello").apply(
    ) must ==/ () diff --git a/core/util/src/test/scala/net/liftweb/util/MailerSpec.scala b/core/util/src/test/scala/net/liftweb/util/MailerSpec.scala index 66bf60bc26..3566767cfe 100644 --- a/core/util/src/test/scala/net/liftweb/util/MailerSpec.scala +++ b/core/util/src/test/scala/net/liftweb/util/MailerSpec.scala @@ -23,6 +23,7 @@ import org.specs2.mutable.Specification import common._ +import Mailer._ /** * Systems under specification for Lift Mailer. diff --git a/core/util/src/test/scala/net/liftweb/util/PropsSpec.scala b/core/util/src/test/scala/net/liftweb/util/PropsSpec.scala index b2ad585d65..73dd52d7d7 100644 --- a/core/util/src/test/scala/net/liftweb/util/PropsSpec.scala +++ b/core/util/src/test/scala/net/liftweb/util/PropsSpec.scala @@ -18,17 +18,38 @@ package net.liftweb package util import org.specs2.mutable.Specification - +import Props.RunModes._ /** * Systems under specification for Lift Mailer. */ object PropsSpec extends Specification { "Props Specification".title + sequential "Props" should { "Detect test mode correctly" in { Props.testMode must_== true } + + "Allow modification of run-mode properties before the run-mode is set" in { + val before = Props.autoDetectRunModeFn.get + try { + Props.runModeInitialised = false + Props.autoDetectRunModeFn.allowModification must_== true + Props.autoDetectRunModeFn.set(() => Test) must_== true + Props.autoDetectRunModeFn.get must_!= before + } finally { + Props.autoDetectRunModeFn.set(before) + Props.runModeInitialised = true + } + } + + "Prohibit modification of run-mode properties when the run-mode is set" in { + val before = Props.autoDetectRunModeFn.get + Props.autoDetectRunModeFn.allowModification must_== false + Props.autoDetectRunModeFn.set(() => Test) must_== false + Props.autoDetectRunModeFn.get must_== before + } } } diff --git a/core/util/src/test/scala/net/liftweb/util/TimeHelpersSpec.scala b/core/util/src/test/scala/net/liftweb/util/TimeHelpersSpec.scala index da3803a8d9..2f6c1ddff8 100644 --- a/core/util/src/test/scala/net/liftweb/util/TimeHelpersSpec.scala +++ b/core/util/src/test/scala/net/liftweb/util/TimeHelpersSpec.scala @@ -128,7 +128,7 @@ object TimeHelpersSpec extends Specification with ScalaCheck with TimeAmountsGen weeks(3) must_== 3 * 7 * 24 * 60 * 60 * 1000 } "provide a noTime function on Date objects to transform a date into a date at the same day but at 00:00" in { - hourFormat(timeNow.noTime) must_== "00:00:00" + hourFormat(now.noTime) must_== "00:00:00" } "make sure noTime does not change the day" in { @@ -171,7 +171,12 @@ object TimeHelpersSpec extends Specification with ScalaCheck with TimeAmountsGen formattedDateNow must beMatching("\\d\\d\\d\\d/\\d\\d/\\d\\d") } "provide a formattedTimeNow function to format now's time with the TimeZone" in { - formattedTimeNow must beMatching("\\d\\d:\\d\\d ...(\\+|\\-\\d\\d:00)?") + val regex = "\\d\\d:\\d\\d (....?|GMT((\\+|\\-)\\d\\d:00)?)" + "10:00 CEST" must beMatching(regex) + "10:00 GMT+02:00" must beMatching(regex) + "10:00 GMT" must beMatching(regex) + "10:00 XXX" must beMatching(regex) + formattedTimeNow must beMatching(regex) } "provide a parseInternetDate function to parse a string formatted using the internet format" in { diff --git a/docs/simply_lift.md b/docs/simply_lift.md new file mode 100644 index 0000000000..844a3f9488 --- /dev/null +++ b/docs/simply_lift.md @@ -0,0 +1,547 @@ +# Simply Lift + +> David Pollak +> February 19, 2013 +>
    +> Copyright © 2010-2013 by David Pollak +> Creative Commons License +>
    +> Simply Lift
    by http://simply.liftweb.net is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.
    Based on a work at https://github.com/lift/framework. + + +## The Lift Web Framework + +### Introduction + +The Lift Web Framework provides web application developers tools to make writing security, interacting, scalable web applications easier than with any other web framework. After reading Part I of this book, you should understand Lift's core concepts and be able to write Lift applications. But with anything, practice is important. I have been writing Lift and Scala for 6 years, and even I learn new things about the language and the framework on a weekly basis. Please consider Lift an path and an exploration, rather than an end point. + +“Yo, David, stop yer yappin'. I'm coming from Rails|Spring|Struts|Django and I want to get started super fast with Lift.” See From MVC. + +Lift is built on top of the [Scala](http://scala-lang.org) programming language. Scala runs on the [Java Virtual Machine](http://www.oracle.com/technetwork/java/index.html). Lift applications are typically packaged as [http://en.wikipedia.org/wiki/WAR_(Sun_file_format)||WAR] files and run as a [http://www.oracle.com/technetwork/java/index-jsp-135475.html||J/EE Servlets] or Servlet Filters. This book will provide you with the core concepts you need to successfully write Lift web applications. The book assumes knowledge of Servlets and Servlet containers, the Scala Language (Chapters 1-6 of [http://apress.com/book/view/9781430219897||Beginning Scala] gives you a good grounding in the language), build tools, program editors, web development including HTML and JavaScript, etc. Further, this book will not explore persistence. Lift has additional modules for persisting to relational and non-relational data stores. Lift doesn't distinguish as to how an object is materialized into the address space... Lift can treat any object any old way you want. There are many resources (including [http://exploring.liftweb.net/||Exploring Lift]) that cover ways to persist data from a JVM. + +Lift is different from most web frameworks and it's likely that Lift's differences will present a challenge and a friction if you are familiar with the MVCMVC school of web frameworksThis includes Ruby on Rails, Struts, Java Server Faces, Django, TurboGears, etc.. But Lift is different and Lift's differences give you more power to create interactive applications. Lift's differences lead to more concise web applications. Lift's differences result in more secure and scalable applications. Lift's differences let you be more productive and make maintaining applications easier for the future you or whoever is writing your applications. Please relax and work to understand Lift's differences... and see how you can make best use of Lift's features to build your web applications. + +Lift creates abstractions that allow easier expression of business logic and then maps those abstractions to HTTP and HTML. This approach differs from traditional web frameworks which build abstractions on top of HTTP and HTML and require the developer to bridge between common business logic patterns and the underlying protocol. The difference means that you spend more time thinking about your application and less time thinking about the plumbing. + +I am a “concept learner.” I learn concepts and then apply them over and over again as situations come up. This book focuses a lot on the concepts. If you're a concept learner and like my stream on conciousness style, this book will likely suit you well. On the other hand, it may not. + +Up to date versions of this book are available in PDF form at http://simply.liftweb.net/Simply_Lift.pdf. The source code for this book is available at [https://github.com/dpp/simply_lift||https://github.com/dpp/simply_lift]. + +If you've got questions, feedback, or improvements to this document, please join the conversation on the [http://groups.google.com/group/liftweb||Lift Google Group]. + +I'm a “roll up your sleaves and get your hands dirty with code” kinda guy... so let's build a simple Chat application in Lift. This application will allow us to demonstrate some of Lift's core features as well as giving a “smack in the face” demonstration of how Lift is different. + +### The ubiquitous Chat app + +Writing a multi-user chat application in Lift is super-simple and illustrates many of Lift's core concepts. + +The Source Code can be found at [https://github.com/dpp/simply_lift/tree/master/chat]. + +#### The View + +When writing a Lift app, it's often best to start off with the user interface... build what the user will see and then add behavior to the HTML page. So, let's look at the Lift template that will make up our chat application. + + + +It's a valid HTML page, but there are some hinky looking class attributes. The first one is . The class in this case says “the actual page content is contained by the element with id='main'.” This allows you to have valid HTML pages for each of your templates, but dynamically add “chrome” around the content based on one or more chrome templates. + +Let's look at the
    . It's got a funky class as well: lift:surround?with=default;at=content. This class invokes a snippet which surrounds the
    with the default template and inserts the
    and its children at the element with id “content” in the default template. Or, it wraps the default chrome around the
    . For more on snippets, see [sec:Snippets]. + +Next, we define how we associate dynamic behavior with the list of chat elements:
    . The “comet” snippet looks for a class named Chat that extends CometActor and enables the mechanics of pushing content from the CometActor to the browser when the state of the CometActor changes. + +#### The Chat Comet component + +The [http://en.wikipedia.org/wiki/Actor_model||Actor Model] provides state in functional languages include Erlang. Lift has an Actor library and LiftActors (see [sec:LiftActor]) provides a powerful state and concurrency model. This may all seem abstract, so let's look at the Chat class. + + + +The Chat component has private state, registers with the ChatServer, handles incoming messages and can render itself. Let's look at each of those pieces. + +The private state, like any private state in prototypical object oriented code, is the state that defines the object's behavior. + +registerWith is a method that defines what component to register the Chat component with. Registration is a part of the Listener (or [http://en.wikipedia.org/wiki/Observer_pattern||Observer]) pattern. We'll look at the definition of the ChatServer in a minute. + +The lowPriority method defines how to process incoming messages. In this case, we're Pattern Matching (see [sec:Pattern-Matching]) the incoming message and if it's a Vector[String], then we perform the action of setting our local state to the Vector and re-rendering the component. The re-rendering will force the changes out to any browser that is displaying the component. + +We define how to render the component by defining the CSS to match and the replacement (See [sec:CSS-Selector-Transforms]). We match all the
  • tags of the template and for each message, create an
  • tag with the child nodes set to the message. Additionally, we clear all the elements that have the clearable in the class attribute. + +That's it for the Chat CometActor component. + +#### The ChatServer + +The ChatServer code is: + + + +The ChatServer is defined as an object rather than a class. This makes it a singleton which can be referenced by the name ChatServer anywhere in the application. Scala's singletons differ from Java's static in that the singleton is an instance of an object and that instance can be passed around like any other instance. This is why we can return the ChatServer instance from the registerWith method in that Chat component. + +The ChatServer has private state, a Vector[String] representing the list of chat messages. Note that Scala's type inferencer infers the type of msgs so you do not have to explicitly define it. + +The createUpdate method generates an update to send to listeners. This update is sent when a listener registers with the ChatServer or when the updateListeners() method is invoked. + +Finally, the lowPriority method defines the messages that this component can handle. If the ChatServer receives a String as a message, it appends the String to the Vector of messages and updates listeners. + +#### User Input + +Let's go back to the view and see how the behavior is defined for adding lines to the chat. + +
    defines an input form and the form.ajax snippet turns a form into an Ajax (see [sec:Ajax]) form that will be submitted back to the server without causing a full page load. + +Next, we define the input form element: . It's a plain old input form, but we've told Lift to modify the 's behavior by calling the ChatIn snippet. + +#### Chat In + +The ChatIn snippet (See [sec:Snippets]) is defined as: + + + +The code is very simple. The snippet is defined as a method that associates a function with form element submission, onSubmit. When the element is submitted, be that normal form submission, Ajax, or whatever, the function is applied to the value of the form. In English, when the user submits the form, the function is called with the user's input. + +The function sends the input as a message to the ChatServer and returns JavaScript that sets the value of the input box to a blank string. + +#### Running it + +Running the application is easy. Make sure you've got Java 1.6 or better installed on your machine. Change directories into the chat directory and type sbt update ~jetty-run. The Simple Build Tool will download all necessary dependencies, compile the program and run it. + +You can point a couple of browsers to http://localhost:8080 and start chatting. + +Oh, and for fun, try entering - +

    Welcome to your project!

    - - + + + +
    diff --git a/web/webkit/src/test/webapp/htmlSnippetWithHead.html b/web/webkit/src/test/webapp/htmlSnippetWithHead.html index 71577c1c25..4e5944771f 100644 --- a/web/webkit/src/test/webapp/htmlSnippetWithHead.html +++ b/web/webkit/src/test/webapp/htmlSnippetWithHead.html @@ -1,5 +1,5 @@

    Welcome to your project!

    -

    +

    diff --git a/web/webkit/src/test/webapp/oneshot.html b/web/webkit/src/test/webapp/oneshot.html index c955adfd9a..51729125d1 100644 --- a/web/webkit/src/test/webapp/oneshot.html +++ b/web/webkit/src/test/webapp/oneshot.html @@ -1,4 +1,4 @@ - +
    @@ -6,6 +6,4 @@ - - - +
    diff --git a/web/webkit/src/test/webapp/templates-hidden/default.html b/web/webkit/src/test/webapp/templates-hidden/default.html index 383afce7a0..00f3c5f922 100644 --- a/web/webkit/src/test/webapp/templates-hidden/default.html +++ b/web/webkit/src/test/webapp/templates-hidden/default.html @@ -1,15 +1,17 @@ + - - - - - + + + + + Lift webapptest - - - + + +
    The main content will get bound here
    +