From 5373dc69bd9912be6b716785ef133a4d4e094fed Mon Sep 17 00:00:00 2001 From: singerdmx Date: Wed, 16 Dec 2020 19:12:00 -0800 Subject: [PATCH] Add embed and update nodes --- lib/models/documents/nodes/block.dart | 35 ++++ lib/models/documents/nodes/container.dart | 68 +++++++ lib/models/documents/nodes/embed.dart | 19 ++ lib/models/documents/nodes/leaf.dart | 168 +++++++++++++++- lib/models/documents/nodes/line.dart | 227 +++++++++++++++++++++- lib/models/documents/nodes/node.dart | 23 +++ lib/models/documents/style.dart | 10 + 7 files changed, 548 insertions(+), 2 deletions(-) create mode 100644 lib/models/documents/nodes/embed.dart diff --git a/lib/models/documents/nodes/block.dart b/lib/models/documents/nodes/block.dart index cee0952b..ab46458b 100644 --- a/lib/models/documents/nodes/block.dart +++ b/lib/models/documents/nodes/block.dart @@ -2,6 +2,7 @@ import 'package:quill_delta/quill_delta.dart'; import 'container.dart'; import 'line.dart'; +import 'node.dart'; class Block extends Container { @override @@ -13,4 +14,38 @@ class Block extends Container { .map((child) => child.toDelta()) .fold(Delta(), (a, b) => a.concat(b)); } + + @override + adjust() { + if (isEmpty) { + Node sibling = previous; + unlink(); + if (sibling != null) { + sibling.adjust(); + } + return; + } + + Block block = this; + Node prev = block.previous; + // merging it with previous block if style is the same + if (!block.isFirst && + block.previous is Block && + prev.style == block.style) { + block.moveChildToNewParent(prev); + block.unlink(); + block = prev; + } + Node next = block.next; + // merging it with next block if style is the same + if (!block.isLast && block.next is Block && next.style == block.style) { + (next as Block).moveChildToNewParent(block); + next.unlink(); + } + } + + @override + Node newInstance() { + return Block(); + } } diff --git a/lib/models/documents/nodes/container.dart b/lib/models/documents/nodes/container.dart index 4940a889..41c48859 100644 --- a/lib/models/documents/nodes/container.dart +++ b/lib/models/documents/nodes/container.dart @@ -1,5 +1,6 @@ import 'dart:collection'; +import '../style.dart'; import 'node.dart'; /* Container of multiple nodes */ @@ -42,12 +43,79 @@ abstract class Container extends Node { _children.remove(node); } + void moveChildToNewParent(Container newParent) { + if (isEmpty) { + return; + } + + T last = newParent.isEmpty ? null : newParent.last; + while (isNotEmpty) { + T child = first; + child.unlink(); + newParent.add(child); + } + + if (last != null) last.adjust(); + } + + ChildQuery queryChild(int offset, bool inclusive) { + assert(offset >= 0 && offset <= length); + + for (Node node in children) { + int len = node.length; + if (offset < len || (inclusive && offset == len && (node.isLast))) { + return ChildQuery(node, offset); + } + offset -= len; + } + return ChildQuery(null, 0); + } + @override String toPlainText() => children.map((child) => child.toPlainText()).join(); @override int get length => _children.fold(0, (cur, node) => cur + node.length); + @override + insert(int index, Object data, Style style) { + assert(index == 0 || (index > 0 && index < length)); + + if (isNotEmpty) { + ChildQuery child = queryChild(index, false); + child.node.insert(child.offset, data, style); + } + + // empty + assert(index == 0); + T node = defaultChild; + add(node); + node.insert(index, data, style); + } + + @override + retain(int index, int length, Style attributes) { + assert(isNotEmpty); + ChildQuery child = queryChild(index, false); + child.node.retain(child.offset, length, attributes); + } + + @override + delete(int index, int length) { + assert(isNotEmpty); + ChildQuery child = queryChild(index, false); + child.node.delete(child.offset, length); + } + @override String toString() => _children.join('\n'); } + +/// Query of a child in a Container +class ChildQuery { + final Node node; // null if not found + + final int offset; + + ChildQuery(this.node, this.offset); +} diff --git a/lib/models/documents/nodes/embed.dart b/lib/models/documents/nodes/embed.dart new file mode 100644 index 00000000..66dbb8ff --- /dev/null +++ b/lib/models/documents/nodes/embed.dart @@ -0,0 +1,19 @@ +class Embeddable { + static const TYPE_KEY = '_type'; + static const INLINE_KEY = '_inline'; + final String type; + final bool inline; + final Map _data; + + Embeddable(this.type, this.inline, Map data) + : assert(type != null), + assert(inline != null), + _data = Map.from(data); + + Map toJson() { + Map m = Map.from(_data); + m[TYPE_KEY] = type; + m[INLINE_KEY] = inline; + return m; + } +} diff --git a/lib/models/documents/nodes/leaf.dart b/lib/models/documents/nodes/leaf.dart index 08998bf1..fb1dc9d9 100644 --- a/lib/models/documents/nodes/leaf.dart +++ b/lib/models/documents/nodes/leaf.dart @@ -1,5 +1,10 @@ +import 'dart:math' as math; + import 'package:quill_delta/quill_delta.dart'; +import '../style.dart'; +import 'embed.dart'; +import 'line.dart'; import 'node.dart'; /* A leaf node in document tree */ @@ -12,6 +17,27 @@ abstract class Leaf extends Node { : assert(val != null), _value = val; + factory Leaf([Object data]) { + assert(data != null); + + if (data is Embeddable) { + return Embed(data); + } + String text = data as String; + assert(text.isNotEmpty); + return Text(text); + } + + @override + void applyStyle(Style value) { + assert(value != null && (value.isInline || value.isEmpty), + 'Unable to apply Style to leaf: $value'); + super.applyStyle(value); + } + + @override + Line get parent => super.parent as Line; + @override int get length { if (_value is String) { @@ -23,7 +49,122 @@ abstract class Leaf extends Node { @override Delta toDelta() { - return null; // TODO + var data = _value is Embeddable ? (_value as Embeddable).toJson() : _value; + return Delta()..insert(data, style.toJson()); + } + + @override + insert(int index, Object data, Style style) { + assert(data != null && index >= 0 && index <= length); + Leaf node = Leaf(data); + if (index < length) { + splitAt(index).insertBefore(node); + } else { + insertAfter(node); + } + node.format(style); + } + + @override + retain(int index, int len, Style style) { + if (style == null) { + return; + } + + int local = math.min(this.length - index, len); + int remain = len - local; + Leaf node = _isolate(index, local); + + if (remain > 0) { + assert(node.next != null); + node.next.retain(0, remain, style); + } + node.format(style); + } + + @override + delete(int index, int len) { + assert(index < this.length); + + int local = math.min(this.length - index, len); + Leaf target = _isolate(index, local); + Leaf prev = target.previous; + Leaf next = target.next; + target.unlink(); + + int remain = len - local; + if (remain > 0) { + assert(next != null); + next.delete(0, remain); + } + + if (prev != null) { + prev.adjust(); + } + } + + @override + adjust() { + if (this is Embed) { + return; + } + + Text node = this as Text; + // merging it with previous node if style is the same + Node prev = node.previous; + if (!node.isFirst && prev is Text && prev.style == node.style) { + prev._value = prev.value + node.value; + node.unlink(); + node = prev; + } + + // merging it with next node if style is the same + Node next = node.next; + if (!node.isLast && next is Text && next.style == node.style) { + node._value = node.value + next.value; + next.unlink(); + } + } + + Leaf cutAt(int index) { + assert(index >= 0 && index <= length); + Leaf cut = splitAt(index); + cut?.unlink(); + return cut; + } + + Leaf splitAt(int index) { + assert(index >= 0 && index <= length); + if (index == 0) { + return this; + } + if (index == length) { + return isLast ? null : next as Leaf; + } + + assert(this is Text); + String text = _value as String; + _value = text.substring(0, index); + Leaf split = Leaf(text.substring(index)); + split.applyStyle(style); + insertAfter(split); + return split; + } + + format(Style style) { + if (style != null && style.isNotEmpty) { + applyStyle(style); + } + + adjust(); + } + + Leaf _isolate(int index, int length) { + assert( + index >= 0 && index < this.length && (index + length <= this.length)); + Leaf target = splitAt(index); + target.splitAt(length); + return target; } } @@ -39,4 +180,29 @@ class Text extends Leaf { String toPlainText() { return value; } + + @override + Node newInstance() { + return Text(); + } +} + +/// An embedded node such as image or video +class Embed extends Leaf { + Embed(Embeddable data) : super.val(data); + + @override + Embeddable get value => super.value as Embeddable; + + @override + String toPlainText() { + return '\uFFFC'; + } + + @override + Node newInstance() { + // TODO: implement newInstance + throw UnimplementedError(); + } + } diff --git a/lib/models/documents/nodes/line.dart b/lib/models/documents/nodes/line.dart index 1a9b1ce6..19e0ac8e 100644 --- a/lib/models/documents/nodes/line.dart +++ b/lib/models/documents/nodes/line.dart @@ -1,17 +1,36 @@ +import 'dart:math' as math; + +import 'package:flutter_quill/models/documents/attribute.dart'; +import 'package:flutter_quill/models/documents/nodes/node.dart'; import 'package:quill_delta/quill_delta.dart'; +import '../style.dart'; import 'block.dart'; import 'container.dart'; +import 'embed.dart'; import 'leaf.dart'; class Line extends Container { - @override Leaf get defaultChild => Text(); @override int get length => super.length + 1; + Line get nextLine { + if (!isLast) { + return next is Block ? (next as Block).first : next; + } + if (parent is! Block) { + return null; + } + + if (parent.isLast) { + return null; + } + return parent.next is Block ? (parent.next as Block).first : parent.next; + } + @override Delta toDelta() { final delta = children @@ -28,4 +47,210 @@ class Line extends Container { @override String toPlainText() => super.toPlainText() + '\n'; + + @override + insert(int index, Object data, Style style) { + if (data is Embeddable) { + _insert(index, data, style); + return; + } + + String text = data as String; + int lineBreak = text.indexOf('\n'); + if (lineBreak == -1) { + _insert(index, text, style); + return; + } + + String prefix = text.substring(0, lineBreak); + _insert(index, prefix, style); + if (prefix.isNotEmpty) { + index += prefix.length; + } + + Line nextLine = _getNextLine(index); + + clearStyle(); + + if (parent is Block) { + _unwrap(); + } + + _format(style); + + // Continue with the remaining + String remain = text.substring(lineBreak + 1); + nextLine.insert(0, remain, style); + } + + @override + retain(int index, int len, Style style) { + if (style == null) { + return; + } + int thisLen = this.length; + + int local = math.min(thisLen - index, length); + assert(style.values.every((attr) => attr.scope == AttributeScope.BLOCK)); + + if (index + local == thisLen && local == 1) { + _format(style); + } else { + assert(index + local != thisLen); + super.retain(index, local, style); + } + + int remain = len - local; + if (remain > 0) { + assert(nextLine != null); + nextLine.retain(0, remain, style); + } + } + + @override + delete(int index, int len) { + int local = math.min(this.length - index, len); + bool deleted = index + local == this.length; + if (deleted) { + clearStyle(); + if (local > 1) { + super.delete(index, local - 1); + } + } else { + super.delete(index, local); + } + + int remain = length - local; + if (remain > 0) { + assert(nextLine != null); + nextLine.delete(0, remain); + } + + if (deleted && isNotEmpty) { + assert(nextLine != null); + nextLine.moveChildToNewParent(this); + moveChildToNewParent(nextLine); + } + + if (deleted) { + Node p = parent; + unlink(); + p.adjust(); + } + } + + void _format(Style newStyle) { + if (newStyle == null || newStyle.isEmpty) { + return; + } + + applyStyle(newStyle); + Attribute blockStyle = newStyle.getBlockExceptHeader(); + if (blockStyle == null) { + return; + } + + if (parent is Block) { + Attribute parentStyle = (parent as Block).style.getBlockExceptHeader(); + if (blockStyle.value == null) { + _unwrap(); + } else if (blockStyle != parentStyle) { + _unwrap(); + Block block = Block(); + block.applyAttribute(blockStyle); + _wrap(block); + block.adjust(); + } + } else if (blockStyle.value != null) { + Block block = Block(); + block.applyAttribute(blockStyle); + _wrap(block); + block.adjust(); + } + } + + _wrap(Block block) { + assert(parent != null && parent is! Block); + insertAfter(block); + unlink(); + block.add(this); + } + + _unwrap() { + if (parent is! Block) { + throw ArgumentError('Invalid parent'); + } + Block block = parent; + + assert(block.children.contains(this)); + + if (isFirst) { + unlink(); + block.insertBefore(this); + } else if (isLast) { + unlink(); + block.insertAfter(this); + } else { + Block before = block.clone(); + block.insertBefore(before); + + Line child = block.first; + 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)); + + Line line = clone() as Line; + insertAfter(line); + if (index == length - 1) { + return line; + } + + ChildQuery query = queryChild(index, false); + while (!query.node.isLast) { + Leaf next = last; + next.unlink(); + line.addFirst(next); + } + Leaf child = query.node; + Leaf cut = child.splitAt(query.offset); + cut?.unlink(); + line.addFirst(cut); + return line; + } + + _insert(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 (isNotEmpty) { + ChildQuery result = queryChild(index, true); + result.node.insert(result.offset, data, style); + return; + } + + Leaf child = Leaf(data); + add(child); + child.format(style); + } + + @override + Node newInstance() { + return Line(); + } } diff --git a/lib/models/documents/nodes/node.dart b/lib/models/documents/nodes/node.dart index c5c3e48b..48ba63d7 100644 --- a/lib/models/documents/nodes/node.dart +++ b/lib/models/documents/nodes/node.dart @@ -35,6 +35,12 @@ abstract class Node extends LinkedListEntry { int get length; + Node clone() { + Node node = newInstance(); + node.applyStyle(style); + return node; + } + @override void insertBefore(Node entry) { assert(entry.parent == null && parent != null); @@ -56,12 +62,24 @@ abstract class Node extends LinkedListEntry { super.unlink(); } + adjust() { + // do nothing + } + /// abstract methods begin + Node newInstance(); + String toPlainText(); Delta toDelta(); + insert(int index, Object data, Style style); + + retain(int index, int len, Style style); + + delete(int index, int len); + /// abstract methods end } @@ -75,4 +93,9 @@ class Root extends Container> { Delta toDelta() => children .map((child) => child.toDelta()) .fold(Delta(), (a, b) => a.concat(b)); + + @override + Node newInstance() { + return Root(); + } } diff --git a/lib/models/documents/style.dart b/lib/models/documents/style.dart index 2453b011..e4315063 100644 --- a/lib/models/documents/style.dart +++ b/lib/models/documents/style.dart @@ -39,6 +39,16 @@ class Style { bool containsKey(String key) => _attributes.containsKey(key); + Attribute getBlockExceptHeader() { + for (Attribute val in values) { + if (val.scope == AttributeScope.BLOCK && + val.key != Attribute.header.key) { + return val; + } + } + return null; + } + Style merge(Attribute attribute) { Map merged = Map.from(_attributes); if (attribute.value == null) {