From de2c82f4bd5ef42be3f64d3d3a0b936c95e24480 Mon Sep 17 00:00:00 2001 From: Vishesh Handa Date: Mon, 6 Apr 2020 01:10:44 +0200 Subject: [PATCH 1/4] Try to support the proper checklist format According the markdownguide.org, the checklist items are always inside a list. It can any kind of list, but they are inside a list. This commit breaks the old syntax and most of the tests. It also requires a custom version of markd :/ --- lib/core/checklist.dart | 319 ++++++++++++++++++++++++--------------- pubspec.lock | 7 + pubspec.yaml | 2 + test/checklist_test.dart | 88 +++++++++-- 4 files changed, 281 insertions(+), 135 deletions(-) diff --git a/lib/core/checklist.dart b/lib/core/checklist.dart index c80b3f26..23d305c9 100644 --- a/lib/core/checklist.dart +++ b/lib/core/checklist.dart @@ -1,27 +1,62 @@ -import 'package:markdown/markdown.dart' as md; +import 'package:markd/markdown.dart' as md; import 'package:gitjournal/core/note.dart'; class ChecklistItem { + md.Element parentListElement; md.Element element; bool get checked { - return element.attributes['checked'] != "false"; + if (element.children == null || element.children.isEmpty) { + return false; + } + + var inputEl = element.children[0] as md.Element; + assert(inputEl.attributes['class'] == 'todo'); + return inputEl.attributes.containsKey('checked'); } set checked(bool val) { - element.attributes['checked'] = val.toString(); + if (element.children == null || element.children.isEmpty) { + return; + } + var inputEl = element.children[0] as md.Element; + assert(inputEl.attributes['class'] == 'todo'); + + if (val) { + inputEl.attributes["checked"] = "checked"; + } else { + inputEl.attributes.remove('checked'); + } } String get text { - return element.attributes['text']; + if (element.children == null || element.children.isEmpty) { + return ""; + } + if (element.children.length > 1) { + return element.children[1].textContent.substring(1); + } + return ""; } set text(String val) { - element.attributes['text'] = val; + if (element.children == null || element.children.isEmpty) { + return; + } + if (element.children.length > 1) { + element.children[1] = md.Text(" $val"); + } } - ChecklistItem.fromMarkdownElement(this.element); + ChecklistItem.fromMarkdownElement(this.element, this.parentListElement) { + assert(element.children.isNotEmpty); + + // FIXME: Maybe this shouldn't be allowed + if (parentListElement != null) { + assert(parentListElement.children.contains(element)); + } + } @override String toString() => 'ChecklistItem: $checked $text'; @@ -30,51 +65,49 @@ class ChecklistItem { class Checklist { Note _note; List items; - List nodes; + + List _nodes; Checklist(this._note) { var doc = md.Document( encodeHtml: false, - inlineSyntaxes: [TaskListSyntax()], - extensionSet: md.ExtensionSet.gitHubFlavored, + blockSyntaxes: md.BlockParser.standardBlockSyntaxes, + extensionSet: md.ExtensionSet.gitHubWeb, ); - nodes = doc.parseInline(_note.body); - _cleanupNodes(nodes); - - items = ChecklistBuilder().build(nodes); - } - - void _cleanupNodes(List nodes) { - if (nodes.length <= 1) { - return; - } - - var last = nodes.last; - var secLast = nodes[nodes.length - 2]; - - if (last is! md.Text) { - return; - } - if (secLast is! md.Element) { - return; - } - var elem = secLast as md.Element; - if (elem.tag != 'input' || elem.attributes['type'] != 'checkbox') { - return; - } - - // Some times we get an extra \n in the end, not sure why. - if (last.textContent == '\n') { - nodes.length = nodes.length - 1; - if (!elem.attributes["text"].endsWith('\n')) { - elem.attributes["text"] += '\n'; + _nodes = doc.parseLines(_note.body.split('\n')); + for (var node in _nodes) { + if (node is md.Element) { + var elem = node; + _printElement(elem, ""); } } + print('---------'); + + var builder = ChecklistBuilder(); + items = builder.build(_nodes); + } + + void _printElement(md.Element elem, String indent) { + print("$indent Begin ${elem.toString()}"); + print("$indent E TAG ${elem.tag}"); + print("$indent E ATTRIBUTES ${elem.attributes}"); + print("$indent E generatedId ${elem.generatedId}"); + print("$indent E children ${elem.children}"); + if (elem.children != null) { + for (var child in elem.children) { + if (child is md.Element) { + _printElement(child, indent + " "); + } else { + print("$indent $child - ${child.textContent}"); + } + } + } + print("$indent End ${elem.toString()}"); } Note get note { - if (nodes.isEmpty) return _note; + if (_nodes.isEmpty) return _note; // Remove empty trailing items while (true) { @@ -89,8 +122,8 @@ class Checklist { } } - var renderer = CustomRenderer(); - _note.body = renderer.render(nodes); + var renderer = MarkdownRenderer(); + _note.body = renderer.render(_nodes); return _note; } @@ -105,109 +138,103 @@ class Checklist { } ChecklistItem buildItem(bool value, String text) { - var elem = md.Element.withTag("input"); - elem.attributes["type"] = "checkbox"; - elem.attributes["checked"] = value.toString(); - elem.attributes["xUpperCase"] = "false"; - elem.attributes["text"] = text; + var inputElement = md.Element.withTag('input'); + inputElement.attributes['class'] = 'todo'; + inputElement.attributes['type'] = 'checkbox'; + inputElement.attributes['disabled'] = 'disabled'; + if (value) { + inputElement.attributes['checked'] = 'checked'; + } - return ChecklistItem.fromMarkdownElement(elem); + var liElement = md.Element('li', [inputElement, md.Text(' $text')]); + liElement.attributes['class'] = 'todo'; + + // FIXME: Come on, there must be a simpler way + return ChecklistItem.fromMarkdownElement(liElement, null); } void removeItem(ChecklistItem item) { - assert(nodes.contains(item.element)); assert(items.contains(item)); - - nodes.remove(item.element); items.remove(item); + + bool foundChild = false; + var parentList = item.parentListElement; + for (var i = 0; i < parentList.children.length; i++) { + var child = parentList.children[i]; + if (child == item.element) { + foundChild = true; + parentList.children.removeAt(i); + break; + } + } + assert(foundChild); } ChecklistItem removeAt(int index) { assert(index >= 0 && index <= items.length); var item = items[index]; - assert(nodes.contains(item.element)); - - nodes.remove(item.element); - items.removeAt(index); + removeItem(item); return item; } void addItem(ChecklistItem item) { - _insertNewLineIfRequired(nodes.length - 1); + if (items.isEmpty) { + var listElement = md.Element.withTag('ul'); + _nodes.add(listElement); + item.parentListElement = listElement; + } else { + var prevItem = items.last; + item.parentListElement = prevItem.parentListElement; + } items.add(item); - nodes.add(item.element); + item.parentListElement.children.add(item.element); } void insertItem(int index, ChecklistItem item) { - assert(index <= items.length, "Trying to insert beyond the end"); - if (index == 0) { - items.insert(0, item); - nodes.insert(0, item.element); + if (index == 0 && items.isEmpty) { + addItem(item); return; } + + assert(index <= items.length, "Trying to insert beyond the end"); if (index == items.length) { addItem(item); return; } - var prevItem = items[index]; - var nodeIndex = nodes.indexOf(prevItem.element); + var prevItem = index - 1 > 0 ? items[index - 1] : items[index]; + item.parentListElement = prevItem.parentListElement; + var parentList = item.parentListElement; - _insertNewLineIfRequired(nodeIndex); - - nodes.insert(nodeIndex, item.element); - items.insert(index, item); - } - - void _insertNewLineIfRequired(int pos) { - if (nodes.isEmpty) return; - - var node = nodes[pos]; - if (node is md.Text) { - if (!node.text.endsWith('\n')) { - nodes.add(md.Text("\n")); + // Insert in correct place + bool foundChild = false; + for (var i = 0; i < parentList.children.length; i++) { + var child = parentList.children[i]; + if (child == prevItem.element) { + foundChild = true; + parentList.children.insert(i, item.element); + break; } } - } -} + assert(foundChild); -/// Copied from flutter-markdown - cannot be merged as we added xUpperCase and changed the regexp -/// Parse [task list items](https://github.github.com/gfm/#task-list-items-extension-). -class TaskListSyntax extends md.InlineSyntax { - // FIXME: Waiting for dart-lang/markdown#269 to land - static final String _pattern = r'^ *\[([ xX])\] +(.*)'; - - TaskListSyntax() : super(_pattern, startCharacter: '['.codeUnitAt(0)); - - @override - bool onMatch(md.InlineParser parser, Match match) { - md.Element el = md.Element.withTag('input'); - el.attributes['type'] = 'checkbox'; - el.attributes['checked'] = '${match[1].trim().isNotEmpty}'; - var m = match[1].trim(); - if (m.isNotEmpty) { - el.attributes['xUpperCase'] = (m[0] == 'X').toString(); - } - el.attributes['text'] = '${match[2]}'; - parser.addNode(el); - - var lenToConsume = match[0].length; - if (match.end + 1 < match.input.length) { - lenToConsume += 1; // Consume \n - } - parser.consume(lenToConsume); - return false; // We are advancing manually + items.insert(index, item); } } class ChecklistBuilder implements md.NodeVisitor { List list; + md.Element listElement; + md.Element parent; @override bool visitElementBefore(md.Element element) { + if (element.tag == 'ul' || element.tag == 'ol') { + listElement = element; + } return true; } @@ -220,9 +247,15 @@ class ChecklistBuilder implements md.NodeVisitor { void visitElementAfter(md.Element el) { final String tag = el.tag; - if (tag == 'input') { - if (el is md.Element && el.attributes['type'] == 'checkbox') { - list.add(ChecklistItem.fromMarkdownElement(el)); + if (tag == 'ul' || tag == 'ol') { + listElement = null; + return; + } + + if (tag == 'li') { + if (el.attributes['class'] == 'todo') { + list.add(ChecklistItem.fromMarkdownElement(el, listElement)); + return; } } //print("builder tag: $tag"); @@ -238,11 +271,45 @@ class ChecklistBuilder implements md.NodeVisitor { } } -class CustomRenderer implements md.NodeVisitor { +class MarkdownRenderer implements md.NodeVisitor { StringBuffer buffer; @override bool visitElementBefore(md.Element element) { + switch (element.tag) { + case 'h1': + buffer.write('# '); + break; + + case 'h2': + buffer.write('## '); + break; + + case 'h3': + buffer.write('### '); + break; + + case 'h4': + buffer.write('#### '); + break; + + case 'h5': + buffer.write('##### '); + break; + + case 'h6': + buffer.write('###### '); + break; + + case 'li': + buffer.write('- '); + break; + + case 'p': + case 'ul': + buffer.write('\n'); + break; + } return true; } @@ -257,26 +324,30 @@ class CustomRenderer implements md.NodeVisitor { final String tag = element.tag; if (tag == 'input') { - var el = element; - if (el is md.Element && el.attributes['type'] == 'checkbox') { - bool val = el.attributes['checked'] != 'false'; + var attr = element.attributes; + print(attr); + if (attr['class'] == 'todo' && attr['type'] == 'checkbox') { + bool val = attr.containsKey('checked'); if (val) { - if (el.attributes['xUpperCase'] != 'false') { - buffer.write('[x] '); - } else { - buffer.write('[X] '); - } + buffer.write('[x]'); } else { - buffer.write('[ ] '); - } - var text = el.attributes['text']; - buffer.write(text); - //print("writeElem $text#"); - if (!text.endsWith('\n')) { - //print("writeElem newLine#"); - buffer.write('\n'); + buffer.write('[ ]'); } } + return; + } + + switch (tag) { + case 'h1': + case 'h2': + case 'h3': + case 'h4': + case 'h5': + case 'h6': + case 'p': + case 'li': + buffer.write('\n'); + break; } } diff --git a/pubspec.lock b/pubspec.lock index f6e71bc2..3acd67ef 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -376,6 +376,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.11.3+2" + markd: + dependency: "direct main" + description: + path: "/Users/vishesh/src/gitjournal/markd" + relative: false + source: path + version: "2.1.3+6" markdown: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1a2745db..33a5b8bf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,6 +41,8 @@ dependencies: font_awesome_flutter: ^8.7.0 sentry: ">=3.0.0 <4.0.0" equatable: ^1.1.0 + markd: + path: /Users/vishesh/src/gitjournal/markd dev_dependencies: flutter_launcher_icons: "^0.7.2" diff --git a/test/checklist_test.dart b/test/checklist_test.dart index cc24b351..3d078ff2 100644 --- a/test/checklist_test.dart +++ b/test/checklist_test.dart @@ -59,17 +59,6 @@ Booga Wooga expect(items[3].text, "item 4"); expect(items[4].text, "item 5"); - // Nodes - var nodes = checklist.nodes; - expect(nodes.length, equals(7)); - expect(nodes[0].textContent, "# Title 1\n\nHow are you doing?\n\n"); - expect(nodes[1], items[0].element); - expect(nodes[2], items[1].element); - expect(nodes[3], items[2].element); - expect(nodes[4], items[3].element); - expect(nodes[5], items[4].element); - expect(nodes[6].textContent, "\nBooga Wooga\n"); - // // Serialization // @@ -123,11 +112,13 @@ Booga Wooga expect(items.length, equals(3)); // Nodes + /* var nodes = checklist.nodes; expect(nodes.length, equals(3)); expect(nodes[0], items[0].element); expect(nodes[1], items[1].element); expect(nodes[2], items[2].element); + */ }); test('Should add \\n before item when adding', () async { @@ -226,5 +217,80 @@ Booga Wooga note = checklist.note; expect(note.body, "[ ] One\n[ ]Two\n[ ] Three\n[ ] Four\n[ ] Five\n"); }); + + test('Should parse simple checklists in a list', () async { + var content = """--- +title: Foo +--- + +# Title 1 + +How are you doing? + +- [ ] item 1 +- [x] item 2 +- [x] item 3 +- [ ] item 4 +- [ ] item 5 + +Booga Wooga +"""; + + var notePath = p.join(tempDir.path, "note.md"); + File(notePath).writeAsString(content); + + var parentFolder = NotesFolderFS(null, tempDir.path); + var note = Note(parentFolder, notePath); + await note.load(); + + var checklist = Checklist(note); + var items = checklist.items; + expect(items.length, equals(5)); + + expect(items[0].checked, false); + expect(items[1].checked, true); + expect(items[2].checked, true); + expect(items[3].checked, false); + expect(items[4].checked, false); + + expect(items[0].text, "item 1"); + expect(items[1].text, "item 2"); + expect(items[2].text, "item 3"); + expect(items[3].text, "item 4"); + expect(items[4].text, "item 5"); + + // + // Serialization + // + checklist.items[0].checked = true; + checklist.items[1].checked = false; + checklist.items[1].text = "Foo"; + var item = checklist.buildItem(false, "Howdy"); + checklist.addItem(item); + + checklist.removeItem(checklist.items[4]); + + await checklist.note.save(); + + var expectedContent = """--- +title: Foo +--- + +# Title 1 + +How are you doing? + +- [x] item 1 +- [ ] Foo +- [x] item 3 +- [ ] item 4 +- [ ] Howdy + +Booga Wooga +"""; + + var actualContent = File(notePath).readAsStringSync(); + expect(actualContent, equals(expectedContent)); + }); }); } From 219e241deb2b3781e29d67fdc75a64495a5cf7b5 Mon Sep 17 00:00:00 2001 From: Vishesh Handa Date: Mon, 6 Apr 2020 01:14:36 +0200 Subject: [PATCH 2/4] Checklist: Add support for upper case 'X' Requires an extra commit in markd --- lib/core/checklist.dart | 9 ++++++--- test/checklist_test.dart | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/core/checklist.dart b/lib/core/checklist.dart index 23d305c9..d9718889 100644 --- a/lib/core/checklist.dart +++ b/lib/core/checklist.dart @@ -327,9 +327,12 @@ class MarkdownRenderer implements md.NodeVisitor { var attr = element.attributes; print(attr); if (attr['class'] == 'todo' && attr['type'] == 'checkbox') { - bool val = attr.containsKey('checked'); - if (val) { - buffer.write('[x]'); + if (attr.containsKey('checked')) { + if (attr.containsKey('uppercase')) { + buffer.write('[X]'); + } else { + buffer.write('[x]'); + } } else { buffer.write('[ ]'); } diff --git a/test/checklist_test.dart b/test/checklist_test.dart index 3d078ff2..def2642c 100644 --- a/test/checklist_test.dart +++ b/test/checklist_test.dart @@ -229,7 +229,7 @@ How are you doing? - [ ] item 1 - [x] item 2 -- [x] item 3 +- [X] item 3 - [ ] item 4 - [ ] item 5 @@ -282,7 +282,7 @@ How are you doing? - [x] item 1 - [ ] Foo -- [x] item 3 +- [X] item 3 - [ ] item 4 - [ ] Howdy From 1c869bc02b45eb883bbb1f9c431df88eccd2e50a Mon Sep 17 00:00:00 2001 From: Vishesh Handa Date: Mon, 6 Apr 2020 01:29:49 +0200 Subject: [PATCH 3/4] Checklist: Adapt other tests This still doesn't fix all the tests --- lib/core/checklist.dart | 3 +- test/checklist_test.dart | 322 +++++++++++++++------------------------ 2 files changed, 123 insertions(+), 202 deletions(-) diff --git a/lib/core/checklist.dart b/lib/core/checklist.dart index d9718889..d06efb4e 100644 --- a/lib/core/checklist.dart +++ b/lib/core/checklist.dart @@ -306,7 +306,6 @@ class MarkdownRenderer implements md.NodeVisitor { break; case 'p': - case 'ul': buffer.write('\n'); break; } @@ -360,6 +359,6 @@ class MarkdownRenderer implements md.NodeVisitor { for (final node in nodes) { node.accept(this); } - return buffer.toString(); + return buffer.toString().trimLeft(); } } diff --git a/test/checklist_test.dart b/test/checklist_test.dart index def2642c..10e47079 100644 --- a/test/checklist_test.dart +++ b/test/checklist_test.dart @@ -27,206 +27,6 @@ title: Foo How are you doing? -[ ] item 1 -[x] item 2 -[x] item 3 -[ ] item 4 -[ ] item 5 - -Booga Wooga -"""; - - var notePath = p.join(tempDir.path, "note.md"); - File(notePath).writeAsString(content); - - var parentFolder = NotesFolderFS(null, tempDir.path); - var note = Note(parentFolder, notePath); - await note.load(); - - var checklist = Checklist(note); - var items = checklist.items; - expect(items.length, equals(5)); - - expect(items[0].checked, false); - expect(items[1].checked, true); - expect(items[2].checked, true); - expect(items[3].checked, false); - expect(items[4].checked, false); - - expect(items[0].text, "item 1"); - expect(items[1].text, "item 2"); - expect(items[2].text, "item 3"); - expect(items[3].text, "item 4"); - expect(items[4].text, "item 5"); - - // - // Serialization - // - - checklist.items[0].checked = true; - checklist.items[1].checked = false; - checklist.items[1].text = "Foo"; - var item = checklist.buildItem(false, "Howdy"); - checklist.addItem(item); - - checklist.removeItem(checklist.items[4]); - - await checklist.note.save(); - - var expectedContent = """--- -title: Foo ---- - -# Title 1 - -How are you doing? - -[x] item 1 -[ ] Foo -[X] item 3 -[ ] item 4 - -Booga Wooga -[ ] Howdy -"""; - - var actualContent = File(notePath).readAsStringSync(); - expect(actualContent, equals(expectedContent)); - }); - - test('Should not add line breaks', () async { - var content = """ -[ ] item 1 -[x] item 2 -[x] item 3"""; - - var notePath = p.join(tempDir.path, "note2.md"); - await File(notePath).writeAsString(content); - - var parentFolder = NotesFolderFS(null, tempDir.path); - var note = Note(parentFolder, notePath); - await note.load(); - - var checklist = Checklist(note); - var items = checklist.items; - expect(items.length, equals(3)); - - // Nodes - /* - var nodes = checklist.nodes; - expect(nodes.length, equals(3)); - expect(nodes[0], items[0].element); - expect(nodes[1], items[1].element); - expect(nodes[2], items[2].element); - */ - }); - - test('Should add \\n before item when adding', () async { - var content = "Hi."; - - var notePath = p.join(tempDir.path, "note3.md"); - await File(notePath).writeAsString(content); - - var parentFolder = NotesFolderFS(null, tempDir.path); - var note = Note(parentFolder, notePath); - await note.load(); - - var checklist = Checklist(note); - var items = checklist.items; - expect(items.length, equals(0)); - - checklist.addItem(checklist.buildItem(false, "item")); - - note = checklist.note; - expect(note.body, "Hi.\n[ ] item\n"); - }); - - test('Should not add \\n when adding after item', () async { - var content = "[ ] one"; - - var notePath = p.join(tempDir.path, "note13.md"); - await File(notePath).writeAsString(content); - - var parentFolder = NotesFolderFS(null, tempDir.path); - var note = Note(parentFolder, notePath); - await note.load(); - - var checklist = Checklist(note); - var items = checklist.items; - expect(items.length, equals(1)); - - checklist.addItem(checklist.buildItem(false, "item")); - - note = checklist.note; - expect(note.body, "[ ] one\n[ ] item\n"); - }); - - test('insertItem works', () async { - var content = "Hi.\n[ ] One\nTwo\n[ ] Three"; - - var notePath = p.join(tempDir.path, "note4.md"); - await File(notePath).writeAsString(content); - - var parentFolder = NotesFolderFS(null, tempDir.path); - var note = Note(parentFolder, notePath); - await note.load(); - - var checklist = Checklist(note); - var items = checklist.items; - expect(items.length, equals(2)); - - checklist.insertItem(1, checklist.buildItem(false, "item")); - - note = checklist.note; - expect(note.body, "Hi.\n[ ] One\nTwo\n[ ] item\n[ ] Three\n"); - }); - - test('Removes empty trailing items', () async { - var content = "Hi.\n[ ] One\nTwo\n[ ] \n[ ] "; - - var notePath = p.join(tempDir.path, "note4.md"); - await File(notePath).writeAsString(content); - - var parentFolder = NotesFolderFS(null, tempDir.path); - var note = Note(parentFolder, notePath); - await note.load(); - - var checklist = Checklist(note); - - note = checklist.note; - expect(note.body, "Hi.\n[ ] One\nTwo\n"); - }); - - test('Does not add extra new line', () async { - var content = "[ ] One\n[ ]Two\n[ ] Three\n[ ] Four\n"; - - var notePath = p.join(tempDir.path, "note449.md"); - await File(notePath).writeAsString(content); - - var parentFolder = NotesFolderFS(null, tempDir.path); - var note = Note(parentFolder, notePath); - await note.load(); - - var checklist = Checklist(note); - /* - for (var node in checklist.nodes) { - print("node $node - '${node.textContent}'"); - }*/ - checklist.addItem(checklist.buildItem(false, "Five")); - - note = checklist.note; - expect(note.body, "[ ] One\n[ ]Two\n[ ] Three\n[ ] Four\n[ ] Five\n"); - }); - - test('Should parse simple checklists in a list', () async { - var content = """--- -title: Foo ---- - -# Title 1 - -How are you doing? - - [ ] item 1 - [x] item 2 - [X] item 3 @@ -262,6 +62,7 @@ Booga Wooga // // Serialization // + checklist.items[0].checked = true; checklist.items[1].checked = false; checklist.items[1].text = "Foo"; @@ -292,5 +93,126 @@ Booga Wooga var actualContent = File(notePath).readAsStringSync(); expect(actualContent, equals(expectedContent)); }); + + test('Should not add line breaks', () async { + var content = """ +- [ ] item 1 +- [x] item 2 +- [x] item 3"""; + + var notePath = p.join(tempDir.path, "note2.md"); + await File(notePath).writeAsString(content); + + var parentFolder = NotesFolderFS(null, tempDir.path); + var note = Note(parentFolder, notePath); + await note.load(); + + var checklist = Checklist(note); + var items = checklist.items; + expect(items.length, equals(3)); + + // Nodes + /* + var nodes = checklist.nodes; + expect(nodes.length, equals(3)); + expect(nodes[0], items[0].element); + expect(nodes[1], items[1].element); + expect(nodes[2], items[2].element); + */ + }); + + test('Should add \\n before item when adding', () async { + var content = "Hi."; + + var notePath = p.join(tempDir.path, "note3.md"); + await File(notePath).writeAsString(content); + + var parentFolder = NotesFolderFS(null, tempDir.path); + var note = Note(parentFolder, notePath); + await note.load(); + + var checklist = Checklist(note); + var items = checklist.items; + expect(items.length, equals(0)); + + checklist.addItem(checklist.buildItem(false, "item")); + + note = checklist.note; + expect(note.body, "Hi.\n- [ ] item\n"); + }); + + test('Should not add \\n when adding after item', () async { + var content = "- [ ] one"; + + var notePath = p.join(tempDir.path, "note13.md"); + await File(notePath).writeAsString(content); + + var parentFolder = NotesFolderFS(null, tempDir.path); + var note = Note(parentFolder, notePath); + await note.load(); + + var checklist = Checklist(note); + var items = checklist.items; + expect(items.length, equals(1)); + + checklist.addItem(checklist.buildItem(false, "item")); + + note = checklist.note; + expect(note.body, "- [ ] one\n- [ ] item\n"); + }); + + test('insertItem works', () async { + var content = "Hi.\n- [ ] One\n- Two\n- [ ] Three"; + + var notePath = p.join(tempDir.path, "note4.md"); + await File(notePath).writeAsString(content); + + var parentFolder = NotesFolderFS(null, tempDir.path); + var note = Note(parentFolder, notePath); + await note.load(); + + var checklist = Checklist(note); + var items = checklist.items; + expect(items.length, equals(2)); + + checklist.insertItem(1, checklist.buildItem(false, "item")); + + note = checklist.note; + expect(note.body, "Hi.\n- [ ] One\n- Two\n- [ ] item\n- [ ] Three\n"); + }); + + test('Does not Remove empty trailing items', () async { + var content = "Hi.\n- [ ] One\n- Two\n- [ ] \n- [ ] "; + + var notePath = p.join(tempDir.path, "note4.md"); + await File(notePath).writeAsString(content); + + var parentFolder = NotesFolderFS(null, tempDir.path); + var note = Note(parentFolder, notePath); + await note.load(); + + var checklist = Checklist(note); + + note = checklist.note; + expect(note.body, "Hi.\n- [ ] One\n- Two\n- [ ] \n- [ ] \n"); + }); + + test('Does not add extra new line', () async { + var content = "- [ ] One\n- [ ]Two\n- [ ] Three\n- [ ] Four\n"; + + var notePath = p.join(tempDir.path, "note449.md"); + await File(notePath).writeAsString(content); + + var parentFolder = NotesFolderFS(null, tempDir.path); + var note = Note(parentFolder, notePath); + await note.load(); + + var checklist = Checklist(note); + checklist.addItem(checklist.buildItem(false, "Five")); + + note = checklist.note; + expect(note.body, + "- [ ] One\n- [ ]Two\n- [ ] Three\n- [ ] Four\n- [ ] Five\n"); + }); }); } From 0c53c9469c04b03624415160f44187925528abb1 Mon Sep 17 00:00:00 2001 From: Vishesh Handa Date: Mon, 6 Apr 2020 01:36:46 +0200 Subject: [PATCH 4/4] Add the legacy checklist format migrator Ideally, I would love to just skip this, but it would break existing checklist and we do have a decent number of users now. --- lib/core/checklist_legacy_migrator.dart | 137 ++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 lib/core/checklist_legacy_migrator.dart diff --git a/lib/core/checklist_legacy_migrator.dart b/lib/core/checklist_legacy_migrator.dart new file mode 100644 index 00000000..8228f641 --- /dev/null +++ b/lib/core/checklist_legacy_migrator.dart @@ -0,0 +1,137 @@ +import 'package:markdown/markdown.dart' as md; + +import 'package:gitjournal/core/note.dart'; + +class ChecklistLegacyMigrator { + Note _note; + List nodes; + + ChecklistLegacyMigrator(this._note) { + var doc = md.Document( + encodeHtml: false, + inlineSyntaxes: [TaskListSyntax()], + extensionSet: md.ExtensionSet.gitHubFlavored, + ); + + nodes = doc.parseInline(_note.body); + _cleanupNodes(nodes); + } + + void _cleanupNodes(List nodes) { + if (nodes.length <= 1) { + return; + } + + var last = nodes.last; + var secLast = nodes[nodes.length - 2]; + + if (last is! md.Text) { + return; + } + if (secLast is! md.Element) { + return; + } + var elem = secLast as md.Element; + if (elem.tag != 'input' || elem.attributes['type'] != 'checkbox') { + return; + } + + // Some times we get an extra \n in the end, not sure why. + if (last.textContent == '\n') { + nodes.length = nodes.length - 1; + if (!elem.attributes["text"].endsWith('\n')) { + elem.attributes["text"] += '\n'; + } + } + } + + Note get note { + if (nodes.isEmpty) return _note; + + var renderer = CustomRenderer(); + _note.body = renderer.render(nodes); + + return _note; + } +} + +/// Copied from flutter-markdown - cannot be merged as we added xUpperCase and changed the regexp +/// Parse [task list items](https://github.github.com/gfm/#task-list-items-extension-). +class TaskListSyntax extends md.InlineSyntax { + // FIXME: Waiting for dart-lang/markdown#269 to land + static final String _pattern = r'^ *\[([ xX])\] +(.*)'; + + TaskListSyntax() : super(_pattern, startCharacter: '['.codeUnitAt(0)); + + @override + bool onMatch(md.InlineParser parser, Match match) { + md.Element el = md.Element.withTag('input'); + el.attributes['type'] = 'checkbox'; + el.attributes['checked'] = '${match[1].trim().isNotEmpty}'; + var m = match[1].trim(); + if (m.isNotEmpty) { + el.attributes['xUpperCase'] = (m[0] == 'X').toString(); + } + el.attributes['text'] = '${match[2]}'; + parser.addNode(el); + + var lenToConsume = match[0].length; + if (match.end + 1 < match.input.length) { + lenToConsume += 1; // Consume \n + } + parser.consume(lenToConsume); + return false; // We are advancing manually + } +} + +class CustomRenderer implements md.NodeVisitor { + StringBuffer buffer; + + @override + bool visitElementBefore(md.Element element) { + return true; + } + + @override + void visitText(md.Text text) { + //print("visitText ${text.text}#"); + buffer.write(text.text); + } + + @override + void visitElementAfter(md.Element element) { + final String tag = element.tag; + + if (tag == 'input') { + var el = element; + if (el is md.Element && el.attributes['type'] == 'checkbox') { + bool val = el.attributes['checked'] != 'false'; + if (val) { + if (el.attributes['xUpperCase'] != 'false') { + buffer.write('- [x] '); + } else { + buffer.write('- [X] '); + } + } else { + buffer.write('- [ ] '); + } + var text = el.attributes['text']; + buffer.write(text); + //print("writeElem $text#"); + if (!text.endsWith('\n')) { + //print("writeElem newLine#"); + buffer.write('\n'); + } + } + } + } + + String render(List nodes) { + buffer = StringBuffer(); + + for (final node in nodes) { + node.accept(this); + } + return buffer.toString(); + } +}