/*
* Copyright 2010 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 _root_.scala.util.parsing.combinator.{Parsers, ImplicitConversions}
import _root_.net.liftweb.common._
sealed trait CssSelector {
def subNodes: Box[SubNode]
}
final case class ElemSelector(elem: String, subNodes: Box[SubNode]) extends
CssSelector
final case class StarSelector(subNodes: Box[SubNode]) extends CssSelector
final case class IdSelector(id: String, subNodes: Box[SubNode]) extends
CssSelector
final case class ClassSelector(clss: String, subNodes: Box[SubNode]) extends
CssSelector
final case class NameSelector(name: String, subNodes: Box[SubNode]) extends
CssSelector
final case class AttrSelector(name: String, value: String,
subNodes: Box[SubNode]) extends CssSelector
sealed trait SubNode
object SubNode {
def unapply(bind: CssBind): Option[Box[SubNode]] =
Some(bind.css.flatMap(_.subNodes))
}
final case class KidsSubNode() extends SubNode
final case class AttrSubNode(attr: String) extends SubNode
final case class AttrAppendSubNode(attr: String) extends SubNode
final case class SelectThisNode() extends SubNode
/**
* Parse a subset of CSS into the appropriate selector objects
*/
object CssSelectorParser extends Parsers with ImplicitConversions {
private val cache = new LRUMap[String, CssSelector](25000)
/**
* Parse a String into a CSS Selector
*/
def parse(toParse: String): Box[CssSelector] = synchronized {
// this method is synchronized because the Parser combinator is not
// thread safe, so we'll only parse one at a time, but given that most
// of the selectors will be cached, it's not really a performance hit
cache.get(toParse) or {
internalParse(toParse).map {
sel => {
// cache the result
cache(toParse) = sel
sel
}
}
}
}
import scala.util.parsing.input.CharSequenceReader
type Elem = Char
type UnitParser=Parser[Unit]
private def internalParse(toParse: String): Box[CssSelector] = {
val reader: Input = new CharSequenceReader(toParse, 0)
topParser(reader) match {
case Success(v, _) => Full(v)
case x => Empty
}
}
private implicit def str2chars(s: String): List[Char] = stringWrapper(s).toList
private lazy val topParser: Parser[CssSelector] = {
idMatch |
classMatch |
attrMatch |
elemMatch |
starMatch
}
private lazy val idMatch: Parser[CssSelector] = '#' ~> id ~ opt(subNode) ^^ {
case id ~ sn => IdSelector(id, sn)
}
private lazy val elemMatch: Parser[CssSelector] = id ~ opt(subNode) ^^ {
case elem ~ sn => ElemSelector(elem, sn)
}
private lazy val starMatch: Parser[CssSelector] = '*' ~> opt(subNode) ^^ {
case sn => StarSelector(sn)
}
private lazy val id: Parser[String] = letter ~
rep(letter | number | '-' | '_' | ':' | '.') ^^ {
case first ~ rest => (first :: rest).mkString
}
private def isLetter(c: Char): Boolean = c.isLetter
private def isNumber(c: Char): Boolean = c.isDigit
private lazy val letter: Parser[Char] = elem("letter", isLetter)
private lazy val number: Parser[Char] = elem("number", isNumber)
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 subNode: Parser[SubNode] = rep1(' ') ~>
((opt('*') ~ '[' ~> attrName <~ '+' ~ ']' ^^ {
name => AttrAppendSubNode(name)
}) |
(opt('*') ~ '[' ~> attrName <~ ']' ^^ {
name => AttrSubNode(name)
}) |
'*' ^^ (a => KidsSubNode()) |
'^' ~ '^' ^^ (a => SelectThisNode()))
private lazy val attrName: Parser[String] = (letter | '_' | ':') ~
rep(letter | number | '-' | '_' | ':' | '.') ^^ {
case first ~ rest => (first :: rest).mkString
}
private lazy val attrConst: Parser[String] = {
(('\'' ~> rep(elem("isValid", (c: Char) => {
c != '\'' && c >= ' '
})) <~ '\'') ^^ {
case s => s.mkString
}) |
(('"' ~> rep(elem("isValid", (c: Char) => {
c != '"' && c >= ' '
})) <~ '"') ^^ {
case s => s.mkString
}) |
(rep1(elem("isValid", (c: Char) => {
c != '\'' && c != '"' && c > ' '
})) ^^ {
case s => s.mkString
})
}
}
}
}