blob: b2d3e9e6178d6af73022915c84eb0a56e86bde28 [file] [log] [blame] [edit]
// 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")));
}