Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Markdown block shortcuts #558

Merged
merged 1 commit into from
Nov 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 78 additions & 12 deletions packages/notus/lib/src/heuristics/insert_rules.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import 'package:quill_delta/quill_delta.dart';

import '../document/attributes.dart';
import 'utils.dart';

/// The result of [_findNextNewline] function.
class _FindResult {
Expand Down Expand Up @@ -211,9 +212,7 @@ class AutoExitBlockRule extends InsertRule {
// therefore we can exit this block.
final attributes = target.attributes ?? <String, dynamic>{};
attributes.addAll(NotusAttribute.block.unset.toJson());
return Delta()
..retain(index)
..retain(1, attributes);
return Delta()..retain(index)..retain(1, attributes);
}
}

Expand Down Expand Up @@ -337,14 +336,10 @@ class ForceNewlineForInsertsAroundEmbedRule extends InsertRule {
if (cursorBeforeEmbed || cursorAfterEmbed) {
final delta = Delta()..retain(index);
if (cursorBeforeEmbed && !data.endsWith('\n')) {
return delta
..insert(data)
..insert('\n');
return delta..insert(data)..insert('\n');
}
if (cursorAfterEmbed && !data.startsWith('\n')) {
return delta
..insert('\n')
..insert(data);
return delta..insert('\n')..insert(data);
}
return delta..insert(data);
}
Expand Down Expand Up @@ -424,9 +419,7 @@ class PreserveBlockStyleOnInsertRule extends InsertRule {
result.retain(nextNewline.skippedLength!);
final opText = nextNewline.op!.data as String;
final lf = opText.indexOf('\n');
result
..retain(lf)
..retain(1, resetStyle);
result..retain(lf)..retain(1, resetStyle);
}

return result;
Expand Down Expand Up @@ -492,3 +485,76 @@ class InsertEmbedsRule extends InsertRule {
return attributes;
}
}

/// Replaces certain Markdown shortcuts with actual line or block styles.
class MarkdownBlockShortcutsInsertRule extends InsertRule {
static final rules = {
'-': NotusAttribute.block.bulletList,
'*': NotusAttribute.block.bulletList,
'1.': NotusAttribute.block.numberList,
"'''": NotusAttribute.block.code,
'```': NotusAttribute.block.code,
'>': NotusAttribute.block.quote,
'#': NotusAttribute.h1,
'##': NotusAttribute.h2,
'###': NotusAttribute.h3,
};
const MarkdownBlockShortcutsInsertRule();

@override
Delta? apply(Delta document, int index, Object data) {
if (data is! String) return null;
if (data != ' ') return null;

final iter = DeltaIterator(document);
final prefixOps = skipToLineAt(iter, index);

if (prefixOps.any((element) => element.data is! String)) return null;

final prefix = prefixOps.map((e) => e.data).cast<String>().join();

if (prefix.isEmpty) return null;

final attribute = rules[prefix];
if (attribute == null) return null;

/// First, delete the shortcut prefix itself.
final result = Delta()
..retain(index - prefix.length)
..delete(prefix.length);

// Scan to the end of line to apply the style attribute.
while (iter.hasNext) {
final op = iter.next();
if (op.data is! String) {
result.retain(op.length);
continue;
}

final text = op.data as String;
final pos = text.indexOf('\n');

if (pos == -1) {
result.retain(op.length);
continue;
}

result.retain(pos);

final attrs = <String, dynamic>{};
final currentLineAttrs = op.attributes;
if (currentLineAttrs != null) {
// the attribute already exists abort
if (currentLineAttrs[attribute.key] == attribute.value) return null;
attrs.addAll(currentLineAttrs);
}
attrs.addAll(attribute.toJson());

result.retain(1, attrs);

break;
}

return result;
}
}
32 changes: 32 additions & 0 deletions packages/notus/lib/src/heuristics/utils.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import 'dart:math' as math;

import 'package:quill_delta/quill_delta.dart';

/// Skips to the beginning of line containing position at specified [length]
/// and returns contents of the line skipped so far.
List<Operation> skipToLineAt(DeltaIterator iter, int length) {
assert(length > 0);

final prefix = <Operation>[];

var skipped = 0;
while (skipped < length && iter.hasNext) {
final opLength = iter.peekLength();
final skip = math.min(length - skipped, opLength);
final op = iter.next(skip);
if (op.data is! String) {
prefix.add(op);
} else {
var text = op.data as String;
var pos = text.lastIndexOf('\n');
if (pos == -1) {
prefix.add(op);
} else {
prefix.clear();
prefix.add(Operation.insert(text.substring(pos + 1), op.attributes));
}
}
skipped += op.length;
}
return prefix;
}
32 changes: 32 additions & 0 deletions packages/notus/test/heuristics/insert_rules_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -352,4 +352,36 @@ void main() {
expect(actual, expected);
});
});

group('$MarkdownBlockShortcutsInsertRule', () {
final rule = MarkdownBlockShortcutsInsertRule();

test('apply markdown shortcut on single-line document', () {
final doc = Delta()..insert('#\n');
final actual = rule.apply(doc, 1, ' ');
final expected = Delta()
..delete(1)
..retain(1, NotusAttribute.h1.toJson());
expect(actual, expected);
});

test('ignores if already formatted with the same style', () {
final doc = Delta()
..insert('#')
..insert('\n', NotusAttribute.h1.toJson());
final actual = rule.apply(doc, 1, ' ');
expect(actual, isNull);
});

test('changes existing style', () {
final doc = Delta()
..insert('##')
..insert('\n', NotusAttribute.h1.toJson());
final actual = rule.apply(doc, 2, ' ');
final expected = Delta()
..delete(2)
..retain(1, NotusAttribute.h2.toJson());
expect(actual, expected);
});
});
}