/*
* Copyright (c) 2007-2013 Mozilla Foundation
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
package org.whattf.datatype;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.Set;
import org.relaxng.datatype.DatatypeException;
public class MediaQuery extends AbstractDatatype {
/**
* The singleton instance.
*/
public static final MediaQuery THE_INSTANCE = new MediaQuery();
private static final boolean WARN = System.getProperty(
"org.whattf.datatype.warn", "").equals("true") ? true : false;
private enum State {
INITIAL_WS, OPEN_PAREN_SEEN, IN_ONLY_OR_NOT, IN_MEDIA_TYPE, IN_MEDIA_FEATURE, WS_BEFORE_MEDIA_TYPE, WS_BEFORE_AND, IN_AND, WS_BEFORE_EXPRESSION, WS_BEFORE_COLON, WS_BEFORE_VALUE, IN_VALUE_DIGITS, IN_VALUE_SCAN, IN_VALUE_ORIENTATION, WS_BEFORE_CLOSE_PAREN, IN_VALUE_UNIT, IN_VALUE_DIGITS_AFTER_DOT, RATIO_SECOND_INTEGER_START, IN_VALUE_BEFORE_DIGITS, IN_VALUE_DIGITS_AFTER_DOT_TRAIL, AFTER_CLOSE_PAREN, IN_VALUE_ONEORZERO
}
private enum ValueType {
LENGTH, RATIO, INTEGER, RESOLUTION, SCAN, ORIENTATION, NONZEROINTEGER, ONEORZERO
}
private static final Set<String> LENGTH_UNITS = new HashSet<String>();
static {
LENGTH_UNITS.add("em");
LENGTH_UNITS.add("ex");
LENGTH_UNITS.add("px");
LENGTH_UNITS.add("gd");
LENGTH_UNITS.add("rem");
LENGTH_UNITS.add("vw");
LENGTH_UNITS.add("vh");
LENGTH_UNITS.add("vm");
LENGTH_UNITS.add("ch");
LENGTH_UNITS.add("in");
LENGTH_UNITS.add("cm");
LENGTH_UNITS.add("mm");
LENGTH_UNITS.add("pt");
LENGTH_UNITS.add("pc");
}
private static final Set<String> MEDIA_TYPES = new HashSet<String>();
static {
MEDIA_TYPES.add("all");
MEDIA_TYPES.add("aural");
MEDIA_TYPES.add("braille");
MEDIA_TYPES.add("handheld");
MEDIA_TYPES.add("print");
MEDIA_TYPES.add("projection");
MEDIA_TYPES.add("screen");
MEDIA_TYPES.add("tty");
MEDIA_TYPES.add("tv");
MEDIA_TYPES.add("embossed");
MEDIA_TYPES.add("speech");
}
private enum MediaType {
ALL, AURAL, BRAILLE, HANDHELD, PRINT, PROJECTION, SCREEN, TTY, TV, EMBOSSED, SPEECH, INVALID;
private static MediaType toCaps(String str) {
try {
return valueOf(toAsciiUpperCase(str));
} catch (Exception ex) {
return INVALID;
}
}
}
private static final Map<String, ValueType> FEATURES_TO_VALUE_TYPES = new HashMap<String, ValueType>();
static {
FEATURES_TO_VALUE_TYPES.put("width", ValueType.LENGTH);
FEATURES_TO_VALUE_TYPES.put("min-width", ValueType.LENGTH);
FEATURES_TO_VALUE_TYPES.put("max-width", ValueType.LENGTH);
FEATURES_TO_VALUE_TYPES.put("height", ValueType.LENGTH);
FEATURES_TO_VALUE_TYPES.put("min-height", ValueType.LENGTH);
FEATURES_TO_VALUE_TYPES.put("max-height", ValueType.LENGTH);
FEATURES_TO_VALUE_TYPES.put("device-width", ValueType.LENGTH);
FEATURES_TO_VALUE_TYPES.put("min-device-width", ValueType.LENGTH);
FEATURES_TO_VALUE_TYPES.put("max-device-width", ValueType.LENGTH);
FEATURES_TO_VALUE_TYPES.put("device-height", ValueType.LENGTH);
FEATURES_TO_VALUE_TYPES.put("min-device-height", ValueType.LENGTH);
FEATURES_TO_VALUE_TYPES.put("max-device-height", ValueType.LENGTH);
FEATURES_TO_VALUE_TYPES.put("device-aspect-ratio", ValueType.RATIO);
FEATURES_TO_VALUE_TYPES.put("min-device-aspect-ratio", ValueType.RATIO);
FEATURES_TO_VALUE_TYPES.put("max-device-aspect-ratio", ValueType.RATIO);
FEATURES_TO_VALUE_TYPES.put("aspect-ratio", ValueType.RATIO);
FEATURES_TO_VALUE_TYPES.put("min-aspect-ratio", ValueType.RATIO);
FEATURES_TO_VALUE_TYPES.put("max-aspect-ratio", ValueType.RATIO);
FEATURES_TO_VALUE_TYPES.put("color", ValueType.INTEGER);
FEATURES_TO_VALUE_TYPES.put("min-color", ValueType.INTEGER);
FEATURES_TO_VALUE_TYPES.put("max-color", ValueType.INTEGER);
FEATURES_TO_VALUE_TYPES.put("color-index", ValueType.INTEGER);
FEATURES_TO_VALUE_TYPES.put("min-color-index", ValueType.INTEGER);
FEATURES_TO_VALUE_TYPES.put("max-color-index", ValueType.INTEGER);
FEATURES_TO_VALUE_TYPES.put("monochrome", ValueType.INTEGER);
FEATURES_TO_VALUE_TYPES.put("min-monochrome", ValueType.INTEGER);
FEATURES_TO_VALUE_TYPES.put("max-monochrome", ValueType.INTEGER);
FEATURES_TO_VALUE_TYPES.put("resolution", ValueType.RESOLUTION);
FEATURES_TO_VALUE_TYPES.put("min-resolution", ValueType.RESOLUTION);
FEATURES_TO_VALUE_TYPES.put("max-resolution", ValueType.RESOLUTION);
FEATURES_TO_VALUE_TYPES.put("scan", ValueType.SCAN);
FEATURES_TO_VALUE_TYPES.put("orientation", ValueType.ORIENTATION);
FEATURES_TO_VALUE_TYPES.put("grid", ValueType.ONEORZERO);
}
private static final String[] visualFeatures = { "aspect-ratio", "color",
"color-index", "device-aspect-ratio", "max-aspect-ratio",
"max-color", "max-color-index", "max-device-aspect-ratio",
"max-monochrome", "max-resolution", "min-aspect-ratio",
"min-color", "min-color-index", "min-device-aspect-ratio",
"min-monochrome", "min-resolution", "monochrome", "orientation",
"resolution", };
private static final String[] bitmapFeatures = { "aspect-ratio",
"device-aspect-ratio", "max-aspect-ratio",
"max-device-aspect-ratio", "max-resolution", "min-aspect-ratio",
"min-device-aspect-ratio", "min-resolution", "orientation",
"resolution", };
private static final String scanWarning = "The media feature \u201cscan\u201d is applicable only to the media type \u201ctv\u201d. ";
private MediaQuery() {
super();
}
@Override public void checkValid(CharSequence literal)
throws DatatypeException {
List<String> warnings = new ArrayList<String>();
List<CharSequenceWithOffset> queries = split(literal, ',');
for (CharSequenceWithOffset query : queries) {
warnings = checkQuery(query.getSequence(), query.getOffset(),
warnings);
}
if (!warnings.isEmpty() && WARN) {
StringBuilder sb = new StringBuilder();
for (String s : warnings) {
sb.append(s + " ");
}
throw newDatatypeException(sb.toString().trim(), WARN);
}
}
private List<String> checkQuery(CharSequence query, int offset,
List<String> warnings) throws DatatypeException {
boolean containsAural = false;
boolean zero = true;
String type = null;
String feature = null;
ValueType valueExpectation = null;
query = toAsciiLowerCase(query);
StringBuilder sb = new StringBuilder();
State state = State.INITIAL_WS;
for (int i = 0; i < query.length(); i++) {
char c = query.charAt(i);
switch (state) {
case INITIAL_WS:
if (isWhitespace(c)) {
continue;
} else if ('(' == c) {
state = State.OPEN_PAREN_SEEN;
continue;
} else if ('o' == c || 'n' == c) {
sb.append(c);
state = State.IN_ONLY_OR_NOT;
continue;
} else if ('a' <= c && 'z' >= c) {
sb.append(c);
state = State.IN_MEDIA_TYPE;
continue;
} else {
throw newDatatypeException(
offset + i,
"Expected \u201C(\u201D or letter at start of a media query part but saw ",
c, " instead.");
}
case IN_ONLY_OR_NOT:
if ('a' <= c && 'z' >= c) {
sb.append(c);
continue;
} else if (isWhitespace(c)) {
String kw = sb.toString();
sb.setLength(0);
if ("only".equals(kw) || "not".equals(kw)) {
state = State.WS_BEFORE_MEDIA_TYPE;
continue;
} else {
throw newDatatypeException(offset + i,
"Expected \u201Conly\u201D or \u201Cnot\u201D but saw \u201C"
+ kw + "\u201D instead.");
}
} else {
throw newDatatypeException(offset + i,
"Expected a letter or whitespace but saw \u201C"
+ c + "\u201D instead.");
}
case WS_BEFORE_MEDIA_TYPE:
if (isWhitespace(c)) {
continue;
} else if ('a' <= c && 'z' >= c) {
sb.append(c);
state = State.IN_MEDIA_TYPE;
continue;
} else {
throw newDatatypeException(offset + i,
"Expected a letter or whitespace but saw \u201C"
+ c + "\u201D instead.");
}
case IN_MEDIA_TYPE:
if (('a' <= c && 'z' >= c) || c == '-') {
sb.append(c);
continue;
} else if (isWhitespace(c)) {
/*
* store media type for later media-feature
* applicability check
*/
type = sb.toString();
sb.setLength(0);
if (isMediaType(type)) {
if ("aural".equals(type)) {
containsAural = true;
}
state = State.WS_BEFORE_AND;
continue;
} else {
throw newDatatypeException(offset + i,
"Expected a CSS media type but saw \u201C"
+ type + "\u201D instead.");
}
} else {
throw newDatatypeException(offset + i,
"Expected a letter, hyphen or whitespace but saw \u201C"
+ c + "\u201D instead.");
}
case WS_BEFORE_AND:
if (isWhitespace(c)) {
continue;
} else if ('a' == c) {
sb.append(c);
state = State.IN_AND;
continue;
} else {
throw newDatatypeException(offset + i,
"Expected whitespace or \u201Cand\u201D but saw"
+ " \u201C" + c + "\u201D instead.");
}
case IN_AND:
if ('a' <= c && 'z' >= c) {
sb.append(c);
continue;
} else if (isWhitespace(c)) {
String kw = sb.toString();
sb.setLength(0);
if ("and".equals(kw)) {
state = State.WS_BEFORE_EXPRESSION;
continue;
} else {
throw newDatatypeException(offset + i,
"Expected \u201Cand\u201D but saw \u201C"
+ kw + "\u201D instead.");
}
} else {
throw newDatatypeException(offset + i,
"Expected a letter or whitespace but saw \u201C"
+ c + "\u201D instead.");
}
case WS_BEFORE_EXPRESSION:
if (isWhitespace(c)) {
continue;
} else if ('(' == c) {
state = State.OPEN_PAREN_SEEN;
continue;
} else {
throw newDatatypeException(offset + i,
"Expected \u201C(\u201D or whitespace but saw \u201C"
+ c + "\u201D instead.");
}
case OPEN_PAREN_SEEN:
if (isWhitespace(c)) {
continue;
} else if ('a' <= c && 'z' >= c) {
sb.append(c);
state = State.IN_MEDIA_FEATURE;
continue;
} else {
throw newDatatypeException(offset + i,
"Expected a letter at start of a media feature part but saw \u201C"
+ c + "\u201D instead.");
}
case IN_MEDIA_FEATURE:
if (('a' <= c && 'z' >= c) || c == '-') {
sb.append(c);
continue;
} else if (c == ')') {
String kw = sb.toString();
sb.setLength(0);
checkApplicability(offset + i, kw, type, warnings);
checkIfValueRequired(offset + i, kw);
state = State.AFTER_CLOSE_PAREN;
continue;
} else if (isWhitespace(c) || c == ':') {
String kw = sb.toString();
sb.setLength(0);
checkApplicability(offset + i, kw, type, warnings);
feature = kw;
valueExpectation = valueExpectationFor(kw);
if (valueExpectation != null) {
if (c == ':') {
state = State.WS_BEFORE_VALUE;
continue;
} else {
state = State.WS_BEFORE_COLON;
continue;
}
} else {
throw newDatatypeException(offset + i,
"Expected a CSS media feature but saw \u201C"
+ kw + "\u201D instead.");
}
} else {
throw newDatatypeException(offset + i,
"Expected a letter, hyphen, colon or whitespace but saw \u201C"
+ c + "\u201D instead.");
}
case WS_BEFORE_COLON:
if (isWhitespace(c)) {
continue;
} else if (':' == c) {
state = State.WS_BEFORE_VALUE;
continue;
} else {
throw newDatatypeException(offset + i,
"Expected whitespace or colon but saw \u201C"
+ c + "\u201D instead.");
}
case WS_BEFORE_VALUE:
if (isWhitespace(c)) {
continue;
} else {
zero = true;
switch (valueExpectation) {
case SCAN:
if ('a' <= c && 'z' >= c) {
sb.append(c);
state = State.IN_VALUE_SCAN;
continue;
} else {
throw newDatatypeException(offset + i,
"Expected a letter but saw \u201C"
+ c + "\u201D instead.");
}
case ORIENTATION:
if ('a' <= c && 'z' >= c) {
sb.append(c);
state = State.IN_VALUE_ORIENTATION;
continue;
} else {
throw newDatatypeException(offset + i,
"Expected a letter but saw \u201C"
+ c + "\u201D instead.");
}
case ONEORZERO:
if (c == '0' || c == '1') {
sb.append(c);
state = State.IN_VALUE_ONEORZERO;
continue;
} else {
throw newDatatypeException(
offset + i,
"Expected \u201C0\u201D or \u201C1\u201D as \u201c"
+ feature
+ "\u201d value but found \u201C"
+ c + "\u201D instead.");
}
default:
if ('1' <= c && '9' >= c) {
zero = false;
state = State.IN_VALUE_DIGITS;
continue;
} else if ('0' == c) {
state = State.IN_VALUE_DIGITS;
continue;
} else if ('+' == c) {
state = State.IN_VALUE_BEFORE_DIGITS;
continue;
} else if ('.' == c
&& valueExpectation == ValueType.LENGTH) {
state = State.IN_VALUE_DIGITS_AFTER_DOT;
continue;
} else if (valueExpectation == ValueType.LENGTH) {
throw newDatatypeException(offset + i,
"Expected a digit, a dot or a plus sign but saw \u201C"
+ c + "\u201D instead.");
} else {
throw newDatatypeException(offset + i,
"Expected a digit or a plus sign but saw \u201C"
+ c + "\u201D instead.");
}
}
}
case IN_VALUE_SCAN:
if ('a' <= c && 'z' >= c) {
sb.append(c);
continue;
} else if (isWhitespace(c) || c == ')') {
String kw = sb.toString();
sb.setLength(0);
if (!("progressive".equals(kw) || "interlace".equals(kw))) {
throw newDatatypeException(
offset + i,
"Expected \u201Cprogressive\u201D or \u201Cinterlace\u201D as the scan mode value but saw \u201C"
+ kw + "\u201D instead.");
}
if (c == ')') {
state = State.AFTER_CLOSE_PAREN;
continue;
} else {
state = State.WS_BEFORE_CLOSE_PAREN;
continue;
}
} else {
throw newDatatypeException(offset + i,
"Expected a letter, whitespace or \u201C)\u201D but saw \u201C"
+ c + "\u201D instead.");
}
case IN_VALUE_ORIENTATION:
if ('a' <= c && 'z' >= c) {
sb.append(c);
continue;
} else if (isWhitespace(c) || c == ')') {
String kw = sb.toString();
sb.setLength(0);
if (!("portrait".equals(kw) || "landscape".equals(kw))) {
throw newDatatypeException(
offset + i,
"Expected \u201Cportrait\u201D or \u201Clandscape\u201D as the \u201corientation\u201d value but saw \u201C"
+ kw + "\u201D instead.");
}
if (c == ')') {
state = State.AFTER_CLOSE_PAREN;
continue;
} else {
state = State.WS_BEFORE_CLOSE_PAREN;
continue;
}
} else {
throw newDatatypeException(offset + i,
"Expected a letter, whitespace or \u201C)\u201D but saw \u201C"
+ c + "\u201D instead.");
}
case IN_VALUE_ONEORZERO:
if (isWhitespace(c) || c == ')') {
sb.setLength(0);
if (c == ')') {
state = State.AFTER_CLOSE_PAREN;
continue;
} else {
state = State.WS_BEFORE_CLOSE_PAREN;
continue;
}
} else {
sb.append(c);
String kw = sb.toString();
throw newDatatypeException(offset + i,
"Expected \u201C0\u201D or \u201C1\u201D as \u201c"
+ feature
+ "\u201d value but saw \u201C" + kw
+ "\u201D instead.");
}
case IN_VALUE_BEFORE_DIGITS:
if ('0' == c) {
state = State.IN_VALUE_DIGITS;
continue;
} else if ('1' <= c && '9' >= c) {
zero = false;
state = State.IN_VALUE_DIGITS;
continue;
} else {
switch (valueExpectation) {
case LENGTH:
case RESOLUTION:
if ('.' == c) {
state = State.IN_VALUE_DIGITS_AFTER_DOT;
continue;
} else {
throw newDatatypeException(offset + i,
"Expected a dot or a digit but saw \u201C"
+ c + "\u201D instead.");
}
case INTEGER:
case RATIO:
throw newDatatypeException(offset + i,
"Expected a digit but saw \u201C" + c
+ "\u201D instead.");
default:
throw new RuntimeException("Impossible state.");
}
}
case IN_VALUE_DIGITS:
if ('0' == c) {
continue;
} else if ('1' <= c && '9' >= c) {
zero = false;
continue;
} else {
switch (valueExpectation) {
case LENGTH:
case RESOLUTION:
if ('.' == c) {
state = State.IN_VALUE_DIGITS_AFTER_DOT;
continue;
} else if ('a' <= c && 'z' >= c) {
sb.append(c);
state = State.IN_VALUE_UNIT;
continue;
} else if (isWhitespace(c) || c == ')') {
if (!zero) {
if (valueExpectation == ValueType.LENGTH) {
throw newDatatypeException(offset
+ i,
"Non-zero lengths require a unit.");
} else {
throw newDatatypeException(offset
+ i,
"Non-zero resolutions require a unit.");
}
}
if (c == ')') {
state = State.AFTER_CLOSE_PAREN;
continue;
} else {
state = State.WS_BEFORE_CLOSE_PAREN;
continue;
}
} else {
throw newDatatypeException(offset + i,
"Expected a letter, a dot or a digit but saw \u201C"
+ c + "\u201D instead.");
}
case INTEGER:
if (c == ')') {
state = State.AFTER_CLOSE_PAREN;
continue;
} else if (isWhitespace(c)) {
state = State.WS_BEFORE_CLOSE_PAREN;
continue;
} else {
throw newDatatypeException(offset + i,
"Expected a digit, whitespace or \u201C)\u201D but saw \u201C"
+ c + "\u201D instead.");
}
case NONZEROINTEGER:
if (c == ')') {
if (zero) {
throw newDatatypeException(offset + i,
"Expected a non-zero positive integer.");
}
state = State.AFTER_CLOSE_PAREN;
continue;
} else if (isWhitespace(c)) {
state = State.WS_BEFORE_CLOSE_PAREN;
continue;
} else {
throw newDatatypeException(offset + i,
"Expected a digit, whitespace or \u201C)\u201D but saw \u201C"
+ c + "\u201D instead.");
}
case RATIO:
if (isWhitespace(c)) {
continue;
} else if (c == '/') {
if (zero) {
throw newDatatypeException(offset + i,
"Expected non-zero positive integer in ratio value.");
}
valueExpectation = ValueType.NONZEROINTEGER;
state = State.RATIO_SECOND_INTEGER_START;
continue;
} else {
throw newDatatypeException(offset + i,
"Expected a digit, whitespace or"
+ " \u201C/\u201D for "
+ feature
+ " value but saw \u201C"
+ c + "\u201D instead.");
}
default:
throw new RuntimeException("Impossible state.");
}
}
case IN_VALUE_DIGITS_AFTER_DOT:
if ('0' == c) {
state = State.IN_VALUE_DIGITS_AFTER_DOT_TRAIL;
continue;
} else if ('1' <= c && '9' >= c) {
state = State.IN_VALUE_DIGITS_AFTER_DOT_TRAIL;
zero = false;
continue;
} else {
throw newDatatypeException(offset + i,
"Expected a digit but saw \u201C" + c
+ "\u201D instead.");
}
case IN_VALUE_DIGITS_AFTER_DOT_TRAIL:
if ('0' == c) {
continue;
} else if ('1' <= c && '9' >= c) {
zero = false;
continue;
} else {
switch (valueExpectation) {
case LENGTH:
case RESOLUTION:
if ('a' <= c && 'z' >= c) {
sb.append(c);
state = State.IN_VALUE_UNIT;
continue;
} else if (isWhitespace(c) || c == ')') {
if (!zero) {
if (valueExpectation == ValueType.LENGTH) {
throw newDatatypeException(offset
+ i,
"Non-zero lengths require a unit.");
} else {
throw newDatatypeException(offset
+ i,
"Non-zero resolutions require a unit.");
}
}
if (c == ')') {
state = State.AFTER_CLOSE_PAREN;
continue;
} else {
state = State.WS_BEFORE_CLOSE_PAREN;
continue;
}
} else {
throw newDatatypeException(offset + i,
"Expected a letter, a digit, whitespace or \u201C)\u201D but saw \u201C"
+ c + "\u201D instead.");
}
default:
throw new RuntimeException("Impossible state.");
}
}
case IN_VALUE_UNIT:
if ('a' <= c && 'z' >= c) {
sb.append(c);
continue;
} else if (isWhitespace(c) || c == ')') {
String kw = sb.toString();
sb.setLength(0);
if (valueExpectation == ValueType.LENGTH) {
if (!isLengthUnit(kw)) {
throw newDatatypeException(offset + i,
"Expected a length unit but saw \u201C"
+ c + "\u201D instead.");
}
} else {
if (!("dpi".equals(kw) || "dpcm".equals(kw))) {
throw newDatatypeException(offset + i,
"Expected a resolution unit but saw \u201C"
+ c + "\u201D instead.");
}
}
if (c == ')') {
state = State.AFTER_CLOSE_PAREN;
continue;
} else {
state = State.WS_BEFORE_CLOSE_PAREN;
continue;
}
} else {
throw newDatatypeException(offset + i,
"Expected a letter, a dot or a digit but saw \u201C"
+ c + "\u201D instead.");
}
case RATIO_SECOND_INTEGER_START:
valueExpectation = ValueType.NONZEROINTEGER;
if (isWhitespace(c)) {
continue;
} else if ('1' <= c && '9' >= c) {
zero = false;
state = State.IN_VALUE_DIGITS;
continue;
} else if ('0' == c) {
zero = true;
state = State.IN_VALUE_DIGITS;
continue;
} else if ('+' == c) {
state = State.IN_VALUE_BEFORE_DIGITS;
continue;
} else {
throw newDatatypeException(offset + i,
"Expected a digit, whitespace or a plus sign"
+ " for " + feature
+ " value but saw \u201C" + c
+ "\u201D instead.");
}
case AFTER_CLOSE_PAREN:
if (isWhitespace(c)) {
state = State.WS_BEFORE_AND;
continue;
} else {
throw newDatatypeException(offset + i,
"Expected whitespace but saw \u201C" + c
+ "\u201D instead.");
}
case WS_BEFORE_CLOSE_PAREN:
if (isWhitespace(c)) {
continue;
} else if (c == ')') {
state = State.AFTER_CLOSE_PAREN;
continue;
} else {
throw newDatatypeException(offset + i,
"Expected whitespace or \u201C)\u201D but saw \u201C"
+ c + "\u201D instead.");
}
}
}
switch (state) {
case AFTER_CLOSE_PAREN:
case WS_BEFORE_AND:
if (containsAural && WARN) {
warnings.add("The media type \u201caural\u201d is deprecated. Use \u201cspeech\u201d instead. ");
}
return warnings;
case IN_MEDIA_TYPE:
String kw = sb.toString();
sb.setLength(0);
if (isMediaType(kw)) {
if ("aural".equals(kw) && WARN) {
warnings.add("The media type \u201caural\u201d is deprecated. Use \u201cspeech\u201d instead. ");
}
return warnings;
} else {
throw newDatatypeException("Expected a CSS media type but the query ended.");
}
default:
throw newDatatypeException("Media query ended prematurely.");
}
}
private boolean isMediaFeature(String feature) {
return FEATURES_TO_VALUE_TYPES.containsKey(feature);
}
private ValueType valueExpectationFor(String feature) {
return FEATURES_TO_VALUE_TYPES.get(feature);
}
private boolean isMediaType(String type) {
return MEDIA_TYPES.contains(type);
}
private boolean isLengthUnit(String unit) {
return LENGTH_UNITS.contains(unit);
}
private List<String> checkApplicability(int index, String feature,
String type, List<String> warnings) throws DatatypeException {
if (!isMediaType(type)) {
return warnings;
}
if (!isMediaFeature(feature)) {
throw newDatatypeException(index,
"Expected a CSS media feature but saw \u201C" + feature
+ "\u201D instead.");
}
if ("scan".equals(feature) && !"tv".equals(type)) {
warnings.add(scanWarning);
return warnings;
}
switch (MediaType.toCaps(type)) {
case SPEECH:
warnings.add("The media feature \u201c"
+ feature
+ "\u201d is not applicable to the media type \u201cspeech\u201d. ");
return warnings;
case BRAILLE:
case EMBOSSED:
if (Arrays.binarySearch(visualFeatures, feature) > -1) {
warnings.add("The visual media feature \u201c"
+ feature
+ "\u201d is not applicable to the tactile media type \u201c"
+ type + "\u201d. ");
}
return warnings;
case TTY:
if (Arrays.binarySearch(bitmapFeatures, feature) > -1) {
warnings.add("The bitmap media feature \u201c"
+ feature
+ "\u201d is not applicable to the media type \u201ctty\u201d. ");
}
return warnings;
default:
return warnings;
}
}
private void checkIfValueRequired(int index, String feature)
throws DatatypeException {
if (feature.startsWith("min-") || feature.startsWith("max-")) {
throw newDatatypeException(index,
"Expected a value for the media feature \u201C" + feature
+ "\u201D.");
}
}
@Override public String getName() {
return "media query";
}
}