Skip to content

Commit

Permalink
Merge pull request #12 from BazinC/july_16_2024_stable_updates
Browse files Browse the repository at this point in the history
July 16 2024 stable updates
  • Loading branch information
BazinC authored Jul 16, 2024
2 parents e3bb0b7 + 69a8394 commit 4d50537
Show file tree
Hide file tree
Showing 426 changed files with 21,976 additions and 3,009 deletions.
22 changes: 22 additions & 0 deletions .github/workflows/pr_validation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,28 @@ jobs:
# Run all tests
- run: flutter test

test_super_editor_quill:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./super_editor_quill
steps:
# Checkout the PR branch
- uses: actions/checkout@v3

# Setup Flutter environment
- uses: subosito/flutter-action@v2
with:
channel: "master"

# Download all the packages that the app uses
- run: flutter pub get

# TODO: Enforce static analysis

# Run all tests
- run: flutter test

test_super_text_layout:
runs-on: ubuntu-latest
defaults:
Expand Down
8 changes: 8 additions & 0 deletions attributed_text/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## [0.3.2] - June, 2024
* Fix crash when adding attributions that overlap others - you can now control whether a new attribution overwrites conflicting spans when you add it.

## [0.3.1] - June, 2024
* Added query `getAllAttributionsThroughout()` to `AttributedText`.
* Added `copy()` to `AttributedText()`.
* Added ability to insert an attribution that splits an existing attribution.

## [0.3.0] - Feb, 2024
* [BREAKING] - `AttributedText` and `SpanRange` constructors now use positional parameters istead of named parameters.
* [FIX] - `AttributedText` now supports differents links for different URLs in the same text blob - previously all links were sent to the same URL withing a single `AttributedText`.
Expand Down
174 changes: 154 additions & 20 deletions attributed_text/lib/src/attributed_spans.dart
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ class AttributedSpans {
final matchingAttributions = <Attribution>{};
for (int i = start; i <= end; ++i) {
for (final attribution in attributions) {
final otherAttributions = getAllAttributionsAt(start);
final otherAttributions = getAllAttributionsAt(i);
for (final otherAttribution in otherAttributions) {
if (otherAttribution.id == attribution.id) {
matchingAttributions.add(otherAttribution);
Expand Down Expand Up @@ -280,18 +280,48 @@ class AttributedSpans {

/// Applies the [newAttribution] from [start] to [end], inclusive.
///
/// When [autoMerge] is `true`, the new attribution is merged with any
/// preceding or following attribution whose [Attribution.canMergeWith] returns
/// `true`.
/// If [start] is less than `0`, nothing happens.
///
/// It [newAttribution] overlaps a conflicting span, or if [newAttribution]
/// overlaps a merge-able span but [autoMerge] is `false`, a
/// [IncompatibleOverlappingAttributionsException] is thrown.
/// [AttributedSpans] doesn't have any knowledge about content length, so [end] can
/// take any value that's desired. However, users of [AttributedSpans] should take
/// care to avoid values for [end] that exceed the content length.
///
/// The effect of adding an attribution is straight forward when the text doesn't
/// contain any other attributions with the same ID. However, there are various
/// situations where [newAttribution] can't necessarily co-exist with other
/// attribution spans that already exist in the text.
///
/// Attribution overlaps can take one of two forms: mergeable or conflicting.
///
/// ## Mergeable Attribution Spans
/// An example of a mergeable overlap is where two bold spans overlap each
/// other. All bold attributions are interchangeable, so when two bold spans
/// overlap, those spans can be merged together into a single span.
///
/// However, mergeable overlapping spans are not automatically merged. Instead,
/// this decision is left to the user of this class. If you want [AttributedSpans] to
/// merge overlapping mergeable spans, pass `true` for [autoMerge]. Otherwise,
/// if [autoMerge] is `false`, an exception is thrown when two mergeable spans
/// overlap each other.
///
///
/// ## Conflicting Attribution Spans
/// An example of a conflicting overlap is where a black text color overlaps a red
/// text color. Text is either black, OR red, but never both. Therefore, the black
/// attribution cannot co-exist with the red attribution. Something must be done
/// to resolve this.
///
/// There are two possible ways to handle conflicting overlaps. The new attribution
/// can overwrite the existing attribution where they overlap. Or, an exception can be
/// thrown. To overwrite the existing attribution with the new attribution, pass `true`
/// for [overwriteConflictingSpans]. Otherwise, if [overwriteConflictingSpans]
/// is `false`, an exception is thrown.
void addAttribution({
required Attribution newAttribution,
required int start,
required int end,
bool autoMerge = true,
bool overwriteConflictingSpans = true,
}) {
if (start < 0 || start > end) {
_log.warning("Tried to add an attribution ($newAttribution) at an invalid start/end: $start -> $end");
Expand All @@ -301,32 +331,94 @@ class AttributedSpans {
_log.info("Adding attribution ($newAttribution) from $start to $end");
_log.finer("Has ${_markers.length} markers before addition");

// Ensure that no conflicting attribution overlaps the new attribution.
// If a conflict exists, throw an exception.
final conflicts = <_AttributionConflict>[];

// Check if conflicting attributions overlap the new attribution.
final matchingAttributions = getMatchingAttributionsWithin(attributions: {newAttribution}, start: start, end: end);
if (matchingAttributions.isNotEmpty) {
for (final matchingAttribution in matchingAttributions) {
if (!newAttribution.canMergeWith(matchingAttribution) || !autoMerge) {
late int conflictStart;
bool areAttributionsMergeable = newAttribution.canMergeWith(matchingAttribution);
if (!areAttributionsMergeable || !autoMerge) {
int? conflictStart;
int? conflictEnd;

for (int i = start; i <= end; ++i) {
if (hasAttributionAt(i, attribution: matchingAttribution)) {
conflictStart = i;
break;
conflictStart ??= i;
conflictEnd = i;

if (areAttributionsMergeable) {
// Both attributions are mergeable, but the caller doesn't want to merge them.
throw IncompatibleOverlappingAttributionsException(
existingAttribution: matchingAttribution,
newAttribution: newAttribution,
conflictStart: conflictStart,
);
}
} else if (conflictStart != null) {
// We found the end of the conflict.
conflicts.add(_AttributionConflict(
newAttribution: newAttribution,
existingAttribution: matchingAttribution,
conflictStart: conflictStart,
conflictEnd: conflictEnd!,
));

// Reset so we can find the next conflict.
conflictStart = null;
conflictEnd = null;
}
}

throw IncompatibleOverlappingAttributionsException(
existingAttribution: matchingAttribution,
newAttribution: newAttribution,
conflictStart: conflictStart,
);
if (conflictStart != null && conflictEnd != null) {
// We found a conflict that extends to the end of the range.
conflicts.add(_AttributionConflict(
newAttribution: newAttribution,
existingAttribution: matchingAttribution,
conflictStart: conflictStart,
conflictEnd: conflictEnd,
));
}
}
}

if (conflicts.isNotEmpty && !overwriteConflictingSpans) {
// We found conflicting attributions and we are configured not to overwrite them.
// For example, the user tried to apply a blue color attribution to a range of text
// that already has another color attribution.
throw IncompatibleOverlappingAttributionsException(
existingAttribution: conflicts.first.existingAttribution,
newAttribution: newAttribution,
conflictStart: conflicts.first.conflictStart,
);
}
}

// Removes any conflicting attributions. For example, consider the following text,
// with a blue color attribution that spans the entire text:
//
// one two three
// |bbbbbbbbbbbbb|
//
// We can't apply a green color attribution to the word "two", because it's already
// attributed with blue. So, we need to remove the blue attribution from the word "two",
// which results in the following text:
//
// one two three
// |bbbb---bbbbbb|
//
// After that, we can apply the desired attribution, because there isn't a conflicting attribution
// in this range anymore.
for (final conflict in conflicts) {
removeAttribution(
attributionToRemove: conflict.existingAttribution,
start: conflict.conflictStart,
end: conflict.conflictEnd,
);
}

if (!autoMerge) {
// There are not conflicting attributions in the desired range, and we don't
// want to merge this new attribution with any other nearby attribution.
// We don't want to merge this new attribution with any other nearby attribution.
// Therefore, we can blindly create the new attribution range without any
// further adjustments, and then be done.
_insertMarker(SpanMarker(
Expand Down Expand Up @@ -1197,3 +1289,45 @@ class IncompatibleOverlappingAttributionsException implements Exception {
return 'Tried to insert attribution ($newAttribution) over a conflicting existing attribution ($existingAttribution). The overlap began at index $conflictStart';
}
}

/// A conflict between the [newAttribution] and [existingAttribution] between [conflictStart] and [conflictEnd] (inclusive).
///
/// This means [newAttribution] and [existingAttribution] have the same id, but they can't be merged.
class _AttributionConflict {
_AttributionConflict({
required this.newAttribution,
required this.existingAttribution,
required this.conflictStart,
required this.conflictEnd,
});

/// The new attribution that conflicts with the existing attribution.
final Attribution newAttribution;

/// The conflicting attribution.
final Attribution existingAttribution;

/// The first conflicting index.
final int conflictStart;

/// The last conflicting index (inclusive).
final int conflictEnd;

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is _AttributionConflict &&
runtimeType == other.runtimeType &&
newAttribution == other.newAttribution &&
existingAttribution == other.existingAttribution &&
conflictStart == other.conflictStart &&
conflictEnd == other.conflictEnd;

@override
int get hashCode =>
newAttribution.hashCode ^ existingAttribution.hashCode ^ conflictStart.hashCode ^ conflictEnd.hashCode;

@override
String toString() =>
'[AttributionConflict] - newAttribution: $newAttribution existingAttribution: $existingAttribution, conflictStart: $conflictStart, conflictEnd: $conflictEnd';
}
42 changes: 38 additions & 4 deletions attributed_text/lib/src/attributed_text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -171,15 +171,49 @@ class AttributedText {
/// Adds the given [attribution] to all characters within the given
/// [range], inclusive.
///
/// When [autoMerge] is `true`, the new attribution is merged with any
/// preceding or following attribution whose [Attribution.canMergeWith] returns
/// `true`.
/// The effect of adding an attribution is straight forward when the text doesn't
/// contain any other attributions with the same ID. However, there are various
/// situations where the [attribution] can't necessarily co-exist with other
/// attribution spans that already exist in the text.
///
/// Attribution overlaps can take one of two forms: mergeable or conflicting.
///
/// ## Mergeable Attribution Spans
/// An example of a mergeable overlap is where two bold spans overlap each
/// other. All bold attributions are interchangeable, so when two bold spans
/// overlap, those spans can be merged together into a single span.
///
/// However, mergeable overlapping spans are not automatically merged. Instead,
/// this decision is left to the user of this class. If you want [AttributedText] to
/// merge overlapping mergeable spans, pass `true` for [autoMerge]. Otherwise,
/// if [autoMerge] is `false`, an exception is thrown when two mergeable spans
/// overlap each other.
///
///
/// ## Conflicting Attribution Spans
/// An example of a conflicting overlap is where a black text color overlaps a red
/// text color. Text is either black, OR red, but never both. Therefore, the black
/// attribution cannot co-exist with the red attribution. Something must be done
/// to resolve this.
///
/// There are two possible ways to handle conflicting overlaps. The new attribution
/// can overwrite the existing attribution where they overlap. Or, an exception can be
/// thrown. To overwrite the existing attribution with the new attribution, pass `true`
/// for [overwriteConflictingSpans]. Otherwise, if [overwriteConflictingSpans]
/// is `false`, an exception is thrown.
void addAttribution(
Attribution attribution,
SpanRange range, {
bool autoMerge = true,
bool overwriteConflictingSpans = false,
}) {
spans.addAttribution(newAttribution: attribution, start: range.start, end: range.end, autoMerge: autoMerge);
spans.addAttribution(
newAttribution: attribution,
start: range.start,
end: range.end,
autoMerge: autoMerge,
overwriteConflictingSpans: overwriteConflictingSpans,
);
_notifyListeners();
}

Expand Down
4 changes: 2 additions & 2 deletions attributed_text/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
name: attributed_text
description: Text with metadata spans for easy text editing and styling.
version: 0.3.0
version: 0.3.2
homepage: https://github.com/superlistapp/super_editor

environment:
sdk: ">=2.12.0 <4.0.0"
sdk: ">=3.0.0 <4.0.0"

dependencies:
characters: ^1.2.0
Expand Down
Loading

0 comments on commit 4d50537

Please sign in to comment.