|
|
|
@ -9,6 +9,12 @@ 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(); |
|
|
|
@ -16,6 +22,7 @@ class Line extends Container<Leaf?> { |
|
|
|
|
@override |
|
|
|
|
int get length => super.length + 1; |
|
|
|
|
|
|
|
|
|
/// Returns `true` if this line contains an embedded object. |
|
|
|
|
bool get hasEmbed { |
|
|
|
|
if (childCount != 1) { |
|
|
|
|
return false; |
|
|
|
@ -24,6 +31,7 @@ class Line extends Container<Leaf?> { |
|
|
|
|
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?; |
|
|
|
@ -40,6 +48,9 @@ class Line extends Container<Leaf?> { |
|
|
|
|
: parent!.next as Line?; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
Node newInstance() => Line(); |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
Delta toDelta() { |
|
|
|
|
final delta = children |
|
|
|
@ -67,34 +78,42 @@ class Line extends Container<Leaf?> { |
|
|
|
|
@override |
|
|
|
|
void insert(int index, Object data, Style? style) { |
|
|
|
|
if (data is Embeddable) { |
|
|
|
|
_insert(index, data, style); |
|
|
|
|
// 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) { |
|
|
|
|
_insert(index, text, style); |
|
|
|
|
_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); |
|
|
|
|
_insert(index, prefix, style); |
|
|
|
|
_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 the remaining |
|
|
|
|
// Continue with remaining part. |
|
|
|
|
final remain = text.substring(lineBreak + 1); |
|
|
|
|
nextLine.insert(0, remain, style); |
|
|
|
|
} |
|
|
|
@ -104,16 +123,20 @@ class Line extends Container<Leaf?> { |
|
|
|
|
if (style == null) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
final thisLen = length; |
|
|
|
|
final thisLength = length; |
|
|
|
|
|
|
|
|
|
final local = math.min(thisLen - index, len!); |
|
|
|
|
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 (index + local == thisLen && local == 1) { |
|
|
|
|
assert(style.values.every((attr) => attr.scope == AttributeScope.BLOCK)); |
|
|
|
|
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 != thisLen); |
|
|
|
|
assert(index + local != thisLength); |
|
|
|
|
super.retain(index, local, style); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -127,35 +150,47 @@ class Line extends Container<Leaf?> { |
|
|
|
|
@override |
|
|
|
|
void delete(int index, int? len) { |
|
|
|
|
final local = math.min(length - index, len!); |
|
|
|
|
final deleted = index + local == length; |
|
|
|
|
if (deleted) { |
|
|
|
|
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 remain = len - local; |
|
|
|
|
if (remain > 0) { |
|
|
|
|
final remaining = len - local; |
|
|
|
|
if (remaining > 0) { |
|
|
|
|
assert(nextLine != null); |
|
|
|
|
nextLine!.delete(0, remain); |
|
|
|
|
nextLine!.delete(0, remaining); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (deleted && isNotEmpty) { |
|
|
|
|
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 (deleted) { |
|
|
|
|
final Node p = parent!; |
|
|
|
|
if (isLFDeleted) { |
|
|
|
|
// Now we can remove this line. |
|
|
|
|
final block = parent!; // remember reference before un-linking. |
|
|
|
|
unlink(); |
|
|
|
|
p.adjust(); |
|
|
|
|
block.adjust(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Formats this line. |
|
|
|
|
void _format(Style? newStyle) { |
|
|
|
|
if (newStyle == null || newStyle.isEmpty) { |
|
|
|
|
return; |
|
|
|
@ -165,7 +200,7 @@ class Line extends Container<Leaf?> { |
|
|
|
|
final blockStyle = newStyle.getBlockExceptHeader(); |
|
|
|
|
if (blockStyle == null) { |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
} // No block-level changes |
|
|
|
|
|
|
|
|
|
if (parent is Block) { |
|
|
|
|
final parentStyle = (parent as Block).style.getBlockExceptHeader(); |
|
|
|
@ -176,14 +211,18 @@ class Line extends Container<Leaf?> { |
|
|
|
|
final block = Block()..applyAttribute(blockStyle); |
|
|
|
|
_wrap(block); |
|
|
|
|
block.adjust(); |
|
|
|
|
} |
|
|
|
|
} // else the same style, no-op. |
|
|
|
|
} else if (blockStyle.value != null) { |
|
|
|
|
// Only wrap with a new block if this is not an unset |
|
|
|
|
final block = Block()..applyAttribute(blockStyle); |
|
|
|
|
_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); |
|
|
|
@ -191,6 +230,9 @@ class Line extends Container<Leaf?> { |
|
|
|
|
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'); |
|
|
|
@ -242,7 +284,7 @@ class Line extends Container<Leaf?> { |
|
|
|
|
return line; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
void _insert(int index, Object data, Style? style) { |
|
|
|
|
void _insertSafe(int index, Object data, Style? style) { |
|
|
|
|
assert(index == 0 || (index > 0 && index < length)); |
|
|
|
|
|
|
|
|
|
if (data is String) { |
|
|
|
@ -252,46 +294,50 @@ class Line extends Container<Leaf?> { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (isNotEmpty) { |
|
|
|
|
if (isEmpty) { |
|
|
|
|
final child = Leaf(data); |
|
|
|
|
add(child); |
|
|
|
|
child.format(style); |
|
|
|
|
} else { |
|
|
|
|
final result = queryChild(index, true); |
|
|
|
|
result.node!.insert(result.offset, data, style); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
final child = Leaf(data); |
|
|
|
|
add(child); |
|
|
|
|
child.format(style); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@override |
|
|
|
|
Node newInstance() { |
|
|
|
|
return Line(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// 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 res = Style(); |
|
|
|
|
var result = Style(); |
|
|
|
|
final excluded = <Attribute>{}; |
|
|
|
|
|
|
|
|
|
void _handle(Style style) { |
|
|
|
|
if (res.isEmpty) { |
|
|
|
|
if (result.isEmpty) { |
|
|
|
|
excluded.addAll(style.values); |
|
|
|
|
} else { |
|
|
|
|
for (final attr in res.values) { |
|
|
|
|
for (final attr in result.values) { |
|
|
|
|
if (!style.containsKey(attr.key)) { |
|
|
|
|
excluded.add(attr); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
final remain = style.removeAll(excluded); |
|
|
|
|
res = res.removeAll(excluded); |
|
|
|
|
res = res.mergeAll(remain); |
|
|
|
|
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) { |
|
|
|
|
res = res.mergeAll(node.style); |
|
|
|
|
result = result.mergeAll(node.style); |
|
|
|
|
var pos = node.length - data.offset; |
|
|
|
|
while (!node!.isLast && pos < local) { |
|
|
|
|
node = node.next as Leaf?; |
|
|
|
@ -300,17 +346,18 @@ class Line extends Container<Leaf?> { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
res = res.mergeAll(style); |
|
|
|
|
result = result.mergeAll(style); |
|
|
|
|
if (parent is Block) { |
|
|
|
|
final block = parent as Block; |
|
|
|
|
res = res.mergeAll(block.style); |
|
|
|
|
result = result.mergeAll(block.style); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
final remain = len - local; |
|
|
|
|
if (remain > 0) { |
|
|
|
|
_handle(nextLine!.collectStyle(0, remain)); |
|
|
|
|
final remaining = len - local; |
|
|
|
|
if (remaining > 0) { |
|
|
|
|
final rest = nextLine!.collectStyle(0, remaining); |
|
|
|
|
_handle(rest); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return res; |
|
|
|
|
return result; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|