// 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 { 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 split(String lines, [int start = 0, int? end]) { end = RangeError.checkValidRange(start, end, lines.length); return _LineSplitIterable(lines, start, end); } List convert(String data) { var lines = []; 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 sink) { return _LineSplitterSink( sink is StringConversionSink ? sink : StringConversionSink.from(sink), ); } Stream bind(Stream stream) { return Stream.eventTransformed( stream, (EventSink 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 { final EventSink _eventSink; _LineSplitterEventSink(EventSink eventSink) : _eventSink = eventSink, super(StringConversionSink.from(eventSink)); void addError(Object o, [StackTrace? stackTrace]) { _eventSink.addError(o, stackTrace); } } class _LineSplitIterable extends Iterable { final String _source; final int _start, _end; _LineSplitIterable(this._source, this._start, this._end); Iterator get iterator => _LineSplitIterator(_source, _start, _end); } class _LineSplitIterator implements Iterator { 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"))); }