From a4898354c045c7463d6184eef10ddf1b7026cae9 Mon Sep 17 00:00:00 2001 From: Nicholas Albion Date: Thu, 24 Oct 2019 03:55:54 +1100 Subject: [PATCH 1/8] can decode Markdown headings, bold, italic and (WIP) links --- packages/notus/lib/src/convert/markdown.dart | 103 +++++++- .../notus/test/convert/markdown_test.dart | 246 +++++++++++++++++- 2 files changed, 342 insertions(+), 7 deletions(-) diff --git a/packages/notus/lib/src/convert/markdown.dart b/packages/notus/lib/src/convert/markdown.dart index ecf1dc6f3..b4e140e1b 100644 --- a/packages/notus/lib/src/convert/markdown.dart +++ b/packages/notus/lib/src/convert/markdown.dart @@ -11,13 +11,110 @@ class NotusMarkdownCodec extends Codec { const NotusMarkdownCodec(); @override - Converter get decoder => - throw UnimplementedError('Decoding is not implemented yet.'); + Converter get decoder => _NotusMarkdownDecoder(); +// throw UnimplementedError('Decoding is not implemented yet.'); @override Converter get encoder => _NotusMarkdownEncoder(); } +class _NotusMarkdownDecoder extends Converter { + final RegExp _headingRegExp = RegExp(r'(#+) (.+)'); + final RegExp _styleRegExp = RegExp(r'((?:\*|_){1,3})(.*?[^\1 ])\1'); + final RegExp _linkRegExp = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)'); + final attributesByStyleLength = [null, {'i': true}, {'b': true }, {'i': true, 'b': true }]; + + @override + Delta convert(String input) { + final doc = NotusDocument(); + + final delta = new Delta(); + + final lines = input.split('\n'); + var index = 0; + + for (var line in lines) { + _handleLine(line, delta); + index++; + } + + return delta; + } + + _handleLine(String line, Delta delta) { + line = line.trim(); + var match = _headingRegExp.matchAsPrefix(line); + if (match != null) { + var attribute = NotusAttribute.heading.withValue(match.group(1).length); +// delta..insert(match.group(2)); + _handleSpan(match.group(2), delta, false, null); + delta.insert('\n', attribute.toJson()); + } else if (line.isNotEmpty) { + _handleSpan(line, delta, true, null); + } + } + + _handleSpan(String span, Delta delta, bool addNewLine, Map outerStyle) { + var start = _handleStyles(span, delta, outerStyle); + span = span.substring(start); + start = _handleLinks(span, delta, outerStyle); + + var remaining = span.substring(start); + if (remaining.isNotEmpty) { + if (addNewLine) { + delta.insert('$remaining\n', outerStyle); + } else { + delta.insert(remaining, outerStyle); + } + } else if (addNewLine) { + delta.insert('\n', outerStyle); + } + } + + _handleStyles(String span, Delta delta, Map outerStyle) { + var start = 0; + + var matches = _styleRegExp.allMatches(span); + matches.forEach((match) { + if (match.start > start) { + delta.insert(span.substring(start, match.start), outerStyle); + } + + var text = match.group(2); + var newStyle = 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 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 attributes = {'a': href}; // NotusAttribute.link.fromString(href).toJson(); + if (outerStyle != null) { + attributes.addAll(outerStyle); + } + _handleSpan(text, delta, false, attributes); + start = match.end; + }); + + return start; + } +} + class _NotusMarkdownEncoder extends Converter { static const kBold = '**'; static const kItalic = '_'; @@ -142,7 +239,7 @@ class _NotusMarkdownEncoder extends Converter { 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; diff --git a/packages/notus/test/convert/markdown_test.dart b/packages/notus/test/convert/markdown_test.dart index e593790e3..d7ee52713 100644 --- a/packages/notus/test/convert/markdown_test.dart +++ b/packages/notus/test/convert/markdown_test.dart @@ -10,10 +10,248 @@ import 'package:notus/convert.dart'; void main() { group('$NotusMarkdownCodec.encode', () { - test('unimplemented', () { - expect(() { - notusMarkdown.decode('test'); - }, throwsUnimplementedError); + test('paragraphs', () { + final markdown = 'First line\n\nSecond line\n\n'; + final delta = notusMarkdown.decode(markdown); + expect(delta.elementAt(0).data, 'First line\nSecond line\n'); + final andBack = notusMarkdown.encode(delta); + expect(andBack, markdown); + }); + + test('italics', () { + runFor(String markdown, bool testEncode) { + final delta = notusMarkdown.decode(markdown); + expect(delta.elementAt(0).data, 'italics'); + expect(delta.elementAt(0).attributes["i"], true); + expect(delta.elementAt(0).attributes["b"], null); + if (testEncode) { + final andBack = notusMarkdown.encode(delta); + expect(andBack, markdown); + } + } + + runFor('_italics_\n\n', true); + runFor('*italics*\n\n', false); + }); + + test('multi-word italics', () { + runFor(String markdown, bool testEncode) { + final delta = notusMarkdown.decode(markdown); + expect(delta.elementAt(0).data, 'Okay, '); + expect(delta.elementAt(0).attributes, null); + + expect(delta.elementAt(1).data, 'this is in italics'); + expect(delta.elementAt(1).attributes["i"], true); + expect(delta.elementAt(1).attributes["b"], null); + + expect(delta.elementAt(3).data, 'so is all of _ this'); + expect(delta.elementAt(3).attributes["i"], true); + + expect(delta.elementAt(4).data, ' but this is not\n'); + expect(delta.elementAt(4).attributes, null); + if (testEncode) { + final andBack = notusMarkdown.encode(delta); + expect(andBack, markdown); + } + } + + runFor('Okay, _this is in italics_ and _so is all of _ this_ but this is not\n\n', true); + runFor('Okay, *this is in italics* and *so is all of _ this* but this is not\n\n', false); + }); + + test('bold', () { + runFor(String markdown, bool testEncode) { + final delta = notusMarkdown.decode(markdown); + expect(delta.elementAt(0).data, 'bold'); + expect(delta.elementAt(0).attributes["b"], true); + expect(delta.elementAt(0).attributes["i"], null); + if (testEncode) { + final andBack = notusMarkdown.encode(delta); + expect(andBack, markdown); + } + } + + runFor('**bold**\n\n', true); + runFor('__bold__\n\n', false); + }); + + test('multi-word bold', () { + runFor(String markdown, bool testEncode) { + final delta = notusMarkdown.decode(markdown); + expect(delta.elementAt(0).data, 'Okay, '); + expect(delta.elementAt(0).attributes, null); + + expect(delta.elementAt(1).data, 'this is bold'); + expect(delta.elementAt(1).attributes["b"], true); + expect(delta.elementAt(1).attributes["i"], null); + + expect(delta.elementAt(3).data, 'so is all of __ this'); + expect(delta.elementAt(3).attributes["b"], true); + + expect(delta.elementAt(4).data, ' but this is not\n'); + expect(delta.elementAt(4).attributes, null); + if (testEncode) { + final andBack = notusMarkdown.encode(delta); + expect(andBack, markdown); + } + } + + runFor('Okay, **this is bold** and **so is all of __ this** but this is not\n\n', true); + runFor('Okay, __this is bold__ and __so is all of __ this__ but this is not\n\n', false); + }); + + test('intersecting inline styles', () { + var markdown = 'This **house _is a_ circus**\n\n'; + final delta = notusMarkdown.decode(markdown); + expect(delta.elementAt(1).data, 'house '); + expect(delta.elementAt(1).attributes["b"], true); + expect(delta.elementAt(1).attributes["i"], null); + + expect(delta.elementAt(2).data, 'is a'); + expect(delta.elementAt(2).attributes["b"], true); + expect(delta.elementAt(2).attributes["i"], true); + + expect(delta.elementAt(3).data, ' circus'); + expect(delta.elementAt(3).attributes["b"], true); + expect(delta.elementAt(3).attributes["i"], null); + + final andBack = notusMarkdown.encode(delta); + expect(andBack, markdown); + }); + + test('bold and italics', () { + runFor(String markdown, bool testEncode) { + final delta = notusMarkdown.decode(markdown); + expect(delta.elementAt(0).data, 'this is bold and italic'); + expect(delta.elementAt(0).attributes["b"], true); + expect(delta.elementAt(0).attributes["i"], true); + + expect(delta.elementAt(1).data, '\n'); + expect(delta.length, 2); + + if (testEncode) { + final andBack = notusMarkdown.encode(delta); + expect(andBack, markdown); + } + } + + runFor('**_this is bold and italic_**\n\n', true); + runFor('_**this is bold and italic**_\n\n', true); + runFor('***this is bold and italic***\n\n', false); + runFor('___this is bold and italic___\n\n', false); + }); + + test('bold and italics combinations', () { + runFor(String markdown, bool testEncode) { + final delta = notusMarkdown.decode(markdown); + expect(delta.elementAt(0).data, 'this is bold'); + expect(delta.elementAt(0).attributes["b"], true); + expect(delta.elementAt(0).attributes["i"], null); + + expect(delta.elementAt(2).data, 'this is in italics'); + expect(delta.elementAt(2).attributes["b"], null); + expect(delta.elementAt(2).attributes["i"], true); + + expect(delta.elementAt(4).data, 'this is both'); + expect(delta.elementAt(4).attributes["b"], true); + expect(delta.elementAt(4).attributes["i"], true); + + if (testEncode) { + final andBack = notusMarkdown.encode(delta); + expect(andBack, markdown); + } + } + + runFor('**this is bold** _this is in italics_ and **_this is both_**\n\n', true); + runFor('**this is bold** *this is in italics* and ***this is both***\n\n', false); + runFor('__this is bold__ _this is in italics_ and ___this is both___\n\n', false); + }); + + test('link', () { + var markdown = 'This **house** is a [circus](https://github.com)\n\n'; + final delta = notusMarkdown.decode(markdown); + + expect(delta.elementAt(1).data, 'house'); + expect(delta.elementAt(1).attributes["b"], true); + expect(delta.elementAt(1).attributes["a"], null); + + expect(delta.elementAt(3).data, 'circus'); + expect(delta.elementAt(3).attributes["b"], null); + expect(delta.elementAt(3).attributes["a"], 'https://github.com'); + + final andBack = notusMarkdown.encode(delta); + expect(andBack, markdown); + }); + + test('style around link', () { + var markdown = 'This **house** is a **[circus](https://github.com)**\n\n'; + final delta = notusMarkdown.decode(markdown); + + expect(delta.elementAt(1).data, 'house'); + expect(delta.elementAt(1).attributes["b"], true); + expect(delta.elementAt(1).attributes["a"], null); + + expect(delta.elementAt(3).data, 'circus'); + expect(delta.elementAt(3).attributes["b"], true); + expect(delta.elementAt(3).attributes["a"], 'https://github.com'); + + final andBack = notusMarkdown.encode(delta); + expect(andBack, markdown); + }); + + test('style within link', () { + var markdown = 'This **house** is a [**circus**](https://github.com)\n\n'; + final delta = notusMarkdown.decode(markdown); + + expect(delta.elementAt(1).data, 'house'); + expect(delta.elementAt(1).attributes["b"], true); + expect(delta.elementAt(1).attributes["a"], null); + + expect(delta.elementAt(3).data, 'circus'); + expect(delta.elementAt(3).attributes["b"], true); + expect(delta.elementAt(3).attributes["a"], 'https://github.com'); + + final andBack = notusMarkdown.encode(delta); + expect(andBack, markdown); + }); + + test('foo', () { + runFor(NotusAttribute attribute, String markdown) { + final delta = notusMarkdown.decode(markdown); +// expect(delta.elementAt(0).data, 'First line\nSecond line\n'); + final andBack = notusMarkdown.encode(delta); + expect(andBack, markdown); + +// ..insert('This ') +// ..insert('house', attribute.toJson()) +// ..insert(' is a ') +// ..insert('circus', attribute.toJson()) +// ..insert('\n'); + } + +// runFor(NotusAttribute.bold, 'This **house** is a __circus__\n\n'); +// runFor(NotusAttribute.italic, 'This _house_ is a *circus*\n\n'); +// runFor(NotusAttribute.bold, '*italic*\n\n'); + runFor(NotusAttribute.bold, '_italic_\n\n'); + runFor(NotusAttribute.bold, '**bold**\n\n'); +// runFor(NotusAttribute.bold, '__bold__\n\n'); + runFor(NotusAttribute.bold, '**_bold and italic_**\n\n'); +// runFor(NotusAttribute.bold, '***italic and bold***\n\n'); + runFor(NotusAttribute.bold, 'Here is some *italic * star* and _italic _ lodash_ and **bold * star** and __bold _ lodash__\n\n'); + }); + + test('heading styles', () { + runFor(String markdown, int level) { + final delta = notusMarkdown.decode(markdown); + expect(delta.elementAt(0).data, 'This is an H$level'); + expect(delta.elementAt(1).attributes['heading'], level); + final andBack = notusMarkdown.encode(delta); + expect(andBack, markdown); + } + + runFor('# This is an H1\n\n', 1); + runFor('## This is an H2\n\n', 2); + runFor('### This is an H3\n\n', 3); }); }); From 7407b2ecdcd032999fcc73eafe6272e6754c8c7f Mon Sep 17 00:00:00 2001 From: Nicholas Albion Date: Thu, 24 Oct 2019 04:46:16 +1100 Subject: [PATCH 2/8] can decode links --- packages/notus/lib/src/convert/markdown.dart | 12 +++++++++--- packages/notus/test/convert/markdown_test.dart | 6 ++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/notus/lib/src/convert/markdown.dart b/packages/notus/lib/src/convert/markdown.dart index b4e140e1b..8ec8f0afb 100644 --- a/packages/notus/lib/src/convert/markdown.dart +++ b/packages/notus/lib/src/convert/markdown.dart @@ -22,7 +22,7 @@ class _NotusMarkdownDecoder extends Converter { final RegExp _headingRegExp = RegExp(r'(#+) (.+)'); final RegExp _styleRegExp = RegExp(r'((?:\*|_){1,3})(.*?[^\1 ])\1'); final RegExp _linkRegExp = RegExp(r'\[([^\]]+)\]\(([^\)]+)\)'); - final attributesByStyleLength = [null, {'i': true}, {'b': true }, {'i': true, 'b': true }]; + final List> attributesByStyleLength = [null, {'i': true}, {'b': true }, {'i': true, 'b': true }]; @override Delta convert(String input) { @@ -77,11 +77,17 @@ class _NotusMarkdownDecoder extends Converter { var matches = _styleRegExp.allMatches(span); matches.forEach((match) { if (match.start > start) { - delta.insert(span.substring(start, match.start), outerStyle); + 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 = attributesByStyleLength[match.group(1).length]; + Map newStyle = attributesByStyleLength[match.group(1).length]; if (outerStyle != null) { newStyle.addAll(outerStyle); } diff --git a/packages/notus/test/convert/markdown_test.dart b/packages/notus/test/convert/markdown_test.dart index d7ee52713..0f4762bd5 100644 --- a/packages/notus/test/convert/markdown_test.dart +++ b/packages/notus/test/convert/markdown_test.dart @@ -207,10 +207,16 @@ void main() { expect(delta.elementAt(1).attributes["b"], true); expect(delta.elementAt(1).attributes["a"], null); + expect(delta.elementAt(2).data, ' is a '); + expect(delta.elementAt(2).attributes, null); + expect(delta.elementAt(3).data, 'circus'); expect(delta.elementAt(3).attributes["b"], true); expect(delta.elementAt(3).attributes["a"], 'https://github.com'); + expect(delta.elementAt(4).data, '\n'); + expect(delta.length, 5); + final andBack = notusMarkdown.encode(delta); expect(andBack, markdown); }); From 1bc2adcd784a525f80b103e78e4898787df1a00b Mon Sep 17 00:00:00 2001 From: Nicholas Albion Date: Thu, 24 Oct 2019 14:43:33 +1100 Subject: [PATCH 3/8] _NotusMarkdownDecoder implemented with tests, no nested blocks --- packages/notus/lib/src/convert/markdown.dart | 158 +++++++++++++++--- .../notus/test/convert/markdown_test.dart | 113 ++++++++++--- 2 files changed, 217 insertions(+), 54 deletions(-) diff --git a/packages/notus/lib/src/convert/markdown.dart b/packages/notus/lib/src/convert/markdown.dart index 8ec8f0afb..ca31f5a06 100644 --- a/packages/notus/lib/src/convert/markdown.dart +++ b/packages/notus/lib/src/convert/markdown.dart @@ -12,59 +12,163 @@ class NotusMarkdownCodec extends Codec { @override Converter get decoder => _NotusMarkdownDecoder(); -// throw UnimplementedError('Decoding is not implemented yet.'); @override Converter get encoder => _NotusMarkdownEncoder(); } class _NotusMarkdownDecoder extends Converter { - final RegExp _headingRegExp = RegExp(r'(#+) (.+)'); + final List> _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 List> attributesByStyleLength = [null, {'i': true}, {'b': true }, {'i': true, 'b': true }]; + 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 _blockStack = []; +// int _olDepth = 0; @override Delta convert(String input) { - final doc = NotusDocument(); - - final delta = new Delta(); - final lines = input.split('\n'); - var index = 0; + final delta = Delta(); for (var line in lines) { _handleLine(line, delta); - index++; } return delta; } - _handleLine(String line, Delta delta) { - line = line.trim(); + _handleLine(String line, Delta delta, [Map 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 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 attributes]) { + var match = _bqRegExp.matchAsPrefix(line); + if (match != null) { + var span = match.group(1); + Map 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 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 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 attributes]) { + var match = _ulRegExp.matchAsPrefix(line); + if (match != null) { + var depth = match.group(1).length / 3; + var span = match.group(2); + Map 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 attributes]) { var match = _headingRegExp.matchAsPrefix(line); if (match != null) { - var attribute = NotusAttribute.heading.withValue(match.group(1).length); -// delta..insert(match.group(2)); - _handleSpan(match.group(2), delta, false, null); - delta.insert('\n', attribute.toJson()); - } else if (line.isNotEmpty) { - _handleSpan(line, delta, true, null); + var level = match.group(1).length; + Map 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 outerStyle) { var start = _handleStyles(span, delta, outerStyle); span = span.substring(start); - start = _handleLinks(span, delta, outerStyle); - var remaining = span.substring(start); - if (remaining.isNotEmpty) { + if (span.isNotEmpty) { + start = _handleLinks(span, delta, outerStyle); + span = span.substring(start); + } + + if (span.isNotEmpty) { if (addNewLine) { - delta.insert('$remaining\n', outerStyle); + delta.insert('$span\n', outerStyle); } else { - delta.insert(remaining, outerStyle); + delta.insert(span, outerStyle); } } else if (addNewLine) { delta.insert('\n', outerStyle); @@ -87,7 +191,7 @@ class _NotusMarkdownDecoder extends Converter { } var text = match.group(2); - Map newStyle = attributesByStyleLength[match.group(1).length]; + var newStyle = Map.from(_attributesByStyleLength[match.group(1).length]); if (outerStyle != null) { newStyle.addAll(outerStyle); } @@ -109,11 +213,11 @@ class _NotusMarkdownDecoder extends Converter { var text = match.group(1); var href = match.group(2); - Map attributes = {'a': href}; // NotusAttribute.link.fromString(href).toJson(); + Map newAttributes = {'a': href}; // NotusAttribute.link.fromString(href).toJson(); if (outerStyle != null) { - attributes.addAll(outerStyle); + newAttributes.addAll(outerStyle); } - _handleSpan(text, delta, false, attributes); + _handleSpan(text, delta, false, newAttributes); start = match.end; }); diff --git a/packages/notus/test/convert/markdown_test.dart b/packages/notus/test/convert/markdown_test.dart index 0f4762bd5..4c8c164ca 100644 --- a/packages/notus/test/convert/markdown_test.dart +++ b/packages/notus/test/convert/markdown_test.dart @@ -221,36 +221,11 @@ void main() { expect(andBack, markdown); }); - test('foo', () { - runFor(NotusAttribute attribute, String markdown) { - final delta = notusMarkdown.decode(markdown); -// expect(delta.elementAt(0).data, 'First line\nSecond line\n'); - final andBack = notusMarkdown.encode(delta); - expect(andBack, markdown); - -// ..insert('This ') -// ..insert('house', attribute.toJson()) -// ..insert(' is a ') -// ..insert('circus', attribute.toJson()) -// ..insert('\n'); - } - -// runFor(NotusAttribute.bold, 'This **house** is a __circus__\n\n'); -// runFor(NotusAttribute.italic, 'This _house_ is a *circus*\n\n'); -// runFor(NotusAttribute.bold, '*italic*\n\n'); - runFor(NotusAttribute.bold, '_italic_\n\n'); - runFor(NotusAttribute.bold, '**bold**\n\n'); -// runFor(NotusAttribute.bold, '__bold__\n\n'); - runFor(NotusAttribute.bold, '**_bold and italic_**\n\n'); -// runFor(NotusAttribute.bold, '***italic and bold***\n\n'); - runFor(NotusAttribute.bold, 'Here is some *italic * star* and _italic _ lodash_ and **bold * star** and __bold _ lodash__\n\n'); - }); - test('heading styles', () { runFor(String markdown, int level) { final delta = notusMarkdown.decode(markdown); - expect(delta.elementAt(0).data, 'This is an H$level'); - expect(delta.elementAt(1).attributes['heading'], level); + expect(delta.elementAt(0).data, 'This is an H$level\n'); + expect(delta.elementAt(0).attributes['heading'], level); final andBack = notusMarkdown.encode(delta); expect(andBack, markdown); } @@ -259,6 +234,90 @@ void main() { runFor('## This is an H2\n\n', 2); runFor('### This is an H3\n\n', 3); }); + + test('ul', () { + var markdown = '* a bullet point\n* another bullet point\n\n'; + final delta = notusMarkdown.decode(markdown); + + final andBack = notusMarkdown.encode(delta); + expect(andBack, markdown); + }); + + test('ol', () { + var markdown = '1. 1st point\n1. 2nd point\n\n'; + final delta = notusMarkdown.decode(markdown); + + final andBack = notusMarkdown.encode(delta); + expect(andBack, markdown); + }); + + test('simple bq', () { +// var markdown = '> quote\n> > nested\n>#Heading\n>**bold**\n>_italics_\n>* bullet\n>1. 1st point\n>1. 2nd point\n\n'; + var markdown = '> quote\n> # Heading in Quote\n> # **Styled** heading in _block quote_\n> **bold text**\n> _text in italics_\n\n'; + final delta = notusMarkdown.decode(markdown); + + expect(delta.elementAt(0).data, 'quote\n'); + expect(delta.elementAt(0).attributes['block'], 'quote'); + expect(delta.elementAt(0).attributes.length, 1); + + expect(delta.elementAt(1).data, 'Heading in Quote\n'); + expect(delta.elementAt(1).attributes['block'], 'quote'); + expect(delta.elementAt(1).attributes['heading'], 1); + expect(delta.elementAt(1).attributes.length, 2); + + expect(delta.elementAt(2).data, 'Styled'); + expect(delta.elementAt(2).attributes['block'], 'quote'); + expect(delta.elementAt(2).attributes['heading'], 1); + expect(delta.elementAt(2).attributes['b'], true); + expect(delta.elementAt(2).attributes.length, 3); + + expect(delta.elementAt(3).data, ' heading in '); + expect(delta.elementAt(3).attributes['block'], 'quote'); + expect(delta.elementAt(3).attributes['heading'], 1); + expect(delta.elementAt(3).attributes.length, 2); + + expect(delta.elementAt(4).data, 'block quote'); + expect(delta.elementAt(4).attributes['block'], 'quote'); + expect(delta.elementAt(4).attributes['heading'], 1); + expect(delta.elementAt(4).attributes['i'], true); + expect(delta.elementAt(4).attributes.length, 3); + + expect(delta.elementAt(6).data, 'bold text'); + expect(delta.elementAt(6).attributes['block'], 'quote'); + expect(delta.elementAt(6).attributes['b'], true); + expect(delta.elementAt(6).attributes.length, 2); + + expect(delta.elementAt(8).data, 'text in italics'); + expect(delta.elementAt(8).attributes['block'], 'quote'); + expect(delta.elementAt(8).attributes['i'], true); + expect(delta.elementAt(8).attributes.length, 2); + + final andBack = notusMarkdown.encode(delta); + expect(andBack, markdown); + }); + + /*test('nested bq', () { + var markdown = '> > nested\n>* bullet\n>1. 1st point\n>1. 2nd point\n\n'; + final delta = notusMarkdown.decode(markdown); + + final andBack = notusMarkdown.encode(delta); + expect(andBack, markdown); + }); + + test('code in bq', () { + var markdown = '> ```\n> print("Hello world!")\n> ```\n\n'; + final delta = notusMarkdown.decode(markdown); + + final andBack = notusMarkdown.encode(delta); + expect(andBack, markdown); + });*/ + + test('multiple styles', () { + final delta = notusMarkdown.decode(expectedMarkdown); +// expect(delta, doc); + final andBack = notusMarkdown.encode(delta); + expect(andBack, expectedMarkdown); + }); }); group('$NotusMarkdownCodec.encode', () { From 5d833dd2dfdff6b641edd001cea79cbe7b4fbc62 Mon Sep 17 00:00:00 2001 From: Nicholas Albion Date: Thu, 24 Oct 2019 15:05:12 +1100 Subject: [PATCH 4/8] :art: dartfmt --- packages/notus/lib/src/convert/markdown.dart | 56 ++++++++++++------- .../notus/test/convert/markdown_test.dart | 36 ++++++++---- 2 files changed, 61 insertions(+), 31 deletions(-) diff --git a/packages/notus/lib/src/convert/markdown.dart b/packages/notus/lib/src/convert/markdown.dart index ca31f5a06..ad49b71ab 100644 --- a/packages/notus/lib/src/convert/markdown.dart +++ b/packages/notus/lib/src/convert/markdown.dart @@ -20,9 +20,9 @@ class NotusMarkdownCodec extends Codec { class _NotusMarkdownDecoder extends Converter { final List> _attributesByStyleLength = [ null, - {'i': true}, // _ - {'b': true }, // ** - {'i': true, 'b': true } // **_ + {'i': true}, // _ + {'b': true}, // ** + {'i': true, 'b': true} // **_ ]; final RegExp _headingRegExp = RegExp(r'(#+) *(.+)'); final RegExp _styleRegExp = RegExp(r'((?:\*|_){1,3})(.*?[^\1 ])\1'); @@ -46,7 +46,7 @@ class _NotusMarkdownDecoder extends Converter { return delta; } - + _handleLine(String line, Delta delta, [Map attributes]) { if (_handleBlockQuote(line, delta, attributes)) { return; @@ -65,7 +65,8 @@ class _NotusMarkdownDecoder extends Converter { /// 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 attributes]) { + bool _handleBlock(String line, Delta delta, + [Map attributes]) { var match; match = _codeRegExp.matchAsPrefix(line); @@ -74,12 +75,16 @@ class _NotusMarkdownDecoder extends Converter { return true; } if (_inBlockStack) { - delta.insert(line + '\n', NotusAttribute.code.toJson()); // TODO: replace with?: {'quote': true}) + 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)) { + if (_handleOrderedList(line, delta, attributes) || + _handleUnorderedList(line, delta, attributes)) { return true; } @@ -87,11 +92,14 @@ class _NotusMarkdownDecoder extends Converter { } /// all blocks are supported within bq - bool _handleBlockQuote(String line, Delta delta, [Map attributes]) { + bool _handleBlockQuote(String line, Delta delta, + [Map attributes]) { var match = _bqRegExp.matchAsPrefix(line); if (match != null) { var span = match.group(1); - Map newAttributes = {'block': 'quote'}; // NotusAttribute.bq.toJson(); + Map newAttributes = { + 'block': 'quote' + }; // NotusAttribute.bq.toJson(); if (attributes != null) { newAttributes.addAll(attributes); } @@ -103,7 +111,8 @@ class _NotusMarkdownDecoder extends Converter { } /// ol is supported within ol and bq, but not supported within ul - bool _handleOrderedList(String line, Delta delta, [Map attributes]) { + bool _handleOrderedList(String line, Delta delta, + [Map attributes]) { var match = _olRegExp.matchAsPrefix(line); if (match != null) { // TODO: support nesting @@ -120,10 +129,11 @@ class _NotusMarkdownDecoder extends Converter { return false; } - bool _handleUnorderedList(String line, Delta delta, [Map attributes]) { + bool _handleUnorderedList(String line, Delta delta, + [Map attributes]) { var match = _ulRegExp.matchAsPrefix(line); if (match != null) { - var depth = match.group(1).length / 3; + var depth = match.group(1).length / 3; var span = match.group(2); Map newAttributes = NotusAttribute.ul.toJson(); if (attributes != null) { @@ -135,12 +145,14 @@ class _NotusMarkdownDecoder extends Converter { } return false; } - + _handleHeading(String line, Delta delta, [Map attributes]) { var match = _headingRegExp.matchAsPrefix(line); if (match != null) { var level = match.group(1).length; - Map newAttributes = {'heading': level}; // NotusAttribute.heading.withValue(level).toJson(); + Map newAttributes = { + 'heading': level + }; // NotusAttribute.heading.withValue(level).toJson(); if (attributes != null) { newAttributes.addAll(attributes); } @@ -151,11 +163,12 @@ class _NotusMarkdownDecoder extends Converter { // delta.insert('\n', attribute.toJson()); return true; } - + return false; } - _handleSpan(String span, Delta delta, bool addNewLine, Map outerStyle) { + _handleSpan(String span, Delta delta, bool addNewLine, + Map outerStyle) { var start = _handleStyles(span, delta, outerStyle); span = span.substring(start); @@ -183,7 +196,9 @@ class _NotusMarkdownDecoder extends Converter { 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); + start = match.start - + 1 + + _handleLinks(span.substring(match.start - 1), delta, outerStyle); return; } else { delta.insert(span.substring(start, match.start), outerStyle); @@ -191,7 +206,8 @@ class _NotusMarkdownDecoder extends Converter { } var text = match.group(2); - var newStyle = Map.from(_attributesByStyleLength[match.group(1).length]); + var newStyle = Map.from( + _attributesByStyleLength[match.group(1).length]); if (outerStyle != null) { newStyle.addAll(outerStyle); } @@ -213,7 +229,9 @@ class _NotusMarkdownDecoder extends Converter { var text = match.group(1); var href = match.group(2); - Map newAttributes = {'a': href}; // NotusAttribute.link.fromString(href).toJson(); + Map newAttributes = { + 'a': href + }; // NotusAttribute.link.fromString(href).toJson(); if (outerStyle != null) { newAttributes.addAll(outerStyle); } diff --git a/packages/notus/test/convert/markdown_test.dart b/packages/notus/test/convert/markdown_test.dart index 4c8c164ca..545967e70 100644 --- a/packages/notus/test/convert/markdown_test.dart +++ b/packages/notus/test/convert/markdown_test.dart @@ -55,8 +55,12 @@ void main() { } } - runFor('Okay, _this is in italics_ and _so is all of _ this_ but this is not\n\n', true); - runFor('Okay, *this is in italics* and *so is all of _ this* but this is not\n\n', false); + runFor( + 'Okay, _this is in italics_ and _so is all of _ this_ but this is not\n\n', + true); + runFor( + 'Okay, *this is in italics* and *so is all of _ this* but this is not\n\n', + false); }); test('bold', () { @@ -80,14 +84,14 @@ void main() { final delta = notusMarkdown.decode(markdown); expect(delta.elementAt(0).data, 'Okay, '); expect(delta.elementAt(0).attributes, null); - + expect(delta.elementAt(1).data, 'this is bold'); expect(delta.elementAt(1).attributes["b"], true); expect(delta.elementAt(1).attributes["i"], null); - + expect(delta.elementAt(3).data, 'so is all of __ this'); expect(delta.elementAt(3).attributes["b"], true); - + expect(delta.elementAt(4).data, ' but this is not\n'); expect(delta.elementAt(4).attributes, null); if (testEncode) { @@ -95,9 +99,13 @@ void main() { expect(andBack, markdown); } } - - runFor('Okay, **this is bold** and **so is all of __ this** but this is not\n\n', true); - runFor('Okay, __this is bold__ and __so is all of __ this__ but this is not\n\n', false); + + runFor( + 'Okay, **this is bold** and **so is all of __ this** but this is not\n\n', + true); + runFor( + 'Okay, __this is bold__ and __so is all of __ this__ but this is not\n\n', + false); }); test('intersecting inline styles', () { @@ -162,9 +170,12 @@ void main() { } } - runFor('**this is bold** _this is in italics_ and **_this is both_**\n\n', true); - runFor('**this is bold** *this is in italics* and ***this is both***\n\n', false); - runFor('__this is bold__ _this is in italics_ and ___this is both___\n\n', false); + runFor('**this is bold** _this is in italics_ and **_this is both_**\n\n', + true); + runFor('**this is bold** *this is in italics* and ***this is both***\n\n', + false); + runFor('__this is bold__ _this is in italics_ and ___this is both___\n\n', + false); }); test('link', () { @@ -253,7 +264,8 @@ void main() { test('simple bq', () { // var markdown = '> quote\n> > nested\n>#Heading\n>**bold**\n>_italics_\n>* bullet\n>1. 1st point\n>1. 2nd point\n\n'; - var markdown = '> quote\n> # Heading in Quote\n> # **Styled** heading in _block quote_\n> **bold text**\n> _text in italics_\n\n'; + var markdown = + '> quote\n> # Heading in Quote\n> # **Styled** heading in _block quote_\n> **bold text**\n> _text in italics_\n\n'; final delta = notusMarkdown.decode(markdown); expect(delta.elementAt(0).data, 'quote\n'); From 06c4ebc1fc451ce265010fcc9f2707617ce2e99f Mon Sep 17 00:00:00 2001 From: Nicholas Albion Date: Thu, 24 Oct 2019 15:20:38 +1100 Subject: [PATCH 5/8] :ok_hand: fixed dartanalyzer warning --- packages/notus/lib/src/convert/markdown.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/notus/lib/src/convert/markdown.dart b/packages/notus/lib/src/convert/markdown.dart index ad49b71ab..21440b5c7 100644 --- a/packages/notus/lib/src/convert/markdown.dart +++ b/packages/notus/lib/src/convert/markdown.dart @@ -133,7 +133,7 @@ class _NotusMarkdownDecoder extends Converter { [Map attributes]) { var match = _ulRegExp.matchAsPrefix(line); if (match != null) { - var depth = match.group(1).length / 3; +// var depth = match.group(1).length / 3; var span = match.group(2); Map newAttributes = NotusAttribute.ul.toJson(); if (attributes != null) { From ea87eede4aa519dbd149012bdfb45ad568bf14fd Mon Sep 17 00:00:00 2001 From: Ambrose Date: Fri, 7 Aug 2020 13:43:56 +0100 Subject: [PATCH 6/8] Fix markdown decoder to produce well formed delta and attributes --- packages/notus/lib/src/convert/markdown.dart | 96 ++++++--- .../notus/test/convert/markdown_test.dart | 195 +++++++++--------- 2 files changed, 169 insertions(+), 122 deletions(-) diff --git a/packages/notus/lib/src/convert/markdown.dart b/packages/notus/lib/src/convert/markdown.dart index f77bc0b83..673c4a779 100644 --- a/packages/notus/lib/src/convert/markdown.dart +++ b/packages/notus/lib/src/convert/markdown.dart @@ -4,8 +4,8 @@ import 'dart:convert'; -import 'package:notus/notus.dart'; import 'package:quill_delta/quill_delta.dart'; +import 'package:notus/notus.dart'; class NotusMarkdownCodec extends Codec { const NotusMarkdownCodec(); @@ -47,7 +47,7 @@ class _NotusMarkdownDecoder extends Converter { return delta; } - _handleLine(String line, Delta delta, [Map attributes]) { + void _handleLine(String line, Delta delta, [Map attributes, bool isBlock]) { if (_handleBlockQuote(line, delta, attributes)) { return; } @@ -59,7 +59,7 @@ class _NotusMarkdownDecoder extends Converter { } if (line.isNotEmpty) { - _handleSpan(line, delta, true, attributes); + _handleSpan(line, delta, true, attributes, isBlock); } } @@ -97,14 +97,12 @@ class _NotusMarkdownDecoder extends Converter { var match = _bqRegExp.matchAsPrefix(line); if (match != null) { var span = match.group(1); - Map newAttributes = { - 'block': 'quote' - }; // NotusAttribute.bq.toJson(); + var newAttributes = NotusAttribute.bq.toJson(); // NotusAttribute.bq.toJson(); if (attributes != null) { newAttributes.addAll(attributes); } // all blocks are supported within bq - _handleLine(span, delta, newAttributes); + _handleLine(span, delta, newAttributes, true); return true; } return false; @@ -118,12 +116,12 @@ class _NotusMarkdownDecoder extends Converter { // TODO: support nesting // var depth = match.group(1).length / 3; var span = match.group(2); - Map newAttributes = NotusAttribute.ol.toJson(); + 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); + _handleSpan(span, delta, true, newAttributes, true); return true; } return false; @@ -135,22 +133,22 @@ class _NotusMarkdownDecoder extends Converter { if (match != null) { // var depth = match.group(1).length / 3; var span = match.group(2); - Map newAttributes = NotusAttribute.ul.toJson(); + 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); + _handleSpan(span, delta, true, newAttributes, true); return true; } return false; } - _handleHeading(String line, Delta delta, [Map attributes]) { + bool _handleHeading(String line, Delta delta, [Map attributes]) { var match = _headingRegExp.matchAsPrefix(line); if (match != null) { var level = match.group(1).length; - Map newAttributes = { + var newAttributes = { 'heading': level }; // NotusAttribute.heading.withValue(level).toJson(); if (attributes != null) { @@ -159,7 +157,7 @@ class _NotusMarkdownDecoder extends Converter { var span = match.group(2); // TODO: true or false? - _handleSpan(span, delta, true, newAttributes); + _handleSpan(span, delta, true, newAttributes, true); // delta.insert('\n', attribute.toJson()); return true; } @@ -167,8 +165,8 @@ class _NotusMarkdownDecoder extends Converter { return false; } - _handleSpan(String span, Delta delta, bool addNewLine, - Map outerStyle) { + void _handleSpan(String span, Delta delta, bool addNewLine, + Map outerStyle, [bool isBlock]) { var start = _handleStyles(span, delta, outerStyle); span = span.substring(start); @@ -179,7 +177,12 @@ class _NotusMarkdownDecoder extends Converter { if (span.isNotEmpty) { if (addNewLine) { - delta.insert('$span\n', outerStyle); + if(isBlock != null && isBlock){ + delta.insert(span); + delta.insert('\n', outerStyle); + } else { + delta.insert('$span\n', outerStyle); + } } else { delta.insert(span, outerStyle); } @@ -188,29 +191,36 @@ class _NotusMarkdownDecoder extends Converter { } } - _handleStyles(String span, Delta delta, Map outerStyle) { + int _handleStyles(String span, Delta delta, Map 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) == '[') { - delta.insert(span.substring(start, match.start - 1), outerStyle); + 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, outerStyle); + _handleLinks(span.substring(match.start - 1), delta, validInlineStyles); return; } else { - delta.insert(span.substring(start, match.start), outerStyle); + var text = span.substring(start, match.start); + + validInlineStyles != null ? delta.insert(text, validInlineStyles) : delta.insert(text); } } var text = match.group(2); var newStyle = Map.from( _attributesByStyleLength[match.group(1).length]); - if (outerStyle != null) { - newStyle.addAll(outerStyle); + + var validInlineStyles = _getValidInlineStyles(outerStyle); + if (validInlineStyles != null) { + newStyle.addAll(validInlineStyles); } + _handleSpan(text, delta, false, newStyle); start = match.end; }); @@ -218,23 +228,49 @@ class _NotusMarkdownDecoder extends Converter { return start; } - _handleLinks(String span, Delta delta, Map outerStyle) { + Map _getValidInlineStyles(Map outerStyle) { + Map 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 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 = span.substring(start, match.start); + delta.insert(text); //, outerStyle); } var text = match.group(1); var href = match.group(2); - Map newAttributes = { + var newAttributes = { 'a': href }; // NotusAttribute.link.fromString(href).toJson(); - if (outerStyle != null) { - newAttributes.addAll(outerStyle); + + var validInlineStyles = _getValidInlineStyles(outerStyle); + if (validInlineStyles != null) { + newAttributes.addAll(validInlineStyles); } + _handleSpan(text, delta, false, newAttributes); start = match.end; }); @@ -259,7 +295,7 @@ class _NotusMarkdownEncoder extends Converter { final lineBuffer = StringBuffer(); NotusAttribute currentBlockStyle; var currentInlineStyle = NotusStyle(); - var currentBlockLines = []; + var currentBlockLines = []; void _handleBlock(NotusAttribute blockStyle) { if (currentBlockLines.isEmpty) { @@ -435,4 +471,4 @@ class _NotusMarkdownEncoder extends Converter { buffer.write(tag); } } -} +} \ No newline at end of file diff --git a/packages/notus/test/convert/markdown_test.dart b/packages/notus/test/convert/markdown_test.dart index 052a3faa6..2ecb93df3 100644 --- a/packages/notus/test/convert/markdown_test.dart +++ b/packages/notus/test/convert/markdown_test.dart @@ -1,12 +1,13 @@ + // Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -import 'dart:convert'; -import 'package:notus/convert.dart'; -import 'package:notus/notus.dart'; -import 'package:quill_delta/quill_delta.dart'; +import 'dart:convert'; import 'package:test/test.dart'; +import 'package:quill_delta/quill_delta.dart'; +import 'package:notus/notus.dart'; +import 'package:notus/convert.dart'; void main() { group('$NotusMarkdownCodec.encode', () { @@ -19,11 +20,11 @@ void main() { }); test('italics', () { - runFor(String markdown, bool testEncode) { + void runFor(String markdown, bool testEncode) { final delta = notusMarkdown.decode(markdown); expect(delta.elementAt(0).data, 'italics'); - expect(delta.elementAt(0).attributes["i"], true); - expect(delta.elementAt(0).attributes["b"], null); + expect(delta.elementAt(0).attributes['i'], true); + expect(delta.elementAt(0).attributes['b'], null); if (testEncode) { final andBack = notusMarkdown.encode(delta); expect(andBack, markdown); @@ -35,17 +36,17 @@ void main() { }); test('multi-word italics', () { - runFor(String markdown, bool testEncode) { + void runFor(String markdown, bool testEncode) { final delta = notusMarkdown.decode(markdown); expect(delta.elementAt(0).data, 'Okay, '); expect(delta.elementAt(0).attributes, null); expect(delta.elementAt(1).data, 'this is in italics'); - expect(delta.elementAt(1).attributes["i"], true); - expect(delta.elementAt(1).attributes["b"], null); + expect(delta.elementAt(1).attributes['i'], true); + expect(delta.elementAt(1).attributes['b'], null); expect(delta.elementAt(3).data, 'so is all of _ this'); - expect(delta.elementAt(3).attributes["i"], true); + expect(delta.elementAt(3).attributes['i'], true); expect(delta.elementAt(4).data, ' but this is not\n'); expect(delta.elementAt(4).attributes, null); @@ -64,11 +65,11 @@ void main() { }); test('bold', () { - runFor(String markdown, bool testEncode) { + void runFor(String markdown, bool testEncode) { final delta = notusMarkdown.decode(markdown); expect(delta.elementAt(0).data, 'bold'); - expect(delta.elementAt(0).attributes["b"], true); - expect(delta.elementAt(0).attributes["i"], null); + expect(delta.elementAt(0).attributes['b'], true); + expect(delta.elementAt(0).attributes['i'], null); if (testEncode) { final andBack = notusMarkdown.encode(delta); expect(andBack, markdown); @@ -80,17 +81,17 @@ void main() { }); test('multi-word bold', () { - runFor(String markdown, bool testEncode) { + void runFor(String markdown, bool testEncode) { final delta = notusMarkdown.decode(markdown); expect(delta.elementAt(0).data, 'Okay, '); expect(delta.elementAt(0).attributes, null); expect(delta.elementAt(1).data, 'this is bold'); - expect(delta.elementAt(1).attributes["b"], true); - expect(delta.elementAt(1).attributes["i"], null); + expect(delta.elementAt(1).attributes['b'], true); + expect(delta.elementAt(1).attributes['i'], null); expect(delta.elementAt(3).data, 'so is all of __ this'); - expect(delta.elementAt(3).attributes["b"], true); + expect(delta.elementAt(3).attributes['b'], true); expect(delta.elementAt(4).data, ' but this is not\n'); expect(delta.elementAt(4).attributes, null); @@ -112,27 +113,27 @@ void main() { var markdown = 'This **house _is a_ circus**\n\n'; final delta = notusMarkdown.decode(markdown); expect(delta.elementAt(1).data, 'house '); - expect(delta.elementAt(1).attributes["b"], true); - expect(delta.elementAt(1).attributes["i"], null); + expect(delta.elementAt(1).attributes['b'], true); + expect(delta.elementAt(1).attributes['i'], null); expect(delta.elementAt(2).data, 'is a'); - expect(delta.elementAt(2).attributes["b"], true); - expect(delta.elementAt(2).attributes["i"], true); + expect(delta.elementAt(2).attributes['b'], true); + expect(delta.elementAt(2).attributes['i'], true); expect(delta.elementAt(3).data, ' circus'); - expect(delta.elementAt(3).attributes["b"], true); - expect(delta.elementAt(3).attributes["i"], null); + expect(delta.elementAt(3).attributes['b'], true); + expect(delta.elementAt(3).attributes['i'], null); final andBack = notusMarkdown.encode(delta); expect(andBack, markdown); }); test('bold and italics', () { - runFor(String markdown, bool testEncode) { + void runFor(String markdown, bool testEncode) { final delta = notusMarkdown.decode(markdown); expect(delta.elementAt(0).data, 'this is bold and italic'); - expect(delta.elementAt(0).attributes["b"], true); - expect(delta.elementAt(0).attributes["i"], true); + expect(delta.elementAt(0).attributes['b'], true); + expect(delta.elementAt(0).attributes['i'], true); expect(delta.elementAt(1).data, '\n'); expect(delta.length, 2); @@ -150,19 +151,19 @@ void main() { }); test('bold and italics combinations', () { - runFor(String markdown, bool testEncode) { + void runFor(String markdown, bool testEncode) { final delta = notusMarkdown.decode(markdown); expect(delta.elementAt(0).data, 'this is bold'); - expect(delta.elementAt(0).attributes["b"], true); - expect(delta.elementAt(0).attributes["i"], null); + expect(delta.elementAt(0).attributes['b'], true); + expect(delta.elementAt(0).attributes['i'], null); expect(delta.elementAt(2).data, 'this is in italics'); - expect(delta.elementAt(2).attributes["b"], null); - expect(delta.elementAt(2).attributes["i"], true); + expect(delta.elementAt(2).attributes['b'], null); + expect(delta.elementAt(2).attributes['i'], true); expect(delta.elementAt(4).data, 'this is both'); - expect(delta.elementAt(4).attributes["b"], true); - expect(delta.elementAt(4).attributes["i"], true); + expect(delta.elementAt(4).attributes['b'], true); + expect(delta.elementAt(4).attributes['i'], true); if (testEncode) { final andBack = notusMarkdown.encode(delta); @@ -183,12 +184,12 @@ void main() { final delta = notusMarkdown.decode(markdown); expect(delta.elementAt(1).data, 'house'); - expect(delta.elementAt(1).attributes["b"], true); - expect(delta.elementAt(1).attributes["a"], null); + expect(delta.elementAt(1).attributes['b'], true); + expect(delta.elementAt(1).attributes['a'], null); expect(delta.elementAt(3).data, 'circus'); - expect(delta.elementAt(3).attributes["b"], null); - expect(delta.elementAt(3).attributes["a"], 'https://github.com'); + expect(delta.elementAt(3).attributes['b'], null); + expect(delta.elementAt(3).attributes['a'], 'https://github.com'); final andBack = notusMarkdown.encode(delta); expect(andBack, markdown); @@ -199,12 +200,12 @@ void main() { final delta = notusMarkdown.decode(markdown); expect(delta.elementAt(1).data, 'house'); - expect(delta.elementAt(1).attributes["b"], true); - expect(delta.elementAt(1).attributes["a"], null); + expect(delta.elementAt(1).attributes['b'], true); + expect(delta.elementAt(1).attributes['a'], null); expect(delta.elementAt(3).data, 'circus'); - expect(delta.elementAt(3).attributes["b"], true); - expect(delta.elementAt(3).attributes["a"], 'https://github.com'); + expect(delta.elementAt(3).attributes['b'], true); + expect(delta.elementAt(3).attributes['a'], 'https://github.com'); final andBack = notusMarkdown.encode(delta); expect(andBack, markdown); @@ -215,15 +216,15 @@ void main() { final delta = notusMarkdown.decode(markdown); expect(delta.elementAt(1).data, 'house'); - expect(delta.elementAt(1).attributes["b"], true); - expect(delta.elementAt(1).attributes["a"], null); + expect(delta.elementAt(1).attributes['b'], true); + expect(delta.elementAt(1).attributes['a'], null); expect(delta.elementAt(2).data, ' is a '); expect(delta.elementAt(2).attributes, null); expect(delta.elementAt(3).data, 'circus'); - expect(delta.elementAt(3).attributes["b"], true); - expect(delta.elementAt(3).attributes["a"], 'https://github.com'); + expect(delta.elementAt(3).attributes['b'], true); + expect(delta.elementAt(3).attributes['a'], 'https://github.com'); expect(delta.elementAt(4).data, '\n'); expect(delta.length, 5); @@ -233,10 +234,12 @@ void main() { }); test('heading styles', () { - runFor(String markdown, int level) { + void runFor(String markdown, int level) { final delta = notusMarkdown.decode(markdown); - expect(delta.elementAt(0).data, 'This is an H$level\n'); - expect(delta.elementAt(0).attributes['heading'], level); + expect(delta.elementAt(0).data, 'This is an H$level'); + + expect(delta.elementAt(1).data, '\n'); + expect(delta.elementAt(1).attributes['heading'], level); final andBack = notusMarkdown.encode(delta); expect(andBack, markdown); } @@ -249,6 +252,7 @@ void main() { test('ul', () { var markdown = '* a bullet point\n* another bullet point\n\n'; final delta = notusMarkdown.decode(markdown); + print(delta); final andBack = notusMarkdown.encode(delta); expect(andBack, markdown); @@ -268,61 +272,71 @@ void main() { '> quote\n> # Heading in Quote\n> # **Styled** heading in _block quote_\n> **bold text**\n> _text in italics_\n\n'; final delta = notusMarkdown.decode(markdown); - expect(delta.elementAt(0).data, 'quote\n'); - expect(delta.elementAt(0).attributes['block'], 'quote'); - expect(delta.elementAt(0).attributes.length, 1); + expect(delta.elementAt(0).data, 'quote'); + expect(delta.elementAt(0).attributes, null); - expect(delta.elementAt(1).data, 'Heading in Quote\n'); + expect(delta.elementAt(1).data, '\n'); expect(delta.elementAt(1).attributes['block'], 'quote'); - expect(delta.elementAt(1).attributes['heading'], 1); - expect(delta.elementAt(1).attributes.length, 2); + expect(delta.elementAt(1).attributes.length, 1); - expect(delta.elementAt(2).data, 'Styled'); - expect(delta.elementAt(2).attributes['block'], 'quote'); - expect(delta.elementAt(2).attributes['heading'], 1); - expect(delta.elementAt(2).attributes['b'], true); - expect(delta.elementAt(2).attributes.length, 3); + expect(delta.elementAt(2).data, 'Heading in Quote'); + expect(delta.elementAt(2).attributes, null); - expect(delta.elementAt(3).data, ' heading in '); + expect(delta.elementAt(3).data, '\n'); expect(delta.elementAt(3).attributes['block'], 'quote'); expect(delta.elementAt(3).attributes['heading'], 1); expect(delta.elementAt(3).attributes.length, 2); - expect(delta.elementAt(4).data, 'block quote'); - expect(delta.elementAt(4).attributes['block'], 'quote'); - expect(delta.elementAt(4).attributes['heading'], 1); - expect(delta.elementAt(4).attributes['i'], true); - expect(delta.elementAt(4).attributes.length, 3); + expect(delta.elementAt(4).data, 'Styled'); + expect(delta.elementAt(4).attributes['b'], true); + expect(delta.elementAt(4).attributes.length, 1); - expect(delta.elementAt(6).data, 'bold text'); - expect(delta.elementAt(6).attributes['block'], 'quote'); - expect(delta.elementAt(6).attributes['b'], true); - expect(delta.elementAt(6).attributes.length, 2); + expect(delta.elementAt(5).data, ' heading in '); + expect(delta.elementAt(5).attributes, null); - expect(delta.elementAt(8).data, 'text in italics'); - expect(delta.elementAt(8).attributes['block'], 'quote'); - expect(delta.elementAt(8).attributes['i'], true); - expect(delta.elementAt(8).attributes.length, 2); + expect(delta.elementAt(6).data, 'block quote'); + expect(delta.elementAt(6).attributes['i'], true); + expect(delta.elementAt(6).attributes.length, 1); - final andBack = notusMarkdown.encode(delta); - expect(andBack, markdown); - }); + expect(delta.elementAt(7).data, '\n'); + expect(delta.elementAt(7).attributes['block'], 'quote'); + expect(delta.elementAt(7).attributes['heading'], 1); + expect(delta.elementAt(7).attributes.length, 2); - /*test('nested bq', () { - var markdown = '> > nested\n>* bullet\n>1. 1st point\n>1. 2nd point\n\n'; - final delta = notusMarkdown.decode(markdown); + expect(delta.elementAt(8).data, 'bold text'); + expect(delta.elementAt(8).attributes['b'], true); + expect(delta.elementAt(8).attributes.length, 1); + + expect(delta.elementAt(9).data, '\n'); + expect(delta.elementAt(9).attributes['block'], 'quote'); + expect(delta.elementAt(9).attributes.length, 1); + + expect(delta.elementAt(10).data, 'text in italics'); + expect(delta.elementAt(10).attributes['i'], true); + expect(delta.elementAt(10).attributes.length, 1); + + expect(delta.elementAt(11).data, '\n'); + expect(delta.elementAt(11).attributes['block'], 'quote'); + expect(delta.elementAt(11).attributes.length, 1); final andBack = notusMarkdown.encode(delta); expect(andBack, markdown); }); - test('code in bq', () { - var markdown = '> ```\n> print("Hello world!")\n> ```\n\n'; - final delta = notusMarkdown.decode(markdown); +// test('nested bq', () { +// var markdown = '> > nested\n>* bullet\n>1. 1st point\n>1. 2nd point\n\n'; +// final delta = notusMarkdown.decode(markdown); +// final andBack = notusMarkdown.encode(delta); +// expect(andBack, markdown); +// }); - final andBack = notusMarkdown.encode(delta); - expect(andBack, markdown); - });*/ + +// test('code in bq', () { +// var markdown = '> ```\n> print("Hello world!")\n> ```\n\n'; +// final delta = notusMarkdown.decode(markdown); +// final andBack = notusMarkdown.encode(delta); +// expect(andBack, markdown); +// }); test('multiple styles', () { final delta = notusMarkdown.decode(expectedMarkdown); @@ -402,8 +416,7 @@ void main() { }); test('heading styles', () { - void runFor( - NotusAttribute attribute, String source, String expected) { + void runFor(NotusAttribute attribute, String source, String expected) { final delta = Delta()..insert(source)..insert('\n', attribute.toJson()); final result = notusMarkdown.encode(delta); expect(result, expected); @@ -415,8 +428,7 @@ void main() { }); test('block styles', () { - void runFor( - NotusAttribute attribute, String source, String expected) { + void runFor(NotusAttribute attribute, String source, String expected) { final delta = Delta()..insert(source)..insert('\n', attribute.toJson()); final result = notusMarkdown.encode(delta); expect(result, expected); @@ -429,8 +441,7 @@ void main() { }); test('multiline blocks', () { - void runFor( - NotusAttribute attribute, String source, String expected) { + void runFor(NotusAttribute attribute, String source, String expected) { final delta = Delta() ..insert(source) ..insert('\n', attribute.toJson()) From a4fa0e8324a979e33af1912584300409c6cab9ef Mon Sep 17 00:00:00 2001 From: Ambrose Date: Fri, 7 Aug 2020 13:50:11 +0100 Subject: [PATCH 7/8] Handle empty documents and documents with only line breaks --- packages/notus/lib/src/convert/markdown.dart | 34 ++++++++++++++++-- .../notus/test/convert/markdown_test.dart | 35 ++++++++++++++++++- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/packages/notus/lib/src/convert/markdown.dart b/packages/notus/lib/src/convert/markdown.dart index 673c4a779..0432d7a3c 100644 --- a/packages/notus/lib/src/convert/markdown.dart +++ b/packages/notus/lib/src/convert/markdown.dart @@ -40,13 +40,28 @@ class _NotusMarkdownDecoder extends Converter { final lines = input.split('\n'); final delta = Delta(); - for (var line in lines) { - _handleLine(line, delta); + if(_allLinesEmpty(lines)) { + Map style; + _handleSpan(lines[0], delta, true, style); + } else { + for (var line in lines) { + _handleLine(line, delta); + } } return delta; } + bool _allLinesEmpty(List lines) { + for (var line in lines) { + if (line != '') { + return false; + } + } + + return true; + } + void _handleLine(String line, Delta delta, [Map attributes, bool isBlock]) { if (_handleBlockQuote(line, delta, attributes)) { return; @@ -165,6 +180,7 @@ class _NotusMarkdownDecoder extends Converter { return false; } + // ignore: always_declare_return_types void _handleSpan(String span, Delta delta, bool addNewLine, Map outerStyle, [bool isBlock]) { var start = _handleStyles(span, delta, outerStyle); @@ -297,11 +313,25 @@ class _NotusMarkdownEncoder extends Converter { var currentInlineStyle = NotusStyle(); var currentBlockLines = []; + bool _allLinesEmpty(List lines) { + for (var line in lines) { + if (line != '') { + return false; + } + } + + return true; + } + void _handleBlock(NotusAttribute blockStyle) { if (currentBlockLines.isEmpty) { return; // Empty block } + if(_allLinesEmpty(currentBlockLines)){ + return; + } + if (blockStyle == null) { buffer.write(currentBlockLines.join('\n\n')); buffer.writeln(); diff --git a/packages/notus/test/convert/markdown_test.dart b/packages/notus/test/convert/markdown_test.dart index 2ecb93df3..9db191185 100644 --- a/packages/notus/test/convert/markdown_test.dart +++ b/packages/notus/test/convert/markdown_test.dart @@ -10,7 +10,23 @@ import 'package:notus/notus.dart'; import 'package:notus/convert.dart'; void main() { - group('$NotusMarkdownCodec.encode', () { + group('$NotusMarkdownCodec.decode', () { + test('should convert empty markdown to valid empty notus document', () { + final markdown = ''; + final newNotusDoc = NotusDocument(); + final delta = notusMarkdown.decode(markdown); + expect(delta.elementAt(0).data, '\n'); + expect(delta, newNotusDoc.toDelta()); + }); + + test('should convert invalid markdown with only line breaks to valid empty notus document', () { + final markdown = '\n\n\n'; + final delta = notusMarkdown.decode(markdown); + expect(delta.elementAt(0).data, '\n'); + final newNotusDoc = NotusDocument(); + expect(delta, newNotusDoc.toDelta()); + }); + test('paragraphs', () { final markdown = 'First line\n\nSecond line\n\n'; final delta = notusMarkdown.decode(markdown); @@ -347,6 +363,23 @@ void main() { }); group('$NotusMarkdownCodec.encode', () { + test('should convert empty valid notus document to empty markdown', () { + final delta = NotusDocument().toDelta(); + final result = notusMarkdown.encode(delta); + expect(result, ''); + }); + + test('should convert delta with only line breaks to empty markdown', () { + final delta = Delta() + ..insert('\n') + ..insert('\n') + ..insert('\n') + ..insert('\n'); + + final result = notusMarkdown.encode(delta); + expect(result, ''); + }); + test('split adjacent paragraphs', () { final delta = Delta()..insert('First line\nSecond line\n'); final result = notusMarkdown.encode(delta); From b0f7069c7f799236ddedb7354c92fecf95f6153a Mon Sep 17 00:00:00 2001 From: Ambrose Date: Fri, 7 Aug 2020 14:30:29 +0100 Subject: [PATCH 8/8] Remove lint ignore --- packages/notus/lib/src/convert/markdown.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/notus/lib/src/convert/markdown.dart b/packages/notus/lib/src/convert/markdown.dart index 0432d7a3c..a1d3a99e8 100644 --- a/packages/notus/lib/src/convert/markdown.dart +++ b/packages/notus/lib/src/convert/markdown.dart @@ -180,7 +180,6 @@ class _NotusMarkdownDecoder extends Converter { return false; } - // ignore: always_declare_return_types void _handleSpan(String span, Delta delta, bool addNewLine, Map outerStyle, [bool isBlock]) { var start = _handleStyles(span, delta, outerStyle);