Skip to content

Markdown decoder and encoder improvements #381

New issue

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

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

Already on GitHub? # to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
302 changes: 296 additions & 6 deletions packages/notus/lib/src/convert/markdown.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,296 @@

import 'dart:convert';

import 'package:notus/notus.dart';
import 'package:quill_delta/quill_delta.dart';
import 'package:notus/notus.dart';

class NotusMarkdownCodec extends Codec<Delta, String> {
const NotusMarkdownCodec();

@override
Converter<String, Delta> get decoder =>
throw UnimplementedError('Decoding is not implemented yet.');
Converter<String, Delta> get decoder => _NotusMarkdownDecoder();

@override
Converter<Delta, String> get encoder => _NotusMarkdownEncoder();
}

class _NotusMarkdownDecoder extends Converter<String, Delta> {
final List<Map<String, dynamic>> _attributesByStyleLength = [
null,
{'i': true}, // _
{'b': true}, // **
{'i': true, 'b': true} // **_
];
final RegExp _headingRegExp = RegExp(r'(#+) *(.+)');
final RegExp _styleRegExp = RegExp(r'((?:\*|_){1,3})(.*?[^\1 ])\1');
final RegExp _linkRegExp = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)');
final RegExp _ulRegExp = RegExp(r'^( *)\* +(.*)');
final RegExp _olRegExp = RegExp(r'^( *)\d+[\.)] +(.*)');
final RegExp _bqRegExp = RegExp(r'^> *(.*)');
final RegExp _codeRegExp = RegExp(r'^( *)```'); // TODO: inline code
bool _inBlockStack = false;
// final List<String> _blockStack = [];
// int _olDepth = 0;

@override
Delta convert(String input) {
final lines = input.split('\n');
final delta = Delta();

if(_allLinesEmpty(lines)) {
Map<String, dynamic> style;
_handleSpan(lines[0], delta, true, style);
} else {
for (var line in lines) {
_handleLine(line, delta);
}
}

return delta;
}

bool _allLinesEmpty(List<String> lines) {
for (var line in lines) {
if (line != '') {
return false;
}
}

return true;
}

void _handleLine(String line, Delta delta, [Map<String, dynamic> attributes, bool isBlock]) {
if (_handleBlockQuote(line, delta, attributes)) {
return;
}
if (_handleBlock(line, delta, attributes)) {
return;
}
if (_handleHeading(line, delta, attributes)) {
return;
}

if (line.isNotEmpty) {
_handleSpan(line, delta, true, attributes, isBlock);
}
}

/// Markdown supports headings and blocks within blocks (except for within code)
/// but not blocks within headers, or ul within
bool _handleBlock(String line, Delta delta,
[Map<String, dynamic> attributes]) {
var match;

match = _codeRegExp.matchAsPrefix(line);
if (match != null) {
_inBlockStack = !_inBlockStack;
return true;
}
if (_inBlockStack) {
delta.insert(
line + '\n',
NotusAttribute.code
.toJson()); // TODO: replace with?: {'quote': true})
// Don't bother testing for code blocks within block stacks
return true;
}

if (_handleOrderedList(line, delta, attributes) ||
_handleUnorderedList(line, delta, attributes)) {
return true;
}

return false;
}

/// all blocks are supported within bq
bool _handleBlockQuote(String line, Delta delta,
[Map<String, dynamic> attributes]) {
var match = _bqRegExp.matchAsPrefix(line);
if (match != null) {
var span = match.group(1);
var newAttributes = NotusAttribute.bq.toJson(); // NotusAttribute.bq.toJson();
if (attributes != null) {
newAttributes.addAll(attributes);
}
// all blocks are supported within bq
_handleLine(span, delta, newAttributes, true);
return true;
}
return false;
}

/// ol is supported within ol and bq, but not supported within ul
bool _handleOrderedList(String line, Delta delta,
[Map<String, dynamic> attributes]) {
var match = _olRegExp.matchAsPrefix(line);
if (match != null) {
// TODO: support nesting
// var depth = match.group(1).length / 3;
var span = match.group(2);
var newAttributes = NotusAttribute.ol.toJson();
if (attributes != null) {
newAttributes.addAll(attributes);
}
// There's probably no reason why you would have other block types on the same line
_handleSpan(span, delta, true, newAttributes, true);
return true;
}
return false;
}

bool _handleUnorderedList(String line, Delta delta,
[Map<String, dynamic> attributes]) {
var match = _ulRegExp.matchAsPrefix(line);
if (match != null) {
// var depth = match.group(1).length / 3;
var span = match.group(2);
var newAttributes = NotusAttribute.ul.toJson();
if (attributes != null) {
newAttributes.addAll(attributes);
}
// There's probably no reason why you would have other block types on the same line
_handleSpan(span, delta, true, newAttributes, true);
return true;
}
return false;
}

bool _handleHeading(String line, Delta delta, [Map<String, dynamic> attributes]) {
var match = _headingRegExp.matchAsPrefix(line);
if (match != null) {
var level = match.group(1).length;
var newAttributes = <String, dynamic>{
'heading': level
}; // NotusAttribute.heading.withValue(level).toJson();
if (attributes != null) {
newAttributes.addAll(attributes);
}

var span = match.group(2);
// TODO: true or false?
_handleSpan(span, delta, true, newAttributes, true);
// delta.insert('\n', attribute.toJson());
return true;
}

return false;
}

void _handleSpan(String span, Delta delta, bool addNewLine,
Map<String, dynamic> outerStyle, [bool isBlock]) {
var start = _handleStyles(span, delta, outerStyle);
span = span.substring(start);

if (span.isNotEmpty) {
start = _handleLinks(span, delta, outerStyle);
span = span.substring(start);
}

if (span.isNotEmpty) {
if (addNewLine) {
if(isBlock != null && isBlock){
delta.insert(span);
delta.insert('\n', outerStyle);
} else {
delta.insert('$span\n', outerStyle);
}
} else {
delta.insert(span, outerStyle);
}
} else if (addNewLine) {
delta.insert('\n', outerStyle);
}
}

int _handleStyles(String span, Delta delta, Map<String, dynamic> outerStyle) {
var start = 0;

var matches = _styleRegExp.allMatches(span);
matches.forEach((match) {
if (match.start > start) {
var validInlineStyles = _getValidInlineStyles(outerStyle);
if (span.substring(match.start - 1, match.start) == '[') {
var text = span.substring(start, match.start - 1);
validInlineStyles != null ? delta.insert(text, validInlineStyles) : delta.insert(text);
start = match.start -
1 +
_handleLinks(span.substring(match.start - 1), delta, validInlineStyles);
return;
} else {
var text = span.substring(start, match.start);

validInlineStyles != null ? delta.insert(text, validInlineStyles) : delta.insert(text);
}
}

var text = match.group(2);
var newStyle = Map<String, dynamic>.from(
_attributesByStyleLength[match.group(1).length]);

var validInlineStyles = _getValidInlineStyles(outerStyle);
if (validInlineStyles != null) {
newStyle.addAll(validInlineStyles);
}

_handleSpan(text, delta, false, newStyle);
start = match.end;
});

return start;
}

Map<String, dynamic> _getValidInlineStyles(Map<String, dynamic> outerStyle) {
Map<String, dynamic> leafStyles;

if(outerStyle == null) {
return null;
}

if(outerStyle.containsKey(NotusAttribute.bold.key)){
leafStyles = {'b': true};
}

if(outerStyle.containsKey(NotusAttribute.italic.key)){
leafStyles = {'i': true};
}

if(outerStyle.containsKey(NotusAttribute.link.key)){
leafStyles = {NotusAttribute.link.key: outerStyle[NotusAttribute.link.key]};
}

return leafStyles;
}

int _handleLinks(String span, Delta delta, Map<String, dynamic> outerStyle) {
var start = 0;

var matches = _linkRegExp.allMatches(span);
matches.forEach((match) {
if (match.start > start) {
var text = span.substring(start, match.start);
delta.insert(text); //, outerStyle);
}

var text = match.group(1);
var href = match.group(2);
var newAttributes = <String, dynamic>{
'a': href
}; // NotusAttribute.link.fromString(href).toJson();

var validInlineStyles = _getValidInlineStyles(outerStyle);
if (validInlineStyles != null) {
newAttributes.addAll(validInlineStyles);
}

_handleSpan(text, delta, false, newAttributes);
start = match.end;
});

return start;
}
}

class _NotusMarkdownEncoder extends Converter<Delta, String> {
static const kBold = '**';
static const kItalic = '_';
Expand All @@ -34,13 +310,27 @@ class _NotusMarkdownEncoder extends Converter<Delta, String> {
final lineBuffer = StringBuffer();
NotusAttribute<String> currentBlockStyle;
var currentInlineStyle = NotusStyle();
var currentBlockLines = [];
var currentBlockLines = <String>[];

bool _allLinesEmpty(List<String> lines) {
for (var line in lines) {
if (line != '') {
return false;
}
}

return true;
}

void _handleBlock(NotusAttribute<String> blockStyle) {
if (currentBlockLines.isEmpty) {
return; // Empty block
}

if(_allLinesEmpty(currentBlockLines)){
return;
}

if (blockStyle == null) {
buffer.write(currentBlockLines.join('\n\n'));
buffer.writeln();
Expand Down Expand Up @@ -142,7 +432,7 @@ class _NotusMarkdownEncoder extends Converter<Delta, String> {
if (padding.isNotEmpty) buffer.write(padding);
}
// Now open any new styles.
for (var value in style.values) {
for (var value in style.values.toList().reversed) {
if (value.scope == NotusAttributeScope.line) continue;
if (currentStyle.containsSame(value)) continue;
final originalText = text;
Expand Down Expand Up @@ -210,4 +500,4 @@ class _NotusMarkdownEncoder extends Converter<Delta, String> {
buffer.write(tag);
}
}
}
}
Loading