// Copyright (C) 2006 Google Inc.
//
// 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 com.google.caja.parser.css;
import com.google.caja.lexer.CssLexer;
import com.google.caja.lexer.FilePosition;
import com.google.caja.lexer.Token;
import com.google.caja.lexer.TokenConsumer;
import com.google.caja.lexer.escaping.Escaping;
import com.google.caja.parser.AncestorChain;
import com.google.caja.parser.ParseTreeNodeVisitor;
import com.google.caja.parser.ParseTreeNode;
import com.google.caja.parser.ParseTreeNodes;
import com.google.caja.parser.Visitor;
import com.google.caja.render.Concatenator;
import com.google.caja.render.JsPrettyPrinter;
import com.google.caja.reporting.MessageContext;
import com.google.caja.reporting.MessagePart;
import com.google.caja.reporting.RenderContext;
import com.google.caja.util.Callback;
import com.google.caja.util.Name;
import com.google.caja.util.Pair;
import com.google.caja.util.SyntheticAttributeKey;
import com.google.caja.util.SyntheticAttributes;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A description of the values that can occur after a property.
*
* <p>See the value description language from
* http://www.w3.org/TR/CSS21/about.html#property-defs
*
* @author mikesamuel@gmail.com
*/
public abstract class CssPropertySignature implements ParseTreeNode {
private final List<CssPropertySignature> children;
private CssPropertySignature parent, nextSibling, prevSibling;
// We have not implemented immutability for CssPropertySignature yet
// due to the pervasive public access to the mutable 'children' member.
@Override
public boolean makeImmutable() { return false; }
@Override
public boolean isImmutable() { return false; }
CssPropertySignature(List<? extends CssPropertySignature> children) {
this.children = children.isEmpty()
? Collections.<CssPropertySignature>emptyList()
: Collections.unmodifiableList(
new ArrayList<CssPropertySignature>(children));
CssPropertySignature last = null;
for (CssPropertySignature child : children) {
child.parent = this;
child.prevSibling = last;
if (null != last) { last.nextSibling = child; }
last = child;
}
assert !this.children.contains(null);
}
@Override
public CssPropertySignature clone() {
return ParseTreeNodes.newNodeInstance(
getClass(), getFilePosition(), getValue(), children());
}
public final TokenConsumer makeRenderer(
Appendable out, Callback<IOException> exHandler) {
return new JsPrettyPrinter(new Concatenator(out, exHandler));
}
/** A signature that can be repeated zero or more times. */
public static final class RepeatedSignature extends CssPropertySignature {
public final int minCount, maxCount;
private RepeatedSignature(
CssPropertySignature sig, int minCount, int maxCount) {
super(Collections.singletonList(sig));
this.minCount = minCount;
this.maxCount = maxCount;
}
public Pair<Integer, Integer> getValue() {
return Pair.pair(Integer.valueOf(minCount), Integer.valueOf(maxCount));
}
public CssPropertySignature getRepeatedSignature() {
return children().get(0);
}
public void render(RenderContext r) {
TokenConsumer out = r.getOut();
children().get(0).render(r);
out.consume("{");
out.consume(String.valueOf(minCount));
if (minCount != maxCount) {
out.consume(",");
if (Integer.MAX_VALUE != maxCount) {
out.consume(String.valueOf(maxCount));
}
}
out.consume("}");
}
}
/** A signature that matches one of its children. */
public static class SetSignature extends CssPropertySignature {
public SetSignature(List<CssPropertySignature> alternatives) {
super(alternatives);
}
public Object getValue() { return null; }
public void render(RenderContext r) {
TokenConsumer out = r.getOut();
out.consume("[");
boolean first = true;
for (CssPropertySignature sig : children()) {
if (!first) {
out.consume("|");
} else {
first = false;
}
sig.render(r);
}
out.consume("]");
}
}
/** A signature that matches its children. */
public static final class ExclusiveSetSignature
extends SetSignature {
private ExclusiveSetSignature(
List<CssPropertySignature> alternatives) {
super(alternatives);
}
@Override
public void render(RenderContext r) {
TokenConsumer out = r.getOut();
out.consume("[");
boolean first = true;
for (CssPropertySignature sig : children()) {
if (!first) {
out.consume("||");
} else {
first = false;
}
sig.render(r);
}
out.consume("]");
}
}
/** A signature that matches its children in order. */
public static final class SeriesSignature extends CssPropertySignature {
private SeriesSignature(List<? extends CssPropertySignature> children) {
super(children);
}
public Object getValue() { return null; }
public void render(RenderContext r) {
TokenConsumer out = r.getOut();
out.consume("[");
boolean first = true;
for (CssPropertySignature sig : children()) {
if (!first) {
out.consume(" ");
} else {
first = false;
}
sig.render(r);
}
out.consume("]");
}
}
/** A signature that matches a literal string. */
public static final class LiteralSignature extends CssPropertySignature {
public final String value;
private LiteralSignature(String value) {
super(Collections.<CssPropertySignature>emptyList());
this.value = value;
}
public String getValue() { return value; }
public void render(RenderContext r) {
r.getOut().consume(value);
}
}
/**
* A signature that matches a quoted string.
* This production does not occur in the CSS property signature scheme, but
* is required because of IE progid extensions which use quotes around keyword
* values.
*/
public static final class QuotedLiteralSignature
extends CssPropertySignature {
public final String value;
private QuotedLiteralSignature(String value) {
super(Collections.<CssPropertySignature>emptyList());
this.value = value;
}
public String getValue() { return value; }
public void render(RenderContext r) {
StringBuilder sb = new StringBuilder(value.length() + 16);
sb.append('"');
Escaping.escapeCssString(value, sb);
sb.append('"');
r.getOut().consume(sb.toString());
}
}
/** A signature that defers to a CSS 2 property signature. */
public static final class PropertyRefSignature extends CssPropertySignature {
public final Name name;
private PropertyRefSignature(Name name) {
super(Collections.<CssPropertySignature>emptyList());
this.name = name;
}
public Name getValue() { return name; }
public Name getPropertyName() { return name; }
public void render(RenderContext r) {
r.getOut().consume("\'" + name + "\'");
}
}
/** A signature that defers to a CSS 2 symbol. */
public static final class SymbolSignature extends CssPropertySignature {
public final Name symbolName;
private SymbolSignature(Name symbolName) {
super(Collections.<CssPropertySignature>emptyList());
this.symbolName = symbolName;
}
public Name getValue() { return symbolName; }
public void render(RenderContext r) {
TokenConsumer out = r.getOut();
out.consume("<");
out.consume(symbolName.getCanonicalForm());
out.consume(">");
}
}
/** A signature that matches a function call. */
public static final class CallSignature extends CssPropertySignature {
private CallSignature(List<CssPropertySignature> children) {
super(children);
}
public Object getValue() { return null; }
public String getName() {
CssPropertySignature name = children().get(0);
if (name instanceof CssPropertySignature.LiteralSignature) {
return ((CssPropertySignature.LiteralSignature) name).getValue();
}
return null;
}
public CssPropertySignature getArgumentsSignature() {
List<? extends CssPropertySignature> children = children();
// The first is the name, the following ones match the argument list
// excluding parentheses.
if (children.size() == 2) {
return children.get(1);
}
return new SeriesSignature(children.subList(1, children.size()));
}
public void render(RenderContext r) {
TokenConsumer out = r.getOut();
ListIterator<? extends CssPropertySignature> childIt =
children().listIterator();
childIt.next().render(r);
out.consume("(");
while (childIt.hasNext()) {
out.consume(" ");
childIt.next().render(r);
}
out.consume(")");
}
}
/**
* Matches an IE CSS Filter, as described at
* http://msdn.microsoft.com/en-us/library/ms532847(VS.85).aspx
*/
public static final class ProgIdSignature extends CssPropertySignature {
private final Name name;
/**
* @param name A dotted CSS name like DXImageTransform.Microsoft.Alpha.
* @param attrs name value pairs.
*/
private ProgIdSignature(Name name, List<ProgIdAttrSignature> attrs) {
super(attrs);
this.name = name;
}
public Name getValue() { return name; }
public Name getName() { return name; }
public void render(RenderContext r) {
TokenConsumer out = r.getOut();
out.consume("progid");
out.consume(":");
out.consume(name.getCanonicalForm());
out.consume("(");
boolean comma = false;
for (CssPropertySignature a : children()) {
if (comma) { out.consume(","); }
comma = true;
a.render(r);
}
out.consume(")");
}
public ProgIdAttrSignature getProgIdAttr(Name attrName) {
for (CssPropertySignature child : children()) {
ProgIdAttrSignature attr = (ProgIdAttrSignature) child;
if (attrName.equals(attr.getName())) { return attr; }
}
return null;
}
}
public static final class ProgIdAttrSignature extends CssPropertySignature {
private final Name name;
private ProgIdAttrSignature(Name name, CssPropertySignature valueSig) {
super(Collections.singletonList(valueSig));
this.name = name;
}
public Name getValue() { return name; }
public Name getName() { return name; }
public CssPropertySignature getValueSig() { return children().get(0); }
public void render(RenderContext r) {
TokenConsumer out = r.getOut();
out.consume(name.getCanonicalForm());
out.consume("=");
children().get(0).render(r);
}
}
public ParseTreeNode getParent() { return this.parent; }
public ParseTreeNode getNextSibling() { return this.nextSibling; }
public ParseTreeNode getPrevSibling() { return this.prevSibling; }
public FilePosition getFilePosition() {
throw new UnsupportedOperationException();
}
public List<Token<?>> getComments() {
return Collections.<Token<?>>emptyList();
}
private SyntheticAttributes attribs;
private static final boolean DEBUG = false;
public SyntheticAttributes getAttributes() {
if (!DEBUG) {
throw new UnsupportedOperationException();
} else {
// may be mutable for debugging
if (null == attribs) { attribs = new SyntheticAttributes(); }
return attribs;
}
}
public List<? extends CssPropertySignature> children() { return children; }
public final boolean acceptPreOrder(Visitor v, AncestorChain<?> ancestors) {
ancestors = AncestorChain.instance(ancestors, this);
if (!v.visit(ancestors)) { return false; }
for (CssPropertySignature child : children) {
child.acceptPreOrder(v, ancestors);
}
return true;
}
public final boolean acceptPostOrder(Visitor v, AncestorChain<?> ancestors) {
ancestors = AncestorChain.instance(ancestors, this);
for (CssPropertySignature child : children) {
if (!child.acceptPostOrder(v, ancestors)) {
return false;
}
}
return v.visit(ancestors);
}
public final boolean visitPreOrder(ParseTreeNodeVisitor v) {
if (!v.visit(this)) { return false; }
for (CssPropertySignature child : children) {
child.visitPreOrder(v);
}
return true;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
RenderContext rc = new RenderContext(makeRenderer(sb, null));
render(rc);
rc.getOut().noMoreTokens();
return sb.toString();
}
protected void formatSelf(MessageContext context, Appendable out)
throws IOException {
out.append(getClass().getSimpleName());
Object value = getValue();
if (null != value) {
out.append(" : ");
if (value instanceof MessagePart) {
((MessagePart) value).format(context, out);
} else {
out.append(value.toString());
}
}
if (null != attribs && !context.relevantKeys.isEmpty()) {
for (SyntheticAttributeKey<?> k : attribs.keySet()) {
if (context.relevantKeys.contains(k)) {
out.append(" ; ").append(k.getName()).append('=');
Object attribValue = attribs.get(k);
if (attribValue instanceof MessagePart) {
((MessagePart) attribValue).format(context, out);
} else {
out.append(String.valueOf(attribValue));
}
}
}
}
}
public void format(MessageContext context, Appendable out)
throws IOException {
formatTree(context, 0, out);
}
public void formatTree(MessageContext context, int depth, Appendable out)
throws IOException {
for (int d = depth; --d >= 0;) { out.append(" "); }
formatSelf(context, out);
for (CssPropertySignature child : children()) {
out.append("\n");
child.formatTree(context, depth + 1, out);
}
}
private static String unescape(String s, boolean removeSpaces) {
int pos = 0;
StringBuilder sb = null;
for (int i = 0, n = s.length(); i < n; ++i) {
char ch = s.charAt(i);
// http://www.w3.org/TR/CSS21/syndata.html#value-def-string
// string {string1}|{string2}
// string1 \"([^\n\r\f\\"]|\\{nl}|{escape})*\"
// string2 \'([^\n\r\f\\']|\\{nl}|{escape})*\'
// escape {unicode}|\\[^\n\r\f0-9a-f]
// unicode \\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])?
if ('\\' == ch && i + 1 < n) {
if (null == sb) { sb = new StringBuilder(); }
sb.append(s, pos, i);
char ch1 = s.charAt(++i);
if (CssLexer.isHexChar(ch1)) {
// up to 6 hex digits
int end = i;
while (end < n && end - i < 6 && CssLexer.isHexChar(s.charAt(end))) {
++end;
}
int chi = Integer.parseInt(s.substring(i, end), 16);
i = end - 1;
// Hex escape may be followed by a single space character to separate
// it from any following character that happens to be a hex digit.
if (i + 1 < n) {
char nextChar = s.charAt(i + 1);
if (CssLexer.isSpaceChar(nextChar)) {
++i;
// "\r\n" is specially handled in the {escape} production above.
if ('\r' == nextChar && i + 1 < n && '\n' == s.charAt(i + 1)) {
++i;
}
}
}
// chi may have up to 6 digits, so may be outside Java's char's range,
// but is within the range supported by java.lang.String codepoints.
sb.appendCodePoint(chi);
} else if ('\r' == ch1) {
// Newline not considered part of string
if (i + 1 < n && s.charAt(i + 1) == '\n') { ++i; }
} else if ('\n' == ch1) {
// Newline not considered part of string
} else {
sb.append(ch1);
}
pos = i + 1;
} else if (CssLexer.isSpaceChar(ch) && removeSpaces) {
if (null == sb) { sb = new StringBuilder(); }
if (i > pos) { sb.append(s, pos, i); }
pos = i + 1;
}
}
if (null == sb) {
return s;
} else {
sb.append(s, pos, s.length());
return sb.toString();
}
}
public static final class Parser {
private static Pattern[] TOKENS = {
// whitespace
Pattern.compile("^\\s+"),
// a symbol, possibly with numeric bounds
Pattern.compile("^(<[a-zA-Z][\\w\\-]*(?:\\:-?\\d+,-?\\d*)?>)"),
// a property reference
Pattern.compile("^('[a-zA-Z][\\w\\-]*')"),
// a literal keyword
Pattern.compile("^(-?[a-zA-Z][\\w\\-]*)"),
// a quoted literal
Pattern.compile("^(\"[^\"]*\")"),
// a number
Pattern.compile("^(-?[0-9]+)\\b"),
// multi-character punctuation
Pattern.compile("^(\\|\\|)"),
// other single character tokens
Pattern.compile("^([\\(\\)=\\{\\}\\*\\+\\,\\/\\|\\[\\]\\?\\.:])"),
};
static ListIterator<String> tokenizeSignature(String sig) {
List<String> toks = new ArrayList<String>();
while (!"".equals(sig)) {
boolean match = false;
for (Pattern p : TOKENS) {
Matcher m = p.matcher(sig);
if (m.find()) {
if (m.groupCount() > 0) { toks.add(m.group(1)); }
sig = sig.substring(m.end(0));
match = true;
break;
}
}
if (!match) { throw new IllegalArgumentException(sig); }
}
return toks.listIterator();
}
public static CssPropertySignature parseSignature(String sig) {
ListIterator<String> toks = tokenizeSignature(sig);
CssPropertySignature signature = parseSignature(toks);
if (toks.hasNext()) {
throw new IllegalArgumentException(unroll(toks));
}
return signature;
}
static CssPropertySignature parseSignature(ListIterator<String> toks) {
CssPropertySignature child = parseSeries(toks);
if (toks.hasNext()) {
String s = toks.next();
// || matches multiple of its elements but each only once.
if ("||".equals(s) || "|".equals(s)) {
List<CssPropertySignature> children =
new ArrayList<CssPropertySignature>();
children.add(child);
do {
children.add(parseSeries(toks));
if (!toks.hasNext()) { break; }
if (!s.equals(toks.next())) {
toks.previous();
break;
}
} while (true);
if ("||".equals(s)) {
child = new ExclusiveSetSignature(children);
child = new RepeatedSignature(child, 1, children.size());
} else {
child = new SetSignature(children);
}
} else {
toks.previous();
}
}
return child;
}
static CssPropertySignature parseSeries(ListIterator<String> toks) {
CssPropertySignature first = parseSignatureAtom(toks);
if (!toks.hasNext()) { return first; }
String s = toks.next();
if ("]".equals(s) || "|".equals(s) || "||".equals(s) || ")".equals(s)) {
toks.previous();
return first;
}
List<CssPropertySignature> children =
new ArrayList<CssPropertySignature>();
children.add(first);
toks.previous();
do {
children.add(parseSignatureAtom(toks));
if (!toks.hasNext()) { break; }
s = toks.next();
toks.previous();
} while (!("]".equals(s) || "|".equals(s) || "||".equals(s)
|| ")".equals(s)));
return new SeriesSignature(children);
}
static CssPropertySignature parseSignatureAtom(ListIterator<String> toks) {
String s = toks.next();
CssPropertySignature sig;
if ("[".equals(s)) {
sig = parseSignature(toks);
expect(toks, "]");
} else if (Name.css("progid").equals(Name.css(s))) {
if (":".equals(toks.next())) {
sig = parseProgId(toks);
} else {
toks.previous();
sig = new LiteralSignature(s);
}
} else {
char ch0 = s.charAt(0);
if (Character.isLetter(ch0)) { // a literal identifier
sig = new LiteralSignature(s);
} else if (ch0 == '\'') { // a property reference
sig = new PropertyRefSignature(
Name.css(s.substring(1, s.length() - 1)));
} else if (ch0 == '"') { // a quoted literal
sig = new QuotedLiteralSignature(
CssPropertySignature.unescape(s.substring(1, s.length() - 1), false));
} else if (ch0 == '<') { // a symbol
sig = new SymbolSignature(Name.css(s.substring(1, s.length() - 1)));
} else { // a literal number or punctuation mark
sig = new LiteralSignature(s);
}
}
return parsePostOp(parseBracketOp(sig, toks), toks);
}
static CssPropertySignature parsePostOp(
CssPropertySignature sig, ListIterator<String> toks) {
if (!toks.hasNext()) { return sig; }
String s = toks.next();
int min, max;
if (s.equals("{")) {
try {
min = Integer.parseInt(toks.next());
s = toks.next();
if (",".equals(s)) {
max = Integer.parseInt(toks.next());
} else {
max = min;
}
} catch (NumberFormatException ex) {
throw new IllegalArgumentException(unroll(toks), ex);
}
if (!"}".equals(toks.next())) {
throw new IllegalArgumentException(unroll(toks));
}
} else if (s.equals("*")) {
min = 0;
max = Integer.MAX_VALUE;
} else if (s.equals("?")) {
min = 0;
max = 1;
} else if (s.equals("+")) {
min = 1;
max = Integer.MAX_VALUE;
} else {
toks.previous();
return sig;
}
if (sig instanceof RepeatedSignature) {
RepeatedSignature rsig = (RepeatedSignature) sig;
sig = rsig.children().get(0);
min = Math.min(min, rsig.minCount);
long lmax = max * rsig.maxCount;
max = (lmax > Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int) lmax;
}
return new RepeatedSignature(sig, min, max);
}
static CssPropertySignature parseBracketOp(
CssPropertySignature sig, ListIterator<String> toks) {
if (!toks.hasNext()) { return sig; }
if ("(".equals(toks.next())) {
List<CssPropertySignature> children =
new ArrayList<CssPropertySignature>();
children.add(sig);
if (!")".equals(toks.next())) {
toks.previous();
children.add(parseSignature(toks));
if (!")".equals(toks.next())) {
throw new IllegalArgumentException(unroll(toks));
}
}
return new CallSignature(children);
} else {
toks.previous();
return sig;
}
}
private static final Pattern DOTTED_NAME = Pattern.compile(
"^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$", Pattern.CASE_INSENSITIVE);
static ProgIdSignature parseProgId(ListIterator<String> toks) {
StringBuilder name = new StringBuilder();
while (true) {
String t = toks.next();
if ("(".equals(t)) { break; }
name.append(t);
}
if (!DOTTED_NAME.matcher(name).matches()) {
throw new IllegalArgumentException(
"Not dotted name: " + name.toString());
}
List<ProgIdAttrSignature> attrs = new ArrayList<ProgIdAttrSignature>();
String t = toks.next();
if (!")".equals(t)) {
while (true) {
if (!Character.isLetter(t.charAt(0))) {
throw new IllegalArgumentException("Not attr name: " + t);
}
Name attrName = Name.css(t);
t = toks.next();
if (!"=".equals(t)) {
throw new IllegalArgumentException("Not '=':" + t);
}
CssPropertySignature valueSig = parseSignatureAtom(toks);
attrs.add(new ProgIdAttrSignature(attrName, valueSig));
t = toks.next();
if (")".equals(t)) { break; }
if (!",".equals(t)) {
throw new IllegalArgumentException("Not comma: " + t);
}
t = toks.next();
}
}
return new ProgIdSignature(Name.css(name.toString()), attrs);
}
/**
* Used to generate exception messages.
* @param it an iterator over items being processed, where the item which
* caused the problem is immediately behind the current posisiton.
* Consumed.
* @return a string containing the items left on an iterator, separated by
* spaces.
*/
private static String unroll(ListIterator<?> it) {
if (it.hasPrevious()) { it.previous(); }
if (!it.hasNext()) { return ""; }
StringBuilder sb = new StringBuilder();
sb.append(it.next());
while (it.hasNext()) { sb.append(' ').append(it.next()); }
return sb.toString();
}
private static void expect(ListIterator<String> it, String tok) {
if (!it.hasNext()) {
throw new IllegalArgumentException(
"Expected " + tok + ", not end of sig");
}
String next = it.next();
if (!tok.equals(next)) {
throw new IllegalArgumentException(
"Expected " + tok + ", not " + next + " : " + unroll(it));
}
}
private Parser() {
// uninstantiable
}
}
}