Follow GitHub's Checklist format

Fixes #71

It's easier to not use the Markdown parser and just go line by line and
do it myself. This does make it a bit less extensible though.
This commit is contained in:
Vishesh Handa
2020-05-01 18:12:41 +02:00
parent 1bae5c7a19
commit b919f1f2ba
4 changed files with 122 additions and 315 deletions

View File

@ -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<ChecklistItem> items;
static final _pattern = RegExp(
r'^(.*)- \[([ xX])\] +(.*)$',
multiLine: false,
);
List<md.Node> _nodes;
Note _note;
List<ChecklistItem> items = [];
List<String> _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<ChecklistItem> 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<ChecklistItem> build(List<md.Node> nodes) {
list = <ChecklistItem>[];
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<md.Node> nodes) {
buffer = StringBuffer();
for (final node in nodes) {
node.accept(this);
}
return buffer.toString().trimLeft();
}
}

View File

@ -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:

View File

@ -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"

View File

@ -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);