diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5fdd3e33b..1c020c25b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,11 +1,13 @@ name: Build the example on: - pull_request: - branches: [master] + push: + paths: + - 'pubspec.yaml' jobs: - tests: + build_linux: + name: Build Linux and Web Apps runs-on: ubuntu-latest steps: @@ -17,10 +19,10 @@ jobs: - name: Check flutter version run: flutter --version - + - name: Enable Local Dev run: ./scripts/enable_local_dev.sh - + - name: Install dependencies run: flutter pub get @@ -34,3 +36,56 @@ jobs: - name: Flutter build Linux run: flutter build linux --release --verbose --dart-define=CI=true working-directory: ./example + + # build_windows: + # name: Build Windows App + # runs-on: windows-latest + + # steps: + # - uses: actions/checkout@v4 + # - uses: subosito/flutter-action@v2 + # with: + # channel: 'stable' + # cache: true + + # - name: Check flutter version + # run: flutter --version + + # # Sh scripts is not supported on windows + # - name: Enable Local Dev + # run: ./scripts/enable_local_dev.sh + + # - name: Install dependencies + # run: flutter pub get + + # - name: Flutter build windows + # run: flutter build windows --release --verbose --dart-define=CI=true + # working-directory: ./example + + # build_macOS: + # name: Build macOS App + # runs-on: macos-latest + + # steps: + # - uses: actions/checkout@v4 + # - uses: subosito/flutter-action@v2 + # with: + # channel: 'stable' + # cache: true + + # - name: Check flutter version + # run: flutter --version + + # - name: Enable Local Dev + # run: ./scripts/enable_local_dev.sh + + # - name: Install dependencies + # run: flutter pub get + + # - name: Flutter build macOS + # run: flutter build macos --release --verbose --dart-define=CI=true + # working-directory: ./example + + # - name: Flutter build iOS + # run: flutter build ios --release --verbose --dart-define=CI=true + # working-directory: ./example \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9e4b19eeb..c4528f8d1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,9 +2,9 @@ name: CI Tests on: push: - branches: [master] + branches: [master, dev] pull_request: - branches: [master] + branches: [master, dev] jobs: tests: @@ -33,7 +33,7 @@ jobs: run: flutter pub get -C flutter_quill_test - name: Install quill_html_converter dependencies - run: flutter pub get -C packages/quill_html_converter + run: flutter pub get -C quill_html_converter - name: Run flutter analysis run: flutter analyze @@ -49,14 +49,3 @@ jobs: - name: Run flutter tests run: flutter test - - # - name: Flutter build Web - # run: flutter build web --release --verbose --dart-define=CI=true - # working-directory: ./example - - # - name: Install flutter Linux prerequisites - # run: sudo apt-get install clang cmake git ninja-build pkg-config libgtk-3-dev liblzma-dev libstdc++-12-dev -y - - # - name: Flutter build Linux - # run: flutter build linux --release --verbose --dart-define=CI=true - # working-directory: ./example diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 45970dc53..af93476e6 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -51,4 +51,4 @@ jobs: - name: Publish quill_html_converter run: flutter pub publish --force - working-directory: ./packages/quill_html_converter/ + working-directory: ./quill_html_converter/ diff --git a/.gitignore b/.gitignore index fc03c2767..9290cda2b 100644 --- a/.gitignore +++ b/.gitignore @@ -80,7 +80,5 @@ pubspec.lock # For local development pubspec_overrides.yaml -old_example - # A directory where you put all of your local things that you don't want to push .flutter-quill \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 09d629a35..4392c9d05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to this project will be documented in this file. +## 9.0.0-dev-11 +* Test new GitHub workflows + +## 9.0.0-dev-10 +* Fix a bug of the improved pasting HTML contents contents into the editor + +## 9.0.0-dev-9 +* Improves the new logic of pasting HTML contents into the Editor +* Update `README.md` and the doc +* Dispose the `QuillToolbarSelectHeaderStyleButton` state listener in `dispose` +* Upgrade the font family button to material 3 +* Rework the font family and font size functionalities to change the font once and type all over the editor + ## 9.0.0-dev-8 * Better support for pasting HTML contents from external websites to the editor * The experimental support of converting the HTML from `quill_html_converter` is now built-in in the `flutter_quill` and removed from there (Breaking change for `quill_html_converter`) diff --git a/README.md b/README.md index ecd67fb83..0bc825333 100644 --- a/README.md +++ b/README.md @@ -240,7 +240,7 @@ it to other formats such as HTML to publish it, or send an email. You have two options: -1. Using [quill_html_converter](./packages/quill_html_converter/) to convert to HTML, the package can convert the Quill delta to HTML well +1. Using [quill_html_converter](./quill_html_converter/) to convert to HTML, the package can convert the Quill delta to HTML well (it uses [vsc_quill_delta_to_html](https://pub.dev/packages/vsc_quill_delta_to_html)), it just a handy extension to do it more quickly 1. Another option is to use [vsc_quill_delta_to_html](https://pub.dev/packages/vsc_quill_delta_to_html) to convert your document diff --git a/dart_quill_delta/.gitignore b/dart_quill_delta/.gitignore new file mode 100644 index 000000000..3cceda557 --- /dev/null +++ b/dart_quill_delta/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/dart_quill_delta/CHANGELOG.md b/dart_quill_delta/CHANGELOG.md new file mode 100644 index 000000000..59f8a732d --- /dev/null +++ b/dart_quill_delta/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* Initial version. diff --git a/packages/quill_html_converter/LICENSE b/dart_quill_delta/LICENSE similarity index 100% rename from packages/quill_html_converter/LICENSE rename to dart_quill_delta/LICENSE diff --git a/dart_quill_delta/README.md b/dart_quill_delta/README.md new file mode 100644 index 000000000..7d7aad35f --- /dev/null +++ b/dart_quill_delta/README.md @@ -0,0 +1,2 @@ +# Dart Quill Delta +A port of [quill-js-delta](https://github.com/quilljs/delta/) from typescript to dart \ No newline at end of file diff --git a/dart_quill_delta/analysis_options.yaml b/dart_quill_delta/analysis_options.yaml new file mode 100644 index 000000000..dee8927aa --- /dev/null +++ b/dart_quill_delta/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/dart_quill_delta/example/dart_quill_delta_example.dart b/dart_quill_delta/example/dart_quill_delta_example.dart new file mode 100644 index 000000000..ab73b3a23 --- /dev/null +++ b/dart_quill_delta/example/dart_quill_delta_example.dart @@ -0,0 +1 @@ +void main() {} diff --git a/dart_quill_delta/lib/dart_quill_delta.dart b/dart_quill_delta/lib/dart_quill_delta.dart new file mode 100644 index 000000000..d29a9063a --- /dev/null +++ b/dart_quill_delta/lib/dart_quill_delta.dart @@ -0,0 +1,5 @@ +library; + +export './src/delta/delta.dart'; +export './src/delta/delta_iterator.dart'; +export './src/operation/operation.dart'; diff --git a/dart_quill_delta/lib/src/delta/delta.dart b/dart_quill_delta/lib/src/delta/delta.dart new file mode 100644 index 000000000..cb3a60be6 --- /dev/null +++ b/dart_quill_delta/lib/src/delta/delta.dart @@ -0,0 +1,562 @@ +import 'dart:math' as math; + +import 'package:collection/collection.dart'; +import 'package:diff_match_patch/diff_match_patch.dart' as dmp; +import 'package:quiver/core.dart'; + +import '../operation/operation.dart'; +import 'delta_iterator.dart'; + +/// Delta represents a document or a modification of a document as a sequence of +/// insert, delete and retain operations. +/// +/// Delta consisting of only "insert" operations is usually referred to as +/// "document delta". When delta includes also "retain" or "delete" operations +/// it is a "change delta". +class Delta { + /// Creates new empty [Delta]. + factory Delta() => Delta._([]); + + Delta._(this.operations); + + /// Creates new [Delta] from [other]. + factory Delta.from(Delta other) => + Delta._(List.from(other.operations)); + + /// Creates new [Delta] from a List of Operation + factory Delta.fromOperations(List operations) => + Delta._(operations.toList()); + + // Placeholder char for embed in diff() + static final String _kNullCharacter = String.fromCharCode(0); + + /// Transforms two attribute sets. + static Map? transformAttributes( + Map? a, Map? b, bool priority) { + if (a == null) return b; + if (b == null) return null; + + if (!priority) return b; + + final result = b.keys.fold>({}, (attributes, key) { + if (!a.containsKey(key)) attributes[key] = b[key]; + return attributes; + }); + + return result.isEmpty ? null : result; + } + + /// Composes two attribute sets. + static Map? composeAttributes( + Map? a, Map? b, + {bool keepNull = false}) { + a ??= const {}; + b ??= const {}; + + final result = Map.from(a)..addAll(b); + final keys = result.keys.toList(growable: false); + + if (!keepNull) { + for (final key in keys) { + if (result[key] == null) result.remove(key); + } + } + + return result.isEmpty ? null : result; + } + + ///get anti-attr result base on base + static Map invertAttributes( + Map? attr, Map? base) { + attr ??= const {}; + base ??= const {}; + + final baseInverted = base.keys.fold({}, (dynamic memo, key) { + if (base![key] != attr![key] && attr.containsKey(key)) { + memo[key] = base[key]; + } + return memo; + }); + + final inverted = + Map.from(attr.keys.fold(baseInverted, (memo, key) { + if (base![key] != attr![key] && !base.containsKey(key)) { + memo[key] = null; + } + return memo; + })); + return inverted; + } + + /// Returns diff between two attribute sets + static Map? diffAttributes( + Map? a, Map? b) { + a ??= const {}; + b ??= const {}; + + final attributes = {}; + for (final key in (a.keys.toList()..addAll(b.keys))) { + if (a[key] != b[key]) { + attributes[key] = b.containsKey(key) ? b[key] : null; + } + } + + return attributes.keys.isNotEmpty ? attributes : null; + } + + final List operations; + + int modificationCount = 0; + + /// Creates [Delta] from de-serialized JSON representation. + /// + /// If `dataDecoder` parameter is not null then it is used to additionally + /// decode the operation's data object. Only applied to insert operations. + static Delta fromJson(List data, {DataDecoder? dataDecoder}) { + return Delta._(data + .map((op) => Operation.fromJson(op, dataDecoder: dataDecoder)) + .toList()); + } + + /// Returns list of operations in this delta. + List toList() => List.from(operations); + + /// Returns JSON-serializable version of this delta. + List toJson() => toList().map((operation) => operation.toJson()).toList(); + + /// Returns `true` if this delta is empty. + bool get isEmpty => operations.isEmpty; + + /// Returns `true` if this delta is not empty. + bool get isNotEmpty => operations.isNotEmpty; + + /// Returns number of operations in this delta. + int get length => operations.length; + + /// Returns [Operation] at specified [index] in this delta. + Operation operator [](int index) => operations[index]; + + /// Returns [Operation] at specified [index] in this delta. + Operation elementAt(int index) => operations.elementAt(index); + + /// Returns the first [Operation] in this delta. + Operation get first => operations.first; + + /// Returns the last [Operation] in this delta. + Operation get last => operations.last; + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) return true; + if (other is! Delta) return false; + final typedOther = other; + const comparator = ListEquality(DefaultEquality()); + return comparator.equals(operations, typedOther.operations); + } + + @override + int get hashCode => hashObjects(operations); + + /// Retain [count] of characters from current position. + void retain(int count, [Map? attributes]) { + assert(count >= 0); + if (count == 0) return; // no-op + push(Operation.retain(count, attributes)); + } + + /// Insert [data] at current position. + void insert(dynamic data, [Map? attributes]) { + if (data is String && data.isEmpty) return; // no-op + push(Operation.insert(data, attributes)); + } + + /// Delete [count] characters from current position. + void delete(int count) { + assert(count >= 0); + if (count == 0) return; + push(Operation.delete(count)); + } + + void _mergeWithTail(Operation operation) { + assert(isNotEmpty); + assert(last.key == operation.key); + assert(operation.data is String && last.data is String); + + final length = operation.length! + last.length!; + final lastText = last.data as String; + final opText = operation.data as String; + final resultText = lastText + opText; + final index = operations.length; + operations.replaceRange(index - 1, index, [ + Operation(operation.key, length, resultText, operation.attributes), + ]); + } + + /// Pushes new operation into this delta. + /// + /// Performs compaction by composing [operation] with current tail operation + /// of this delta, when possible. For instance, if current tail is + /// `insert('abc')` and pushed operation is `insert('123')` then existing + /// tail is replaced with `insert('abc123')` - a compound result of the two + /// operations. + void push(Operation operation) { + if (operation.isEmpty) return; + + var index = operations.length; + final lastOp = operations.isNotEmpty ? operations.last : null; + if (lastOp != null) { + if (lastOp.isDelete && operation.isDelete) { + _mergeWithTail(operation); + return; + } + + if (lastOp.isDelete && operation.isInsert) { + index -= 1; // Always insert before deleting + final nLastOp = (index > 0) ? operations.elementAt(index - 1) : null; + if (nLastOp == null) { + operations.insert(0, operation); + return; + } + } + + if (lastOp.isInsert && operation.isInsert) { + if (lastOp.hasSameAttributes(operation) && + operation.data is String && + lastOp.data is String) { + _mergeWithTail(operation); + return; + } + } + + if (lastOp.isRetain && operation.isRetain) { + if (lastOp.hasSameAttributes(operation)) { + _mergeWithTail(operation); + return; + } + } + } + if (index == operations.length) { + operations.add(operation); + } else { + final opAtIndex = operations.elementAt(index); + operations.replaceRange(index, index + 1, [operation, opAtIndex]); + } + modificationCount++; + } + + /// Composes next operation from [thisIter] and [otherIter]. + /// + /// Returns new operation or `null` if operations from [thisIter] and + /// [otherIter] nullify each other. For instance, for the pair `insert('abc')` + /// and `delete(3)` composition result would be empty string. + Operation? _composeOperation( + DeltaIterator thisIter, DeltaIterator otherIter) { + if (otherIter.isNextInsert) return otherIter.next(); + if (thisIter.isNextDelete) return thisIter.next(); + + final length = math.min(thisIter.peekLength(), otherIter.peekLength()); + final thisOp = thisIter.next(length); + final otherOp = otherIter.next(length); + assert(thisOp.length == otherOp.length); + + if (otherOp.isRetain) { + final attributes = composeAttributes( + thisOp.attributes, + otherOp.attributes, + keepNull: thisOp.isRetain, + ); + if (thisOp.isRetain) { + return Operation.retain(thisOp.length, attributes); + } else if (thisOp.isInsert) { + return Operation.insert(thisOp.data, attributes); + } else { + throw StateError('Unreachable'); + } + } else { + // otherOp == delete && thisOp in [retain, insert] + assert(otherOp.isDelete); + if (thisOp.isRetain) return otherOp; + assert(thisOp.isInsert); + // otherOp(delete) + thisOp(insert) => null + } + return null; + } + + /// Composes this delta with [other] and returns new [Delta]. + /// + /// It is not required for this and [other] delta to represent a document + /// delta (consisting only of insert operations). + Delta compose(Delta other) { + final result = Delta(); + final thisIter = DeltaIterator(this); + final otherIter = DeltaIterator(other); + + while (thisIter.hasNext || otherIter.hasNext) { + final newOp = _composeOperation(thisIter, otherIter); + if (newOp != null) result.push(newOp); + } + return result..trim(); + } + + /// Returns a new lazy Iterable with elements that are created by calling + /// f on each element of this Iterable in iteration order. + /// + /// Convenience method + Iterable map(T Function(Operation) f) { + return operations.map(f); + } + + /// Returns a [Delta] containing differences between 2 [Delta]s. + /// If [cleanupSemantic] is `true` (default), applies the following: + /// + /// The diff of "mouse" and "sofas" is + /// [delete(1), insert("s"), retain(1), + /// delete("u"), insert("fa"), retain(1), delete(1)]. + /// While this is the optimum diff, it is difficult for humans to understand. + /// Semantic cleanup rewrites the diff, + /// expanding it into a more intelligible format. + /// The above example would become: [(-1, "mouse"), (1, "sofas")]. + /// (source: https://github.com/google/diff-match-patch/wiki/API) + /// + /// Useful when one wishes to display difference between 2 documents + Delta diff(Delta other, {bool cleanupSemantic = true}) { + if (operations.equals(other.operations)) { + return Delta(); + } + final stringThis = map((op) { + if (op.isInsert) { + return op.data is String ? op.data : _kNullCharacter; + } + final prep = this == other ? 'on' : 'with'; + throw ArgumentError('diff() call $prep non-document'); + }).join(); + final stringOther = other.map((op) { + if (op.isInsert) { + return op.data is String ? op.data : _kNullCharacter; + } + final prep = this == other ? 'on' : 'with'; + throw ArgumentError('diff() call $prep non-document'); + }).join(); + + final retDelta = Delta(); + final diffResult = dmp.diff(stringThis, stringOther); + if (cleanupSemantic) { + dmp.DiffMatchPatch().diffCleanupSemantic(diffResult); + } + + final thisIter = DeltaIterator(this); + final otherIter = DeltaIterator(other); + + for (final component in diffResult) { + var length = component.text.length; + while (length > 0) { + var opLength = 0; + switch (component.operation) { + case dmp.DIFF_INSERT: + opLength = math.min(otherIter.peekLength(), length); + retDelta.push(otherIter.next(opLength)); + break; + case dmp.DIFF_DELETE: + opLength = math.min(length, thisIter.peekLength()); + thisIter.next(opLength); + retDelta.delete(opLength); + break; + case dmp.DIFF_EQUAL: + opLength = math.min( + math.min(thisIter.peekLength(), otherIter.peekLength()), + length, + ); + final thisOp = thisIter.next(opLength); + final otherOp = otherIter.next(opLength); + if (thisOp.data == otherOp.data) { + retDelta.retain( + opLength, + diffAttributes(thisOp.attributes, otherOp.attributes), + ); + } else { + retDelta + ..push(otherOp) + ..delete(opLength); + } + break; + } + length -= opLength; + } + } + return retDelta..trim(); + } + + /// Transforms next operation from [otherIter] against next operation in + /// [thisIter]. + /// + /// Returns `null` if both operations nullify each other. + Operation? _transformOperation( + DeltaIterator thisIter, DeltaIterator otherIter, bool priority) { + if (thisIter.isNextInsert && (priority || !otherIter.isNextInsert)) { + return Operation.retain(thisIter.next().length); + } else if (otherIter.isNextInsert) { + return otherIter.next(); + } + + final length = math.min(thisIter.peekLength(), otherIter.peekLength()); + final thisOp = thisIter.next(length); + final otherOp = otherIter.next(length); + assert(thisOp.length == otherOp.length); + + // At this point only delete and retain operations are possible. + if (thisOp.isDelete) { + // otherOp is either delete or retain, so they nullify each other. + return null; + } else if (otherOp.isDelete) { + return otherOp; + } else { + // Retain otherOp which is either retain or insert. + return Operation.retain( + length, + transformAttributes(thisOp.attributes, otherOp.attributes, priority), + ); + } + } + + /// Transforms [other] delta against operations in this delta. + Delta transform(Delta other, bool priority) { + final result = Delta(); + final thisIter = DeltaIterator(this); + final otherIter = DeltaIterator(other); + + while (thisIter.hasNext || otherIter.hasNext) { + final newOp = _transformOperation(thisIter, otherIter, priority); + if (newOp != null) result.push(newOp); + } + return result..trim(); + } + + /// Removes trailing retain operation with empty attributes, if present. + void trim() { + if (isNotEmpty) { + final last = operations.last; + if (last.isRetain && last.isPlain) operations.removeLast(); + } + } + + /// Removes trailing '\n' + void _trimNewLine() { + if (isNotEmpty) { + final lastOp = operations.last; + final lastOpData = lastOp.data; + + if (lastOpData is String && lastOpData.endsWith('\n')) { + operations.removeLast(); + if (lastOpData.length > 1) { + insert(lastOpData.substring(0, lastOpData.length - 1), + lastOp.attributes); + } + } + } + } + + /// Concatenates [other] with this delta and returns the result. + Delta concat(Delta other, {bool trimNewLine = false}) { + final result = Delta.from(this); + if (trimNewLine) { + result._trimNewLine(); + } + if (other.isNotEmpty) { + // In case first operation of other can be merged with last operation in + // our list. + result.push(other.operations.first); + result.operations.addAll(other.operations.sublist(1)); + } + return result; + } + + /// Inverts this delta against [base]. + /// + /// Returns new delta which negates effect of this delta when applied to + /// [base]. This is an equivalent of "undo" operation on deltas. + Delta invert(Delta base) { + final inverted = Delta(); + if (base.isEmpty) return inverted; + + var baseIndex = 0; + for (final op in operations) { + if (op.isInsert) { + inverted.delete(op.length!); + } else if (op.isRetain && op.isPlain) { + inverted.retain(op.length!); + baseIndex += op.length!; + } else if (op.isDelete || (op.isRetain && op.isNotPlain)) { + final length = op.length!; + final sliceDelta = base.slice(baseIndex, baseIndex + length); + sliceDelta.toList().forEach((baseOp) { + if (op.isDelete) { + inverted.push(baseOp); + } else if (op.isRetain && op.isNotPlain) { + final invertAttr = + invertAttributes(op.attributes, baseOp.attributes); + inverted.retain( + baseOp.length!, invertAttr.isEmpty ? null : invertAttr); + } + }); + baseIndex += length; + } else { + throw StateError('Unreachable'); + } + } + inverted.trim(); + return inverted; + } + + /// Returns slice of this delta from [start] index (inclusive) to [end] + /// (exclusive). + Delta slice(int start, [int? end]) { + final delta = Delta(); + var index = 0; + final opIterator = DeltaIterator(this); + + final actualEnd = end ?? DeltaIterator.maxLength; + + while (index < actualEnd && opIterator.hasNext) { + Operation op; + if (index < start) { + op = opIterator.next(start - index); + } else { + op = opIterator.next(actualEnd - index); + delta.push(op); + } + index += op.length!; + } + return delta; + } + + /// Transforms [index] against this delta. + /// + /// Any "delete" operation before specified [index] shifts it backward, as + /// well as any "insert" operation shifts it forward. + /// + /// The [force] argument is used to resolve scenarios when there is an + /// insert operation at the same position as [index]. If [force] is set to + /// `true` (default) then position is forced to shift forward, otherwise + /// position stays at the same index. In other words setting [force] to + /// `false` gives higher priority to the transformed position. + /// + /// Useful to adjust caret or selection positions. + int transformPosition(int index, {bool force = true}) { + final iter = DeltaIterator(this); + var offset = 0; + while (iter.hasNext && offset <= index) { + final op = iter.next(); + if (op.isDelete) { + index -= math.min(op.length!, index - offset); + continue; + } else if (op.isInsert && (offset < index || force)) { + index += op.length!; + } + offset += op.length!; + } + return index; + } + + @override + String toString() => operations.join('\n'); +} diff --git a/dart_quill_delta/lib/src/delta/delta_iterator.dart b/dart_quill_delta/lib/src/delta/delta_iterator.dart new file mode 100644 index 000000000..bb8c41e0b --- /dev/null +++ b/dart_quill_delta/lib/src/delta/delta_iterator.dart @@ -0,0 +1,100 @@ +import 'dart:math' as math; + +import '../operation/operation.dart'; +import 'delta.dart'; + +/// Specialized iterator for [Delta]s. +class DeltaIterator { + DeltaIterator(this.delta) : _modificationCount = delta.modificationCount; + + static const int maxLength = 1073741824; + + final Delta delta; + final int _modificationCount; + int _index = 0; + int _offset = 0; + + bool get isNextInsert => nextOperationKey == Operation.insertKey; + + bool get isNextDelete => nextOperationKey == Operation.deleteKey; + + bool get isNextRetain => nextOperationKey == Operation.retainKey; + + String? get nextOperationKey { + if (_index < delta.length) { + return delta.elementAt(_index).key; + } else { + return null; + } + } + + bool get hasNext => peekLength() < maxLength; + + /// Returns length of next operation without consuming it. + /// + /// Returns [maxLength] if there is no more operations left to iterate. + int peekLength() { + if (_index < delta.length) { + final operation = delta.operations[_index]; + return operation.length! - _offset; + } + return maxLength; + } + + /// Consumes and returns next operation. + /// + /// Optional [length] specifies maximum length of operation to return. Note + /// that actual length of returned operation may be less than specified value. + /// + /// If this iterator reached the end of the Delta then returns a retain + /// operation with its length set to [maxLength]. + // TODO: Note that we used double.infinity as the default value + // for length here + // but this can now cause a type error since operation length is + // expected to be an int. Changing default length to [maxLength] is + // a workaround to avoid breaking changes. + Operation next([int length = maxLength]) { + if (_modificationCount != delta.modificationCount) { + throw ConcurrentModificationError(delta); + } + + if (_index < delta.length) { + final op = delta.elementAt(_index); + final opKey = op.key; + final opAttributes = op.attributes; + final currentOffset = _offset; + final actualLength = math.min(op.length! - currentOffset, length); + if (actualLength == op.length! - currentOffset) { + _index++; + _offset = 0; + } else { + _offset += actualLength; + } + final opData = op.isInsert && op.data is String + ? (op.data as String) + .substring(currentOffset, currentOffset + actualLength) + : op.data; + final opIsNotEmpty = + opData is String ? opData.isNotEmpty : true; // embeds are never empty + final opLength = opData is String ? opData.length : 1; + final opActualLength = opIsNotEmpty ? opLength : actualLength; + return Operation(opKey, opActualLength, opData, opAttributes); + } + return Operation.retain(length); + } + + /// Skips [length] characters in source delta. + /// + /// Returns last skipped operation, or `null` if there was nothing to skip. + Operation? skip(int length) { + var skipped = 0; + Operation? op; + while (skipped < length && hasNext) { + final opLength = peekLength(); + final skip = math.min(length - skipped, opLength); + op = next(skip); + skipped += op.length!; + } + return op; + } +} diff --git a/dart_quill_delta/lib/src/operation/operation.dart b/dart_quill_delta/lib/src/operation/operation.dart new file mode 100644 index 000000000..126942ae5 --- /dev/null +++ b/dart_quill_delta/lib/src/operation/operation.dart @@ -0,0 +1,171 @@ +import 'package:collection/collection.dart'; +import 'package:quiver/core.dart'; + +/// Decoder function to convert raw `data` object into a user-defined data type. +/// +/// Useful with embedded content. +typedef DataDecoder = Object? Function(Object data); + +/// Default data decoder which simply passes through the original value. +Object? _passThroughDataDecoder(Object? data) => data; + +const _attributeEquality = DeepCollectionEquality(); +const _valueEquality = DeepCollectionEquality(); + +/// Operation performed on a rich-text document. +class Operation { + Operation(this.key, this.length, this.data, Map? attributes) + : assert(_validKeys.contains(key), 'Invalid operation key "$key".'), + assert(() { + if (key != Operation.insertKey) return true; + return data is String ? data.length == length : length == 1; + }(), 'Length of insert operation must be equal to the data length.'), + _attributes = + attributes != null ? Map.from(attributes) : null; + + /// Creates operation which deletes [length] of characters. + factory Operation.delete(int length) => + Operation(Operation.deleteKey, length, '', null); + + /// Creates operation which inserts [text] with optional [attributes]. + factory Operation.insert(dynamic data, [Map? attributes]) => + Operation(Operation.insertKey, data is String ? data.length : 1, data, + attributes); + + /// Creates operation which retains [length] of characters and optionally + /// applies attributes. + factory Operation.retain(int? length, [Map? attributes]) => + Operation(Operation.retainKey, length, '', attributes); + + /// Key of insert operations. + static const String insertKey = 'insert'; + + /// Key of delete operations. + static const String deleteKey = 'delete'; + + /// Key of retain operations. + static const String retainKey = 'retain'; + + /// Key of attributes collection. + static const String attributesKey = 'attributes'; + + static const List _validKeys = [insertKey, deleteKey, retainKey]; + + /// Key of this operation, can be "insert", "delete" or "retain". + final String key; + + /// Length of this operation. + final int? length; + + /// Payload of "insert" operation, for other types is set to empty string. + final Object? data; + + /// Rich-text attributes set by this operation, can be `null`. + Map? get attributes => + _attributes == null ? null : Map.from(_attributes); + final Map? _attributes; + + /// Creates new [Operation] from JSON payload. + /// + /// If `dataDecoder` parameter is not null then it is used to additionally + /// decode the operation's data object. Only applied to insert operations. + static Operation fromJson(Map data, {DataDecoder? dataDecoder}) { + dataDecoder ??= _passThroughDataDecoder; + final map = Map.from(data); + if (map.containsKey(Operation.insertKey)) { + final data = dataDecoder(map[Operation.insertKey]); + final dataLength = data is String ? data.length : 1; + return Operation( + Operation.insertKey, dataLength, data, map[Operation.attributesKey]); + } else if (map.containsKey(Operation.deleteKey)) { + final int? length = map[Operation.deleteKey]; + return Operation(Operation.deleteKey, length, '', null); + } else if (map.containsKey(Operation.retainKey)) { + final int? length = map[Operation.retainKey]; + return Operation( + Operation.retainKey, length, '', map[Operation.attributesKey]); + } + throw ArgumentError.value(data, 'Invalid data for Delta operation.'); + } + + /// Returns JSON-serializable representation of this operation. + Map toJson() { + final json = {key: value}; + if (_attributes != null) json[Operation.attributesKey] = attributes; + return json; + } + + /// Returns value of this operation. + /// + /// For insert operations this returns text, for delete and retain - length. + dynamic get value => (key == Operation.insertKey) ? data : length; + + /// Returns `true` if this is a delete operation. + bool get isDelete => key == Operation.deleteKey; + + /// Returns `true` if this is an insert operation. + bool get isInsert => key == Operation.insertKey; + + /// Returns `true` if this is a retain operation. + bool get isRetain => key == Operation.retainKey; + + /// Returns `true` if this operation has no attributes, e.g. is plain text. + bool get isPlain => _attributes == null || _attributes.isEmpty; + + /// Returns `true` if this operation sets at least one attribute. + bool get isNotPlain => !isPlain; + + /// Returns `true` is this operation is empty. + /// + /// An operation is considered empty if its [length] is equal to `0`. + bool get isEmpty => length == 0; + + /// Returns `true` is this operation is not empty. + bool get isNotEmpty => length! > 0; + + @override + bool operator ==(other) { + if (identical(this, other)) return true; + if (other is! Operation) return false; + final typedOther = other; + return key == typedOther.key && + length == typedOther.length && + _valueEquality.equals(data, typedOther.data) && + hasSameAttributes(typedOther); + } + + /// Returns `true` if this operation has attribute specified by [name]. + bool hasAttribute(String name) => + isNotPlain && _attributes!.containsKey(name); + + /// Returns `true` if [other] operation has the same attributes as this one. + bool hasSameAttributes(Operation other) { + // treat null and empty equal + if ((_attributes?.isEmpty ?? true) && + (other._attributes?.isEmpty ?? true)) { + return true; + } + return _attributeEquality.equals(_attributes, other._attributes); + } + + @override + int get hashCode { + if (_attributes != null && _attributes.isNotEmpty) { + final attrsHash = + hashObjects(_attributes.entries.map((e) => hash2(e.key, e.value))); + return hash3(key, value, attrsHash); + } + return hash2(key, value); + } + + @override + String toString() { + final attr = attributes == null ? '' : ' + $attributes'; + final text = isInsert + ? (data is String + ? (data as String).replaceAll('\n', '⏎') + : data.toString()) + : '$length'; + return '$key⟨ $text ⟩$attr'; + } +} diff --git a/dart_quill_delta/pubspec.yaml b/dart_quill_delta/pubspec.yaml new file mode 100644 index 000000000..6f853b27e --- /dev/null +++ b/dart_quill_delta/pubspec.yaml @@ -0,0 +1,20 @@ +name: dart_quill_delta +description: A port of quill-js-delta from typescript to dart +version: 0.0.1 +homepage: https://github.com/singerdmx/flutter-quill/tree/master/dart_quill_delta/ +repository: https://github.com/singerdmx/flutter-quill/tree/master/dart_quill_delta/ +issue_tracker: https://github.com/singerdmx/flutter-quill/issues/ +documentation: https://github.com/singerdmx/flutter-quill/tree/master/dart_quill_delta/ + +environment: + sdk: ^3.2.3 + +# Add regular dependencies here. +dependencies: + collection: ^1.17.0 + diff_match_patch: ^0.4.1 + quiver: ^3.2.1 + +dev_dependencies: + lints: ^3.0.0 + test: ^1.24.0 diff --git a/dart_quill_delta/test/dart_quill_delta_test.dart b/dart_quill_delta/test/dart_quill_delta_test.dart new file mode 100644 index 000000000..7de0162a4 --- /dev/null +++ b/dart_quill_delta/test/dart_quill_delta_test.dart @@ -0,0 +1,11 @@ +import 'package:test/test.dart'; + +void main() { + group('A group of tests', () { + setUp(() { + // Additional setup goes here. + }); + + test('First Test', () {}); + }); +} diff --git a/doc/todo.md b/doc/todo.md index 813a90669..21de47fe8 100644 --- a/doc/todo.md +++ b/doc/todo.md @@ -33,6 +33,11 @@ This is a todo list page that added recently and will be updated soon. - Change the color of the numbers and dots in ol/ul to match the ones in the item list - Fix the bugs of the font family and font size - Try to update Quill Html Converter + - When pasting a HTML text from cliboard by not using the context menu builder, the new logic won't work + - When selecting all text and paste HTML text, it will not replace the current text, instead it will add a text + - Add strike-through in checkbox text when the checkpoint is checked + - No more using of dynamic + - There is a bug here, the first character is not being formatted when choosing font family or font size and type in the editor ### Bugs diff --git a/example/devtools_options.yaml b/example/devtools_options.yaml new file mode 100644 index 000000000..7e7e7f67d --- /dev/null +++ b/example/devtools_options.yaml @@ -0,0 +1 @@ +extensions: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index b3d2e15c1..2fa4358ad 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -54,7 +54,7 @@ dependency_overrides: flutter_quill_test: path: ../flutter_quill_test quill_html_converter: - path: ../packages/quill_html_converter + path: ../quill_html_converter dev_dependencies: diff --git a/flutter_quill_extensions/CHANGELOG.md b/flutter_quill_extensions/CHANGELOG.md index 09d629a35..4392c9d05 100644 --- a/flutter_quill_extensions/CHANGELOG.md +++ b/flutter_quill_extensions/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to this project will be documented in this file. +## 9.0.0-dev-11 +* Test new GitHub workflows + +## 9.0.0-dev-10 +* Fix a bug of the improved pasting HTML contents contents into the editor + +## 9.0.0-dev-9 +* Improves the new logic of pasting HTML contents into the Editor +* Update `README.md` and the doc +* Dispose the `QuillToolbarSelectHeaderStyleButton` state listener in `dispose` +* Upgrade the font family button to material 3 +* Rework the font family and font size functionalities to change the font once and type all over the editor + ## 9.0.0-dev-8 * Better support for pasting HTML contents from external websites to the editor * The experimental support of converting the HTML from `quill_html_converter` is now built-in in the `flutter_quill` and removed from there (Breaking change for `quill_html_converter`) diff --git a/flutter_quill_extensions/lib/embeds/video/editor/video_embed.dart b/flutter_quill_extensions/lib/embeds/video/editor/video_embed.dart index fd1cd8fc5..292f7ada0 100644 --- a/flutter_quill_extensions/lib/embeds/video/editor/video_embed.dart +++ b/flutter_quill_extensions/lib/embeds/video/editor/video_embed.dart @@ -18,6 +18,9 @@ class QuillEditorVideoEmbedBuilder extends EmbedBuilder { @override String get key => BlockEmbed.videoType; + @override + bool get expanded => false; + @override Widget build( BuildContext context, diff --git a/flutter_quill_extensions/pubspec.yaml b/flutter_quill_extensions/pubspec.yaml index 8f3cf2068..d328ff175 100644 --- a/flutter_quill_extensions/pubspec.yaml +++ b/flutter_quill_extensions/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill_extensions description: Embed extensions for flutter_quill including image, video, formula and etc. -version: 9.0.0-dev-8 +version: 9.0.0-dev-11 homepage: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_extensions/ repository: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_extensions/ issue_tracker: https://github.com/singerdmx/flutter-quill/issues/ diff --git a/flutter_quill_test/CHANGELOG.md b/flutter_quill_test/CHANGELOG.md index 09d629a35..4392c9d05 100644 --- a/flutter_quill_test/CHANGELOG.md +++ b/flutter_quill_test/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to this project will be documented in this file. +## 9.0.0-dev-11 +* Test new GitHub workflows + +## 9.0.0-dev-10 +* Fix a bug of the improved pasting HTML contents contents into the editor + +## 9.0.0-dev-9 +* Improves the new logic of pasting HTML contents into the Editor +* Update `README.md` and the doc +* Dispose the `QuillToolbarSelectHeaderStyleButton` state listener in `dispose` +* Upgrade the font family button to material 3 +* Rework the font family and font size functionalities to change the font once and type all over the editor + ## 9.0.0-dev-8 * Better support for pasting HTML contents from external websites to the editor * The experimental support of converting the HTML from `quill_html_converter` is now built-in in the `flutter_quill` and removed from there (Breaking change for `quill_html_converter`) diff --git a/flutter_quill_test/pubspec.yaml b/flutter_quill_test/pubspec.yaml index e50efc52b..e5856b719 100644 --- a/flutter_quill_test/pubspec.yaml +++ b/flutter_quill_test/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill_test description: Test utilities for flutter_quill which includes methods to simplify interacting with the editor in test cases. -version: 9.0.0-dev-8 +version: 9.0.0-dev-11 homepage: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_test/ repository: https://github.com/singerdmx/flutter-quill/tree/master/flutter_quill_test/ issue_tracker: https://github.com/singerdmx/flutter-quill/issues/ diff --git a/lib/src/models/config/toolbar/buttons/font_family_configurations.dart b/lib/src/models/config/toolbar/buttons/font_family_configurations.dart index 5674232cf..74b9f35bd 100644 --- a/lib/src/models/config/toolbar/buttons/font_family_configurations.dart +++ b/lib/src/models/config/toolbar/buttons/font_family_configurations.dart @@ -50,17 +50,10 @@ class QuillToolbarFontFamilyButtonOptions extends QuillToolbarBaseButtonOptions< this.itemPadding, this.defaultItemColor = Colors.red, this.renderFontFamilies = true, - this.highlightElevation = 1, - this.hoverElevation = 1, - this.fillColor, this.iconSize, this.iconButtonFactor, }); - final Color? fillColor; - final double hoverElevation; - final double highlightElevation; - /// By default it will be [fontFamilyValues] from [QuillSimpleToolbarConfigurations] /// You can override this if you want final Map? rawItemsMap; @@ -83,9 +76,6 @@ class QuillToolbarFontFamilyButtonOptions extends QuillToolbarBaseButtonOptions< final double? iconButtonFactor; QuillToolbarFontFamilyButtonOptions copyWith({ - Color? fillColor, - double? hoverElevation, - double? highlightElevation, List>? items, Map? rawItemsMap, ValueChanged? onSelected, @@ -131,9 +121,6 @@ class QuillToolbarFontFamilyButtonOptions extends QuillToolbarBaseButtonOptions< defaultItemColor: defaultItemColor ?? this.defaultItemColor, iconSize: iconSize ?? this.iconSize, iconButtonFactor: iconButtonFactor ?? this.iconButtonFactor, - fillColor: fillColor ?? this.fillColor, - hoverElevation: hoverElevation ?? this.hoverElevation, - highlightElevation: highlightElevation ?? this.highlightElevation, ); } } diff --git a/lib/src/models/config/toolbar/buttons/font_size_configurations.dart b/lib/src/models/config/toolbar/buttons/font_size_configurations.dart index ec3c66825..37ba61f11 100644 --- a/lib/src/models/config/toolbar/buttons/font_size_configurations.dart +++ b/lib/src/models/config/toolbar/buttons/font_size_configurations.dart @@ -2,13 +2,18 @@ import 'dart:ui'; import 'package:flutter/foundation.dart' show immutable; import 'package:flutter/material.dart' - show Colors, PopupMenuEntry, ValueChanged; + show ButtonStyle, Colors, PopupMenuEntry, ValueChanged; import 'package:flutter/widgets.dart' - show Color, EdgeInsets, EdgeInsetsGeometry, TextOverflow, TextStyle; + show + Color, + EdgeInsets, + EdgeInsetsGeometry, + OutlinedBorder, + TextOverflow, + TextStyle; import '../../../../widgets/quill/quill_controller.dart'; import '../../../documents/attribute.dart'; -import '../../../themes/quill_icon_theme.dart'; import '../../quill_configurations.dart'; class QuillToolbarFontSizeButtonExtraOptions @@ -31,12 +36,8 @@ class QuillToolbarFontSizeButtonOptions extends QuillToolbarBaseButtonOptions< const QuillToolbarFontSizeButtonOptions({ this.iconSize, this.iconButtonFactor, - this.fillColor, - this.hoverElevation = 1, - this.highlightElevation = 1, this.rawItemsMap, this.onSelected, - super.iconTheme, this.attribute = Attribute.size, super.controller, super.afterButtonPressed, @@ -50,13 +51,13 @@ class QuillToolbarFontSizeButtonOptions extends QuillToolbarBaseButtonOptions< this.itemPadding, this.defaultItemColor = Colors.red, super.childBuilder, + this.shape, }); final double? iconSize; final double? iconButtonFactor; - final Color? fillColor; - final double hoverElevation; - final double highlightElevation; + + final ButtonStyle? shape; /// By default it will be [fontSizesValues] from [QuillSimpleToolbarConfigurations] /// You can override this if you want @@ -92,15 +93,12 @@ class QuillToolbarFontSizeButtonOptions extends QuillToolbarBaseButtonOptions< Color? defaultItemColor, VoidCallback? afterButtonPressed, String? tooltip, - QuillIconTheme? iconTheme, QuillController? controller, + OutlinedBorder? shape, }) { return QuillToolbarFontSizeButtonOptions( iconSize: iconSize ?? this.iconSize, iconButtonFactor: iconButtonFactor ?? this.iconButtonFactor, - fillColor: fillColor ?? this.fillColor, - hoverElevation: hoverElevation ?? this.hoverElevation, - highlightElevation: highlightElevation ?? this.highlightElevation, rawItemsMap: rawItemsMap ?? this.rawItemsMap, onSelected: onSelected ?? this.onSelected, attribute: attribute ?? this.attribute, @@ -113,7 +111,6 @@ class QuillToolbarFontSizeButtonOptions extends QuillToolbarBaseButtonOptions< itemPadding: itemPadding ?? this.itemPadding, defaultItemColor: defaultItemColor ?? this.defaultItemColor, tooltip: tooltip ?? super.tooltip, - iconTheme: iconTheme ?? super.iconTheme, afterButtonPressed: afterButtonPressed ?? super.afterButtonPressed, controller: controller ?? super.controller, ); diff --git a/lib/src/models/documents/document.dart b/lib/src/models/documents/document.dart index e0efbdf8e..70c48f67d 100644 --- a/lib/src/models/documents/document.dart +++ b/lib/src/models/documents/document.dart @@ -1,4 +1,4 @@ -import 'dart:async'; +import 'dart:async' show StreamController; import '../../widgets/quill/embeds.dart'; import '../quill_delta.dart'; diff --git a/lib/src/models/quill_delta.dart b/lib/src/models/quill_delta.dart index 196651e94..8b31045e5 100644 --- a/lib/src/models/quill_delta.dart +++ b/lib/src/models/quill_delta.dart @@ -1,827 +1,829 @@ -/// Implementation of Quill Delta format in Dart. -library; - -import 'dart:math' as math; - -import 'package:collection/collection.dart'; -import 'package:diff_match_patch/diff_match_patch.dart' as dmp; -import 'package:quiver/core.dart'; - -const _attributeEquality = DeepCollectionEquality(); -const _valueEquality = DeepCollectionEquality(); - -/// Decoder function to convert raw `data` object into a user-defined data type. -/// -/// Useful with embedded content. -typedef DataDecoder = Object? Function(Object data); - -/// Default data decoder which simply passes through the original value. -Object? _passThroughDataDecoder(Object? data) => data; - -/// Operation performed on a rich-text document. -class Operation { - Operation._(this.key, this.length, this.data, Map? attributes) - : assert(_validKeys.contains(key), 'Invalid operation key "$key".'), - assert(() { - if (key != Operation.insertKey) return true; - return data is String ? data.length == length : length == 1; - }(), 'Length of insert operation must be equal to the data length.'), - _attributes = - attributes != null ? Map.from(attributes) : null; - - /// Creates operation which deletes [length] of characters. - factory Operation.delete(int length) => - Operation._(Operation.deleteKey, length, '', null); - - /// Creates operation which inserts [text] with optional [attributes]. - factory Operation.insert(dynamic data, [Map? attributes]) => - Operation._(Operation.insertKey, data is String ? data.length : 1, data, - attributes); - - /// Creates operation which retains [length] of characters and optionally - /// applies attributes. - factory Operation.retain(int? length, [Map? attributes]) => - Operation._(Operation.retainKey, length, '', attributes); - - /// Key of insert operations. - static const String insertKey = 'insert'; - - /// Key of delete operations. - static const String deleteKey = 'delete'; - - /// Key of retain operations. - static const String retainKey = 'retain'; - - /// Key of attributes collection. - static const String attributesKey = 'attributes'; - - static const List _validKeys = [insertKey, deleteKey, retainKey]; - - /// Key of this operation, can be "insert", "delete" or "retain". - final String key; - - /// Length of this operation. - final int? length; - - /// Payload of "insert" operation, for other types is set to empty string. - final Object? data; - - /// Rich-text attributes set by this operation, can be `null`. - Map? get attributes => - _attributes == null ? null : Map.from(_attributes!); - final Map? _attributes; - - /// Creates new [Operation] from JSON payload. - /// - /// If `dataDecoder` parameter is not null then it is used to additionally - /// decode the operation's data object. Only applied to insert operations. - static Operation fromJson(Map data, {DataDecoder? dataDecoder}) { - dataDecoder ??= _passThroughDataDecoder; - final map = Map.from(data); - if (map.containsKey(Operation.insertKey)) { - final data = dataDecoder(map[Operation.insertKey]); - final dataLength = data is String ? data.length : 1; - return Operation._( - Operation.insertKey, dataLength, data, map[Operation.attributesKey]); - } else if (map.containsKey(Operation.deleteKey)) { - final int? length = map[Operation.deleteKey]; - return Operation._(Operation.deleteKey, length, '', null); - } else if (map.containsKey(Operation.retainKey)) { - final int? length = map[Operation.retainKey]; - return Operation._( - Operation.retainKey, length, '', map[Operation.attributesKey]); - } - throw ArgumentError.value(data, 'Invalid data for Delta operation.'); - } - - /// Returns JSON-serializable representation of this operation. - Map toJson() { - final json = {key: value}; - if (_attributes != null) json[Operation.attributesKey] = attributes; - return json; - } - - /// Returns value of this operation. - /// - /// For insert operations this returns text, for delete and retain - length. - dynamic get value => (key == Operation.insertKey) ? data : length; - - /// Returns `true` if this is a delete operation. - bool get isDelete => key == Operation.deleteKey; - - /// Returns `true` if this is an insert operation. - bool get isInsert => key == Operation.insertKey; - - /// Returns `true` if this is a retain operation. - bool get isRetain => key == Operation.retainKey; - - /// Returns `true` if this operation has no attributes, e.g. is plain text. - bool get isPlain => _attributes == null || _attributes!.isEmpty; - - /// Returns `true` if this operation sets at least one attribute. - bool get isNotPlain => !isPlain; - - /// Returns `true` is this operation is empty. - /// - /// An operation is considered empty if its [length] is equal to `0`. - bool get isEmpty => length == 0; - - /// Returns `true` is this operation is not empty. - bool get isNotEmpty => length! > 0; - - @override - bool operator ==(other) { - if (identical(this, other)) return true; - if (other is! Operation) return false; - final typedOther = other; - return key == typedOther.key && - length == typedOther.length && - _valueEquality.equals(data, typedOther.data) && - hasSameAttributes(typedOther); - } - - /// Returns `true` if this operation has attribute specified by [name]. - bool hasAttribute(String name) => - isNotPlain && _attributes!.containsKey(name); - - /// Returns `true` if [other] operation has the same attributes as this one. - bool hasSameAttributes(Operation other) { - // treat null and empty equal - if ((_attributes?.isEmpty ?? true) && - (other._attributes?.isEmpty ?? true)) { - return true; - } - return _attributeEquality.equals(_attributes, other._attributes); - } - - @override - int get hashCode { - if (_attributes != null && _attributes!.isNotEmpty) { - final attrsHash = - hashObjects(_attributes!.entries.map((e) => hash2(e.key, e.value))); - return hash3(key, value, attrsHash); - } - return hash2(key, value); - } - - @override - String toString() { - final attr = attributes == null ? '' : ' + $attributes'; - final text = isInsert - ? (data is String - ? (data as String).replaceAll('\n', '⏎') - : data.toString()) - : '$length'; - return '$key⟨ $text ⟩$attr'; - } -} - -/// Delta represents a document or a modification of a document as a sequence of -/// insert, delete and retain operations. -/// -/// Delta consisting of only "insert" operations is usually referred to as -/// "document delta". When delta includes also "retain" or "delete" operations -/// it is a "change delta". -class Delta { - /// Creates new empty [Delta]. - factory Delta() => Delta._([]); - - Delta._(List operations) : _operations = operations; - - /// Creates new [Delta] from [other]. - factory Delta.from(Delta other) => - Delta._(List.from(other._operations)); - - /// Creates new [Delta] from a List of Operation - factory Delta.fromOperations(List operations) => - Delta._(operations.toList()); - - // Placeholder char for embed in diff() - static final String _kNullCharacter = String.fromCharCode(0); - - /// Transforms two attribute sets. - static Map? transformAttributes( - Map? a, Map? b, bool priority) { - if (a == null) return b; - if (b == null) return null; - - if (!priority) return b; - - final result = b.keys.fold>({}, (attributes, key) { - if (!a.containsKey(key)) attributes[key] = b[key]; - return attributes; - }); - - return result.isEmpty ? null : result; - } - - /// Composes two attribute sets. - static Map? composeAttributes( - Map? a, Map? b, - {bool keepNull = false}) { - a ??= const {}; - b ??= const {}; - - final result = Map.from(a)..addAll(b); - final keys = result.keys.toList(growable: false); - - if (!keepNull) { - for (final key in keys) { - if (result[key] == null) result.remove(key); - } - } - - return result.isEmpty ? null : result; - } - - ///get anti-attr result base on base - static Map invertAttributes( - Map? attr, Map? base) { - attr ??= const {}; - base ??= const {}; - - final baseInverted = base.keys.fold({}, (dynamic memo, key) { - if (base![key] != attr![key] && attr.containsKey(key)) { - memo[key] = base[key]; - } - return memo; - }); - - final inverted = - Map.from(attr.keys.fold(baseInverted, (memo, key) { - if (base![key] != attr![key] && !base.containsKey(key)) { - memo[key] = null; - } - return memo; - })); - return inverted; - } - - /// Returns diff between two attribute sets - static Map? diffAttributes( - Map? a, Map? b) { - a ??= const {}; - b ??= const {}; - - final attributes = {}; - for (final key in (a.keys.toList()..addAll(b.keys))) { - if (a[key] != b[key]) { - attributes[key] = b.containsKey(key) ? b[key] : null; - } - } - - return attributes.keys.isNotEmpty ? attributes : null; - } - - final List _operations; - - int _modificationCount = 0; - - /// Creates [Delta] from de-serialized JSON representation. - /// - /// If `dataDecoder` parameter is not null then it is used to additionally - /// decode the operation's data object. Only applied to insert operations. - static Delta fromJson(List data, {DataDecoder? dataDecoder}) { - return Delta._(data - .map((op) => Operation.fromJson(op, dataDecoder: dataDecoder)) - .toList()); - } - - /// Returns list of operations in this delta. - List toList() => List.from(_operations); - - /// Returns JSON-serializable version of this delta. - List toJson() => toList().map((operation) => operation.toJson()).toList(); - - /// Returns `true` if this delta is empty. - bool get isEmpty => _operations.isEmpty; - - /// Returns `true` if this delta is not empty. - bool get isNotEmpty => _operations.isNotEmpty; - - /// Returns number of operations in this delta. - int get length => _operations.length; - - /// Returns [Operation] at specified [index] in this delta. - Operation operator [](int index) => _operations[index]; - - /// Returns [Operation] at specified [index] in this delta. - Operation elementAt(int index) => _operations.elementAt(index); - - /// Returns the first [Operation] in this delta. - Operation get first => _operations.first; - - /// Returns the last [Operation] in this delta. - Operation get last => _operations.last; - - @override - bool operator ==(dynamic other) { - if (identical(this, other)) return true; - if (other is! Delta) return false; - final typedOther = other; - const comparator = ListEquality(DefaultEquality()); - return comparator.equals(_operations, typedOther._operations); - } - - @override - int get hashCode => hashObjects(_operations); - - /// Retain [count] of characters from current position. - void retain(int count, [Map? attributes]) { - assert(count >= 0); - if (count == 0) return; // no-op - push(Operation.retain(count, attributes)); - } - - /// Insert [data] at current position. - void insert(dynamic data, [Map? attributes]) { - if (data is String && data.isEmpty) return; // no-op - push(Operation.insert(data, attributes)); - } - - /// Delete [count] characters from current position. - void delete(int count) { - assert(count >= 0); - if (count == 0) return; - push(Operation.delete(count)); - } - - void _mergeWithTail(Operation operation) { - assert(isNotEmpty); - assert(last.key == operation.key); - assert(operation.data is String && last.data is String); - - final length = operation.length! + last.length!; - final lastText = last.data as String; - final opText = operation.data as String; - final resultText = lastText + opText; - final index = _operations.length; - _operations.replaceRange(index - 1, index, [ - Operation._(operation.key, length, resultText, operation.attributes), - ]); - } - - /// Pushes new operation into this delta. - /// - /// Performs compaction by composing [operation] with current tail operation - /// of this delta, when possible. For instance, if current tail is - /// `insert('abc')` and pushed operation is `insert('123')` then existing - /// tail is replaced with `insert('abc123')` - a compound result of the two - /// operations. - void push(Operation operation) { - if (operation.isEmpty) return; - - var index = _operations.length; - final lastOp = _operations.isNotEmpty ? _operations.last : null; - if (lastOp != null) { - if (lastOp.isDelete && operation.isDelete) { - _mergeWithTail(operation); - return; - } - - if (lastOp.isDelete && operation.isInsert) { - index -= 1; // Always insert before deleting - final nLastOp = (index > 0) ? _operations.elementAt(index - 1) : null; - if (nLastOp == null) { - _operations.insert(0, operation); - return; - } - } - - if (lastOp.isInsert && operation.isInsert) { - if (lastOp.hasSameAttributes(operation) && - operation.data is String && - lastOp.data is String) { - _mergeWithTail(operation); - return; - } - } - - if (lastOp.isRetain && operation.isRetain) { - if (lastOp.hasSameAttributes(operation)) { - _mergeWithTail(operation); - return; - } - } - } - if (index == _operations.length) { - _operations.add(operation); - } else { - final opAtIndex = _operations.elementAt(index); - _operations.replaceRange(index, index + 1, [operation, opAtIndex]); - } - _modificationCount++; - } - - /// Composes next operation from [thisIter] and [otherIter]. - /// - /// Returns new operation or `null` if operations from [thisIter] and - /// [otherIter] nullify each other. For instance, for the pair `insert('abc')` - /// and `delete(3)` composition result would be empty string. - Operation? _composeOperation( - DeltaIterator thisIter, DeltaIterator otherIter) { - if (otherIter.isNextInsert) return otherIter.next(); - if (thisIter.isNextDelete) return thisIter.next(); - - final length = math.min(thisIter.peekLength(), otherIter.peekLength()); - final thisOp = thisIter.next(length); - final otherOp = otherIter.next(length); - assert(thisOp.length == otherOp.length); - - if (otherOp.isRetain) { - final attributes = composeAttributes( - thisOp.attributes, - otherOp.attributes, - keepNull: thisOp.isRetain, - ); - if (thisOp.isRetain) { - return Operation.retain(thisOp.length, attributes); - } else if (thisOp.isInsert) { - return Operation.insert(thisOp.data, attributes); - } else { - throw StateError('Unreachable'); - } - } else { - // otherOp == delete && thisOp in [retain, insert] - assert(otherOp.isDelete); - if (thisOp.isRetain) return otherOp; - assert(thisOp.isInsert); - // otherOp(delete) + thisOp(insert) => null - } - return null; - } - - /// Composes this delta with [other] and returns new [Delta]. - /// - /// It is not required for this and [other] delta to represent a document - /// delta (consisting only of insert operations). - Delta compose(Delta other) { - final result = Delta(); - final thisIter = DeltaIterator(this); - final otherIter = DeltaIterator(other); - - while (thisIter.hasNext || otherIter.hasNext) { - final newOp = _composeOperation(thisIter, otherIter); - if (newOp != null) result.push(newOp); - } - return result..trim(); - } - - /// Returns a new lazy Iterable with elements that are created by calling - /// f on each element of this Iterable in iteration order. - /// - /// Convenience method - Iterable map(T Function(Operation) f) { - return _operations.map(f); - } - - /// Returns a [Delta] containing differences between 2 [Delta]s. - /// If [cleanupSemantic] is `true` (default), applies the following: - /// - /// The diff of "mouse" and "sofas" is - /// [delete(1), insert("s"), retain(1), - /// delete("u"), insert("fa"), retain(1), delete(1)]. - /// While this is the optimum diff, it is difficult for humans to understand. - /// Semantic cleanup rewrites the diff, - /// expanding it into a more intelligible format. - /// The above example would become: [(-1, "mouse"), (1, "sofas")]. - /// (source: https://github.com/google/diff-match-patch/wiki/API) - /// - /// Useful when one wishes to display difference between 2 documents - Delta diff(Delta other, {bool cleanupSemantic = true}) { - if (_operations.equals(other._operations)) { - return Delta(); - } - final stringThis = map((op) { - if (op.isInsert) { - return op.data is String ? op.data : _kNullCharacter; - } - final prep = this == other ? 'on' : 'with'; - throw ArgumentError('diff() call $prep non-document'); - }).join(); - final stringOther = other.map((op) { - if (op.isInsert) { - return op.data is String ? op.data : _kNullCharacter; - } - final prep = this == other ? 'on' : 'with'; - throw ArgumentError('diff() call $prep non-document'); - }).join(); - - final retDelta = Delta(); - final diffResult = dmp.diff(stringThis, stringOther); - if (cleanupSemantic) { - dmp.DiffMatchPatch().diffCleanupSemantic(diffResult); - } - - final thisIter = DeltaIterator(this); - final otherIter = DeltaIterator(other); - - for (final component in diffResult) { - var length = component.text.length; - while (length > 0) { - var opLength = 0; - switch (component.operation) { - case dmp.DIFF_INSERT: - opLength = math.min(otherIter.peekLength(), length); - retDelta.push(otherIter.next(opLength)); - break; - case dmp.DIFF_DELETE: - opLength = math.min(length, thisIter.peekLength()); - thisIter.next(opLength); - retDelta.delete(opLength); - break; - case dmp.DIFF_EQUAL: - opLength = math.min( - math.min(thisIter.peekLength(), otherIter.peekLength()), - length, - ); - final thisOp = thisIter.next(opLength); - final otherOp = otherIter.next(opLength); - if (thisOp.data == otherOp.data) { - retDelta.retain( - opLength, - diffAttributes(thisOp.attributes, otherOp.attributes), - ); - } else { - retDelta - ..push(otherOp) - ..delete(opLength); - } - break; - } - length -= opLength; - } - } - return retDelta..trim(); - } - - /// Transforms next operation from [otherIter] against next operation in - /// [thisIter]. - /// - /// Returns `null` if both operations nullify each other. - Operation? _transformOperation( - DeltaIterator thisIter, DeltaIterator otherIter, bool priority) { - if (thisIter.isNextInsert && (priority || !otherIter.isNextInsert)) { - return Operation.retain(thisIter.next().length); - } else if (otherIter.isNextInsert) { - return otherIter.next(); - } - - final length = math.min(thisIter.peekLength(), otherIter.peekLength()); - final thisOp = thisIter.next(length); - final otherOp = otherIter.next(length); - assert(thisOp.length == otherOp.length); - - // At this point only delete and retain operations are possible. - if (thisOp.isDelete) { - // otherOp is either delete or retain, so they nullify each other. - return null; - } else if (otherOp.isDelete) { - return otherOp; - } else { - // Retain otherOp which is either retain or insert. - return Operation.retain( - length, - transformAttributes(thisOp.attributes, otherOp.attributes, priority), - ); - } - } - - /// Transforms [other] delta against operations in this delta. - Delta transform(Delta other, bool priority) { - final result = Delta(); - final thisIter = DeltaIterator(this); - final otherIter = DeltaIterator(other); - - while (thisIter.hasNext || otherIter.hasNext) { - final newOp = _transformOperation(thisIter, otherIter, priority); - if (newOp != null) result.push(newOp); - } - return result..trim(); - } - - /// Removes trailing retain operation with empty attributes, if present. - void trim() { - if (isNotEmpty) { - final last = _operations.last; - if (last.isRetain && last.isPlain) _operations.removeLast(); - } - } - - /// Removes trailing '\n' - void _trimNewLine() { - if (isNotEmpty) { - final lastOp = _operations.last; - final lastOpData = lastOp.data; - - if (lastOpData is String && lastOpData.endsWith('\n')) { - _operations.removeLast(); - if (lastOpData.length > 1) { - insert(lastOpData.substring(0, lastOpData.length - 1), - lastOp.attributes); - } - } - } - } - - /// Concatenates [other] with this delta and returns the result. - Delta concat(Delta other, {bool trimNewLine = false}) { - final result = Delta.from(this); - if (trimNewLine) { - result._trimNewLine(); - } - if (other.isNotEmpty) { - // In case first operation of other can be merged with last operation in - // our list. - result.push(other._operations.first); - result._operations.addAll(other._operations.sublist(1)); - } - return result; - } - - /// Inverts this delta against [base]. - /// - /// Returns new delta which negates effect of this delta when applied to - /// [base]. This is an equivalent of "undo" operation on deltas. - Delta invert(Delta base) { - final inverted = Delta(); - if (base.isEmpty) return inverted; - - var baseIndex = 0; - for (final op in _operations) { - if (op.isInsert) { - inverted.delete(op.length!); - } else if (op.isRetain && op.isPlain) { - inverted.retain(op.length!); - baseIndex += op.length!; - } else if (op.isDelete || (op.isRetain && op.isNotPlain)) { - final length = op.length!; - final sliceDelta = base.slice(baseIndex, baseIndex + length); - sliceDelta.toList().forEach((baseOp) { - if (op.isDelete) { - inverted.push(baseOp); - } else if (op.isRetain && op.isNotPlain) { - final invertAttr = - invertAttributes(op.attributes, baseOp.attributes); - inverted.retain( - baseOp.length!, invertAttr.isEmpty ? null : invertAttr); - } - }); - baseIndex += length; - } else { - throw StateError('Unreachable'); - } - } - inverted.trim(); - return inverted; - } - - /// Returns slice of this delta from [start] index (inclusive) to [end] - /// (exclusive). - Delta slice(int start, [int? end]) { - final delta = Delta(); - var index = 0; - final opIterator = DeltaIterator(this); - - final actualEnd = end ?? DeltaIterator.maxLength; - - while (index < actualEnd && opIterator.hasNext) { - Operation op; - if (index < start) { - op = opIterator.next(start - index); - } else { - op = opIterator.next(actualEnd - index); - delta.push(op); - } - index += op.length!; - } - return delta; - } - - /// Transforms [index] against this delta. - /// - /// Any "delete" operation before specified [index] shifts it backward, as - /// well as any "insert" operation shifts it forward. - /// - /// The [force] argument is used to resolve scenarios when there is an - /// insert operation at the same position as [index]. If [force] is set to - /// `true` (default) then position is forced to shift forward, otherwise - /// position stays at the same index. In other words setting [force] to - /// `false` gives higher priority to the transformed position. - /// - /// Useful to adjust caret or selection positions. - int transformPosition(int index, {bool force = true}) { - final iter = DeltaIterator(this); - var offset = 0; - while (iter.hasNext && offset <= index) { - final op = iter.next(); - if (op.isDelete) { - index -= math.min(op.length!, index - offset); - continue; - } else if (op.isInsert && (offset < index || force)) { - index += op.length!; - } - offset += op.length!; - } - return index; - } - - @override - String toString() => _operations.join('\n'); -} - -/// Specialized iterator for [Delta]s. -class DeltaIterator { - DeltaIterator(this.delta) : _modificationCount = delta._modificationCount; - - static const int maxLength = 1073741824; - - final Delta delta; - final int _modificationCount; - int _index = 0; - int _offset = 0; - - bool get isNextInsert => nextOperationKey == Operation.insertKey; - - bool get isNextDelete => nextOperationKey == Operation.deleteKey; - - bool get isNextRetain => nextOperationKey == Operation.retainKey; - - String? get nextOperationKey { - if (_index < delta.length) { - return delta.elementAt(_index).key; - } else { - return null; - } - } - - bool get hasNext => peekLength() < maxLength; - - /// Returns length of next operation without consuming it. - /// - /// Returns [maxLength] if there is no more operations left to iterate. - int peekLength() { - if (_index < delta.length) { - final operation = delta._operations[_index]; - return operation.length! - _offset; - } - return maxLength; - } - - /// Consumes and returns next operation. - /// - /// Optional [length] specifies maximum length of operation to return. Note - /// that actual length of returned operation may be less than specified value. - /// - /// If this iterator reached the end of the Delta then returns a retain - /// operation with its length set to [maxLength]. - // TODO: Note that we used double.infinity as the default value - // for length here - // but this can now cause a type error since operation length is - // expected to be an int. Changing default length to [maxLength] is - // a workaround to avoid breaking changes. - Operation next([int length = maxLength]) { - if (_modificationCount != delta._modificationCount) { - throw ConcurrentModificationError(delta); - } - - if (_index < delta.length) { - final op = delta.elementAt(_index); - final opKey = op.key; - final opAttributes = op.attributes; - final currentOffset = _offset; - final actualLength = math.min(op.length! - currentOffset, length); - if (actualLength == op.length! - currentOffset) { - _index++; - _offset = 0; - } else { - _offset += actualLength; - } - final opData = op.isInsert && op.data is String - ? (op.data as String) - .substring(currentOffset, currentOffset + actualLength) - : op.data; - final opIsNotEmpty = - opData is String ? opData.isNotEmpty : true; // embeds are never empty - final opLength = opData is String ? opData.length : 1; - final opActualLength = opIsNotEmpty ? opLength : actualLength; - return Operation._(opKey, opActualLength, opData, opAttributes); - } - return Operation.retain(length); - } - - /// Skips [length] characters in source delta. - /// - /// Returns last skipped operation, or `null` if there was nothing to skip. - Operation? skip(int length) { - var skipped = 0; - Operation? op; - while (skipped < length && hasNext) { - final opLength = peekLength(); - final skip = math.min(length - skipped, opLength); - op = next(skip); - skipped += op.length!; - } - return op; - } -} +export 'package:dart_quill_delta/dart_quill_delta.dart'; + +// /// Implementation of Quill Delta format in Dart. +// library; + +// import 'dart:math' as math; + +// import 'package:collection/collection.dart'; +// import 'package:diff_match_patch/diff_match_patch.dart' as dmp; +// import 'package:quiver/core.dart'; + +// const _attributeEquality = DeepCollectionEquality(); +// const _valueEquality = DeepCollectionEquality(); + +// /// Decoder function to convert raw `data` object into a user-defined data type. +// /// +// /// Useful with embedded content. +// typedef DataDecoder = Object? Function(Object data); + +// /// Default data decoder which simply passes through the original value. +// Object? _passThroughDataDecoder(Object? data) => data; + +// /// Operation performed on a rich-text document. +// class Operation { +// Operation._(this.key, this.length, this.data, Map? attributes) +// : assert(_validKeys.contains(key), 'Invalid operation key "$key".'), +// assert(() { +// if (key != Operation.insertKey) return true; +// return data is String ? data.length == length : length == 1; +// }(), 'Length of insert operation must be equal to the data length.'), +// _attributes = +// attributes != null ? Map.from(attributes) : null; + +// /// Creates operation which deletes [length] of characters. +// factory Operation.delete(int length) => +// Operation._(Operation.deleteKey, length, '', null); + +// /// Creates operation which inserts [text] with optional [attributes]. +// factory Operation.insert(dynamic data, [Map? attributes]) => +// Operation._(Operation.insertKey, data is String ? data.length : 1, data, +// attributes); + +// /// Creates operation which retains [length] of characters and optionally +// /// applies attributes. +// factory Operation.retain(int? length, [Map? attributes]) => +// Operation._(Operation.retainKey, length, '', attributes); + +// /// Key of insert operations. +// static const String insertKey = 'insert'; + +// /// Key of delete operations. +// static const String deleteKey = 'delete'; + +// /// Key of retain operations. +// static const String retainKey = 'retain'; + +// /// Key of attributes collection. +// static const String attributesKey = 'attributes'; + +// static const List _validKeys = [insertKey, deleteKey, retainKey]; + +// /// Key of this operation, can be "insert", "delete" or "retain". +// final String key; + +// /// Length of this operation. +// final int? length; + +// /// Payload of "insert" operation, for other types is set to empty string. +// final Object? data; + +// /// Rich-text attributes set by this operation, can be `null`. +// Map? get attributes => +// _attributes == null ? null : Map.from(_attributes!); +// final Map? _attributes; + +// /// Creates new [Operation] from JSON payload. +// /// +// /// If `dataDecoder` parameter is not null then it is used to additionally +// /// decode the operation's data object. Only applied to insert operations. +// static Operation fromJson(Map data, {DataDecoder? dataDecoder}) { +// dataDecoder ??= _passThroughDataDecoder; +// final map = Map.from(data); +// if (map.containsKey(Operation.insertKey)) { +// final data = dataDecoder(map[Operation.insertKey]); +// final dataLength = data is String ? data.length : 1; +// return Operation._( +// Operation.insertKey, dataLength, data, map[Operation.attributesKey]); +// } else if (map.containsKey(Operation.deleteKey)) { +// final int? length = map[Operation.deleteKey]; +// return Operation._(Operation.deleteKey, length, '', null); +// } else if (map.containsKey(Operation.retainKey)) { +// final int? length = map[Operation.retainKey]; +// return Operation._( +// Operation.retainKey, length, '', map[Operation.attributesKey]); +// } +// throw ArgumentError.value(data, 'Invalid data for Delta operation.'); +// } + +// /// Returns JSON-serializable representation of this operation. +// Map toJson() { +// final json = {key: value}; +// if (_attributes != null) json[Operation.attributesKey] = attributes; +// return json; +// } + +// /// Returns value of this operation. +// /// +// /// For insert operations this returns text, for delete and retain - length. +// dynamic get value => (key == Operation.insertKey) ? data : length; + +// /// Returns `true` if this is a delete operation. +// bool get isDelete => key == Operation.deleteKey; + +// /// Returns `true` if this is an insert operation. +// bool get isInsert => key == Operation.insertKey; + +// /// Returns `true` if this is a retain operation. +// bool get isRetain => key == Operation.retainKey; + +// /// Returns `true` if this operation has no attributes, e.g. is plain text. +// bool get isPlain => _attributes == null || _attributes!.isEmpty; + +// /// Returns `true` if this operation sets at least one attribute. +// bool get isNotPlain => !isPlain; + +// /// Returns `true` is this operation is empty. +// /// +// /// An operation is considered empty if its [length] is equal to `0`. +// bool get isEmpty => length == 0; + +// /// Returns `true` is this operation is not empty. +// bool get isNotEmpty => length! > 0; + +// @override +// bool operator ==(other) { +// if (identical(this, other)) return true; +// if (other is! Operation) return false; +// final typedOther = other; +// return key == typedOther.key && +// length == typedOther.length && +// _valueEquality.equals(data, typedOther.data) && +// hasSameAttributes(typedOther); +// } + +// /// Returns `true` if this operation has attribute specified by [name]. +// bool hasAttribute(String name) => +// isNotPlain && _attributes!.containsKey(name); + +// /// Returns `true` if [other] operation has the same attributes as this one. +// bool hasSameAttributes(Operation other) { +// // treat null and empty equal +// if ((_attributes?.isEmpty ?? true) && +// (other._attributes?.isEmpty ?? true)) { +// return true; +// } +// return _attributeEquality.equals(_attributes, other._attributes); +// } + +// @override +// int get hashCode { +// if (_attributes != null && _attributes!.isNotEmpty) { +// final attrsHash = +// hashObjects(_attributes!.entries.map((e) => hash2(e.key, e.value))); +// return hash3(key, value, attrsHash); +// } +// return hash2(key, value); +// } + +// @override +// String toString() { +// final attr = attributes == null ? '' : ' + $attributes'; +// final text = isInsert +// ? (data is String +// ? (data as String).replaceAll('\n', '⏎') +// : data.toString()) +// : '$length'; +// return '$key⟨ $text ⟩$attr'; +// } +// } + +// /// Delta represents a document or a modification of a document as a sequence of +// /// insert, delete and retain operations. +// /// +// /// Delta consisting of only "insert" operations is usually referred to as +// /// "document delta". When delta includes also "retain" or "delete" operations +// /// it is a "change delta". +// class Delta { +// /// Creates new empty [Delta]. +// factory Delta() => Delta._([]); + +// Delta._(List operations) : _operations = operations; + +// /// Creates new [Delta] from [other]. +// factory Delta.from(Delta other) => +// Delta._(List.from(other._operations)); + +// /// Creates new [Delta] from a List of Operation +// factory Delta.fromOperations(List operations) => +// Delta._(operations.toList()); + +// // Placeholder char for embed in diff() +// static final String _kNullCharacter = String.fromCharCode(0); + +// /// Transforms two attribute sets. +// static Map? transformAttributes( +// Map? a, Map? b, bool priority) { +// if (a == null) return b; +// if (b == null) return null; + +// if (!priority) return b; + +// final result = b.keys.fold>({}, (attributes, key) { +// if (!a.containsKey(key)) attributes[key] = b[key]; +// return attributes; +// }); + +// return result.isEmpty ? null : result; +// } + +// /// Composes two attribute sets. +// static Map? composeAttributes( +// Map? a, Map? b, +// {bool keepNull = false}) { +// a ??= const {}; +// b ??= const {}; + +// final result = Map.from(a)..addAll(b); +// final keys = result.keys.toList(growable: false); + +// if (!keepNull) { +// for (final key in keys) { +// if (result[key] == null) result.remove(key); +// } +// } + +// return result.isEmpty ? null : result; +// } + +// ///get anti-attr result base on base +// static Map invertAttributes( +// Map? attr, Map? base) { +// attr ??= const {}; +// base ??= const {}; + +// final baseInverted = base.keys.fold({}, (dynamic memo, key) { +// if (base![key] != attr![key] && attr.containsKey(key)) { +// memo[key] = base[key]; +// } +// return memo; +// }); + +// final inverted = +// Map.from(attr.keys.fold(baseInverted, (memo, key) { +// if (base![key] != attr![key] && !base.containsKey(key)) { +// memo[key] = null; +// } +// return memo; +// })); +// return inverted; +// } + +// /// Returns diff between two attribute sets +// static Map? diffAttributes( +// Map? a, Map? b) { +// a ??= const {}; +// b ??= const {}; + +// final attributes = {}; +// for (final key in (a.keys.toList()..addAll(b.keys))) { +// if (a[key] != b[key]) { +// attributes[key] = b.containsKey(key) ? b[key] : null; +// } +// } + +// return attributes.keys.isNotEmpty ? attributes : null; +// } + +// final List _operations; + +// int _modificationCount = 0; + +// /// Creates [Delta] from de-serialized JSON representation. +// /// +// /// If `dataDecoder` parameter is not null then it is used to additionally +// /// decode the operation's data object. Only applied to insert operations. +// static Delta fromJson(List data, {DataDecoder? dataDecoder}) { +// return Delta._(data +// .map((op) => Operation.fromJson(op, dataDecoder: dataDecoder)) +// .toList()); +// } + +// /// Returns list of operations in this delta. +// List toList() => List.from(_operations); + +// /// Returns JSON-serializable version of this delta. +// List toJson() => toList().map((operation) => operation.toJson()).toList(); + +// /// Returns `true` if this delta is empty. +// bool get isEmpty => _operations.isEmpty; + +// /// Returns `true` if this delta is not empty. +// bool get isNotEmpty => _operations.isNotEmpty; + +// /// Returns number of operations in this delta. +// int get length => _operations.length; + +// /// Returns [Operation] at specified [index] in this delta. +// Operation operator [](int index) => _operations[index]; + +// /// Returns [Operation] at specified [index] in this delta. +// Operation elementAt(int index) => _operations.elementAt(index); + +// /// Returns the first [Operation] in this delta. +// Operation get first => _operations.first; + +// /// Returns the last [Operation] in this delta. +// Operation get last => _operations.last; + +// @override +// bool operator ==(dynamic other) { +// if (identical(this, other)) return true; +// if (other is! Delta) return false; +// final typedOther = other; +// const comparator = ListEquality(DefaultEquality()); +// return comparator.equals(_operations, typedOther._operations); +// } + +// @override +// int get hashCode => hashObjects(_operations); + +// /// Retain [count] of characters from current position. +// void retain(int count, [Map? attributes]) { +// assert(count >= 0); +// if (count == 0) return; // no-op +// push(Operation.retain(count, attributes)); +// } + +// /// Insert [data] at current position. +// void insert(dynamic data, [Map? attributes]) { +// if (data is String && data.isEmpty) return; // no-op +// push(Operation.insert(data, attributes)); +// } + +// /// Delete [count] characters from current position. +// void delete(int count) { +// assert(count >= 0); +// if (count == 0) return; +// push(Operation.delete(count)); +// } + +// void _mergeWithTail(Operation operation) { +// assert(isNotEmpty); +// assert(last.key == operation.key); +// assert(operation.data is String && last.data is String); + +// final length = operation.length! + last.length!; +// final lastText = last.data as String; +// final opText = operation.data as String; +// final resultText = lastText + opText; +// final index = _operations.length; +// _operations.replaceRange(index - 1, index, [ +// Operation._(operation.key, length, resultText, operation.attributes), +// ]); +// } + +// /// Pushes new operation into this delta. +// /// +// /// Performs compaction by composing [operation] with current tail operation +// /// of this delta, when possible. For instance, if current tail is +// /// `insert('abc')` and pushed operation is `insert('123')` then existing +// /// tail is replaced with `insert('abc123')` - a compound result of the two +// /// operations. +// void push(Operation operation) { +// if (operation.isEmpty) return; + +// var index = _operations.length; +// final lastOp = _operations.isNotEmpty ? _operations.last : null; +// if (lastOp != null) { +// if (lastOp.isDelete && operation.isDelete) { +// _mergeWithTail(operation); +// return; +// } + +// if (lastOp.isDelete && operation.isInsert) { +// index -= 1; // Always insert before deleting +// final nLastOp = (index > 0) ? _operations.elementAt(index - 1) : null; +// if (nLastOp == null) { +// _operations.insert(0, operation); +// return; +// } +// } + +// if (lastOp.isInsert && operation.isInsert) { +// if (lastOp.hasSameAttributes(operation) && +// operation.data is String && +// lastOp.data is String) { +// _mergeWithTail(operation); +// return; +// } +// } + +// if (lastOp.isRetain && operation.isRetain) { +// if (lastOp.hasSameAttributes(operation)) { +// _mergeWithTail(operation); +// return; +// } +// } +// } +// if (index == _operations.length) { +// _operations.add(operation); +// } else { +// final opAtIndex = _operations.elementAt(index); +// _operations.replaceRange(index, index + 1, [operation, opAtIndex]); +// } +// _modificationCount++; +// } + +// /// Composes next operation from [thisIter] and [otherIter]. +// /// +// /// Returns new operation or `null` if operations from [thisIter] and +// /// [otherIter] nullify each other. For instance, for the pair `insert('abc')` +// /// and `delete(3)` composition result would be empty string. +// Operation? _composeOperation( +// DeltaIterator thisIter, DeltaIterator otherIter) { +// if (otherIter.isNextInsert) return otherIter.next(); +// if (thisIter.isNextDelete) return thisIter.next(); + +// final length = math.min(thisIter.peekLength(), otherIter.peekLength()); +// final thisOp = thisIter.next(length); +// final otherOp = otherIter.next(length); +// assert(thisOp.length == otherOp.length); + +// if (otherOp.isRetain) { +// final attributes = composeAttributes( +// thisOp.attributes, +// otherOp.attributes, +// keepNull: thisOp.isRetain, +// ); +// if (thisOp.isRetain) { +// return Operation.retain(thisOp.length, attributes); +// } else if (thisOp.isInsert) { +// return Operation.insert(thisOp.data, attributes); +// } else { +// throw StateError('Unreachable'); +// } +// } else { +// // otherOp == delete && thisOp in [retain, insert] +// assert(otherOp.isDelete); +// if (thisOp.isRetain) return otherOp; +// assert(thisOp.isInsert); +// // otherOp(delete) + thisOp(insert) => null +// } +// return null; +// } + +// /// Composes this delta with [other] and returns new [Delta]. +// /// +// /// It is not required for this and [other] delta to represent a document +// /// delta (consisting only of insert operations). +// Delta compose(Delta other) { +// final result = Delta(); +// final thisIter = DeltaIterator(this); +// final otherIter = DeltaIterator(other); + +// while (thisIter.hasNext || otherIter.hasNext) { +// final newOp = _composeOperation(thisIter, otherIter); +// if (newOp != null) result.push(newOp); +// } +// return result..trim(); +// } + +// /// Returns a new lazy Iterable with elements that are created by calling +// /// f on each element of this Iterable in iteration order. +// /// +// /// Convenience method +// Iterable map(T Function(Operation) f) { +// return _operations.map(f); +// } + +// /// Returns a [Delta] containing differences between 2 [Delta]s. +// /// If [cleanupSemantic] is `true` (default), applies the following: +// /// +// /// The diff of "mouse" and "sofas" is +// /// [delete(1), insert("s"), retain(1), +// /// delete("u"), insert("fa"), retain(1), delete(1)]. +// /// While this is the optimum diff, it is difficult for humans to understand. +// /// Semantic cleanup rewrites the diff, +// /// expanding it into a more intelligible format. +// /// The above example would become: [(-1, "mouse"), (1, "sofas")]. +// /// (source: https://github.com/google/diff-match-patch/wiki/API) +// /// +// /// Useful when one wishes to display difference between 2 documents +// Delta diff(Delta other, {bool cleanupSemantic = true}) { +// if (_operations.equals(other._operations)) { +// return Delta(); +// } +// final stringThis = map((op) { +// if (op.isInsert) { +// return op.data is String ? op.data : _kNullCharacter; +// } +// final prep = this == other ? 'on' : 'with'; +// throw ArgumentError('diff() call $prep non-document'); +// }).join(); +// final stringOther = other.map((op) { +// if (op.isInsert) { +// return op.data is String ? op.data : _kNullCharacter; +// } +// final prep = this == other ? 'on' : 'with'; +// throw ArgumentError('diff() call $prep non-document'); +// }).join(); + +// final retDelta = Delta(); +// final diffResult = dmp.diff(stringThis, stringOther); +// if (cleanupSemantic) { +// dmp.DiffMatchPatch().diffCleanupSemantic(diffResult); +// } + +// final thisIter = DeltaIterator(this); +// final otherIter = DeltaIterator(other); + +// for (final component in diffResult) { +// var length = component.text.length; +// while (length > 0) { +// var opLength = 0; +// switch (component.operation) { +// case dmp.DIFF_INSERT: +// opLength = math.min(otherIter.peekLength(), length); +// retDelta.push(otherIter.next(opLength)); +// break; +// case dmp.DIFF_DELETE: +// opLength = math.min(length, thisIter.peekLength()); +// thisIter.next(opLength); +// retDelta.delete(opLength); +// break; +// case dmp.DIFF_EQUAL: +// opLength = math.min( +// math.min(thisIter.peekLength(), otherIter.peekLength()), +// length, +// ); +// final thisOp = thisIter.next(opLength); +// final otherOp = otherIter.next(opLength); +// if (thisOp.data == otherOp.data) { +// retDelta.retain( +// opLength, +// diffAttributes(thisOp.attributes, otherOp.attributes), +// ); +// } else { +// retDelta +// ..push(otherOp) +// ..delete(opLength); +// } +// break; +// } +// length -= opLength; +// } +// } +// return retDelta..trim(); +// } + +// /// Transforms next operation from [otherIter] against next operation in +// /// [thisIter]. +// /// +// /// Returns `null` if both operations nullify each other. +// Operation? _transformOperation( +// DeltaIterator thisIter, DeltaIterator otherIter, bool priority) { +// if (thisIter.isNextInsert && (priority || !otherIter.isNextInsert)) { +// return Operation.retain(thisIter.next().length); +// } else if (otherIter.isNextInsert) { +// return otherIter.next(); +// } + +// final length = math.min(thisIter.peekLength(), otherIter.peekLength()); +// final thisOp = thisIter.next(length); +// final otherOp = otherIter.next(length); +// assert(thisOp.length == otherOp.length); + +// // At this point only delete and retain operations are possible. +// if (thisOp.isDelete) { +// // otherOp is either delete or retain, so they nullify each other. +// return null; +// } else if (otherOp.isDelete) { +// return otherOp; +// } else { +// // Retain otherOp which is either retain or insert. +// return Operation.retain( +// length, +// transformAttributes(thisOp.attributes, otherOp.attributes, priority), +// ); +// } +// } + +// /// Transforms [other] delta against operations in this delta. +// Delta transform(Delta other, bool priority) { +// final result = Delta(); +// final thisIter = DeltaIterator(this); +// final otherIter = DeltaIterator(other); + +// while (thisIter.hasNext || otherIter.hasNext) { +// final newOp = _transformOperation(thisIter, otherIter, priority); +// if (newOp != null) result.push(newOp); +// } +// return result..trim(); +// } + +// /// Removes trailing retain operation with empty attributes, if present. +// void trim() { +// if (isNotEmpty) { +// final last = _operations.last; +// if (last.isRetain && last.isPlain) _operations.removeLast(); +// } +// } + +// /// Removes trailing '\n' +// void _trimNewLine() { +// if (isNotEmpty) { +// final lastOp = _operations.last; +// final lastOpData = lastOp.data; + +// if (lastOpData is String && lastOpData.endsWith('\n')) { +// _operations.removeLast(); +// if (lastOpData.length > 1) { +// insert(lastOpData.substring(0, lastOpData.length - 1), +// lastOp.attributes); +// } +// } +// } +// } + +// /// Concatenates [other] with this delta and returns the result. +// Delta concat(Delta other, {bool trimNewLine = false}) { +// final result = Delta.from(this); +// if (trimNewLine) { +// result._trimNewLine(); +// } +// if (other.isNotEmpty) { +// // In case first operation of other can be merged with last operation in +// // our list. +// result.push(other._operations.first); +// result._operations.addAll(other._operations.sublist(1)); +// } +// return result; +// } + +// /// Inverts this delta against [base]. +// /// +// /// Returns new delta which negates effect of this delta when applied to +// /// [base]. This is an equivalent of "undo" operation on deltas. +// Delta invert(Delta base) { +// final inverted = Delta(); +// if (base.isEmpty) return inverted; + +// var baseIndex = 0; +// for (final op in _operations) { +// if (op.isInsert) { +// inverted.delete(op.length!); +// } else if (op.isRetain && op.isPlain) { +// inverted.retain(op.length!); +// baseIndex += op.length!; +// } else if (op.isDelete || (op.isRetain && op.isNotPlain)) { +// final length = op.length!; +// final sliceDelta = base.slice(baseIndex, baseIndex + length); +// sliceDelta.toList().forEach((baseOp) { +// if (op.isDelete) { +// inverted.push(baseOp); +// } else if (op.isRetain && op.isNotPlain) { +// final invertAttr = +// invertAttributes(op.attributes, baseOp.attributes); +// inverted.retain( +// baseOp.length!, invertAttr.isEmpty ? null : invertAttr); +// } +// }); +// baseIndex += length; +// } else { +// throw StateError('Unreachable'); +// } +// } +// inverted.trim(); +// return inverted; +// } + +// /// Returns slice of this delta from [start] index (inclusive) to [end] +// /// (exclusive). +// Delta slice(int start, [int? end]) { +// final delta = Delta(); +// var index = 0; +// final opIterator = DeltaIterator(this); + +// final actualEnd = end ?? DeltaIterator.maxLength; + +// while (index < actualEnd && opIterator.hasNext) { +// Operation op; +// if (index < start) { +// op = opIterator.next(start - index); +// } else { +// op = opIterator.next(actualEnd - index); +// delta.push(op); +// } +// index += op.length!; +// } +// return delta; +// } + +// /// Transforms [index] against this delta. +// /// +// /// Any "delete" operation before specified [index] shifts it backward, as +// /// well as any "insert" operation shifts it forward. +// /// +// /// The [force] argument is used to resolve scenarios when there is an +// /// insert operation at the same position as [index]. If [force] is set to +// /// `true` (default) then position is forced to shift forward, otherwise +// /// position stays at the same index. In other words setting [force] to +// /// `false` gives higher priority to the transformed position. +// /// +// /// Useful to adjust caret or selection positions. +// int transformPosition(int index, {bool force = true}) { +// final iter = DeltaIterator(this); +// var offset = 0; +// while (iter.hasNext && offset <= index) { +// final op = iter.next(); +// if (op.isDelete) { +// index -= math.min(op.length!, index - offset); +// continue; +// } else if (op.isInsert && (offset < index || force)) { +// index += op.length!; +// } +// offset += op.length!; +// } +// return index; +// } + +// @override +// String toString() => _operations.join('\n'); +// } + +// /// Specialized iterator for [Delta]s. +// class DeltaIterator { +// DeltaIterator(this.delta) : _modificationCount = delta._modificationCount; + +// static const int maxLength = 1073741824; + +// final Delta delta; +// final int _modificationCount; +// int _index = 0; +// int _offset = 0; + +// bool get isNextInsert => nextOperationKey == Operation.insertKey; + +// bool get isNextDelete => nextOperationKey == Operation.deleteKey; + +// bool get isNextRetain => nextOperationKey == Operation.retainKey; + +// String? get nextOperationKey { +// if (_index < delta.length) { +// return delta.elementAt(_index).key; +// } else { +// return null; +// } +// } + +// bool get hasNext => peekLength() < maxLength; + +// /// Returns length of next operation without consuming it. +// /// +// /// Returns [maxLength] if there is no more operations left to iterate. +// int peekLength() { +// if (_index < delta.length) { +// final operation = delta._operations[_index]; +// return operation.length! - _offset; +// } +// return maxLength; +// } + +// /// Consumes and returns next operation. +// /// +// /// Optional [length] specifies maximum length of operation to return. Note +// /// that actual length of returned operation may be less than specified value. +// /// +// /// If this iterator reached the end of the Delta then returns a retain +// /// operation with its length set to [maxLength]. +// // TODO: Note that we used double.infinity as the default value +// // for length here +// // but this can now cause a type error since operation length is +// // expected to be an int. Changing default length to [maxLength] is +// // a workaround to avoid breaking changes. +// Operation next([int length = maxLength]) { +// if (_modificationCount != delta._modificationCount) { +// throw ConcurrentModificationError(delta); +// } + +// if (_index < delta.length) { +// final op = delta.elementAt(_index); +// final opKey = op.key; +// final opAttributes = op.attributes; +// final currentOffset = _offset; +// final actualLength = math.min(op.length! - currentOffset, length); +// if (actualLength == op.length! - currentOffset) { +// _index++; +// _offset = 0; +// } else { +// _offset += actualLength; +// } +// final opData = op.isInsert && op.data is String +// ? (op.data as String) +// .substring(currentOffset, currentOffset + actualLength) +// : op.data; +// final opIsNotEmpty = +// opData is String ? opData.isNotEmpty : true; // embeds are never empty +// final opLength = opData is String ? opData.length : 1; +// final opActualLength = opIsNotEmpty ? opLength : actualLength; +// return Operation._(opKey, opActualLength, opData, opAttributes); +// } +// return Operation.retain(length); +// } + +// /// Skips [length] characters in source delta. +// /// +// /// Returns last skipped operation, or `null` if there was nothing to skip. +// Operation? skip(int length) { +// var skipped = 0; +// Operation? op; +// while (skipped < length && hasNext) { +// final opLength = peekLength(); +// final skip = math.min(length - skipped, opLength); +// op = next(skip); +// skipped += op.length!; +// } +// return op; +// } +// } diff --git a/lib/src/widgets/quill/quill_controller.dart b/lib/src/widgets/quill/quill_controller.dart index fd3403003..8bbf10950 100644 --- a/lib/src/widgets/quill/quill_controller.dart +++ b/lib/src/widgets/quill/quill_controller.dart @@ -59,6 +59,22 @@ class QuillController extends ChangeNotifier { notifyListeners(); } + /// The current font family, null to use the default one + String? _selectedFontFamily; + String? get selectedFontFamily => _selectedFontFamily; + + void selectFontFamily(String? newFontFamily) { + _selectedFontFamily = newFontFamily; + } + + /// The current font size, null to use the default one + String? _selectedFontSize; + String? get selectedFontSize => _selectedFontSize; + + void selectFontSize(String? newFontSize) { + _selectedFontSize = newFontSize; + } + /// Tells whether to keep or reset the [toggledStyle] /// when user adds a new line. final bool _keepStyleOnNewLine; diff --git a/lib/src/widgets/quill/text_line.dart b/lib/src/widgets/quill/text_line.dart index c1b68f42d..cdf493ff5 100644 --- a/lib/src/widgets/quill/text_line.dart +++ b/lib/src/widgets/quill/text_line.dart @@ -164,7 +164,8 @@ class _TextLineState extends State { } } final textSpan = _getTextSpanForWholeLine(); - final strutStyle = StrutStyle.fromTextStyle(textSpan.style!); + final strutStyle = + StrutStyle.fromTextStyle(textSpan.style ?? const TextStyle()); final textAlign = _getTextAlign(); final child = RichText( key: _richTextKey, @@ -247,8 +248,11 @@ class _TextLineState extends State { return TextAlign.start; } - TextSpan _buildTextSpan(DefaultStyles defaultStyles, LinkedList nodes, - TextStyle lineStyle) { + TextSpan _buildTextSpan( + DefaultStyles defaultStyles, + LinkedList nodes, + TextStyle lineStyle, + ) { if (nodes.isEmpty && kIsWeb) { nodes = LinkedList()..add(leaf.QuillText('\u{200B}')); } diff --git a/lib/src/widgets/raw_editor/raw_editor_state.dart b/lib/src/widgets/raw_editor/raw_editor_state.dart index 3a9dfd608..84af33167 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state.dart @@ -28,7 +28,6 @@ import '../../models/documents/nodes/embeddable.dart'; import '../../models/documents/nodes/leaf.dart' as leaf; import '../../models/documents/nodes/line.dart'; import '../../models/documents/nodes/node.dart'; -import '../../models/quill_delta.dart'; import '../../models/structs/offset_value.dart'; import '../../models/structs/vertical_spacing.dart'; import '../../utils/cast.dart'; @@ -117,26 +116,6 @@ class QuillRawEditorState extends EditorState .call(content); } - // List get contextMenuButtonItems { - // return EditableText.getEditableButtonItems( - // clipboardStatus: _clipboardStatus.value, - // onLiveTextInput: null, - // onCopy: copyEnabled - // ? () => copySelection(SelectionChangedCause.toolbar) - // : null, - // onCut: - // cutEnabled ? () => cutSelection(SelectionChangedCause.toolbar) : null, - // onPaste: - // pasteEnabled ? () => pasteText(SelectionChangedCause.toolbar) : null, - // onSelectAll: selectAllEnabled - // ? () => selectAll(SelectionChangedCause.toolbar) - // : null, - // onLookUp: null, - // onSearchWeb: null, - // onShare: null, - // ); - // } - /// Copy current selection to [Clipboard]. @override void copySelection(SelectionChangedCause cause) { @@ -226,28 +205,16 @@ class QuillRawEditorState extends EditorState return; } - // TODO: Could be improved - Delta? deltaFromCliboard; + // TODO: Bug, Doesn't replace the selected text, it just add a new one + final reader = await ClipboardReader.readClipboard(); if (reader.canProvide(Formats.htmlText)) { final html = await reader.readValue(Formats.htmlText); if (html == null) { return; } - deltaFromCliboard = QuillController.fromHtml(html); - } - if (deltaFromCliboard != null) { - // final index = selection.baseOffset; - // final length = selection.extentOffset - index; - - final list = controller.document.toDelta().toList() - ..insertAll(controller.document.toDelta().toList().length - 1, - deltaFromCliboard.toList()); - - final delta = controller.document.toDelta(); - for (final operation in list) { - delta.push(operation); - } + final deltaFromCliboard = QuillController.fromHtml(html); + final delta = deltaFromCliboard.compose(controller.document.toDelta()); controller ..updateDocument( diff --git a/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart b/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart index 2c6a08daa..aeb2135e7 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state_text_input_client_mixin.dart @@ -7,8 +7,10 @@ import 'package:flutter/material.dart' show Theme; import 'package:flutter/scheduler.dart' show SchedulerBinding; import 'package:flutter/services.dart'; +import '../../models/documents/attribute.dart'; import '../../models/documents/document.dart'; import '../../utils/delta.dart'; +import '../../utils/font.dart'; import '../editor/editor.dart'; import 'raw_editor.dart'; @@ -200,7 +202,40 @@ mixin RawEditorStateTextInputClientMixin on EditorState .updateSelection(value.selection, ChangeSource.local); } else { widget.configurations.controller.replaceText( - diff.start, diff.deleted.length, diff.inserted, value.selection); + diff.start, + diff.deleted.length, + diff.inserted, + value.selection, + ); + + // TODO: There is a bug here, the first character is not being formatted + + if (widget.configurations.controller.selectedFontFamily != null) { + widget.configurations.controller.formatText( + diff.start, + diff.deleted.length, + Attribute.fromKeyValue( + Attribute.font.key, + widget.configurations.controller.selectedFontFamily, + ), + ); + } + + // TODO: A bug here too + + if (widget.configurations.controller.selectedFontSize != null) { + widget.configurations.controller.formatText( + diff.start, + diff.deleted.length, + Attribute.fromKeyValue( + Attribute.size.key, + widget.configurations.controller.selectedFontSize == '0' + ? null + : getFontSize( + widget.configurations.controller.selectedFontSize), + ), + ); + } } } diff --git a/lib/src/widgets/toolbar/buttons/font_family_button.dart b/lib/src/widgets/toolbar/buttons/font_family_button.dart index 8714d6fa3..17c87fb92 100644 --- a/lib/src/widgets/toolbar/buttons/font_family_button.dart +++ b/lib/src/widgets/toolbar/buttons/font_family_button.dart @@ -5,7 +5,6 @@ import '../../../extensions/quill_configurations_ext.dart'; import '../../../l10n/extensions/localizations.dart'; import '../../../models/config/toolbar/buttons/font_family_configurations.dart'; import '../../../models/documents/attribute.dart'; -import '../../../models/documents/style.dart'; import '../../../models/themes/quill_icon_theme.dart'; import '../../quill/quill_controller.dart'; @@ -41,7 +40,7 @@ class QuillToolbarFontFamilyButtonState return widget.options; } - Style get _selectionStyle => controller.getSelectionStyle(); + // Style get _selectionStyle => controller.getSelectionStyle(); @override void initState() { @@ -51,39 +50,39 @@ class QuillToolbarFontFamilyButtonState void _initState() { _currentValue = _defaultDisplayText; - controller.addListener(_didChangeEditingValue); + // controller.addListener(_didChangeEditingValue); } - @override - void dispose() { - controller.removeListener(_didChangeEditingValue); - super.dispose(); - } + // @override + // void dispose() { + // controller.removeListener(_didChangeEditingValue); + // super.dispose(); + // } String get _defaultDisplayText { return options.initialValue ?? widget.defaultDispalyText; } - @override - void didUpdateWidget(covariant QuillToolbarFontFamilyButton oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.controller == controller) { - return; - } - controller - ..removeListener(_didChangeEditingValue) - ..addListener(_didChangeEditingValue); - } + // @override + // void didUpdateWidget(covariant QuillToolbarFontFamilyButton oldWidget) { + // super.didUpdateWidget(oldWidget); + // if (oldWidget.controller == controller) { + // return; + // } + // controller + // ..removeListener(_didChangeEditingValue) + // ..addListener(_didChangeEditingValue); + // } - void _didChangeEditingValue() { - final attribute = _selectionStyle.attributes[options.attribute.key]; - if (attribute == null) { - setState(() => _currentValue = _defaultDisplayText); - return; - } - final keyName = _getKeyName(attribute.value); - setState(() => _currentValue = keyName ?? _defaultDisplayText); - } + // void _didChangeEditingValue() { + // final attribute = _selectionStyle.attributes[options.attribute.key]; + // if (attribute == null) { + // setState(() => _currentValue = _defaultDisplayText); + // return; + // } + // final keyName = _getKeyName(attribute.value); + // setState(() => _currentValue = keyName ?? _defaultDisplayText); + // } Map get rawItemsMap { final rawItemsMap = @@ -184,17 +183,19 @@ class QuillToolbarFontFamilyButtonState } return Tooltip(message: effectiveTooltip, child: child); }, - child: RawMaterialButton( + child: IconButton( + // tooltip: '', // TODO: Use this here visualDensity: VisualDensity.compact, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(iconTheme?.borderRadius ?? 2), + style: IconButton.styleFrom( + shape: iconTheme?.borderRadius != null + ? RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(iconTheme?.borderRadius ?? -1), + ) + : null, ), - fillColor: options.fillColor, - elevation: 0, - hoverElevation: options.hoverElevation, - highlightElevation: options.hoverElevation, onPressed: _onPressed, - child: _buildContent(context), + icon: _buildContent(context), ), ), ); @@ -222,6 +223,13 @@ class QuillToolbarFontFamilyButtonState value: fontFamily.value, height: options.itemHeight ?? kMinInteractiveDimension, padding: options.itemPadding, + onTap: () { + if (fontFamily.value == 'Clear') { + controller.selectFontFamily(null); + return; + } + controller.selectFontFamily(fontFamily.value); + }, child: Text( fontFamily.key.toString(), style: TextStyle( @@ -246,11 +254,15 @@ class QuillToolbarFontFamilyButtonState } final keyName = _getKeyName(newValue); setState(() { - _currentValue = keyName ?? _defaultDisplayText; + if (keyName != 'Clear') { + _currentValue = keyName ?? _defaultDisplayText; + } else { + _currentValue = _defaultDisplayText; + } if (keyName != null) { controller.formatSelection( Attribute.fromKeyValue( - 'font', + Attribute.font.key, newValue == 'Clear' ? null : newValue, ), ); @@ -272,7 +284,7 @@ class QuillToolbarFontFamilyButtonState enabled: hasFinalWidth, wrapper: (child) => Expanded(child: child), child: Text( - _currentValue, + widget.controller.selectedFontFamily ?? _currentValue, maxLines: 1, overflow: options.labelOverflow, style: options.style ?? diff --git a/lib/src/widgets/toolbar/buttons/font_size_button.dart b/lib/src/widgets/toolbar/buttons/font_size_button.dart index d1ee7752e..54a14b6bd 100644 --- a/lib/src/widgets/toolbar/buttons/font_size_button.dart +++ b/lib/src/widgets/toolbar/buttons/font_size_button.dart @@ -5,7 +5,6 @@ import '../../../extensions/quill_configurations_ext.dart'; import '../../../l10n/extensions/localizations.dart'; import '../../../models/config/quill_configurations.dart'; import '../../../models/documents/attribute.dart'; -import '../../../models/documents/style.dart'; import '../../../models/themes/quill_icon_theme.dart'; import '../../../utils/font.dart'; import '../../quill/quill_controller.dart'; @@ -57,47 +56,17 @@ class QuillToolbarFontSizeButtonState return options.initialValue ?? widget.defaultDisplayText; } - Style get _selectionStyle => controller.getSelectionStyle(); - @override void initState() { super.initState(); - - _initState(); - } - - void _initState() { _currentValue = _defaultDisplayText; - controller.addListener(_didChangeEditingValue); } @override void dispose() { - controller.removeListener(_didChangeEditingValue); super.dispose(); } - @override - void didUpdateWidget(covariant QuillToolbarFontSizeButton oldWidget) { - super.didUpdateWidget(oldWidget); - if (controller == oldWidget.controller) { - return; - } - controller - ..removeListener(_didChangeEditingValue) - ..addListener(_didChangeEditingValue); - } - - void _didChangeEditingValue() { - final attribute = _selectionStyle.attributes[options.attribute.key]; - if (attribute == null) { - setState(() => _currentValue = _defaultDisplayText); - return; - } - final keyName = _getKeyName(attribute.value); - setState(() => _currentValue = keyName ?? _defaultDisplayText); - } - String? _getKeyName(dynamic value) { for (final entry in rawItemsMap.entries) { if (getFontSize(entry.value) == getFontSize(value)) { @@ -157,7 +126,6 @@ class QuillToolbarFontSizeButtonState tooltip: tooltip, iconSize: iconSize, iconButtonFactor: iconButtonFactor, - iconTheme: iconTheme, afterButtonPressed: afterButtonPressed, controller: controller, ), @@ -177,17 +145,18 @@ class QuillToolbarFontSizeButtonState ), child: UtilityWidgets.maybeTooltip( message: tooltip, - child: RawMaterialButton( + child: IconButton( visualDensity: VisualDensity.compact, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(iconTheme?.borderRadius ?? 2), + style: IconButton.styleFrom( + shape: iconTheme?.borderRadius != null + ? RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(iconTheme?.borderRadius ?? -1), + ) + : null, ), - fillColor: options.fillColor, - elevation: 0, - hoverElevation: options.hoverElevation, - highlightElevation: options.hoverElevation, onPressed: _onPressed, - child: _buildContent(context), + icon: _buildContent(context), ), ), ); @@ -215,6 +184,13 @@ class QuillToolbarFontSizeButtonState value: fontSize.value, height: options.itemHeight ?? kMinInteractiveDimension, padding: options.itemPadding, + onTap: () { + if (fontSize.value == '0') { + controller.selectFontSize(null); + return; + } + controller.selectFontSize(fontSize.value); + }, child: Text( fontSize.key.toString(), style: TextStyle( @@ -233,10 +209,18 @@ class QuillToolbarFontSizeButtonState } final keyName = _getKeyName(newValue); setState(() { - _currentValue = keyName ?? _defaultDisplayText; + if (keyName != 'Clear') { + _currentValue = keyName ?? _defaultDisplayText; + } else { + _currentValue = _defaultDisplayText; + } if (keyName != null) { - controller.formatSelection(Attribute.fromKeyValue( - 'size', newValue == '0' ? null : getFontSize(newValue))); + controller.formatSelection( + Attribute.fromKeyValue( + Attribute.size.key, + newValue == '0' ? null : getFontSize(newValue), + ), + ); options.onSelected?.call(newValue); } }); @@ -255,7 +239,7 @@ class QuillToolbarFontSizeButtonState enabled: hasFinalWidth, wrapper: (child) => Expanded(child: child), child: Text( - _currentValue, + widget.controller.selectedFontSize ?? _currentValue, overflow: options.labelOverflow, style: options.style ?? TextStyle( diff --git a/lib/src/widgets/toolbar/buttons/select_header_style_button.dart b/lib/src/widgets/toolbar/buttons/select_header_style_button.dart index 1e92c84fd..da04b84aa 100644 --- a/lib/src/widgets/toolbar/buttons/select_header_style_button.dart +++ b/lib/src/widgets/toolbar/buttons/select_header_style_button.dart @@ -37,6 +37,24 @@ class _QuillToolbarSelectHeaderStyleButtonState widget.controller.addListener(_didChangeEditingValue); } + @override + void dispose() { + widget.controller.removeListener(_didChangeEditingValue); + super.dispose(); + } + + @override + void didUpdateWidget( + covariant QuillToolbarSelectHeaderStyleButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.controller == widget.controller) { + return; + } + widget.controller + ..removeListener(_didChangeEditingValue) + ..addListener(_didChangeEditingValue); + } + void _didChangeEditingValue() { final newSelectedItem = _getOptionsItemByAttribute(_getHeaderValue()); if (newSelectedItem == _selectedItem) { diff --git a/lib/src/widgets/toolbar/simple_toolbar.dart b/lib/src/widgets/toolbar/simple_toolbar.dart index 9794b3127..4f91cb346 100644 --- a/lib/src/widgets/toolbar/simple_toolbar.dart +++ b/lib/src/widgets/toolbar/simple_toolbar.dart @@ -133,6 +133,16 @@ class QuillSimpleToolbar extends StatelessWidget ), spacerWidget, ], + if (configurations.showStrikeThrough) ...[ + QuillToolbarToggleStyleButton( + attribute: Attribute.strikeThrough, + options: toolbarConfigurations.buttonOptions.strikeThrough, + controller: + toolbarConfigurations.buttonOptions.strikeThrough.controller ?? + globalController, + ), + spacerWidget, + ], if (configurations.showInlineCode) ...[ QuillToolbarToggleStyleButton( attribute: Attribute.inlineCode, @@ -172,16 +182,6 @@ class QuillSimpleToolbar extends StatelessWidget ), spacerWidget, ], - if (configurations.showStrikeThrough) ...[ - QuillToolbarToggleStyleButton( - attribute: Attribute.strikeThrough, - options: toolbarConfigurations.buttonOptions.strikeThrough, - controller: - toolbarConfigurations.buttonOptions.strikeThrough.controller ?? - globalController, - ), - spacerWidget, - ], if (configurations.showColorButton) ...[ QuillToolbarColorButton( controller: toolbarConfigurations.buttonOptions.color.controller ?? diff --git a/packages/README.md b/packages/README.md deleted file mode 100644 index 0f8fce772..000000000 --- a/packages/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# Flutter Quill Packages - -This folder contains packages that add more features to the [FlutterQuill](../README.md) -that might be outside of the packages main purpose - -Pub: [quill_html_converter](https://pub.dev/packages/quill_html_converter) - -## Table of contents -- [Flutter Quill Packages](#flutter-quill-packages) - - [Table of contents](#table-of-contents) - - [Packages](#packages) - -## Packages -- [quill_html_converter](./quill_html_converter/) \ No newline at end of file diff --git a/packages/quill_html_converter/pubspec_overrides.yaml.disabled b/packages/quill_html_converter/pubspec_overrides.yaml.disabled deleted file mode 100644 index 844dcdead..000000000 --- a/packages/quill_html_converter/pubspec_overrides.yaml.disabled +++ /dev/null @@ -1,3 +0,0 @@ -dependency_overrides: - flutter_quill: - path: ../../ \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 7d396197b..2d9c3572e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor built for the modern Android, iOS, web and desktop platforms. It is the WYSIWYG editor and a Quill component for Flutter. -version: 9.0.0-dev-8 +version: 9.0.0-dev-11 homepage: https://1o24bbs.com/c/bulletjournal/108/ repository: https://github.com/singerdmx/flutter-quill/ issue_tracker: https://github.com/singerdmx/flutter-quill/issues/ @@ -47,21 +47,20 @@ dependencies: collection: ^1.17.0 flutter_colorpicker: ^1.0.3 quiver: ^3.2.1 - characters: ^1.3.0 - diff_match_patch: ^0.4.1 equatable: ^2.0.5 meta: ^1.9.1 - # For Quill HTML + # For converting HTML to Quill delta markdown: ^7.1.1 html2md: ^1.3.1 + charcode: ^1.3.1 # Plugins url_launcher: ^6.1.14 flutter_keyboard_visibility: ^5.4.1 device_info_plus: ^9.1.0 super_clipboard: ^0.7.3 - charcode: ^1.3.1 + dart_quill_delta: ^0.0.1 dev_dependencies: flutter_lints: ^3.0.1 diff --git a/pubspec_overrides.yaml.disabled b/pubspec_overrides.yaml.disabled index 0c0f849c8..94d344b00 100644 --- a/pubspec_overrides.yaml.disabled +++ b/pubspec_overrides.yaml.disabled @@ -1,3 +1,5 @@ dependency_overrides: flutter_quill_test: - path: ./flutter_quill_test \ No newline at end of file + path: ./flutter_quill_test + dart_quill_delta: + path: ./dart_quill_delta \ No newline at end of file diff --git a/packages/quill_html_converter/.gitignore b/quill_html_converter/.gitignore similarity index 100% rename from packages/quill_html_converter/.gitignore rename to quill_html_converter/.gitignore diff --git a/packages/quill_html_converter/.metadata b/quill_html_converter/.metadata similarity index 100% rename from packages/quill_html_converter/.metadata rename to quill_html_converter/.metadata diff --git a/packages/quill_html_converter/CHANGELOG.md b/quill_html_converter/CHANGELOG.md similarity index 98% rename from packages/quill_html_converter/CHANGELOG.md rename to quill_html_converter/CHANGELOG.md index 09d629a35..4392c9d05 100644 --- a/packages/quill_html_converter/CHANGELOG.md +++ b/quill_html_converter/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to this project will be documented in this file. +## 9.0.0-dev-11 +* Test new GitHub workflows + +## 9.0.0-dev-10 +* Fix a bug of the improved pasting HTML contents contents into the editor + +## 9.0.0-dev-9 +* Improves the new logic of pasting HTML contents into the Editor +* Update `README.md` and the doc +* Dispose the `QuillToolbarSelectHeaderStyleButton` state listener in `dispose` +* Upgrade the font family button to material 3 +* Rework the font family and font size functionalities to change the font once and type all over the editor + ## 9.0.0-dev-8 * Better support for pasting HTML contents from external websites to the editor * The experimental support of converting the HTML from `quill_html_converter` is now built-in in the `flutter_quill` and removed from there (Breaking change for `quill_html_converter`) diff --git a/quill_html_converter/LICENSE b/quill_html_converter/LICENSE new file mode 100644 index 000000000..e82b91ed3 --- /dev/null +++ b/quill_html_converter/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Flutter Quill Team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/quill_html_converter/README.md b/quill_html_converter/README.md similarity index 100% rename from packages/quill_html_converter/README.md rename to quill_html_converter/README.md diff --git a/packages/quill_html_converter/analysis_options.yaml b/quill_html_converter/analysis_options.yaml similarity index 100% rename from packages/quill_html_converter/analysis_options.yaml rename to quill_html_converter/analysis_options.yaml diff --git a/packages/quill_html_converter/lib/quill_html_converter.dart b/quill_html_converter/lib/quill_html_converter.dart similarity index 94% rename from packages/quill_html_converter/lib/quill_html_converter.dart rename to quill_html_converter/lib/quill_html_converter.dart index 2e8d080fa..7ed4fe0b6 100644 --- a/packages/quill_html_converter/lib/quill_html_converter.dart +++ b/quill_html_converter/lib/quill_html_converter.dart @@ -1,6 +1,6 @@ library quill_html_converter; -import 'package:flutter_quill/flutter_quill.dart' show Delta; +import 'package:dart_quill_delta/dart_quill_delta.dart'; import 'package:vsc_quill_delta_to_html/vsc_quill_delta_to_html.dart' as conventer show ConverterOptions, QuillDeltaToHtmlConverter; diff --git a/packages/quill_html_converter/pubspec.yaml b/quill_html_converter/pubspec.yaml similarity index 78% rename from packages/quill_html_converter/pubspec.yaml rename to quill_html_converter/pubspec.yaml index a5aa9589f..d09180207 100644 --- a/packages/quill_html_converter/pubspec.yaml +++ b/quill_html_converter/pubspec.yaml @@ -1,10 +1,10 @@ name: quill_html_converter description: A extension for flutter_quill package to add support for dealing with conversion to/from html -version: 9.0.0-dev-8 -homepage: https://github.com/singerdmx/flutter-quill/tree/master/packages/quill_html_converter/ -repository: https://github.com/singerdmx/flutter-quill/tree/master/packages/quill_html_converter/ +version: 9.0.0-dev-11 +homepage: https://github.com/singerdmx/flutter-quill/tree/master/quill_html_converter/ +repository: https://github.com/singerdmx/flutter-quill/tree/master/quill_html_converter/ issue_tracker: https://github.com/singerdmx/flutter-quill/issues/ -documentation: https://github.com/singerdmx/flutter-quill/tree/master/packages/quill_html_converter/ +documentation: https://github.com/singerdmx/flutter-quill/tree/master/quill_html_converter/ topics: - ui @@ -20,14 +20,13 @@ environment: dependencies: flutter: sdk: flutter - flutter_quill: ^9.0.0-dev-6 vsc_quill_delta_to_html: ^1.0.3 html2md: ^1.3.1 - # markdown: ^7.1.1 - # delta_markdown_converter: ^0.0.3-dev markdown: ^7.1.1 charcode: ^1.3.1 collection: ^1.18.0 + dart_quill_delta: ^0.0.1 + dev_dependencies: flutter_test: diff --git a/quill_html_converter/pubspec_overrides.yaml.disabled b/quill_html_converter/pubspec_overrides.yaml.disabled new file mode 100644 index 000000000..aae8e5dde --- /dev/null +++ b/quill_html_converter/pubspec_overrides.yaml.disabled @@ -0,0 +1,3 @@ +dependency_overrides: + dart_quill_delta: + path: ../dart_quill_delta \ No newline at end of file diff --git a/packages/quill_html_converter/test/quill_html_converter.dart b/quill_html_converter/test/quill_html_converter.dart similarity index 100% rename from packages/quill_html_converter/test/quill_html_converter.dart rename to quill_html_converter/test/quill_html_converter.dart diff --git a/scripts/disable_local_dev.sh b/scripts/disable_local_dev.sh index a64ba4869..ca52b8c20 100755 --- a/scripts/disable_local_dev.sh +++ b/scripts/disable_local_dev.sh @@ -20,7 +20,7 @@ rm flutter_quill_test/pubspec_overrides.yaml echo "" echo "Disable local development for all the other packages..." -rm packages/quill_html_converter/pubspec_overrides.yaml +rm quill_html_converter/pubspec_overrides.yaml echo "" diff --git a/scripts/enable_local_dev.sh b/scripts/enable_local_dev.sh index 603465bca..c37bc8951 100755 --- a/scripts/enable_local_dev.sh +++ b/scripts/enable_local_dev.sh @@ -20,7 +20,7 @@ cp flutter_quill_test/pubspec_overrides.yaml.disabled flutter_quill_test/pubspec echo "" echo "Enable local development for all the other packages..." -cp packages/quill_html_converter/pubspec_overrides.yaml.disabled packages/quill_html_converter/pubspec_overrides.yaml +cp quill_html_converter/pubspec_overrides.yaml.disabled quill_html_converter/pubspec_overrides.yaml echo "" diff --git a/scripts/pub_get.sh b/scripts/pub_get.sh index 6bd082c83..a836d79ff 100644 --- a/scripts/pub_get.sh +++ b/scripts/pub_get.sh @@ -2,4 +2,4 @@ flutter pub get (cd flutter_quill_extensions && flutter pub get) (cd flutter_quill_test && flutter pub get) -(cd packages/quill_html_converter && flutter pub get) +(cd quill_html_converter && flutter pub get) diff --git a/scripts/regenerate_versions.dart b/scripts/regenerate_versions.dart index ed73e523a..279890124 100644 --- a/scripts/regenerate_versions.dart +++ b/scripts/regenerate_versions.dart @@ -13,7 +13,7 @@ final packages = [ './', './flutter_quill_extensions', './flutter_quill_test', - './packages/quill_html_converter' + './quill_html_converter' ]; Future main(List args) async { diff --git a/version.dart b/version.dart index 27a78c104..8ed75fba4 100644 --- a/version.dart +++ b/version.dart @@ -1 +1 @@ -const version = '9.0.0-dev-8'; +const version = '9.0.0-dev-11';