Implemented extended attributes on File issues.
* Created a PEG parser for issue files. * Added parsing and formatting code to `FileIssue` to handle extended properties.
This commit is contained in:
parent
66b68160e5
commit
447e74f956
13
libpit/resources/main/test.groovy
Normal file
13
libpit/resources/main/test.groovy
Normal file
@ -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()
|
@ -11,7 +11,7 @@ public abstract class Issue {
|
|||||||
protected String text
|
protected String text
|
||||||
protected String title
|
protected String title
|
||||||
|
|
||||||
protected Map extendedPropeties = [:]
|
Map extendedProperties = [:]
|
||||||
|
|
||||||
Issue(Map props) {
|
Issue(Map props) {
|
||||||
this.id = props.id
|
this.id = props.id
|
||||||
|
@ -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() }
|
||||||
|
}
|
@ -4,8 +4,8 @@ import com.jdbernard.pit.*
|
|||||||
|
|
||||||
import java.lang.IllegalArgumentException as IAE
|
import java.lang.IllegalArgumentException as IAE
|
||||||
|
|
||||||
import org.joda.time.DateMidnight
|
import org.parboiled.Parboiled
|
||||||
import org.joda.time.DateTime
|
import org.parboiled.parserunners.ReportingParseRunner
|
||||||
|
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
@ -17,6 +17,12 @@ public class FileIssue extends Issue {
|
|||||||
|
|
||||||
public static final String fileExp = /(\d+)([bft])([ajnsv])(\d).*/
|
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) {
|
public FileIssue(File file) {
|
||||||
|
|
||||||
super(id: -1, title: 'REPLACE_ME')
|
super(id: -1, title: 'REPLACE_ME')
|
||||||
@ -30,46 +36,28 @@ public class FileIssue extends Issue {
|
|||||||
"is not a valid Issue file.")
|
"is not a valid Issue file.")
|
||||||
|
|
||||||
// Read issue attributes from the filename.
|
// Read issue attributes from the filename.
|
||||||
super.@id = matcher[0][1]
|
super.id = matcher[0][1]
|
||||||
super.@category = Category.toCategory(matcher[0][2])
|
super.category = Category.toCategory(matcher[0][2])
|
||||||
super.@status = Status.toStatus(matcher[0][3])
|
super.status = Status.toStatus(matcher[0][3])
|
||||||
super.@priority = matcher[0][4].toInteger()
|
super.priority = matcher[0][4].toInteger()
|
||||||
|
|
||||||
log.debug("id: {}\tcategory: {}\tstatus: {}\tpriority: {}",
|
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
|
this.source = file
|
||||||
|
|
||||||
String text = ""
|
// Parse the file and extract the title, text, and extended properties
|
||||||
boolean parsingText = true
|
// TODO: guard against parsing problems (null/empty value stack, etc.)
|
||||||
|
def parsedIssue = parseRunner.run(file.text).valueStack.pop()
|
||||||
|
|
||||||
// Now parse the actual file contents.
|
super.text = parsedIssue.body
|
||||||
file.text.eachLine { line, lineNumber ->
|
super.title = parsedIssue.title
|
||||||
|
|
||||||
log.trace("lineNumber: {}, line: {}", lineNumber, line)
|
// Add the extended properties
|
||||||
|
parsedIssue.extProperties.each { key, value ->
|
||||||
if (lineNumber == 0) {
|
key = key.toLowerCase().replaceAll(/\s/, '_')
|
||||||
super.@title = line
|
super.extendedProperties[key] =
|
||||||
log.debug("Found title: {}", super.@title) }
|
ExtendedPropertyHelp.parse(value) }
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setCategory(Category c) throws IOException {
|
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 "
|
+ "preventing me from doing so (maybe the path to the file is "
|
||||||
+ "no longer valid, or maybe the file is currently open in "
|
+ "no longer valid, or maybe the file is currently open in "
|
||||||
+ "some other program).")
|
+ "some other program).")
|
||||||
else super.setCategory(c)
|
else super.setCategory(c) }
|
||||||
}
|
|
||||||
|
|
||||||
public void setStatus(Status s) throws IOException {
|
public void setStatus(Status s) throws IOException {
|
||||||
boolean renamed
|
boolean renamed
|
||||||
@ -97,8 +84,7 @@ public class FileIssue extends Issue {
|
|||||||
+ "preventing me from doing so (maybe the path to the file is "
|
+ "preventing me from doing so (maybe the path to the file is "
|
||||||
+ "no longer valid, or maybe the file is currently open in "
|
+ "no longer valid, or maybe the file is currently open in "
|
||||||
+ "some other program).")
|
+ "some other program).")
|
||||||
else super.setStatus(s)
|
else super.setStatus(s) }
|
||||||
}
|
|
||||||
|
|
||||||
public void setPriority(int p) throws IOException {
|
public void setPriority(int p) throws IOException {
|
||||||
boolean renamed
|
boolean renamed
|
||||||
@ -111,28 +97,23 @@ public class FileIssue extends Issue {
|
|||||||
+ "preventing me from doing so (maybe the path to the file is "
|
+ "preventing me from doing so (maybe the path to the file is "
|
||||||
+ "no longer valid, or maybe the file is currently open in "
|
+ "no longer valid, or maybe the file is currently open in "
|
||||||
+ "some other program).")
|
+ "some other program).")
|
||||||
else super.setPriority(p)
|
else super.setPriority(p) }
|
||||||
}
|
|
||||||
|
|
||||||
public String getFilename() {
|
public String getFilename() {
|
||||||
return makeFilename(id, category, status, priority)
|
return makeFilename(id, category, status, priority) }
|
||||||
}
|
|
||||||
|
|
||||||
public void setTitle(String title) throws IOException {
|
public void setTitle(String title) throws IOException {
|
||||||
super.setTitle(title)
|
super.setTitle(title)
|
||||||
writeFile()
|
writeFile() }
|
||||||
}
|
|
||||||
|
|
||||||
public void setText(String text) throws IOException {
|
public void setText(String text) throws IOException {
|
||||||
super.setText(text)
|
super.setText(text)
|
||||||
writeFile()
|
writeFile() }
|
||||||
}
|
|
||||||
|
|
||||||
boolean deleteFile() { return source.deleteDir() }
|
boolean deleteFile() { return source.deleteDir() }
|
||||||
|
|
||||||
public static boolean isValidFilename(String name) {
|
public static boolean isValidFilename(String name) {
|
||||||
return name ==~ fileExp
|
return name ==~ fileExp }
|
||||||
}
|
|
||||||
|
|
||||||
public static String makeFilename(String id, Category category,
|
public static String makeFilename(String id, Category category,
|
||||||
Status status, int priority) {
|
Status status, int priority) {
|
||||||
@ -148,27 +129,28 @@ public class FileIssue extends Issue {
|
|||||||
if (!(id ==~ /\d+/))
|
if (!(id ==~ /\d+/))
|
||||||
throw new IAE( "'${id}' is not a legal value for id.")
|
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) {
|
public static String formatIssue(Issue issue) {
|
||||||
def result = new StringBuilder()
|
def result = new StringBuilder()
|
||||||
result.append(issue.@title)
|
result.append(issue.title)
|
||||||
result.append("\n")
|
result.append("\n")
|
||||||
result.append("=".multiply(issue.@title.length()))
|
result.append("=".multiply(issue.title.length()))
|
||||||
result.append("\n")
|
result.append("\n\n")
|
||||||
result.append(issue.@text)
|
result.append(issue.text)
|
||||||
result.append("\n----\n")
|
result.append("\n----\n\n")
|
||||||
|
|
||||||
// If there are any extended properties, let's write those.
|
// If there are any extended properties, let's write those.
|
||||||
if (issue.@extendedProperties.size() > 0) {
|
if (issue.extendedProperties.size() > 0) {
|
||||||
def extOutput = [:]
|
def extOutput = [:]
|
||||||
def maxKeyLen = 0
|
def maxKeyLen = 0
|
||||||
def maxValLen = 0
|
def maxValLen = 0
|
||||||
|
|
||||||
// Find the longest key and value, convert all to strings.
|
// Find the longest key and value, convert all to strings.
|
||||||
issue.@extendedProperties.each { key, val ->
|
issue.extendedProperties.each { key, val ->
|
||||||
def ks = key.toString(), vs = formatProperty(val)
|
def ks = key.toString().split('_').collect({it.capitalize()}).join(' ')
|
||||||
|
def vs = ExtendedPropertyHelp.format(val)
|
||||||
|
|
||||||
extOutput[ks] = vs
|
extOutput[ks] = vs
|
||||||
if (ks.length() > maxKeyLen) { maxKeyLen = ks.length() }
|
if (ks.length() > maxKeyLen) { maxKeyLen = ks.length() }
|
||||||
if (vs.length() > maxKeyLen) { maxValLen = vs.length() } }
|
if (vs.length() > maxKeyLen) { maxValLen = vs.length() } }
|
||||||
@ -194,20 +176,6 @@ public class FileIssue extends Issue {
|
|||||||
catch (IOException ioe) {
|
catch (IOException ioe) {
|
||||||
throw new IOException("I could not save the new text for this "
|
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"
|
+ "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") }
|
|
||||||
}
|
}
|
||||||
|
67
libpit/src/main/com/jdbernard/pit/file/IssuePegParser.java
Normal file
67
libpit/src/main/com/jdbernard/pit/file/IssuePegParser.java
Normal file
@ -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<Object> {
|
||||||
|
|
||||||
|
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(); }
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user