Add comments from Zefyr to nodes folder

Since quite a lot of code was taken from Zefyr, the license should
be changed to BSD to meet the requirements of Zefyr.
pull/145/head
Till Friebe 4 years ago
parent 873bfbcee1
commit 8c663ad9f5
  1. 19
      lib/models/documents/nodes/block.dart
  2. 50
      lib/models/documents/nodes/container.dart
  3. 16
      lib/models/documents/nodes/embed.dart
  4. 108
      lib/models/documents/nodes/leaf.dart
  5. 137
      lib/models/documents/nodes/line.dart
  6. 75
      lib/models/documents/nodes/node.dart
  7. 18
      lib/widgets/editor.dart
  8. 9
      lib/widgets/raw_editor.dart
  9. 34
      lib/widgets/text_block.dart
  10. 10
      lib/widgets/text_line.dart
  11. 4
      lib/widgets/text_selection.dart

@ -3,7 +3,21 @@ import 'container.dart';
import 'line.dart'; import 'line.dart';
import 'node.dart'; import 'node.dart';
/// Represents a group of adjacent [Line]s with the same block style.
///
/// Block elements are:
/// - Blockquote
/// - Header
/// - Indent
/// - List
/// - Text Alignment
/// - Text Direction
/// - Code Block
class Block extends Container<Line?> { class Block extends Container<Line?> {
/// Creates new unmounted [Block].
@override
Node newInstance() => Block();
@override @override
Line get defaultChild => Line(); Line get defaultChild => Line();
@ -55,9 +69,4 @@ class Block extends Container<Line?> {
} }
return buffer.toString(); return buffer.toString();
} }
@override
Node newInstance() {
return Block();
}
} }

@ -1,48 +1,67 @@
import 'dart:collection'; import 'dart:collection';
import '../style.dart'; import '../style.dart';
import 'leaf.dart';
import 'line.dart';
import 'node.dart'; import 'node.dart';
/* Container of multiple nodes */ /// Container can accommodate other nodes.
///
/// Delegates insert, retain and delete operations to children nodes. For each
/// operation container looks for a child at specified index position and
/// forwards operation to that child.
///
/// Most of the operation handling logic is implemented by [Line] and [Text].
abstract class Container<T extends Node?> extends Node { abstract class Container<T extends Node?> extends Node {
final LinkedList<Node> _children = LinkedList<Node>(); final LinkedList<Node> _children = LinkedList<Node>();
/// List of children.
LinkedList<Node> get children => _children; LinkedList<Node> get children => _children;
/// Returns total number of child nodes in this container.
///
/// To get text length of this container see [length].
int get childCount => _children.length; int get childCount => _children.length;
/// Returns the first child [Node].
Node get first => _children.first; Node get first => _children.first;
/// Returns the last child [Node].
Node get last => _children.last; Node get last => _children.last;
/// Returns `true` if this container has no child nodes.
bool get isEmpty => _children.isEmpty; bool get isEmpty => _children.isEmpty;
/// Returns `true` if this container has at least 1 child.
bool get isNotEmpty => _children.isNotEmpty; bool get isNotEmpty => _children.isNotEmpty;
/// abstract methods begin /// Returns an instance of default child for this container node.
///
/// Always returns fresh instance.
T get defaultChild; T get defaultChild;
/// abstract methods end /// Adds [node] to the end of this container children list.
void add(T node) { void add(T node) {
assert(node?.parent == null); assert(node?.parent == null);
node?.parent = this; node?.parent = this;
_children.add(node as Node); _children.add(node as Node);
} }
/// Adds [node] to the beginning of this container children list.
void addFirst(T node) { void addFirst(T node) {
assert(node?.parent == null); assert(node?.parent == null);
node?.parent = this; node?.parent = this;
_children.addFirst(node as Node); _children.addFirst(node as Node);
} }
/// Removes [node] from this container.
void remove(T node) { void remove(T node) {
assert(node?.parent == this); assert(node?.parent == this);
node?.parent = null; node?.parent = null;
_children.remove(node as Node); _children.remove(node as Node);
} }
/// Moves children of this node to [newParent].
void moveChildToNewParent(Container? newParent) { void moveChildToNewParent(Container? newParent) {
if (isEmpty) { if (isEmpty) {
return; return;
@ -55,9 +74,19 @@ abstract class Container<T extends Node?> extends Node {
newParent.add(child); newParent.add(child);
} }
/// In case [newParent] already had children we need to make sure
/// combined list is optimized.
if (last != null) last.adjust(); if (last != null) last.adjust();
} }
/// Queries the child [Node] at specified character [offset] in this container.
///
/// The result may contain the found node or `null` if no node is found
/// at specified offset.
///
/// [ChildQuery.offset] is set to relative offset within returned child node
/// which points at the same character position in the document as the
/// original [offset].
ChildQuery queryChild(int offset, bool inclusive) { ChildQuery queryChild(int offset, bool inclusive) {
if (offset < 0 || offset > length) { if (offset < 0 || offset > length) {
return ChildQuery(null, 0); return ChildQuery(null, 0);
@ -76,6 +105,9 @@ abstract class Container<T extends Node?> extends Node {
@override @override
String toPlainText() => children.map((child) => child.toPlainText()).join(); String toPlainText() => children.map((child) => child.toPlainText()).join();
/// Content length of this node's children.
///
/// To get number of children in this node use [childCount].
@override @override
int get length => _children.fold(0, (cur, node) => cur + node.length); int get length => _children.fold(0, (cur, node) => cur + node.length);
@ -114,11 +146,15 @@ abstract class Container<T extends Node?> extends Node {
String toString() => _children.join('\n'); String toString() => _children.join('\n');
} }
/// Query of a child in a Container /// Result of a child query in a [Container].
class ChildQuery { class ChildQuery {
ChildQuery(this.node, this.offset); ChildQuery(this.node, this.offset);
final Node? node; // null if not found /// The child node if found, otherwise `null`.
final Node? node;
/// Starting offset within the child [node] which points at the same
/// character in the document as the original offset passed to
/// [Container.queryChild] method.
final int offset; final int offset;
} }

@ -1,7 +1,15 @@
/// An object which can be embedded into a Quill document.
///
/// See also:
///
/// * [BlockEmbed] which represents a block embed.
class Embeddable { class Embeddable {
Embeddable(this.type, this.data); Embeddable(this.type, this.data);
/// The type of this object.
final String type; final String type;
/// The data payload of this object.
final dynamic data; final dynamic data;
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@ -17,10 +25,16 @@ class Embeddable {
} }
} }
/// An object which occupies an entire line in a document and cannot co-exist
/// inline with regular text.
///
/// There are two built-in embed types supported by Quill documents, however
/// the document model itself does not make any assumptions about the types
/// of embedded objects and allows users to define their own types.
class BlockEmbed extends Embeddable { class BlockEmbed extends Embeddable {
BlockEmbed(String type, String data) : super(type, data); BlockEmbed(String type, String data) : super(type, data);
static final BlockEmbed horizontalRule = BlockEmbed('divider', 'hr'); static BlockEmbed horizontalRule = BlockEmbed('divider', 'hr');
static BlockEmbed image(String imageUrl) => BlockEmbed('image', imageUrl); static BlockEmbed image(String imageUrl) => BlockEmbed('image', imageUrl);
} }

@ -6,8 +6,9 @@ import 'embed.dart';
import 'line.dart'; import 'line.dart';
import 'node.dart'; import 'node.dart';
/* A leaf node in document tree */ /// A leaf in Quill document tree.
abstract class Leaf extends Node { abstract class Leaf extends Node {
/// Creates a new [Leaf] with specified [data].
factory Leaf(Object data) { factory Leaf(Object data) {
if (data is Embeddable) { if (data is Embeddable) {
return Embed(data); return Embed(data);
@ -19,9 +20,10 @@ abstract class Leaf extends Node {
Leaf.val(Object val) : _value = val; Leaf.val(Object val) : _value = val;
Object _value; /// Contents of this node, either a String if this is a [Text] or an
/// [Embed] if this is an [BlockEmbed].
Object get value => _value; Object get value => _value;
Object _value;
@override @override
void applyStyle(Style value) { void applyStyle(Style value) {
@ -99,14 +101,21 @@ abstract class Leaf extends Node {
} }
} }
/// Adjust this text node by merging it with adjacent nodes if they share
/// the same style.
@override @override
void adjust() { void adjust() {
if (this is Embed) { if (this is Embed) {
// Embed nodes cannot be merged with text nor other embeds (in fact,
// there could be no two adjacent embeds on the same line since an
// embed occupies an entire line).
return; return;
} }
// This is a text node and it can only be merged with other text nodes.
var node = this as Text; var node = this as Text;
// merging it with previous node if style is the same
// Merging it with previous node if style is the same.
final prev = node.previous; final prev = node.previous;
if (!node.isFirst && prev is Text && prev.style == node.style) { if (!node.isFirst && prev is Text && prev.style == node.style) {
prev._value = prev.value + node.value; prev._value = prev.value + node.value;
@ -114,7 +123,7 @@ abstract class Leaf extends Node {
node = prev; node = prev;
} }
// merging it with next node if style is the same // Merging it with next node if style is the same.
final next = node.next; final next = node.next;
if (!node.isLast && next is Text && next.style == node.style) { if (!node.isLast && next is Text && next.style == node.style) {
node._value = node.value + next.value; node._value = node.value + next.value;
@ -122,13 +131,17 @@ abstract class Leaf extends Node {
} }
} }
Leaf? cutAt(int index) { /// Splits this leaf node at [index] and returns new node.
assert(index >= 0 && index <= length); ///
final cut = splitAt(index); /// If this is the last node in its list and [index] equals this node's
cut?.unlink(); /// length then this method returns `null` as there is nothing left to split.
return cut; /// If there is another leaf node after this one and [index] equals this
} /// node's length then the next leaf node is returned.
///
/// If [index] equals to `0` then this node itself is returned unchanged.
///
/// In case a new node is actually split from this one, it inherits this
/// node's style.
Leaf? splitAt(int index) { Leaf? splitAt(int index) {
assert(index >= 0 && index <= length); assert(index >= 0 && index <= length);
if (index == 0) { if (index == 0) {
@ -146,14 +159,33 @@ abstract class Leaf extends Node {
return split; return split;
} }
/// Cuts a leaf from [index] to the end of this node and returns new node
/// in detached state (e.g. [mounted] returns `false`).
///
/// Splitting logic is identical to one described in [splitAt], meaning this
/// method may return `null`.
Leaf? cutAt(int index) {
assert(index >= 0 && index <= length);
final cut = splitAt(index);
cut?.unlink();
return cut;
}
/// Formats this node and optimizes it with adjacent leaf nodes if needed.
void format(Style? style) { void format(Style? style) {
if (style != null && style.isNotEmpty) { if (style != null && style.isNotEmpty) {
applyStyle(style); applyStyle(style);
} }
adjust(); adjust();
} }
/// Isolates a new leaf starting at [index] with specified [length].
///
/// Splitting logic is identical to one described in [splitAt], with one
/// exception that it is required for [index] to always be less than this
/// node's length. As a result this method always returns a [LeafNode]
/// instance. Returned node may still be the same as this node
/// if provided [index] is `0`.
Leaf _isolate(int index, int length) { Leaf _isolate(int index, int length) {
assert( assert(
index >= 0 && index < this.length && (index + length <= this.length)); index >= 0 && index < this.length && (index + length <= this.length));
@ -162,39 +194,59 @@ abstract class Leaf extends Node {
} }
} }
/// A span of formatted text within a line in a Quill document.
///
/// Text is a leaf node of a document tree.
///
/// Parent of a text node is always a [Line], and as a consequence text
/// node's [value] cannot contain any line-break characters.
///
/// See also:
///
/// * [Embed], a leaf node representing an embeddable object.
/// * [Line], a node representing a line of text.
class Text extends Leaf { class Text extends Leaf {
Text([String text = '']) Text([String text = ''])
: assert(!text.contains('\n')), : assert(!text.contains('\n')),
super.val(text); super.val(text);
@override @override
String get value => _value as String; Node newInstance() => Text();
@override @override
String toPlainText() { String get value => _value as String;
return value;
}
@override @override
Node newInstance() { String toPlainText() => value;
return Text();
}
} }
/// An embedded node such as image or video /// An embed node inside of a line in a Quill document.
///
/// Embed node is a leaf node similar to [Text]. It represents an arbitrary
/// piece of non-textual content embedded into a document, such as, image,
/// horizontal rule, video, or any other object with defined structure,
/// like a tweet, for instance.
///
/// Embed node's length is always `1` character and it is represented with
/// unicode object replacement character in the document text.
///
/// Any inline style can be applied to an embed, however this does not
/// necessarily mean the embed will look according to that style. For instance,
/// applying "bold" style to an image gives no effect, while adding a "link" to
/// an image actually makes the image react to user's action.
class Embed extends Leaf { class Embed extends Leaf {
Embed(Embeddable data) : super.val(data); Embed(Embeddable data) : super.val(data);
static const kObjectReplacementCharacter = '\uFFFC';
@override @override
Embeddable get value => super.value as Embeddable; Node newInstance() => throw UnimplementedError();
@override @override
String toPlainText() { Embeddable get value => super.value as Embeddable;
return '\uFFFC';
}
/// // Embed nodes are represented as unicode object replacement character in
// plain text.
@override @override
Node newInstance() { String toPlainText() => kObjectReplacementCharacter;
throw UnimplementedError();
}
} }

@ -9,6 +9,12 @@ import 'embed.dart';
import 'leaf.dart'; import 'leaf.dart';
import 'node.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?> { class Line extends Container<Leaf?> {
@override @override
Leaf get defaultChild => Text(); Leaf get defaultChild => Text();
@ -16,6 +22,7 @@ class Line extends Container<Leaf?> {
@override @override
int get length => super.length + 1; int get length => super.length + 1;
/// Returns `true` if this line contains an embedded object.
bool get hasEmbed { bool get hasEmbed {
if (childCount != 1) { if (childCount != 1) {
return false; return false;
@ -24,6 +31,7 @@ class Line extends Container<Leaf?> {
return children.single is Embed; return children.single is Embed;
} }
/// Returns next [Line] or `null` if this is the last line in the document.
Line? get nextLine { Line? get nextLine {
if (!isLast) { if (!isLast) {
return next is Block ? (next as Block).first as Line? : next as Line?; 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?; : parent!.next as Line?;
} }
@override
Node newInstance() => Line();
@override @override
Delta toDelta() { Delta toDelta() {
final delta = children final delta = children
@ -67,34 +78,42 @@ class Line extends Container<Leaf?> {
@override @override
void insert(int index, Object data, Style? style) { void insert(int index, Object data, Style? style) {
if (data is Embeddable) { 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; return;
} }
final text = data as String; final text = data as String;
final lineBreak = text.indexOf('\n'); final lineBreak = text.indexOf('\n');
if (lineBreak < 0) { 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; return;
} }
final prefix = text.substring(0, lineBreak); final prefix = text.substring(0, lineBreak);
_insert(index, prefix, style); _insertSafe(index, prefix, style);
if (prefix.isNotEmpty) { if (prefix.isNotEmpty) {
index += prefix.length; index += prefix.length;
} }
// Next line inherits our format.
final nextLine = _getNextLine(index); final nextLine = _getNextLine(index);
// Reset our format and unwrap from a block if needed.
clearStyle(); clearStyle();
if (parent is Block) { if (parent is Block) {
_unwrap(); _unwrap();
} }
// Now we can apply new format and re-layout.
_format(style); _format(style);
// Continue with the remaining // Continue with remaining part.
final remain = text.substring(lineBreak + 1); final remain = text.substring(lineBreak + 1);
nextLine.insert(0, remain, style); nextLine.insert(0, remain, style);
} }
@ -104,16 +123,20 @@ class Line extends Container<Leaf?> {
if (style == null) { if (style == null) {
return; 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) { if (isLineFormat) {
assert(style.values.every((attr) => attr.scope == AttributeScope.BLOCK)); assert(style.values.every((attr) => attr.scope == AttributeScope.BLOCK),
'It is not allowed to apply inline attributes to line itself.');
_format(style); _format(style);
} else { } else {
// Otherwise forward to children as it's an inline format update.
assert(style.values.every((attr) => attr.scope == AttributeScope.INLINE)); assert(style.values.every((attr) => attr.scope == AttributeScope.INLINE));
assert(index + local != thisLen); assert(index + local != thisLength);
super.retain(index, local, style); super.retain(index, local, style);
} }
@ -127,35 +150,47 @@ class Line extends Container<Leaf?> {
@override @override
void delete(int index, int? len) { void delete(int index, int? len) {
final local = math.min(length - index, len!); final local = math.min(length - index, len!);
final deleted = index + local == length; final isLFDeleted = index + local == length; // Line feed
if (deleted) { if (isLFDeleted) {
// Our newline character deleted with all style information.
clearStyle(); clearStyle();
if (local > 1) { if (local > 1) {
// Exclude newline character from delete range for children.
super.delete(index, local - 1); super.delete(index, local - 1);
} }
} else { } else {
super.delete(index, local); super.delete(index, local);
} }
final remain = len - local; final remaining = len - local;
if (remain > 0) { if (remaining > 0) {
assert(nextLine != null); 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); assert(nextLine != null);
// Move remaining children in this line to the next line so that all
// attributes of nextLine are preserved.
nextLine!.moveChildToNewParent(this); nextLine!.moveChildToNewParent(this);
moveChildToNewParent(nextLine); moveChildToNewParent(nextLine);
} }
if (deleted) { if (isLFDeleted) {
final Node p = parent!; // Now we can remove this line.
final block = parent!; // remember reference before un-linking.
unlink(); unlink();
p.adjust(); block.adjust();
} }
} }
/// Formats this line.
void _format(Style? newStyle) { void _format(Style? newStyle) {
if (newStyle == null || newStyle.isEmpty) { if (newStyle == null || newStyle.isEmpty) {
return; return;
@ -165,7 +200,7 @@ class Line extends Container<Leaf?> {
final blockStyle = newStyle.getBlockExceptHeader(); final blockStyle = newStyle.getBlockExceptHeader();
if (blockStyle == null) { if (blockStyle == null) {
return; return;
} } // No block-level changes
if (parent is Block) { if (parent is Block) {
final parentStyle = (parent as Block).style.getBlockExceptHeader(); final parentStyle = (parent as Block).style.getBlockExceptHeader();
@ -176,14 +211,18 @@ class Line extends Container<Leaf?> {
final block = Block()..applyAttribute(blockStyle); final block = Block()..applyAttribute(blockStyle);
_wrap(block); _wrap(block);
block.adjust(); block.adjust();
} } // else the same style, no-op.
} else if (blockStyle.value != null) { } else if (blockStyle.value != null) {
// Only wrap with a new block if this is not an unset
final block = Block()..applyAttribute(blockStyle); final block = Block()..applyAttribute(blockStyle);
_wrap(block); _wrap(block);
block.adjust(); 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) { void _wrap(Block block) {
assert(parent != null && parent is! Block); assert(parent != null && parent is! Block);
insertAfter(block); insertAfter(block);
@ -191,6 +230,9 @@ class Line extends Container<Leaf?> {
block.add(this); 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() { void _unwrap() {
if (parent is! Block) { if (parent is! Block) {
throw ArgumentError('Invalid parent'); throw ArgumentError('Invalid parent');
@ -242,7 +284,7 @@ class Line extends Container<Leaf?> {
return line; 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)); assert(index == 0 || (index > 0 && index < length));
if (data is String) { if (data is String) {
@ -252,46 +294,50 @@ class Line extends Container<Leaf?> {
} }
} }
if (isNotEmpty) { if (isEmpty) {
final result = queryChild(index, true);
result.node!.insert(result.offset, data, style);
return;
}
final child = Leaf(data); final child = Leaf(data);
add(child); add(child);
child.format(style); child.format(style);
} else {
final result = queryChild(index, true);
result.node!.insert(result.offset, data, 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) { Style collectStyle(int offset, int len) {
final local = math.min(length - offset, len); final local = math.min(length - offset, len);
var res = Style(); var result = Style();
final excluded = <Attribute>{}; final excluded = <Attribute>{};
void _handle(Style style) { void _handle(Style style) {
if (res.isEmpty) { if (result.isEmpty) {
excluded.addAll(style.values); excluded.addAll(style.values);
} else { } else {
for (final attr in res.values) { for (final attr in result.values) {
if (!style.containsKey(attr.key)) { if (!style.containsKey(attr.key)) {
excluded.add(attr); excluded.add(attr);
} }
} }
} }
final remain = style.removeAll(excluded); final remaining = style.removeAll(excluded);
res = res.removeAll(excluded); result = result.removeAll(excluded);
res = res.mergeAll(remain); result = result.mergeAll(remaining);
} }
final data = queryChild(offset, true); final data = queryChild(offset, true);
var node = data.node as Leaf?; var node = data.node as Leaf?;
if (node != null) { if (node != null) {
res = res.mergeAll(node.style); result = result.mergeAll(node.style);
var pos = node.length - data.offset; var pos = node.length - data.offset;
while (!node!.isLast && pos < local) { while (!node!.isLast && pos < local) {
node = node.next as Leaf?; 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) { if (parent is Block) {
final block = parent as Block; final block = parent as Block;
res = res.mergeAll(block.style); result = result.mergeAll(block.style);
} }
final remain = len - local; final remaining = len - local;
if (remain > 0) { if (remaining > 0) {
_handle(nextLine!.collectStyle(0, remain)); final rest = nextLine!.collectStyle(0, remaining);
_handle(rest);
} }
return res; return result;
} }
} }

@ -6,36 +6,37 @@ import '../style.dart';
import 'container.dart'; import 'container.dart';
import 'line.dart'; import 'line.dart';
/* node in a document tree */ /// An abstract node in a document tree.
///
/// Represents a segment of a Quill document with specified [offset]
/// and [length].
///
/// The [offset] property is relative to [parent]. See also [documentOffset]
/// which provides absolute offset of this node within the document.
///
/// The current parent node is exposed by the [parent] property.
abstract class Node extends LinkedListEntry<Node> { abstract class Node extends LinkedListEntry<Node> {
/// Current parent of this node. May be null if this node is not mounted.
Container? parent; Container? parent;
Style _style = Style();
Style get style => _style; Style get style => _style;
Style _style = Style();
void applyAttribute(Attribute attribute) { /// Returns `true` if this node is the first node in the [parent] list.
_style = _style.merge(attribute);
}
void applyStyle(Style value) {
_style = _style.mergeAll(value);
}
void clearStyle() {
_style = Style();
}
bool get isFirst => list!.first == this; bool get isFirst => list!.first == this;
/// Returns `true` if this node is the last node in the [parent] list.
bool get isLast => list!.last == this; bool get isLast => list!.last == this;
/// Length of this node in characters.
int get length; int get length;
Node clone() { Node clone() => newInstance()..applyStyle(style);
return newInstance()..applyStyle(style);
}
int getOffset() { /// Offset in characters of this node relative to [parent] node.
///
/// To get offset of this node in the document see [documentOffset].
int get offset {
var offset = 0; var offset = 0;
if (list == null || isFirst) { if (list == null || isFirst) {
@ -50,16 +51,31 @@ abstract class Node extends LinkedListEntry<Node> {
return offset; return offset;
} }
int getDocumentOffset() { /// Offset in characters of this node in the document.
final parentOffset = (parent is! Root) ? parent!.getDocumentOffset() : 0; int get documentOffset {
return parentOffset + getOffset(); final parentOffset = (parent is! Root) ? parent!.documentOffset : 0;
return parentOffset + offset;
} }
/// Returns `true` if this node contains character at specified [offset] in
/// the document.
bool containsOffset(int offset) { bool containsOffset(int offset) {
final o = getDocumentOffset(); final o = documentOffset;
return o <= offset && offset < o + length; return o <= offset && offset < o + length;
} }
void applyAttribute(Attribute attribute) {
_style = _style.merge(attribute);
}
void applyStyle(Style value) {
_style = _style.mergeAll(value);
}
void clearStyle() {
_style = Style();
}
@override @override
void insertBefore(Node entry) { void insertBefore(Node entry) {
assert(entry.parent == null && parent != null); assert(entry.parent == null && parent != null);
@ -81,9 +97,7 @@ abstract class Node extends LinkedListEntry<Node> {
super.unlink(); super.unlink();
} }
void adjust() { void adjust() {/* no-op */}
// do nothing
}
/// abstract methods begin /// abstract methods begin
@ -100,11 +114,13 @@ abstract class Node extends LinkedListEntry<Node> {
void delete(int index, int? len); void delete(int index, int? len);
/// abstract methods end /// abstract methods end
} }
/* Root node of document tree */ /// Root node of document tree.
class Root extends Container<Container<Node?>> { class Root extends Container<Container<Node?>> {
@override
Node newInstance() => Root();
@override @override
Container<Node?> get defaultChild => Line(); Container<Node?> get defaultChild => Line();
@ -112,9 +128,4 @@ class Root extends Container<Container<Node?>> {
Delta toDelta() => children Delta toDelta() => children
.map((child) => child.toDelta()) .map((child) => child.toDelta())
.fold(Delta(), (a, b) => a.concat(b)); .fold(Delta(), (a, b) => a.concat(b));
@override
Node newInstance() {
return Root();
}
} }

@ -577,8 +577,7 @@ class RenderEditor extends RenderEditableContainerBox
if (textSelection.isCollapsed) { if (textSelection.isCollapsed) {
final child = childAtPosition(textSelection.extent); final child = childAtPosition(textSelection.extent);
final localPosition = TextPosition( final localPosition = TextPosition(
offset: offset: textSelection.extentOffset - child.getContainer().offset);
textSelection.extentOffset - child.getContainer().getOffset());
final localOffset = child.getOffsetForCaret(localPosition); final localOffset = child.getOffsetForCaret(localPosition);
final parentData = child.parentData as BoxParentData; final parentData = child.parentData as BoxParentData;
return <TextSelectionPoint>[ return <TextSelectionPoint>[
@ -677,7 +676,7 @@ class RenderEditor extends RenderEditableContainerBox
assert(_lastTapDownPosition != null); assert(_lastTapDownPosition != null);
final position = getPositionForOffset(_lastTapDownPosition!); final position = getPositionForOffset(_lastTapDownPosition!);
final child = childAtPosition(position); final child = childAtPosition(position);
final nodeOffset = child.getContainer().getOffset(); final nodeOffset = child.getContainer().offset;
final localPosition = TextPosition( final localPosition = TextPosition(
offset: position.offset - nodeOffset, offset: position.offset - nodeOffset,
affinity: position.affinity, affinity: position.affinity,
@ -738,7 +737,7 @@ class RenderEditor extends RenderEditableContainerBox
@override @override
TextSelection selectWordAtPosition(TextPosition position) { TextSelection selectWordAtPosition(TextPosition position) {
final child = childAtPosition(position); final child = childAtPosition(position);
final nodeOffset = child.getContainer().getOffset(); final nodeOffset = child.getContainer().offset;
final localPosition = TextPosition( final localPosition = TextPosition(
offset: position.offset - nodeOffset, affinity: position.affinity); offset: position.offset - nodeOffset, affinity: position.affinity);
final localWord = child.getWordBoundary(localPosition); final localWord = child.getWordBoundary(localPosition);
@ -755,7 +754,7 @@ class RenderEditor extends RenderEditableContainerBox
@override @override
TextSelection selectLineAtPosition(TextPosition position) { TextSelection selectLineAtPosition(TextPosition position) {
final child = childAtPosition(position); final child = childAtPosition(position);
final nodeOffset = child.getContainer().getOffset(); final nodeOffset = child.getContainer().offset;
final localPosition = TextPosition( final localPosition = TextPosition(
offset: position.offset - nodeOffset, affinity: position.affinity); offset: position.offset - nodeOffset, affinity: position.affinity);
final localLineRange = child.getLineBoundary(localPosition); final localLineRange = child.getLineBoundary(localPosition);
@ -810,8 +809,8 @@ class RenderEditor extends RenderEditableContainerBox
@override @override
double preferredLineHeight(TextPosition position) { double preferredLineHeight(TextPosition position) {
final child = childAtPosition(position); final child = childAtPosition(position);
return child.preferredLineHeight(TextPosition( return child.preferredLineHeight(
offset: position.offset - child.getContainer().getOffset())); TextPosition(offset: position.offset - child.getContainer().offset));
} }
@override @override
@ -823,7 +822,7 @@ class RenderEditor extends RenderEditableContainerBox
final localOffset = local - parentData.offset; final localOffset = local - parentData.offset;
final localPosition = child.getPositionForOffset(localOffset); final localPosition = child.getPositionForOffset(localOffset);
return TextPosition( return TextPosition(
offset: localPosition.offset + child.getContainer().getOffset(), offset: localPosition.offset + child.getContainer().offset,
affinity: localPosition.affinity, affinity: localPosition.affinity,
); );
} }
@ -842,8 +841,7 @@ class RenderEditor extends RenderEditableContainerBox
final caretTop = endpoint.point.dy - final caretTop = endpoint.point.dy -
child.preferredLineHeight(TextPosition( child.preferredLineHeight(TextPosition(
offset: offset: selection.extentOffset - child.getContainer().offset)) -
selection.extentOffset - child.getContainer().getOffset())) -
kMargin + kMargin +
offsetInViewport; offsetInViewport;
final caretBottom = endpoint.point.dy + kMargin + offsetInViewport; final caretBottom = endpoint.point.dy + kMargin + offsetInViewport;

@ -209,8 +209,7 @@ class RawEditorState extends EditorState
final child = getRenderEditor()!.childAtPosition(originPosition); final child = getRenderEditor()!.childAtPosition(originPosition);
final localPosition = TextPosition( final localPosition = TextPosition(
offset: offset: originPosition.offset - child.getContainer().documentOffset);
originPosition.offset - child.getContainer().getDocumentOffset());
var position = upKey var position = upKey
? child.getPositionAbove(localPosition) ? child.getPositionAbove(localPosition)
@ -231,12 +230,12 @@ class RawEditorState extends EditorState
.dy); .dy);
final siblingPosition = sibling.getPositionForOffset(finalOffset); final siblingPosition = sibling.getPositionForOffset(finalOffset);
position = TextPosition( position = TextPosition(
offset: sibling.getContainer().getDocumentOffset() + offset:
siblingPosition.offset); sibling.getContainer().documentOffset + siblingPosition.offset);
} }
} else { } else {
position = TextPosition( position = TextPosition(
offset: child.getContainer().getDocumentOffset() + position.offset); offset: child.getContainer().documentOffset + position.offset);
} }
if (position.offset == newSelection.extentOffset) { if (position.offset == newSelection.extentOffset) {

@ -312,12 +312,12 @@ class RenderEditableTextBlock extends RenderEditableContainerBox
TextRange getLineBoundary(TextPosition position) { TextRange getLineBoundary(TextPosition position) {
final child = childAtPosition(position); final child = childAtPosition(position);
final rangeInChild = child.getLineBoundary(TextPosition( final rangeInChild = child.getLineBoundary(TextPosition(
offset: position.offset - child.getContainer().getOffset(), offset: position.offset - child.getContainer().offset,
affinity: position.affinity, affinity: position.affinity,
)); ));
return TextRange( return TextRange(
start: rangeInChild.start + child.getContainer().getOffset(), start: rangeInChild.start + child.getContainer().offset,
end: rangeInChild.end + child.getContainer().getOffset(), end: rangeInChild.end + child.getContainer().offset,
); );
} }
@ -325,7 +325,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox
Offset getOffsetForCaret(TextPosition position) { Offset getOffsetForCaret(TextPosition position) {
final child = childAtPosition(position); final child = childAtPosition(position);
return child.getOffsetForCaret(TextPosition( return child.getOffsetForCaret(TextPosition(
offset: position.offset - child.getContainer().getOffset(), offset: position.offset - child.getContainer().offset,
affinity: position.affinity, affinity: position.affinity,
)) + )) +
(child.parentData as BoxParentData).offset; (child.parentData as BoxParentData).offset;
@ -338,7 +338,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox
final localPosition = final localPosition =
child.getPositionForOffset(offset - parentData.offset); child.getPositionForOffset(offset - parentData.offset);
return TextPosition( return TextPosition(
offset: localPosition.offset + child.getContainer().getOffset(), offset: localPosition.offset + child.getContainer().offset,
affinity: localPosition.affinity, affinity: localPosition.affinity,
); );
} }
@ -346,7 +346,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox
@override @override
TextRange getWordBoundary(TextPosition position) { TextRange getWordBoundary(TextPosition position) {
final child = childAtPosition(position); final child = childAtPosition(position);
final nodeOffset = child.getContainer().getOffset(); final nodeOffset = child.getContainer().offset;
final childWord = child final childWord = child
.getWordBoundary(TextPosition(offset: position.offset - nodeOffset)); .getWordBoundary(TextPosition(offset: position.offset - nodeOffset));
return TextRange( return TextRange(
@ -360,12 +360,11 @@ class RenderEditableTextBlock extends RenderEditableContainerBox
assert(position.offset < getContainer().length); assert(position.offset < getContainer().length);
final child = childAtPosition(position); final child = childAtPosition(position);
final childLocalPosition = TextPosition( final childLocalPosition =
offset: position.offset - child.getContainer().getOffset()); TextPosition(offset: position.offset - child.getContainer().offset);
final result = child.getPositionAbove(childLocalPosition); final result = child.getPositionAbove(childLocalPosition);
if (result != null) { if (result != null) {
return TextPosition( return TextPosition(offset: result.offset + child.getContainer().offset);
offset: result.offset + child.getContainer().getOffset());
} }
final sibling = childBefore(child); final sibling = childBefore(child);
@ -379,7 +378,7 @@ class RenderEditableTextBlock extends RenderEditableContainerBox
final testOffset = sibling.getOffsetForCaret(testPosition); final testOffset = sibling.getOffsetForCaret(testPosition);
final finalOffset = Offset(caretOffset.dx, testOffset.dy); final finalOffset = Offset(caretOffset.dx, testOffset.dy);
return TextPosition( return TextPosition(
offset: sibling.getContainer().getOffset() + offset: sibling.getContainer().offset +
sibling.getPositionForOffset(finalOffset).offset); sibling.getPositionForOffset(finalOffset).offset);
} }
@ -388,12 +387,11 @@ class RenderEditableTextBlock extends RenderEditableContainerBox
assert(position.offset < getContainer().length); assert(position.offset < getContainer().length);
final child = childAtPosition(position); final child = childAtPosition(position);
final childLocalPosition = TextPosition( final childLocalPosition =
offset: position.offset - child.getContainer().getOffset()); TextPosition(offset: position.offset - child.getContainer().offset);
final result = child.getPositionBelow(childLocalPosition); final result = child.getPositionBelow(childLocalPosition);
if (result != null) { if (result != null) {
return TextPosition( return TextPosition(offset: result.offset + child.getContainer().offset);
offset: result.offset + child.getContainer().getOffset());
} }
final sibling = childAfter(child); final sibling = childAfter(child);
@ -405,15 +403,15 @@ class RenderEditableTextBlock extends RenderEditableContainerBox
final testOffset = sibling.getOffsetForCaret(const TextPosition(offset: 0)); final testOffset = sibling.getOffsetForCaret(const TextPosition(offset: 0));
final finalOffset = Offset(caretOffset.dx, testOffset.dy); final finalOffset = Offset(caretOffset.dx, testOffset.dy);
return TextPosition( return TextPosition(
offset: sibling.getContainer().getOffset() + offset: sibling.getContainer().offset +
sibling.getPositionForOffset(finalOffset).offset); sibling.getPositionForOffset(finalOffset).offset);
} }
@override @override
double preferredLineHeight(TextPosition position) { double preferredLineHeight(TextPosition position) {
final child = childAtPosition(position); final child = childAtPosition(position);
return child.preferredLineHeight(TextPosition( return child.preferredLineHeight(
offset: position.offset - child.getContainer().getOffset())); TextPosition(offset: position.offset - child.getContainer().offset));
} }
@override @override

@ -402,8 +402,8 @@ class RenderEditableTextLine extends RenderEditableBox {
} }
bool containsTextSelection() { bool containsTextSelection() {
return line.getDocumentOffset() <= textSelection.end && return line.documentOffset <= textSelection.end &&
textSelection.start <= line.getDocumentOffset() + line.length - 1; textSelection.start <= line.documentOffset + line.length - 1;
} }
bool containsCursor() { bool containsCursor() {
@ -735,8 +735,8 @@ class RenderEditableTextLine extends RenderEditableBox {
final parentData = _body!.parentData as BoxParentData; final parentData = _body!.parentData as BoxParentData;
final effectiveOffset = offset + parentData.offset; final effectiveOffset = offset + parentData.offset;
if (enableInteractiveSelection && if (enableInteractiveSelection &&
line.getDocumentOffset() <= textSelection.end && line.documentOffset <= textSelection.end &&
textSelection.start <= line.getDocumentOffset() + line.length - 1) { textSelection.start <= line.documentOffset + line.length - 1) {
final local = localSelection(line, textSelection, false); final local = localSelection(line, textSelection, false);
_selectedRects ??= _body!.getBoxesForSelection( _selectedRects ??= _body!.getBoxesForSelection(
local, local,
@ -772,7 +772,7 @@ class RenderEditableTextLine extends RenderEditableBox {
void _paintCursor(PaintingContext context, Offset effectiveOffset) { void _paintCursor(PaintingContext context, Offset effectiveOffset) {
final position = TextPosition( final position = TextPosition(
offset: textSelection.extentOffset - line.getDocumentOffset(), offset: textSelection.extentOffset - line.documentOffset,
affinity: textSelection.base.affinity, affinity: textSelection.base.affinity,
); );
_cursorPainter.paint(context.canvas, effectiveOffset, position); _cursorPainter.paint(context.canvas, effectiveOffset, position);

@ -12,10 +12,10 @@ import '../models/documents/nodes/node.dart';
import 'editor.dart'; import 'editor.dart';
TextSelection localSelection(Node node, TextSelection selection, fromParent) { TextSelection localSelection(Node node, TextSelection selection, fromParent) {
final base = fromParent ? node.getOffset() : node.getDocumentOffset(); final base = fromParent ? node.offset : node.documentOffset;
assert(base <= selection.end && selection.start <= base + node.length - 1); assert(base <= selection.end && selection.start <= base + node.length - 1);
final offset = fromParent ? node.getOffset() : node.getDocumentOffset(); final offset = fromParent ? node.offset : node.documentOffset;
return selection.copyWith( return selection.copyWith(
baseOffset: math.max(selection.start - offset, 0), baseOffset: math.max(selection.start - offset, 0),
extentOffset: math.min(selection.end - offset, node.length - 1)); extentOffset: math.min(selection.end - offset, node.length - 1));

Loading…
Cancel
Save