Merge branch 'checklist_Format2'

This commit is contained in:
Vishesh Handa
2020-05-01 16:54:43 +02:00
5 changed files with 369 additions and 165 deletions

View File

@ -1,27 +1,62 @@
import 'package:markdown/markdown.dart' as md; import 'package:markd/markdown.dart' as md;
import 'package:gitjournal/core/note.dart'; import 'package:gitjournal/core/note.dart';
class ChecklistItem { class ChecklistItem {
md.Element parentListElement;
md.Element element; md.Element element;
bool get checked { 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) { 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 { 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) { 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 @override
String toString() => 'ChecklistItem: $checked $text'; String toString() => 'ChecklistItem: $checked $text';
@ -30,51 +65,49 @@ class ChecklistItem {
class Checklist { class Checklist {
Note _note; Note _note;
List<ChecklistItem> items; List<ChecklistItem> items;
List<md.Node> nodes;
List<md.Node> _nodes;
Checklist(this._note) { Checklist(this._note) {
var doc = md.Document( var doc = md.Document(
encodeHtml: false, encodeHtml: false,
inlineSyntaxes: [TaskListSyntax()], blockSyntaxes: md.BlockParser.standardBlockSyntaxes,
extensionSet: md.ExtensionSet.gitHubFlavored, extensionSet: md.ExtensionSet.gitHubWeb,
); );
nodes = doc.parseInline(_note.body); _nodes = doc.parseLines(_note.body.split('\n'));
_cleanupNodes(nodes); for (var node in _nodes) {
if (node is md.Element) {
var elem = node;
_printElement(elem, "");
}
}
print('---------');
items = ChecklistBuilder().build(nodes); var builder = ChecklistBuilder();
items = builder.build(_nodes);
} }
void _cleanupNodes(List<md.Node> nodes) { void _printElement(md.Element elem, String indent) {
if (nodes.length <= 1) { print("$indent Begin ${elem.toString()}");
return; print("$indent E TAG ${elem.tag}");
} print("$indent E ATTRIBUTES ${elem.attributes}");
print("$indent E generatedId ${elem.generatedId}");
var last = nodes.last; print("$indent E children ${elem.children}");
var secLast = nodes[nodes.length - 2]; if (elem.children != null) {
for (var child in elem.children) {
if (last is! md.Text) { if (child is md.Element) {
return; _printElement(child, indent + " ");
} } else {
if (secLast is! md.Element) { print("$indent $child - ${child.textContent}");
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';
} }
} }
} }
print("$indent End ${elem.toString()}");
}
Note get note { Note get note {
if (nodes.isEmpty) return _note; if (_nodes.isEmpty) return _note;
// Remove empty trailing items // Remove empty trailing items
while (true) { while (true) {
@ -89,8 +122,8 @@ class Checklist {
} }
} }
var renderer = CustomRenderer(); var renderer = MarkdownRenderer();
_note.body = renderer.render(nodes); _note.body = renderer.render(_nodes);
return _note; return _note;
} }
@ -105,111 +138,103 @@ class Checklist {
} }
ChecklistItem buildItem(bool value, String text) { ChecklistItem buildItem(bool value, String text) {
var elem = md.Element.withTag("input"); var inputElement = md.Element.withTag('input');
elem.attributes["type"] = "checkbox"; inputElement.attributes['class'] = 'todo';
elem.attributes["checked"] = value.toString(); inputElement.attributes['type'] = 'checkbox';
elem.attributes["xUpperCase"] = "false"; inputElement.attributes['disabled'] = 'disabled';
elem.attributes["text"] = text; 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) { void removeItem(ChecklistItem item) {
assert(nodes.contains(item.element));
assert(items.contains(item)); assert(items.contains(item));
nodes.remove(item.element);
items.remove(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;
}
}
assert(foundChild);
} }
ChecklistItem removeAt(int index) { ChecklistItem removeAt(int index) {
assert(index >= 0 && index <= items.length); assert(index >= 0 && index <= items.length);
var item = items[index]; var item = items[index];
assert(nodes.contains(item.element)); removeItem(item);
nodes.remove(item.element);
items.removeAt(index);
return item; return item;
} }
void addItem(ChecklistItem 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); items.add(item);
nodes.add(item.element); item.parentListElement.children.add(item.element);
} }
void insertItem(int index, ChecklistItem item) { void insertItem(int index, ChecklistItem item) {
assert(index <= items.length, "Trying to insert beyond the end"); if (index == 0 && items.isEmpty) {
if (index == 0) { addItem(item);
items.insert(0, item);
nodes.insert(0, item.element);
return; return;
} }
assert(index <= items.length, "Trying to insert beyond the end");
if (index == items.length) { if (index == items.length) {
addItem(item); addItem(item);
return; return;
} }
var prevItem = items[index]; var prevItem = index - 1 > 0 ? items[index - 1] : items[index];
var nodeIndex = nodes.indexOf(prevItem.element); item.parentListElement = prevItem.parentListElement;
var parentList = item.parentListElement;
_insertNewLineIfRequired(nodeIndex); // 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);
nodes.insert(nodeIndex, item.element);
items.insert(index, item); 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"));
}
}
}
}
/// 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();
} else {
el.attributes['xUpperCase'] = "false";
}
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 ChecklistBuilder implements md.NodeVisitor { class ChecklistBuilder implements md.NodeVisitor {
List<ChecklistItem> list; List<ChecklistItem> list;
md.Element listElement;
md.Element parent;
@override @override
bool visitElementBefore(md.Element element) { bool visitElementBefore(md.Element element) {
if (element.tag == 'ul' || element.tag == 'ol') {
listElement = element;
}
return true; return true;
} }
@ -222,9 +247,15 @@ class ChecklistBuilder implements md.NodeVisitor {
void visitElementAfter(md.Element el) { void visitElementAfter(md.Element el) {
final String tag = el.tag; final String tag = el.tag;
if (tag == 'input') { if (tag == 'ul' || tag == 'ol') {
if (el is md.Element && el.attributes['type'] == 'checkbox') { listElement = null;
list.add(ChecklistItem.fromMarkdownElement(el)); return;
}
if (tag == 'li') {
if (el.attributes['class'] == 'todo') {
list.add(ChecklistItem.fromMarkdownElement(el, listElement));
return;
} }
} }
//print("builder tag: $tag"); //print("builder tag: $tag");
@ -240,11 +271,44 @@ class ChecklistBuilder implements md.NodeVisitor {
} }
} }
class CustomRenderer implements md.NodeVisitor { class MarkdownRenderer implements md.NodeVisitor {
StringBuffer buffer; StringBuffer buffer;
@override @override
bool visitElementBefore(md.Element element) { 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; return true;
} }
@ -259,12 +323,11 @@ class CustomRenderer implements md.NodeVisitor {
final String tag = element.tag; final String tag = element.tag;
if (tag == 'input') { if (tag == 'input') {
var el = element; var attr = element.attributes;
if (el is md.Element && el.attributes['type'] == 'checkbox') { print(attr);
assert(el.attributes.containsKey('xUpperCase')); if (attr['class'] == 'todo' && attr['type'] == 'checkbox') {
bool val = el.attributes['checked'] != 'false'; if (attr.containsKey('checked')) {
if (val) { if (attr.containsKey('uppercase')) {
if (el.attributes['xUpperCase'] != 'false') {
buffer.write('[X]'); buffer.write('[X]');
} else { } else {
buffer.write('[x]'); buffer.write('[x]');
@ -272,14 +335,21 @@ class CustomRenderer implements md.NodeVisitor {
} else { } else {
buffer.write('[ ]'); buffer.write('[ ]');
} }
var text = el.attributes['text']; }
buffer.write(text); return;
//print("writeElem $text#"); }
if (!text.endsWith('\n')) {
//print("writeElem newLine#"); switch (tag) {
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
case 'p':
case 'li':
buffer.write('\n'); buffer.write('\n');
} break;
}
} }
} }
@ -289,6 +359,6 @@ class CustomRenderer implements md.NodeVisitor {
for (final node in nodes) { for (final node in nodes) {
node.accept(this); node.accept(this);
} }
return buffer.toString(); return buffer.toString().trimLeft();
} }
} }

View File

@ -0,0 +1,137 @@
import 'package:markdown/markdown.dart' as md;
import 'package:gitjournal/core/note.dart';
class ChecklistLegacyMigrator {
Note _note;
List<md.Node> 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<md.Node> 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<md.Node> nodes) {
buffer = StringBuffer();
for (final node in nodes) {
node.accept(this);
}
return buffer.toString();
}
}

View File

@ -413,6 +413,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.11.3+2" 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: markdown:
dependency: transitive dependency: transitive
description: description:

View File

@ -46,6 +46,8 @@ dependencies:
equatable: ^1.1.0 equatable: ^1.1.0
purchases_flutter: ^1.1.0 purchases_flutter: ^1.1.0
cached_network_image: ^2.1.0+1 cached_network_image: ^2.1.0+1
markd:
path: /Users/vishesh/src/gitjournal/markd
dev_dependencies: dev_dependencies:
flutter_launcher_icons: "^0.7.2" flutter_launcher_icons: "^0.7.2"

View File

@ -27,11 +27,11 @@ title: Foo
How are you doing? How are you doing?
[ ] item 1 - [ ] item 1
[x] item 2 - [x] item 2
[x] item 3 - [X] item 3
[ ] item 4 - [ ] item 4
[ ] item 5 - [ ] item 5
Booga Wooga Booga Wooga
"""; """;
@ -59,17 +59,6 @@ Booga Wooga
expect(items[3].text, "item 4"); expect(items[3].text, "item 4");
expect(items[4].text, "item 5"); 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 // Serialization
// //
@ -92,13 +81,13 @@ title: Foo
How are you doing? How are you doing?
[x] item 1 - [x] item 1
[ ] Foo - [ ] Foo
[x] item 3 - [X] item 3
[ ] item 4 - [ ] item 4
- [ ] Howdy
Booga Wooga Booga Wooga
[ ] Howdy
"""; """;
var actualContent = File(notePath).readAsStringSync(); var actualContent = File(notePath).readAsStringSync();
@ -107,9 +96,9 @@ Booga Wooga
test('Should not add line breaks', () async { test('Should not add line breaks', () async {
var content = """ var content = """
[ ] item 1 - [ ] item 1
[x] item 2 - [x] item 2
[x] item 3"""; - [x] item 3""";
var notePath = p.join(tempDir.path, "note2.md"); var notePath = p.join(tempDir.path, "note2.md");
await File(notePath).writeAsString(content); await File(notePath).writeAsString(content);
@ -123,11 +112,13 @@ Booga Wooga
expect(items.length, equals(3)); expect(items.length, equals(3));
// Nodes // Nodes
/*
var nodes = checklist.nodes; var nodes = checklist.nodes;
expect(nodes.length, equals(3)); expect(nodes.length, equals(3));
expect(nodes[0], items[0].element); expect(nodes[0], items[0].element);
expect(nodes[1], items[1].element); expect(nodes[1], items[1].element);
expect(nodes[2], items[2].element); expect(nodes[2], items[2].element);
*/
}); });
test('Should add \\n before item when adding', () async { test('Should add \\n before item when adding', () async {
@ -147,11 +138,11 @@ Booga Wooga
checklist.addItem(checklist.buildItem(false, "item")); checklist.addItem(checklist.buildItem(false, "item"));
note = checklist.note; note = checklist.note;
expect(note.body, "Hi.\n[ ] item\n"); expect(note.body, "Hi.\n- [ ] item\n");
}); });
test('Should not add \\n when adding after item', () async { test('Should not add \\n when adding after item', () async {
var content = "[ ] one"; var content = "- [ ] one";
var notePath = p.join(tempDir.path, "note13.md"); var notePath = p.join(tempDir.path, "note13.md");
await File(notePath).writeAsString(content); await File(notePath).writeAsString(content);
@ -167,11 +158,11 @@ Booga Wooga
checklist.addItem(checklist.buildItem(false, "item")); checklist.addItem(checklist.buildItem(false, "item"));
note = checklist.note; note = checklist.note;
expect(note.body, "[ ] one\n[ ] item\n"); expect(note.body, "- [ ] one\n- [ ] item\n");
}); });
test('insertItem works', () async { test('insertItem works', () async {
var content = "Hi.\n[ ] One\nTwo\n[ ] Three"; var content = "Hi.\n- [ ] One\n- Two\n- [ ] Three";
var notePath = p.join(tempDir.path, "note4.md"); var notePath = p.join(tempDir.path, "note4.md");
await File(notePath).writeAsString(content); await File(notePath).writeAsString(content);
@ -187,11 +178,11 @@ Booga Wooga
checklist.insertItem(1, checklist.buildItem(false, "item")); checklist.insertItem(1, checklist.buildItem(false, "item"));
note = checklist.note; note = checklist.note;
expect(note.body, "Hi.\n[ ] One\nTwo\n[ ] item\n[ ] Three\n"); expect(note.body, "Hi.\n- [ ] One\n- Two\n- [ ] item\n- [ ] Three\n");
}); });
test('Removes empty trailing items', () async { test('Does not Remove empty trailing items', () async {
var content = "Hi.\n[ ] One\nTwo\n[ ] \n[ ] "; var content = "Hi.\n- [ ] One\n- Two\n- [ ] \n- [ ] ";
var notePath = p.join(tempDir.path, "note4.md"); var notePath = p.join(tempDir.path, "note4.md");
await File(notePath).writeAsString(content); await File(notePath).writeAsString(content);
@ -203,11 +194,11 @@ Booga Wooga
var checklist = Checklist(note); var checklist = Checklist(note);
note = checklist.note; note = checklist.note;
expect(note.body, "Hi.\n[ ] One\nTwo\n"); expect(note.body, "Hi.\n- [ ] One\n- Two\n- [ ] \n- [ ] \n");
}); });
test('Does not add extra new line', () async { test('Does not add extra new line', () async {
var content = "[ ] One\n[ ]Two\n[ ] Three\n[ ] Four\n"; var content = "- [ ] One\n- [ ]Two\n- [ ] Three\n- [ ] Four\n";
var notePath = p.join(tempDir.path, "note449.md"); var notePath = p.join(tempDir.path, "note449.md");
await File(notePath).writeAsString(content); await File(notePath).writeAsString(content);
@ -217,14 +208,11 @@ Booga Wooga
await note.load(); await note.load();
var checklist = Checklist(note); var checklist = Checklist(note);
/*
for (var node in checklist.nodes) {
print("node $node - '${node.textContent}'");
}*/
checklist.addItem(checklist.buildItem(false, "Five")); checklist.addItem(checklist.buildItem(false, "Five"));
note = checklist.note; note = checklist.note;
expect(note.body, "[ ] One\n[ ]Two\n[ ] Three\n[ ] Four\n[ ] Five\n"); expect(note.body,
"- [ ] One\n- [ ]Two\n- [ ] Three\n- [ ] Four\n- [ ] Five\n");
}); });
test('Maintain x case', () async { test('Maintain x case', () async {