diff --git a/libpit/resources/main/test.groovy b/libpit/resources/main/test.groovy new file mode 100644 index 0000000..e1ff4fc --- /dev/null +++ b/libpit/resources/main/test.groovy @@ -0,0 +1,13 @@ +import com.jdbernard.pit.* +import com.jdbernard.pit.file.* + +import org.parboiled.Parboiled +import org.parboiled.parserunners.ReportingParseRunner +import org.parboiled.parserunners.RecoveringParseRunner + +parser = Parboiled.createParser(IssuePegParser.class) +parseRunner = new ReportingParseRunner(parser.IssueFile()) +issueFile = new File('/Volumes/NO NAME/Dropbox/tasks/0015tn3.rst') +issueText = issueFile.text +result = parseRunner.run(issueText) +issueMap = result.valueStack.pop() diff --git a/libpit/src/main/com/jdbernard/pit/Issue.groovy b/libpit/src/main/com/jdbernard/pit/Issue.groovy index 3a28421..9ab0e2d 100755 --- a/libpit/src/main/com/jdbernard/pit/Issue.groovy +++ b/libpit/src/main/com/jdbernard/pit/Issue.groovy @@ -11,7 +11,7 @@ public abstract class Issue { protected String text protected String title - protected Map extendedPropeties = [:] + Map extendedProperties = [:] Issue(Map props) { this.id = props.id diff --git a/libpit/src/main/com/jdbernard/pit/file/ExtendedPropertyHelp.groovy b/libpit/src/main/com/jdbernard/pit/file/ExtendedPropertyHelp.groovy new file mode 100644 index 0000000..cd5bf13 --- /dev/null +++ b/libpit/src/main/com/jdbernard/pit/file/ExtendedPropertyHelp.groovy @@ -0,0 +1,45 @@ +package com.jdbernard.pit.file + +import org.joda.time.DateMidnight +import org.joda.time.DateTime + +public enum ExtendedPropertyHelp { + + DATE(/^\d{4}-\d{2}-\d{2}$/, DateMidnight, + { v -> DateMidnight.parse(v) }, + { d -> d.toString("YYYY-MM-dd") }), + DATETIME(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/, DateTime, + { v -> DateTime.parse(v) }, + { d -> d.toString("YYYY-MM-dd'T'HH:mm:ss") }), + INTEGER(/^\d+$/, Integer, + { v -> v as Integer }, + { i -> i as String }); + + String pattern; + Class klass; + def parseFun, formatFun; + + public ExtendedPropertyHelp(String pattern, Class klass, def parseFun, + def formatFun) { + this.pattern = pattern + this.klass = klass + this.parseFun = parseFun + this.formatFun = formatFun } + + public boolean matches(String prop) { return prop ==~ pattern } + + public static Object parse(String value) { + def result = null + ExtendedPropertyHelp.values().each { propType -> + if (propType.matches(value)) { result = propType.parseFun(value) }} + + return result ?: value } + + public static String format(def object) { + def result = null + ExtendedPropertyHelp.values().each { propType -> + if (!result && propType.klass.isInstance(object)) { + result = propType.formatFun(object) }} + + return result ?: object.toString() } +} diff --git a/libpit/src/main/com/jdbernard/pit/file/FileIssue.groovy b/libpit/src/main/com/jdbernard/pit/file/FileIssue.groovy index 19637e5..6f515d1 100755 --- a/libpit/src/main/com/jdbernard/pit/file/FileIssue.groovy +++ b/libpit/src/main/com/jdbernard/pit/file/FileIssue.groovy @@ -4,8 +4,8 @@ import com.jdbernard.pit.* import java.lang.IllegalArgumentException as IAE -import org.joda.time.DateMidnight -import org.joda.time.DateTime +import org.parboiled.Parboiled +import org.parboiled.parserunners.ReportingParseRunner import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -17,6 +17,12 @@ public class FileIssue extends Issue { public static final String fileExp = /(\d+)([bft])([ajnsv])(\d).*/ + protected static parseRunner + + static { + def parser = Parboiled.createParser(IssuePegParser) + parseRunner = new ReportingParseRunner(parser.IssueFile()) } + public FileIssue(File file) { super(id: -1, title: 'REPLACE_ME') @@ -30,46 +36,28 @@ public class FileIssue extends Issue { "is not a valid Issue file.") // Read issue attributes from the filename. - super.@id = matcher[0][1] - super.@category = Category.toCategory(matcher[0][2]) - super.@status = Status.toStatus(matcher[0][3]) - super.@priority = matcher[0][4].toInteger() + super.id = matcher[0][1] + super.category = Category.toCategory(matcher[0][2]) + super.status = Status.toStatus(matcher[0][3]) + super.priority = matcher[0][4].toInteger() log.debug("id: {}\tcategory: {}\tstatus: {}\tpriority: {}", - [super.@id, super.@category, super.@status, super.@priority]) + super.id, super.category, super.status, super.priority) this.source = file - String text = "" - boolean parsingText = true + // Parse the file and extract the title, text, and extended properties + // TODO: guard against parsing problems (null/empty value stack, etc.) + def parsedIssue = parseRunner.run(file.text).valueStack.pop() - // Now parse the actual file contents. - file.text.eachLine { line, lineNumber -> + super.text = parsedIssue.body + super.title = parsedIssue.title - log.trace("lineNumber: {}, line: {}", lineNumber, line) - - if (lineNumber == 0) { - super.@title = line - log.debug("Found title: {}", super.@title) } - - else if (lineNumber > 2) { - // We see the horizontal rule - if (line ==~ /\s*^\-{4}\-*\s*$/) { parsingText = false } - - if (parsingText) { text += "$line\n" } - - else { - def match = (line =~ /^([^:]+):(.+)$/) - if (match) { - def key = match[0][1].trim() - def value = parseValue(match[0][2].trim()) - super.@extendedProperties[key] = value - } - } - } - } - - super.@text = text + // Add the extended properties + parsedIssue.extProperties.each { key, value -> + key = key.toLowerCase().replaceAll(/\s/, '_') + super.extendedProperties[key] = + ExtendedPropertyHelp.parse(value) } } public void setCategory(Category c) throws IOException { @@ -83,8 +71,7 @@ public class FileIssue extends Issue { + "preventing me from doing so (maybe the path to the file is " + "no longer valid, or maybe the file is currently open in " + "some other program).") - else super.setCategory(c) - } + else super.setCategory(c) } public void setStatus(Status s) throws IOException { boolean renamed @@ -97,8 +84,7 @@ public class FileIssue extends Issue { + "preventing me from doing so (maybe the path to the file is " + "no longer valid, or maybe the file is currently open in " + "some other program).") - else super.setStatus(s) - } + else super.setStatus(s) } public void setPriority(int p) throws IOException { boolean renamed @@ -111,28 +97,23 @@ public class FileIssue extends Issue { + "preventing me from doing so (maybe the path to the file is " + "no longer valid, or maybe the file is currently open in " + "some other program).") - else super.setPriority(p) - } + else super.setPriority(p) } public String getFilename() { - return makeFilename(id, category, status, priority) - } + return makeFilename(id, category, status, priority) } public void setTitle(String title) throws IOException { super.setTitle(title) - writeFile() - } + writeFile() } public void setText(String text) throws IOException { super.setText(text) - writeFile() - } + writeFile() } boolean deleteFile() { return source.deleteDir() } public static boolean isValidFilename(String name) { - return name ==~ fileExp - } + return name ==~ fileExp } public static String makeFilename(String id, Category category, Status status, int priority) { @@ -148,27 +129,28 @@ public class FileIssue extends Issue { if (!(id ==~ /\d+/)) throw new IAE( "'${id}' is not a legal value for id.") - return id + category.symbol + status.symbol + priority + ".rst"; - } + return id + category.symbol + status.symbol + priority + ".rst" } public static String formatIssue(Issue issue) { def result = new StringBuilder() - result.append(issue.@title) + result.append(issue.title) result.append("\n") - result.append("=".multiply(issue.@title.length())) - result.append("\n") - result.append(issue.@text) - result.append("\n----\n") + result.append("=".multiply(issue.title.length())) + result.append("\n\n") + result.append(issue.text) + result.append("\n----\n\n") // If there are any extended properties, let's write those. - if (issue.@extendedProperties.size() > 0) { + if (issue.extendedProperties.size() > 0) { def extOutput = [:] def maxKeyLen = 0 def maxValLen = 0 // Find the longest key and value, convert all to strings. - issue.@extendedProperties.each { key, val -> - def ks = key.toString(), vs = formatProperty(val) + issue.extendedProperties.each { key, val -> + def ks = key.toString().split('_').collect({it.capitalize()}).join(' ') + def vs = ExtendedPropertyHelp.format(val) + extOutput[ks] = vs if (ks.length() > maxKeyLen) { maxKeyLen = ks.length() } if (vs.length() > maxKeyLen) { maxValLen = vs.length() } } @@ -194,20 +176,6 @@ public class FileIssue extends Issue { catch (IOException ioe) { throw new IOException("I could not save the new text for this " + "issue. I can not write to the file for this issue. I do not" - + " know why, I am sorry (maybe the file can not be reached).") - } - } + + " know why, I am sorry (maybe the file can not be reached).") } } - public def parseValue(String value) { - switch (value) { - case ~/\d{4}-\d{2}-\d{2}/: return DateMidnight.parse(value) - default: return value - } - } - - public String formatProperty(DateTime prop) { - return prop.format("YYYY-MM-dd'T'HH:mm:ss") } - - public String formatProperty(DateMidnight prop) { - return prop.format("YYYY-MM-dd") } } diff --git a/libpit/src/main/com/jdbernard/pit/file/IssuePegParser.java b/libpit/src/main/com/jdbernard/pit/file/IssuePegParser.java new file mode 100644 index 0000000..ec139de --- /dev/null +++ b/libpit/src/main/com/jdbernard/pit/file/IssuePegParser.java @@ -0,0 +1,67 @@ +package com.jdbernard.pit.file; + +import java.util.HashMap; +import java.util.Map; + +import org.parboiled.Action; +import org.parboiled.BaseParser; +import org.parboiled.Context; +import org.parboiled.Rule; +import org.parboiled.annotations.*; + +@BuildParseTree +public class IssuePegParser extends BaseParser { + + public Rule IssueFile() { + return Sequence(push(makeNode()), + Title(), Body(), Optional(PropertyBlock())); } + + Rule Title() { + return Sequence( + OneOrMore(NOT_EOL), addToNode("title", match()), EOL, + HorizontalRule(), EOL, + ZeroOrMore(SPACE), EOL); } + + Rule Body() { return Sequence(OneOrMore(Sequence( + TestNot(PropertyBlock()), ANY)), addToNode("body", match())); } + + Rule PropertyBlock() { + return Sequence(push(makeNode()), + HorizontalRule(), OneOrMore(EOL), TableSeparator(), EOL, + OneOrMore(PropertyDefinition()), TableSeparator(), + addToNode("extProperties", pop())); } + + Rule PropertyDefinition() { + return Sequence( + PropertyKey(), push(match()), COLON, + PropertyValue(), push(match()), EOL, + swap(), addToNode(popAsString().trim(), popAsString().trim())); } + + Rule PropertyKey() { return OneOrMore(Sequence(TestNot(COLON), NOT_EOL)); } + + Rule PropertyValue() { return OneOrMore(NOT_EOL); } + + Rule TableSeparator() { + return Sequence(OneOrMore(SEPARATOR_CHAR), OneOrMore(SPACE), + OneOrMore(SEPARATOR_CHAR)); } + + Rule HorizontalRule() { + return Sequence(SEPARATOR_CHAR, SEPARATOR_CHAR, SEPARATOR_CHAR, + OneOrMore(SEPARATOR_CHAR)); } + + Rule EOL = Ch('\n'); + Rule NOT_EOL = Sequence(TestNot(EOL), ANY); + Rule SEPARATOR_CHAR = AnyOf("\"'`~:-_=+*^#<>"); + Rule SPACE = AnyOf(" \t"); + Rule COLON = Ch(':'); + + Map makeNode() { return new HashMap(); } + + boolean addToNode(Object key, Object val) { + Map node = (Map) pop(); + node.put(key, val); + push(node); + return true; } + + String popAsString() { return (String) pop(); } +}