// 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; } // => + | 'none' GridTemplate _parseTemplate() { if (_maybeEat('none')) { return null; } final rows = new List(); 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; } // [ [ | | 'start' | 'end' ] // [ | | '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); } // [ | | '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 _tracks; final Map _lineNames; _GridTrackParser._internal(String src) : _tracks = new List(), _lineNames = new Map(), 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; } // => [ [ ]* [ ]* ]+ | 'none' GridTrackList _parseTrackList() { if (_maybeEat('none')) { return null; } _parseTrackListHelper(); return new GridTrackList(_tracks, _lineNames); } /** Code shared by _parseTrackList and _parseTrackGroup */ void _parseTrackListHelper([List 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(); } } // => [ '(' [ [ ]* [ ]* ]+ ')' // [ '[' ']' ]? ] // | void _parseTrackGroup() { if (_maybeEat('(')) { final tracks = new List(); _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())); } } // => 'minmax(' ',' ')' // | 'auto' | 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); } } // => | | // | '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'; } }