mirror of
https://github.com/GitJournal/GitJournal.git
synced 2025-06-28 09:47:35 +08:00
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:
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
|
Reference in New Issue
Block a user