4bc64b8c2b
git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@7 260f80e4-7a28-3924-810f-c04153c831b5
487 lines
13 KiB
Dart
487 lines
13 KiB
Dart
// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
/**
|
|
* Base class for simple recursive descent parsers.
|
|
* Handles the lower level stuff, i.e. what a scanner/tokenizer would do.
|
|
*/
|
|
class _Parser {
|
|
static final WHITESPACE = ' \r\n\t';
|
|
|
|
// TODO(jmesserly): shouldn't need this optimization, but dart_json parser
|
|
// found that they needed this.
|
|
static final A_BIG = 65; // 'A'.charCodeAt(0)
|
|
static final Z_BIG = 90; // 'Z'.charCodeAt(0)
|
|
static final A_SMALL = 97; // 'a'.charCodeAt(0)
|
|
static final Z_SMALL = 122; // 'z'.charCodeAt(0)
|
|
static final TAB = 9; // '\t'.charCodeAt(0)
|
|
static final NEW_LINE = 10; // '\n'.charCodeAt(0)
|
|
static final LINE_FEED = 13; // '\r'.charCodeAt(0)
|
|
static final SPACE = 32; // ' '.charCodeAt(0)
|
|
static final ZERO = 48; // '0'.charCodeAt(0)
|
|
static final NINE = 57; // '9'.charCodeAt(0)
|
|
static final DOT = 46; // '.'.charCodeAt(0)
|
|
static final R_PAREN = 41; // ')'.charCodeAt(0)
|
|
|
|
final String _src;
|
|
int _offset;
|
|
|
|
// TODO(jmesserly): should be this._offset = 0, see bug 5332175.
|
|
_Parser(this._src) : _offset = 0;
|
|
|
|
// TODO(jmesserly): these should exist in the standard lib.
|
|
// I took this from dart_json.dart
|
|
static bool _isWhitespace(int c) {
|
|
switch (c) {
|
|
case SPACE:
|
|
case TAB:
|
|
case NEW_LINE:
|
|
case LINE_FEED:
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool _isDigit(int c) {
|
|
return (ZERO <= c) && (c <= NINE);
|
|
}
|
|
|
|
static bool _isLetter(int c) {
|
|
return (A_SMALL <= c) && (c <= Z_SMALL) || (A_BIG <= c) && (c <= Z_BIG);
|
|
}
|
|
|
|
void _error(String msg) {
|
|
throw new SyntaxErrorException(msg, _src, _offset);
|
|
}
|
|
|
|
int get length() => _src.length;
|
|
|
|
int get remaining() => _src.length - _offset;
|
|
|
|
int _peekChar() => _src.charCodeAt(_offset);
|
|
|
|
bool get endOfInput() => _offset >= _src.length;
|
|
|
|
bool _maybeEatWhitespace() {
|
|
int start = _offset;
|
|
while (_offset < length && _isWhitespace(_peekChar())) {
|
|
_offset++;
|
|
}
|
|
return _offset != start;
|
|
}
|
|
|
|
bool _maybeEatMultiLineComment() {
|
|
if (_maybeEat('/*', /*eatWhitespace:*/false)) {
|
|
while (!_maybeEat('*/', /*eatWhitespace:*/false)) {
|
|
if (_offset >= length) {
|
|
_error('expected */');
|
|
}
|
|
_offset++;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void _maybeEatWhitespaceOrComments() {
|
|
while (_maybeEatWhitespace() || _maybeEatMultiLineComment()) {}
|
|
}
|
|
|
|
void _eatEnd() {
|
|
_maybeEatWhitespaceOrComments();
|
|
if (!endOfInput) {
|
|
_error('expected end of input');
|
|
}
|
|
}
|
|
|
|
bool _maybeEat(String value, [bool eatWhitespace = true]) {
|
|
if (eatWhitespace) {
|
|
_maybeEatWhitespaceOrComments();
|
|
}
|
|
if (remaining < value.length) {
|
|
return false;
|
|
}
|
|
for (int i = 0; i < value.length; i++) {
|
|
if (_src[_offset + i] != value[i]) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// If we're eating something that's like a word, make sure
|
|
// it's not followed by more characters.
|
|
// This is ugly. Proper tokenization would make this cleaner.
|
|
if (_isLetter(value.charCodeAt(value.length - 1))) {
|
|
int i = _offset + value.length;
|
|
if (i < _src.length && _isLetter(_src.charCodeAt(i))) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
_offset += value.length;
|
|
return true;
|
|
}
|
|
|
|
void _eat(String value, [bool eatWhitespace = true]) {
|
|
if (!_maybeEat(value)) {
|
|
_error('expected "$value"');
|
|
}
|
|
}
|
|
|
|
String _maybeEatString() {
|
|
// TODO(jmesserly): make this match CSS string parsing
|
|
String quote = "'";
|
|
if (!_maybeEat(quote)) {
|
|
quote = '"';
|
|
if (!_maybeEat(quote)) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
bool hasEscape = false;
|
|
int start = _offset;
|
|
while (!_maybeEat(quote)) {
|
|
if (endOfInput) {
|
|
_error('expected "$quote"');
|
|
}
|
|
if (_maybeEat('\\')) {
|
|
hasEscape = true;
|
|
}
|
|
_offset++;
|
|
}
|
|
String result = _src.substring(start, _offset - 1);
|
|
if (hasEscape) {
|
|
// TODO(jmesserly): more escape sequences
|
|
result = result.replaceFirst('\\', '');
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/** Eats something like a keyword. */
|
|
String _eatWord() {
|
|
int start = _offset;
|
|
while (_offset < length && _isLetter(_peekChar())) {
|
|
_offset++;
|
|
}
|
|
return _src.substring(start, _offset);
|
|
}
|
|
|
|
/** Eats an integer. */
|
|
int _maybeEatInt() {
|
|
int start = _offset;
|
|
bool dot = false;
|
|
while (_offset < length && _isDigit(_peekChar())) {
|
|
_offset++;
|
|
}
|
|
|
|
if (start == _offset) {
|
|
return null;
|
|
}
|
|
|
|
return Math.parseInt(_src.substring(start, _offset));
|
|
}
|
|
|
|
/** Eats an integer. */
|
|
int _eatInt() {
|
|
int result = _maybeEatInt();
|
|
if (result == null) {
|
|
_error('expected positive integer');
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/** Eats something like a positive decimal: 12.345. */
|
|
num _eatDouble() {
|
|
int start = _offset;
|
|
bool dot = false;
|
|
while (_offset < length) {
|
|
int c = _peekChar();
|
|
if (!_isDigit(c)) {
|
|
if (c == DOT && !dot) {
|
|
dot = true;
|
|
} else {
|
|
// Not a digit or decimal seperator
|
|
break;
|
|
}
|
|
}
|
|
_offset++;
|
|
}
|
|
|
|
if (start == _offset) {
|
|
_error('expected positive decimal number');
|
|
}
|
|
|
|
return Math.parseDouble(_src.substring(start, _offset));
|
|
}
|
|
}
|
|
|
|
/** Parses a grid template. */
|
|
class _GridTemplateParser extends _Parser {
|
|
_GridTemplateParser._internal(String src) : super(src);
|
|
|
|
/** Parses the grid-rows and grid-columns CSS properties into object form. */
|
|
static GridTemplate parse(String str) {
|
|
if (str == null) return null;
|
|
final p = new _GridTemplateParser._internal(str);
|
|
final result = p._parseTemplate();
|
|
p._eatEnd();
|
|
return result;
|
|
}
|
|
|
|
/** Parses a grid-cell value. */
|
|
static String parseCell(String str) {
|
|
if (str == null) return null;
|
|
final p = new _GridTemplateParser._internal(str);
|
|
final result = p._maybeEatString();
|
|
p._eatEnd();
|
|
return result;
|
|
}
|
|
|
|
// => <string>+ | 'none'
|
|
GridTemplate _parseTemplate() {
|
|
if (_maybeEat('none')) {
|
|
return null;
|
|
}
|
|
final rows = new List<String>();
|
|
String row;
|
|
while ((row = _maybeEatString()) != null) {
|
|
rows.add(row);
|
|
}
|
|
if (rows.length == 0) {
|
|
_error('expected at least one cell, or "none"');
|
|
}
|
|
return new GridTemplate(rows);
|
|
}
|
|
}
|
|
|
|
/** Parses a grid-row or grid-column */
|
|
class _GridItemParser extends _Parser {
|
|
_GridItemParser._internal(String src) : super(src);
|
|
|
|
/** Parses the grid-rows and grid-columns CSS properties into object form. */
|
|
static _GridLocation parse(String cell, GridTrackList list) {
|
|
if (cell == null) return null;
|
|
final p = new _GridItemParser._internal(cell);
|
|
final result = p._parseTrack(list);
|
|
p._eatEnd();
|
|
return result;
|
|
}
|
|
|
|
// [ [ <integer> | <string> | 'start' | 'end' ]
|
|
// [ <integer> | <string> | 'start' | 'end' ]? ]
|
|
// | 'auto'
|
|
_GridLocation _parseTrack(GridTrackList list) {
|
|
if (_maybeEat('auto')) {
|
|
return null;
|
|
}
|
|
int start = _maybeParseLine(list);
|
|
if (start == null) {
|
|
_error('expected row/column number or name');
|
|
}
|
|
int end = _maybeParseLine(list);
|
|
int span = null;
|
|
if (end != null) {
|
|
span = end - start;
|
|
if (span <= 0) {
|
|
_error('expected row/column span to be a positive integer');
|
|
}
|
|
}
|
|
return new _GridLocation(start, span);
|
|
}
|
|
|
|
// [ <integer> | <string> | 'start' | 'end' ]
|
|
int _maybeParseLine(GridTrackList list) {
|
|
if (_maybeEat('start')) {
|
|
return 1;
|
|
} else if (_maybeEat('end')) {
|
|
// The end is exclusive and 1-based, so return one past the size of the
|
|
// track list.
|
|
// TODO(jmesserly): this won't interact properly with implicit
|
|
// rows/columns. Instead it will snap to the number of tracks at the point
|
|
// where it is evaluated.
|
|
return list.tracks.length + 1;
|
|
}
|
|
|
|
String name = _maybeEatString();
|
|
if (name == null) {
|
|
return _maybeEatInt();
|
|
} else {
|
|
int edge = list.lineNames[name];
|
|
if (edge == null) {
|
|
_error('row/column name "$name" not found in the parent\'s '
|
|
+ ' grid-row/grid-columns properties');
|
|
}
|
|
return edge;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses grid-rows and grid-column properties, see:
|
|
* [http://dev.w3.org/csswg/css3-grid-align/#grid-columns-and-rows-properties]
|
|
* This is kept as a recursive descent parser for simplicity.
|
|
*/
|
|
// TODO(jmesserly): implement missing features from the spec. Mainly around
|
|
// CSS units, support for all escape sequences, etc.
|
|
class _GridTrackParser extends _Parser {
|
|
final List<GridTrack> _tracks;
|
|
final Map<String, int> _lineNames;
|
|
|
|
_GridTrackParser._internal(String src)
|
|
: super(src),
|
|
_tracks = new List<GridTrack>(),
|
|
_lineNames = new Map<String, int>();
|
|
|
|
/** Parses the grid-rows and grid-columns CSS properties into object form. */
|
|
static GridTrackList parse(String str) {
|
|
if (str == null) return null;
|
|
final p = new _GridTrackParser._internal(str);
|
|
final result = p._parseTrackList();
|
|
p._eatEnd();
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Parses the grid-row-sizing and grid-column-sizing CSS properties into
|
|
* object form.
|
|
*/
|
|
static TrackSizing parseTrackSizing(String str) {
|
|
if (str == null) str = 'auto';
|
|
final p = new _GridTrackParser._internal(str);
|
|
final result = p._parseTrackMinmax();
|
|
p._eatEnd();
|
|
return result;
|
|
}
|
|
|
|
// <track-list> => [ [ <string> ]* <track-group> [ <string> ]* ]+ | 'none'
|
|
GridTrackList _parseTrackList() {
|
|
if (_maybeEat('none')) {
|
|
return null;
|
|
}
|
|
_parseTrackListHelper();
|
|
return new GridTrackList(_tracks, _lineNames);
|
|
}
|
|
|
|
/** Code shared by _parseTrackList and _parseTrackGroup */
|
|
void _parseTrackListHelper([List<GridTrack> resultTracks = null]) {
|
|
_maybeEatWhitespace();
|
|
while (!endOfInput) {
|
|
String name;
|
|
while ((name = _maybeEatString()) != null) {
|
|
_lineNames[name] = _tracks.length + 1; // should be 1-based
|
|
}
|
|
|
|
_maybeEatWhitespace();
|
|
if (endOfInput) {
|
|
return;
|
|
}
|
|
|
|
if (resultTracks != null) {
|
|
if (_peekChar() == R_PAREN) {
|
|
return;
|
|
}
|
|
resultTracks.add(new GridTrack(_parseTrackMinmax()));
|
|
} else {
|
|
_parseTrackGroup();
|
|
}
|
|
|
|
_maybeEatWhitespace();
|
|
}
|
|
}
|
|
|
|
// <track-group> => [ '(' [ [ <string> ]* <track-minmax> [ <string> ]* ]+ ')'
|
|
// [ '[' <positive-number> ']' ]? ]
|
|
// | <track-minmax>
|
|
void _parseTrackGroup() {
|
|
if (_maybeEat('(')) {
|
|
final tracks = new List<GridTrack>();
|
|
_parseTrackListHelper(tracks);
|
|
_eat(')');
|
|
if (_maybeEat('[')) {
|
|
num expand = _eatInt();
|
|
_eat(']');
|
|
|
|
if (expand <= 0) {
|
|
_error('expected positive number');
|
|
}
|
|
|
|
// Repeat the track definition (but not the names) the specified number
|
|
// of times. See:
|
|
// http://dev.w3.org/csswg/css3-grid-align/#grid-repeating-columns-and-rows
|
|
for (int i = 0; i < expand; i++) {
|
|
for (GridTrack t in tracks) {
|
|
_tracks.add(t.clone());
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
_tracks.add(new GridTrack(_parseTrackMinmax()));
|
|
}
|
|
}
|
|
|
|
// <track-minmax> => 'minmax(' <track-breadth> ',' <track-breadth> ')'
|
|
// | 'auto' | <track-breadth>
|
|
TrackSizing _parseTrackMinmax() {
|
|
if (_maybeEat('auto') || _maybeEat('fit-content')) {
|
|
return const TrackSizing.auto();
|
|
}
|
|
if (_maybeEat('minmax(')) {
|
|
final min = _parseTrackBreadth();
|
|
_eat(',');
|
|
final max = _parseTrackBreadth();
|
|
_eat(')');
|
|
return new TrackSizing(min, max);
|
|
} else {
|
|
final breadth = _parseTrackBreadth();
|
|
return new TrackSizing(breadth, breadth);
|
|
}
|
|
}
|
|
|
|
// <track-breadth> => <length> | <percentage> | <fraction>
|
|
// | 'min-content' | 'max-content'
|
|
SizingFunction _parseTrackBreadth() {
|
|
if (_maybeEat('min-content')) {
|
|
return const MinContentSizing();
|
|
} else if (_maybeEat('max-content')) {
|
|
return const MaxContentSizing();
|
|
}
|
|
|
|
num value = _eatDouble();
|
|
|
|
String units;
|
|
if (_maybeEat('%')) {
|
|
units = '%';
|
|
} else {
|
|
units = _eatWord();
|
|
}
|
|
|
|
if (units == 'fr') {
|
|
return new FractionSizing(value);
|
|
} else {
|
|
return new FixedSizing(value, units);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Exception thrown because the grid style properties had incorrect values.
|
|
*/
|
|
class SyntaxErrorException implements Exception {
|
|
final String _message;
|
|
final int _offset;
|
|
final String _source;
|
|
|
|
const SyntaxErrorException(this._message, this._source, this._offset);
|
|
|
|
String toString() {
|
|
String location;
|
|
if (_offset < _source.length) {
|
|
location = 'location: ' + _source.substringToEnd(_offset);
|
|
} else {
|
|
location = 'end of input';
|
|
}
|
|
return 'SyntaxErrorException: $_message at $location';
|
|
}
|
|
}
|