diff --git a/lib/core/checklist.dart b/lib/core/checklist.dart index d06efb4e..fc19e88a 100644 --- a/lib/core/checklist.dart +++ b/lib/core/checklist.dart @@ -1,130 +1,94 @@ -import 'package:markd/markdown.dart' as md; +import 'dart:convert'; + +import 'package:gitjournal/error_reporting.dart'; +import 'package:meta/meta.dart'; import 'package:gitjournal/core/note.dart'; class ChecklistItem { - md.Element parentListElement; - md.Element element; + bool checked; + String text; - bool get checked { - if (element.children == null || element.children.isEmpty) { - return false; - } + String pre; + bool upperCase; + int lineNo; - var inputEl = element.children[0] as md.Element; - assert(inputEl.attributes['class'] == 'todo'); - return inputEl.attributes.containsKey('checked'); - } - - set checked(bool val) { - 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 { - 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) { - if (element.children == null || element.children.isEmpty) { - return; - } - if (element.children.length > 1) { - element.children[1] = md.Text(" $val"); - } - } - - 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)); - } - } + ChecklistItem({ + @required this.checked, + @required this.text, + this.pre = '', + this.upperCase = false, + this.lineNo = -1, + }); @override - String toString() => 'ChecklistItem: $checked $text'; + String toString() => '$pre- [$_x] $text'; + + String get _x => checked ? upperCase ? 'X' : 'x' : ' '; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ChecklistItem && + runtimeType == other.runtimeType && + checked == other.checked && + text == other.text && + pre == other.pre && + upperCase == other.upperCase && + lineNo == other.lineNo; + + @override + int get hashCode => text.hashCode ^ pre.hashCode ^ checked.hashCode; } class Checklist { - Note _note; - List items; + static final _pattern = RegExp( + r'^(.*)- \[([ xX])\] +(.*)$', + multiLine: false, + ); - List _nodes; + Note _note; + List items = []; + + List _lines; + bool endsWithNewLine; Checklist(this._note) { - var doc = md.Document( - encodeHtml: false, - blockSyntaxes: md.BlockParser.standardBlockSyntaxes, - extensionSet: md.ExtensionSet.gitHubWeb, - ); + _lines = LineSplitter.split(_note.body).toList(); + endsWithNewLine = _note.body.endsWith('\n'); - _nodes = doc.parseLines(_note.body.split('\n')); - for (var node in _nodes) { - if (node is md.Element) { - var elem = node; - _printElement(elem, ""); + for (var i = 0; i < _lines.length; i++) { + var line = _lines[i]; + var match = _pattern.firstMatch(line); + if (match == null) { + continue; } - } - print('---------'); - var builder = ChecklistBuilder(); - items = builder.build(_nodes); - } + var pre = match.group(1); + var state = match.group(2); + var post = match.group(3); - 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}"); - } - } + var item = ChecklistItem( + pre: pre, + checked: state != ' ', + upperCase: state == 'X', + text: post, + lineNo: i, + ); + items.add(item); } - print("$indent End ${elem.toString()}"); } Note get note { - if (_nodes.isEmpty) return _note; + if (_lines.isEmpty) return _note; - // Remove empty trailing items - while (true) { - if (items.isEmpty) { - break; - } - var item = items.last; - if (item.checked == false && item.text.trim().isEmpty) { - removeAt(items.length - 1); - } else { - break; - } + for (var item in items) { + _lines[item.lineNo] = item.toString(); + } + _note.body = _lines.join('\n'); + if (endsWithNewLine) { + _note.body += '\n'; } - - var renderer = MarkdownRenderer(); - _note.body = renderer.render(_nodes); - return _note; } @@ -138,227 +102,87 @@ class Checklist { } ChecklistItem buildItem(bool value, String 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'; - } - - 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); + var item = ChecklistItem(checked: value, text: text); + return item; } void removeItem(ChecklistItem item) { assert(items.contains(item)); - 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; - } + var i = items.indexOf(item); + assert(i != -1); + if (i == -1) { + logException( + Exception('Checklist removeItem does not exist'), + StackTrace.current, + ); + return; } - assert(foundChild); + + removeAt(i); } ChecklistItem removeAt(int index) { assert(index >= 0 && index <= items.length); var item = items[index]; - removeItem(item); + items.removeAt(index); + _lines.removeAt(item.lineNo); + for (var j = index; j < items.length; j++) { + items[j].lineNo -= 1; + } return item; } void addItem(ChecklistItem item) { + assert(item.lineNo == -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; + item.lineNo = _lines.length; + items.add(item); + _lines.add(item.toString()); + return; } - items.add(item); - item.parentListElement.children.add(item.element); + var prevItem = items.last; + item.lineNo = prevItem.lineNo + 1; + _lines.insert(item.lineNo, item.toString()); } void insertItem(int index, ChecklistItem item) { + assert(index <= items.length); if (index == 0 && items.isEmpty) { addItem(item); return; } - assert(index <= items.length, "Trying to insert beyond the end"); - if (index == items.length) { - addItem(item); + if (index == 0) { + var nextItem = items[0]; + item.lineNo = nextItem.lineNo; + _lines.insert(item.lineNo, item.toString()); + + for (var item in items) { + item.lineNo++; + } + items.insert(0, item); return; } - var prevItem = index - 1 > 0 ? items[index - 1] : items[index]; - item.parentListElement = prevItem.parentListElement; - var parentList = item.parentListElement; - - // 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; - } + if (index == items.length) { + var prevItem = items.last; + item.lineNo = prevItem.lineNo + 1; + _lines.insert(item.lineNo, item.toString()); + return; } - assert(foundChild); + var prevItem = items[index]; + item.lineNo = prevItem.lineNo; + _lines.insert(item.lineNo, item.toString()); + + for (var i = index; i < items.length; i++) { + items[i].lineNo++; + } 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; - } - - @override - void visitText(md.Text text) { - //print("builder text: ${text.text}#"); - } - - @override - void visitElementAfter(md.Element el) { - final String tag = el.tag; - - 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"); - } - - List build(List nodes) { - list = []; - for (md.Node node in nodes) { - node.accept(this); - } - - return list; - } -} - -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': - buffer.write('\n'); - break; - } - 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 attr = element.attributes; - print(attr); - if (attr['class'] == 'todo' && attr['type'] == 'checkbox') { - if (attr.containsKey('checked')) { - if (attr.containsKey('uppercase')) { - buffer.write('[X]'); - } else { - buffer.write('[x]'); - } - } else { - 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; - } - } - - String render(List nodes) { - buffer = StringBuffer(); - - for (final node in nodes) { - node.accept(this); - } - return buffer.toString().trimLeft(); - } -} diff --git a/pubspec.lock b/pubspec.lock index eba74c23..83902105 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -413,13 +413,6 @@ 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 7a196d41..19e91b6f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,8 +46,6 @@ dependencies: equatable: ^1.1.0 purchases_flutter: ^1.1.0 cached_network_image: ^2.1.0+1 - 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 6a6c875a..b9a7a44a 100644 --- a/test/checklist_test.dart +++ b/test/checklist_test.dart @@ -110,15 +110,6 @@ Booga Wooga 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 { @@ -136,9 +127,10 @@ Booga Wooga expect(items.length, equals(0)); checklist.addItem(checklist.buildItem(false, "item")); + expect(items.length, 1); note = checklist.note; - expect(note.body, "Hi.\n- [ ] item\n"); + expect(note.body, "Hi.\n- [ ] item"); }); test('Should not add \\n when adding after item', () async { @@ -158,7 +150,7 @@ Booga Wooga checklist.addItem(checklist.buildItem(false, "item")); note = checklist.note; - expect(note.body, "- [ ] one\n- [ ] item\n"); + expect(note.body, "- [ ] one\n- [ ] item"); }); test('insertItem works', () async { @@ -173,12 +165,12 @@ Booga Wooga var checklist = Checklist(note); var items = checklist.items; - expect(items.length, equals(2)); + expect(items.length, 2); checklist.insertItem(1, checklist.buildItem(false, "item")); note = checklist.note; - expect(note.body, "Hi.\n- [ ] One\n- Two\n- [ ] item\n- [ ] Three\n"); + expect(note.body, "Hi.\n- [ ] One\n- Two\n- [ ] item\n- [ ] Three"); }); test('Does not Remove empty trailing items', () async { @@ -194,7 +186,7 @@ Booga Wooga var checklist = Checklist(note); note = checklist.note; - expect(note.body, "Hi.\n- [ ] One\n- Two\n- [ ] \n- [ ] \n"); + expect(note.body, "Hi.\n- [ ] One\n- Two\n- [ ] \n- [ ] "); }); test('Does not add extra new line', () async { @@ -216,7 +208,7 @@ Booga Wooga }); test('Maintain x case', () async { - var content = "[X] One\n[ ]Two"; + var content = "- [X] One\n- [ ]Two"; var notePath = p.join(tempDir.path, "note448.md"); await File(notePath).writeAsString(content);