| // 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'; |
| } |
| } |