blob: 71f272df0b81ec3e11fb9c68a87ad702f8451ac9 [file] [log] [blame]
// 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.
// @dart = 2.9
part of layout;
/**
* Base class for simple recursive descent parsers.
* Handles the lower level stuff, i.e. what a scanner/tokenizer would do.
*/
class _Parser {
static const WHITESPACE = ' \r\n\t';
// TODO(jmesserly): shouldn't need this optimization, but dart_json parser
// found that they needed this.
static const A_BIG = 65; // 'A'.codeUnitAt(0)
static const Z_BIG = 90; // 'Z'.codeUnitAt(0)
static const A_SMALL = 97; // 'a'.codeUnitAt(0)
static const Z_SMALL = 122; // 'z'.codeUnitAt(0)
static const TAB = 9; // '\t'.codeUnitAt(0)
static const NEW_LINE = 10; // '\n'.codeUnitAt(0)
static const LINE_FEED = 13; // '\r'.codeUnitAt(0)
static const SPACE = 32; // ' '.codeUnitAt(0)
static const ZERO = 48; // '0'.codeUnitAt(0)
static const NINE = 57; // '9'.codeUnitAt(0)
static const DOT = 46; // '.'.codeUnitAt(0)
static const R_PAREN = 41; // ')'.codeUnitAt(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.codeUnitAt(_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.codeUnitAt(value.length - 1))) {
int i = _offset + value.length;
if (i < _src.length && _isLetter(_src.codeUnitAt(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 int.parse(_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 double.parse(_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)
: _tracks = new List<GridTrack>(),
_lineNames = new Map<String, int>(),
super(src);
/** 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() == _Parser.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.substring(_offset)}';
} else {
location = 'end of input';
}
return 'SyntaxErrorException: $_message at $location';
}
}