Rich text editor for Flutter
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

368 lines
9.9 KiB

import 'dart:math' as math;
import '../../quill_delta.dart';
import '../attribute.dart';
import '../style.dart';
import 'block.dart';
import 'container.dart';
import 'embed.dart';
import 'leaf.dart';
import 'node.dart';
/// A line of rich text in a Quill document.
///
/// Line serves as a container for [Leaf]s, like [Text] and [Embed].
///
/// When a line contains an embed, it fully occupies the line, no other embeds
/// or text nodes are allowed.
class Line extends Container<Leaf?> {
@override
Leaf get defaultChild => Text();
@override
int get length => super.length + 1;
/// Returns `true` if this line contains an embedded object.
bool get hasEmbed {
if (childCount != 1) {
return false;
}
return children.single is Embed;
}
/// Returns next [Line] or `null` if this is the last line in the document.
Line? get nextLine {
if (!isLast) {
return next is Block ? (next as Block).first as Line? : next as Line?;
}
if (parent is! Block) {
return null;
}
if (parent!.isLast) {
return null;
}
return parent!.next is Block
? (parent!.next as Block).first as Line?
: parent!.next as Line?;
}
@override
Node newInstance() => Line();
@override
Delta toDelta() {
final delta = children
.map((child) => child.toDelta())
.fold(Delta(), (dynamic a, b) => a.concat(b));
var attributes = style;
if (parent is Block) {
final block = parent as Block;
attributes = attributes.mergeAll(block.style);
}
delta.insert('\n', attributes.toJson());
return delta;
}
@override
String toPlainText() => '${super.toPlainText()}\n';
@override
String toString() {
final body = children.join('');
final styleString = style.isNotEmpty ? ' $style' : '';
return '$body$styleString';
}
@override
void insert(int index, Object data, Style? style) {
if (data is Embeddable) {
// We do not check whether this line already has any children here as
// inserting an embed into a line with other text is acceptable from the
// Delta format perspective.
// We rely on heuristic rules to ensure that embeds occupy an entire line.
_insertSafe(index, data, style);
return;
}
final text = data as String;
final lineBreak = text.indexOf('\n');
if (lineBreak < 0) {
_insertSafe(index, text, style);
// No need to update line or block format since those attributes can only
// be attached to `\n` character and we already know it's not present.
return;
}
final prefix = text.substring(0, lineBreak);
_insertSafe(index, prefix, style);
if (prefix.isNotEmpty) {
index += prefix.length;
}
// Next line inherits our format.
final nextLine = _getNextLine(index);
// Reset our format and unwrap from a block if needed.
clearStyle();
if (parent is Block) {
_unwrap();
}
// Now we can apply new format and re-layout.
_format(style);
// Continue with remaining part.
final remain = text.substring(lineBreak + 1);
nextLine.insert(0, remain, style);
}
@override
void retain(int index, int? len, Style? style) {
if (style == null) {
return;
}
final thisLength = length;
final local = math.min(thisLength - index, len!);
// If index is at newline character then this is a line/block style update.
final isLineFormat = (index + local == thisLength) && local == 1;
if (isLineFormat) {
assert(style.values.every((attr) => attr.scope == AttributeScope.BLOCK),
'It is not allowed to apply inline attributes to line itself.');
_format(style);
} else {
// Otherwise forward to children as it's an inline format update.
assert(style.values.every((attr) => attr.scope == AttributeScope.INLINE));
assert(index + local != thisLength);
super.retain(index, local, style);
}
final remain = len - local;
if (remain > 0) {
assert(nextLine != null);
nextLine!.retain(0, remain, style);
}
}
@override
void delete(int index, int? len) {
final local = math.min(length - index, len!);
final isLFDeleted = index + local == length; // Line feed
if (isLFDeleted) {
// Our newline character deleted with all style information.
clearStyle();
if (local > 1) {
// Exclude newline character from delete range for children.
super.delete(index, local - 1);
}
} else {
super.delete(index, local);
}
final remaining = len - local;
if (remaining > 0) {
assert(nextLine != null);
nextLine!.delete(0, remaining);
}
if (isLFDeleted && isNotEmpty) {
// Since we lost our line-break and still have child text nodes those must
// migrate to the next line.
// nextLine might have been unmounted since last assert so we need to
// check again we still have a line after us.
assert(nextLine != null);
// Move remaining children in this line to the next line so that all
// attributes of nextLine are preserved.
nextLine!.moveChildToNewParent(this);
moveChildToNewParent(nextLine);
}
if (isLFDeleted) {
// Now we can remove this line.
final block = parent!; // remember reference before un-linking.
unlink();
block.adjust();
}
}
/// Formats this line.
void _format(Style? newStyle) {
if (newStyle == null || newStyle.isEmpty) {
return;
}
applyStyle(newStyle);
final blockStyle = newStyle.getBlockExceptHeader();
if (blockStyle == null) {
return;
} // No block-level changes
if (parent is Block) {
final parentStyle = (parent as Block).style.getBlockExceptHeader();
if (blockStyle.value == null) {
_unwrap();
} else if (blockStyle != parentStyle) {
_unwrap();
_applyBlockStyles(newStyle);
} // else the same style, no-op.
} else if (blockStyle.value != null) {
// Only wrap with a new block if this is not an unset
_applyBlockStyles(newStyle);
}
}
void _applyBlockStyles(Style newStyle) {
var block = Block();
for (final style in newStyle.getBlocksExceptHeader().values) {
block = block..applyAttribute(style);
}
_wrap(block);
block.adjust();
}
/// Wraps this line with new parent [block].
///
/// This line can not be in a [Block] when this method is called.
void _wrap(Block block) {
assert(parent != null && parent is! Block);
insertAfter(block);
unlink();
block.add(this);
}
/// Unwraps this line from it's parent [Block].
///
/// This method asserts if current [parent] of this line is not a [Block].
void _unwrap() {
if (parent is! Block) {
throw ArgumentError('Invalid parent');
}
final block = parent as Block;
assert(block.children.contains(this));
if (isFirst) {
unlink();
block.insertBefore(this);
} else if (isLast) {
unlink();
block.insertAfter(this);
} else {
final before = block.clone() as Block;
block.insertBefore(before);
var child = block.first as Line;
while (child != this) {
child.unlink();
before.add(child);
child = block.first as Line;
}
unlink();
block.insertBefore(this);
}
block.adjust();
}
Line _getNextLine(int index) {
assert(index == 0 || (index > 0 && index < length));
final line = clone() as Line;
insertAfter(line);
if (index == length - 1) {
return line;
}
final query = queryChild(index, false);
while (!query.node!.isLast) {
final next = (last as Leaf)..unlink();
line.addFirst(next);
}
final child = query.node as Leaf;
final cut = child.splitAt(query.offset);
cut?.unlink();
line.addFirst(cut);
return line;
}
void _insertSafe(int index, Object data, Style? style) {
assert(index == 0 || (index > 0 && index < length));
if (data is String) {
assert(!data.contains('\n'));
if (data.isEmpty) {
return;
}
}
if (isEmpty) {
final child = Leaf(data);
add(child);
child.format(style);
} else {
final result = queryChild(index, true);
result.node!.insert(result.offset, data, style);
}
}
/// Returns style for specified text range.
///
/// Only attributes applied to all characters within this range are
/// included in the result. Inline and line level attributes are
/// handled separately, e.g.:
///
/// - line attribute X is included in the result only if it exists for
/// every line within this range (partially included lines are counted).
/// - inline attribute X is included in the result only if it exists
/// for every character within this range (line-break characters excluded).
Style collectStyle(int offset, int len) {
final local = math.min(length - offset, len);
var result = Style();
final excluded = <Attribute>{};
void _handle(Style style) {
if (result.isEmpty) {
excluded.addAll(style.values);
} else {
for (final attr in result.values) {
if (!style.containsKey(attr.key)) {
excluded.add(attr);
}
}
}
final remaining = style.removeAll(excluded);
result = result.removeAll(excluded);
result = result.mergeAll(remaining);
}
final data = queryChild(offset, true);
var node = data.node as Leaf?;
if (node != null) {
result = result.mergeAll(node.style);
var pos = node.length - data.offset;
while (!node!.isLast && pos < local) {
node = node.next as Leaf?;
_handle(node!.style);
pos += node.length;
}
}
result = result.mergeAll(style);
if (parent is Block) {
final block = parent as Block;
result = result.mergeAll(block.style);
}
final remaining = len - local;
if (remaining > 0) {
final rest = nextLine!.collectStyle(0, remaining);
_handle(rest);
}
return result;
}
}