| // Copyright (c) 2013, 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. |
| |
| part of "dart:convert"; |
| |
| // Character constants. |
| const int _LF = 10; |
| const int _CR = 13; |
| |
| /// A [StreamTransformer] that splits a [String] into individual lines. |
| /// |
| /// A line is terminated by either: |
| /// * a CR, carriage return: U+000D ('\r') |
| /// * a LF, line feed (Unix line break): U+000A ('\n') or |
| /// * a CR+LF sequence (DOS/Windows line break), and |
| /// * a final non-empty line can be ended by the end of the input. |
| /// |
| /// The resulting lines do not contain the line terminators. |
| /// |
| /// Example: |
| /// ```dart |
| /// const splitter = LineSplitter(); |
| /// const sampleText = |
| /// 'Dart is: \r an object-oriented \n class-based \n garbage-collected ' |
| /// '\r\n language with C-style syntax \r\n'; |
| /// |
| /// final sampleTextLines = splitter.convert(sampleText); |
| /// for (var i = 0; i < sampleTextLines.length; i++) { |
| /// print('$i: ${sampleTextLines[i]}'); |
| /// } |
| /// // 0: Dart is: |
| /// // 1: an object-oriented |
| /// // 2: class-based |
| /// // 3: garbage-collected |
| /// // 4: language with C-style syntax |
| /// ``` |
| final class LineSplitter extends StreamTransformerBase<String, String> { |
| const LineSplitter(); |
| |
| /// Split [lines] into individual lines. |
| /// |
| /// If [start] and [end] are provided, only split the contents of |
| /// `lines.substring(start, end)`. The [start] and [end] values must |
| /// specify a valid sub-range of [lines] |
| /// (`0 <= start <= end <= lines.length`). |
| static Iterable<String> split(String lines, [int start = 0, int? end]) { |
| end = RangeError.checkValidRange(start, end, lines.length); |
| return _LineSplitIterable(lines, start, end); |
| } |
| |
| List<String> convert(String data) { |
| var lines = <String>[]; |
| var end = data.length; |
| var sliceStart = 0; |
| var char = 0; |
| for (var i = 0; i < end; i++) { |
| var previousChar = char; |
| char = data.codeUnitAt(i); |
| if (char != _CR) { |
| if (char != _LF) continue; |
| if (previousChar == _CR) { |
| sliceStart = i + 1; |
| continue; |
| } |
| } |
| lines.add(data.substring(sliceStart, i)); |
| sliceStart = i + 1; |
| } |
| if (sliceStart < end) { |
| lines.add(data.substring(sliceStart, end)); |
| } |
| return lines; |
| } |
| |
| StringConversionSink startChunkedConversion(Sink<String> sink) { |
| return _LineSplitterSink( |
| sink is StringConversionSink ? sink : StringConversionSink.from(sink), |
| ); |
| } |
| |
| Stream<String> bind(Stream<String> stream) { |
| return Stream<String>.eventTransformed( |
| stream, |
| (EventSink<String> sink) => _LineSplitterEventSink(sink), |
| ); |
| } |
| } |
| |
| class _LineSplitterSink extends StringConversionSink { |
| final StringConversionSink _sink; |
| |
| /// The carry-over from the previous chunk. |
| /// |
| /// If the previous slice ended in a line without a line terminator, |
| /// then the next slice may continue the line. |
| /// |
| /// Set to `null` if there is no carry (the previous chunk ended on |
| /// a line break). |
| /// Set to an empty string if carry-over comes from multiple chunks, |
| /// in which case the parts are stored in [_multiCarry]. |
| String? _carry; |
| |
| /// Cache of multiple parts of carry-over. |
| /// |
| /// If a line is split over multiple chunks, avoid doing |
| /// repeated string concatenation, and instead store the chunks |
| /// into this stringbuffer. |
| /// |
| /// Is empty when `_carry` is `null` or a non-empty string. |
| StringBuffer? _multiCarry; |
| |
| /// Whether to skip a leading LF character from the next slice. |
| /// |
| /// If the previous slice ended on a CR character, a following LF |
| /// would be part of the same line termination, and should be ignored. |
| /// |
| /// Only `true` when [_carry] is `null`. |
| bool _skipLeadingLF = false; |
| |
| _LineSplitterSink(this._sink); |
| |
| void addSlice(String chunk, int start, int end, bool isLast) { |
| end = RangeError.checkValidRange(start, end, chunk.length); |
| // If the chunk is empty, it's probably because it's the last one. |
| // Handle that here, so we know the range is non-empty below. |
| if (start < end) { |
| if (_skipLeadingLF) { |
| if (chunk.codeUnitAt(start) == _LF) { |
| start += 1; |
| } |
| _skipLeadingLF = false; |
| } |
| _addLines(chunk, start, end, isLast); |
| } |
| if (isLast) close(); |
| } |
| |
| void close() { |
| var carry = _carry; |
| if (carry != null) { |
| _sink.add(_useCarry(carry, "")); |
| } |
| _sink.close(); |
| } |
| |
| void _addLines(String lines, int start, int end, bool isLast) { |
| var sliceStart = start; |
| var char = 0; |
| var carry = _carry; |
| |
| for (var i = start; i < end; i++) { |
| var previousChar = char; |
| char = lines.codeUnitAt(i); |
| if (char != _CR) { |
| if (char != _LF) continue; |
| if (previousChar == _CR) { |
| sliceStart = i + 1; |
| continue; |
| } |
| } |
| var slice = lines.substring(sliceStart, i); |
| if (carry != null) { |
| slice = _useCarry(carry, slice); // Resets _carry to `null`. |
| carry = null; |
| } |
| _sink.add(slice); |
| |
| sliceStart = i + 1; |
| } |
| |
| if (sliceStart < end) { |
| var endSlice = lines.substring(sliceStart, end); |
| if (isLast) { |
| // Emit last line instead of carrying it over to the |
| // immediately following `close` call. |
| if (carry != null) { |
| endSlice = _useCarry(carry, endSlice); |
| } |
| _sink.add(endSlice); |
| return; |
| } |
| if (carry == null) { |
| // Common case, this chunk contained at least one line-break. |
| _carry = endSlice; |
| } else { |
| _addCarry(carry, endSlice); |
| } |
| } else { |
| _skipLeadingLF = (char == _CR); |
| } |
| } |
| |
| /// Adds [newCarry] to existing carry-over. |
| /// |
| /// Always goes into [_multiCarry], we only call here if there |
| /// was an existing carry that the new carry needs to be combined with. |
| /// |
| /// Only happens when a line is spread over more than two chunks. |
| /// The [existingCarry] is always the current value of [_carry]. |
| /// (We pass the existing carry as an argument because we have already |
| /// checked that it is non-`null`.) |
| void _addCarry(String existingCarry, String newCarry) { |
| assert(existingCarry == _carry); |
| assert(newCarry.isNotEmpty); |
| var multiCarry = _multiCarry ??= StringBuffer(); |
| if (existingCarry.isNotEmpty) { |
| assert(multiCarry.isEmpty); |
| multiCarry.write(existingCarry); |
| _carry = ""; |
| } |
| multiCarry.write(newCarry); |
| } |
| |
| /// Consumes and combines existing carry-over with continuation string. |
| /// |
| /// The [carry] value is always the current value of [_carry], |
| /// which is non-`null` when this method is called. |
| /// If that value is the empty string, the actual carry-over is stored |
| /// in [_multiCarry]. |
| /// |
| /// The [continuation] is only empty if called from [close]. |
| String _useCarry(String carry, String continuation) { |
| assert(carry == _carry); |
| _carry = null; |
| if (carry.isNotEmpty) { |
| return carry + continuation; |
| } |
| var multiCarry = _multiCarry!; |
| multiCarry.write(continuation); |
| var result = multiCarry.toString(); |
| // If it happened once, it may happen again. |
| // Keep the string buffer around. |
| multiCarry.clear(); |
| return result; |
| } |
| } |
| |
| class _LineSplitterEventSink extends _LineSplitterSink |
| implements EventSink<String> { |
| final EventSink<String> _eventSink; |
| |
| _LineSplitterEventSink(EventSink<String> eventSink) |
| : _eventSink = eventSink, |
| super(StringConversionSink.from(eventSink)); |
| |
| void addError(Object o, [StackTrace? stackTrace]) { |
| _eventSink.addError(o, stackTrace); |
| } |
| } |
| |
| class _LineSplitIterable extends Iterable<String> { |
| final String _source; |
| final int _start, _end; |
| _LineSplitIterable(this._source, this._start, this._end); |
| Iterator<String> get iterator => _LineSplitIterator(_source, _start, _end); |
| } |
| |
| class _LineSplitIterator implements Iterator<String> { |
| final String _source; |
| final int _end; |
| int _start; |
| int _lineStart = 0; |
| int _lineEnd = -1; |
| String? _current; |
| _LineSplitIterator(this._source, this._start, this._end); |
| |
| bool moveNext() { |
| _current = null; |
| _lineStart = _start; |
| _lineEnd = -1; |
| var eolLength = 1; |
| for (var i = _start; i < _end; i++) { |
| var char = _source.codeUnitAt(i); |
| if (char != _CR) { |
| if (char != _LF) continue; |
| } else { |
| // Check for CR+LF |
| if (i + 1 < _end && _source.codeUnitAt(i + 1) == _LF) { |
| eolLength = 2; |
| } |
| } |
| _lineEnd = i; |
| _start = i + eolLength; |
| return true; |
| } |
| if (_start < _end) { |
| _lineEnd = _end; |
| _start = _end; |
| return true; |
| } |
| _start = _end; |
| return false; |
| } |
| |
| // Creates string lazily on first request. |
| // Makes it cheaper to do `LineSplitter.split(input).skip(5)...`. |
| String get current => _current ??= (_lineEnd >= 0 |
| ? _source.substring(_lineStart, _lineEnd) |
| : (throw StateError("No element"))); |
| } |