package com.hdweiss.morgand.synchronizer.parser; import android.text.TextUtils; import android.util.Pair; import com.hdweiss.morgand.data.OrgNodeUtils; import com.hdweiss.morgand.data.dao.DatabaseHelper; import com.hdweiss.morgand.data.dao.OrgFile; import com.hdweiss.morgand.data.dao.OrgNode; import com.hdweiss.morgand.settings.PreferenceUtils; import com.j256.ormlite.dao.RuntimeExceptionDao; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.LineNumberReader; import java.util.ArrayList; import java.util.HashSet; import java.util.Stack; import java.util.concurrent.Callable; import java.util.regex.Matcher; import java.util.regex.Pattern; public class OrgFileParser { private final RuntimeExceptionDao<OrgNode, Integer> nodeDao; private LineNumberReader reader; private ParseStack parseStack; private OrgFile orgFile; private final HashSet<String> excludedTags; public OrgFileParser() { this.excludedTags = PreferenceUtils.getExcludedTags(); this.nodeDao = DatabaseHelper.getOrgNodeDao(); } public void parse(OrgFile orgFile) throws IOException { if (orgFile.node == null) throw new IllegalArgumentException("Received " + orgFile.path + " with null node"); File file = new File(orgFile.path); LineNumberReader reader = new LineNumberReader(new FileReader(file)); OrgFile.getDao().createOrUpdate(orgFile); this.nodeDao.createOrUpdate(orgFile.node); this.orgFile = orgFile; this.parseStack = new ParseStack(excludedTags); this.parseStack.add(0, orgFile.node); parse(reader); orgFile.lastModified = file.lastModified(); OrgFile.getDao().update(orgFile); } public void parse(final LineNumberReader reader) throws IOException { this.reader = reader; nodeDao.callBatchTasks(new Callable<Object>() { @Override public Object call() throws Exception { String currentLine; while ((currentLine = reader.readLine()) != null) parseLine(currentLine); return null; } }); } private void parseLine(String line) throws IOException { if (TextUtils.isEmpty(line)) return; OrgNode.Type type = determineType(line); OrgNode node; switch(determineType(line)) { case Headline: node = getNodeFromHeadline(line); break; case Drawer: node = getNodeFromDrawer(line); break; case Setting: node = getNodeFromSetting(line); break; case Checkbox: node = getNodeFromCheckbox(line); break; case Date: node = getNodeFromLine(type, line); break; case Table: node = getNodeFromTable(line); break; case Body: default: node = getNodeFromBody(line); break; } if (node != null) nodeDao.create(node); } public static OrgNode.Type determineType(final String line) { if (line.matches("[*]+[ ].*")) return OrgNode.Type.Headline; if (line.startsWith("#+")) return OrgNode.Type.Setting; String trimmedLine = line.trim(); if (trimmedLine.startsWith("- [ ]") || trimmedLine.startsWith("- [X]")) return OrgNode.Type.Checkbox; if (trimmedLine.matches(".*<\\d{4}-\\d{2}-\\d{2}.*>.*")) return OrgNode.Type.Date; if (trimmedLine.matches("\\s*:\\w*:\\s*(.+)?")) return OrgNode.Type.Drawer; if (trimmedLine.startsWith("|") && trimmedLine.endsWith("|")) return OrgNode.Type.Table; return OrgNode.Type.Body; } private OrgNode getNodeFromLine(OrgNode.Type type, final String line) { OrgNode node = new OrgNode(); node.file = orgFile; node.parent = parseStack.getCurrentNode(); node.level = parseStack.getCurrentLevel() + 1; node.title = line; node.type = type; node.lineNumber = reader.getLineNumber(); node.inheritedTags = parseStack.getCurrentTags(); return node; } private OrgNode getNodeFromSetting(final String line) { if (line.startsWith("#+FILETAGS:")) { String tags = line.substring("#+FILETAGS:".length()); orgFile.node.tags = OrgNodeUtils.combineTags(tags, "", new HashSet<String>()); nodeDao.update(orgFile.node); } OrgNode node = getNodeFromLine(OrgNode.Type.Setting, line); return node; } private static final String headingRegex = "(?:\\*+\\s)?(.*?)" + // Title "\\s*" + // Whitespaces "(?::([^\\s]+):)?\\s*" + // Tags "$"; // End of line private static final Pattern headingPattern = Pattern.compile(headingRegex); private OrgNode getNodeFromHeadline(final String line) { int starCount = numberOfStars(line); if (starCount == parseStack.getCurrentLevel()) { // Headline on same level parseStack.pop(); } else if (starCount < parseStack.getCurrentLevel()) { // Headline on lower level while (starCount <= parseStack.getCurrentLevel()) parseStack.pop(); } Matcher matcher = headingPattern.matcher(line); matcher.find(); final String heading = matcher.group(1) == null ? "" : matcher.group(1).trim(); OrgNode node = getNodeFromLine(OrgNode.Type.Headline, heading); node.level = starCount; if (matcher.group(2) != null) node.tags = matcher.group(2); parseStack.add(starCount, node); return node; } private OrgNode getNodeFromBody(final String line) throws IOException { OrgNode node = getNodeFromLine(OrgNode.Type.Body, line); StringBuilder builder = new StringBuilder(); builder.append(line); final ArrayList<OrgNode.Type> allowedTypes = new ArrayList<OrgNode.Type>(); allowedTypes.add(OrgNode.Type.Body); node.title = readSection(builder, allowedTypes, null); return node; } private OrgNode getNodeFromDrawer(final String line) throws IOException { OrgNode node = getNodeFromLine(OrgNode.Type.Drawer, line); StringBuilder builder = new StringBuilder(); builder.append(line); final ArrayList<OrgNode.Type> allowedTypes = new ArrayList<OrgNode.Type>(); allowedTypes.add(OrgNode.Type.Drawer); allowedTypes.add(OrgNode.Type.Body); node.title = readSection(builder, allowedTypes, ":END:"); return node; } private OrgNode getNodeFromCheckbox(final String line) throws IOException { OrgNode node = getNodeFromLine(OrgNode.Type.Checkbox, line); StringBuilder builder = new StringBuilder(); builder.append(line); final ArrayList<OrgNode.Type> allowedTypes = new ArrayList<OrgNode.Type>(); allowedTypes.add(OrgNode.Type.Body); node.title = readSection(builder, allowedTypes, ""); return node; } private OrgNode getNodeFromTable(final String line) throws IOException { OrgNode node = getNodeFromLine(OrgNode.Type.Table, line); StringBuilder builder = new StringBuilder(); builder.append(line); final ArrayList<OrgNode.Type> allowedTypes = new ArrayList<OrgNode.Type>(); allowedTypes.add(OrgNode.Type.Table); node.title = readSection(builder, allowedTypes, ""); return node; } private static final Pattern starPattern = Pattern.compile("^(\\**)\\s"); public static int numberOfStars(final String thisLine) { Matcher matcher = starPattern.matcher(thisLine); if(matcher.find()) { return matcher.end(1) - matcher.start(1); } else return 0; } private String readSection(final StringBuilder builder, final ArrayList<OrgNode.Type> allowedTypes, final String endMarker) throws IOException { while (true) { reader.mark(Integer.MAX_VALUE); String currentLine = reader.readLine(); if (currentLine == null) break; OrgNode.Type type = determineType(currentLine); if (allowedTypes.contains(type) == false) { reader.reset(); break; } builder.append("\n").append(currentLine); if (endMarker != null && currentLine.trim().equals(endMarker)) break; } return builder.toString(); } private class ParseStack { private final HashSet<String> excludedTags; private Stack<Pair<Integer, OrgNode>> parseStack; private Stack<String> tagStack; public ParseStack(HashSet<String> excludedTags) { this.parseStack = new Stack<Pair<Integer, OrgNode>>(); this.tagStack = new Stack<String>(); this.excludedTags = excludedTags; } public void add(int level, OrgNode node) { parseStack.push(new Pair<Integer, OrgNode>(level, node)); final String combinedTags = OrgNodeUtils.combineTags(node.tags, node.inheritedTags, excludedTags); tagStack.push(combinedTags); } public void pop() { this.parseStack.pop(); this.tagStack.pop(); } public int getCurrentLevel() { return parseStack.peek().first; } public OrgNode getCurrentNode() { return parseStack.peek().second; } public String getCurrentTags() { return tagStack.peek(); } } }