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 decoder #191

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
231 changes: 228 additions & 3 deletions packages/notus/lib/src/convert/markdown.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,238 @@ 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();

for (var line in lines) {
_handleLine(line, delta);
}

return delta;
}

_handleLine(String line, Delta delta, [Map<String, dynamic> attributes]) {
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);
}
}

/// 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);
Map<String, dynamic> newAttributes = {
'block': 'quote'
}; // NotusAttribute.bq.toJson();
if (attributes != null) {
newAttributes.addAll(attributes);
}
// all blocks are supported within bq
_handleLine(span, delta, newAttributes);
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);
Map<String, dynamic> 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);
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);
Map<String, dynamic> 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);
return true;
}
return false;
}

_handleHeading(String line, Delta delta, [Map<String, dynamic> attributes]) {
var match = _headingRegExp.matchAsPrefix(line);
if (match != null) {
var level = match.group(1).length;
Map<String, dynamic> newAttributes = {
'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);
// delta.insert('\n', attribute.toJson());
return true;
}

return false;
}

_handleSpan(String span, Delta delta, bool addNewLine,
Map<String, dynamic> outerStyle) {
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) {
delta.insert('$span\n', outerStyle);
} else {
delta.insert(span, outerStyle);
}
} else if (addNewLine) {
delta.insert('\n', outerStyle);
}
}

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

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

var text = match.group(2);
var newStyle = Map<String, dynamic>.from(
_attributesByStyleLength[match.group(1).length]);
if (outerStyle != null) {
newStyle.addAll(outerStyle);
}
_handleSpan(text, delta, false, newStyle);
start = match.end;
});

return start;
}

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

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

var text = match.group(1);
var href = match.group(2);
Map<String, dynamic> newAttributes = {
'a': href
}; // NotusAttribute.link.fromString(href).toJson();
if (outerStyle != null) {
newAttributes.addAll(outerStyle);
}
_handleSpan(text, delta, false, newAttributes);
start = match.end;
});

return start;
}
}

class _NotusMarkdownEncoder extends Converter<Delta, String> {
static const kBold = '**';
static const kItalic = '_';
Expand Down Expand Up @@ -142,7 +367,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
Loading