Hi,
I'm working on a Haskell library for interacting with emacs org files. For those that do not know, an org file is a structured outline style file that has nested headings, text, tables and other elements. For example:


* Heading 1
Some text, more text. This is a subelement of Heading 1
1. You can also have list
1. and nested lists
2. more...


** Nested Heading (subelement of Heading 1)
text... (subelement of Nested Heading)


** Another level 2 heading (subelement of Heading 1)
| Desc | Value |
|-----------+--------------------------------------|
| Table | You can also have tables in the file |
| another | row |
| seperator | you can have seps as well, eg |
|-----------+--------------------------------------|


* Another top level heading
There are many more features, see orgmode.org


My library enables read and write access to a subset of this format (eg lists aren't parsed atm).


The data structures used for writing are:


data OrgFile = OrgFile [OrgFileElement]
data OrgFileElement = Table OrgTable
| Paragraph String
| Heading OrgHeading


-- heading level, title, subelements
data OrgHeading = OrgHeading Int String [OrgFileElement]


data OrgTable = OrgTable [OrgTableRow]


data OrgTableRow = OrgTableRow [String] | OrgTableRowSep



To write a file you contruct a OrgFile out of those elements, and pass it to a writeOrgFile func. Eg:


writeOrg $ OrgFile [Heading (OrgHeading 1 "h1" [Paragraph "str"])]
would produce:
* h1
str


I was going to use the same data structures for reading an org file, but it quickly became apparent that this would not be suitable, as you needed the position of the file of an element to be able to report errors. Eg if you needed to report an error that a number was expected, the message "'cat' is not a number" is not very useful, but "Line 2031: 'cat' is not a number" is. So the data structures I used were:


data FilePosition = FilePosition Line Column


data WithPos a = WithPos {
filePos :: FilePosition,
innerValue :: a
}


data OrgTableP = OrgTableP [WithPos OrgTableRow]


data OrgFileElementP = TableP OrgTableP
| ParagraphP String
| HeadingP OrgHeadingP


data OrgHeadingP = OrgHeadingP Int String [WithPos OrgFileElementP]


data OrgFileP = OrgFileP [WithPos OrgFileElementP]



Finally there is a function readOrg, which takes a string, and returns an OrgTableP.



Now, this all works as expected (files are correctly being parsed and written), however I am having a lot of trouble trying to come up with a decent API to work with this. While writing an OrgFile is fairly easy, reading (and accessing inner parts) of an org file is very tedious, and modifying them is horrendous.


For example, to read the description line for the project named "Project14" in the file:


* 2007 Projects
** Project 1
Description: 1
Tags: None
** Project 2
Tags: asdf,fdsa
Description: hello
* 2008 Projects
* 2009 Projects
** Project14
Tags: RightProject
Description: we want this


requires the code:


type ErrorS = String
listToEither str [] = Left str
listToEither _ (x:_) = Right x


get14 :: OrgFileP -> Either ErrorS String
get14 (OrgFileP elements) = getDesc =<< (getRightProject . concatProjects) elements where
concatProjects :: [WithPos OrgFileElementP] -> [OrgHeadingP]
concatProjects [] = []
concatProjects ((WithPos _ (HeadingP h)) : rest) = h : concatProjects rest
concatProjects (_ : rest) = concatProjects rest


getRightProject :: [OrgHeadingP] -> Either ErrorS OrgHeadingP
getRightProject = listToEither "Couldn't find project14" .
filter (\(OrgHeadingP _ name _) -> name == "Project14")


getDesc :: OrgHeadingP -> Either ErrorS String
getDesc (OrgHeadingP _ _ children) =
case filter paragraphWithDesc (map innerValue children) of
[] -> Left $ show (filePos $ head children) ++
": Couldn't find desc in project"
((ParagraphP str):_) -> Right str
_ -> error "should not be possible"


paragraphWithDesc :: OrgFileElementP -> Bool
paragraphWithDesc (ParagraphP str) = str =~ "Description"
paragraphWithDesc _ = False



If you think that is bad, try writing a function that adds the Tag "Hard" to Project2 :(


What I really need is a DSL that would allow sql like queries on an OrgFileP. For example:
select (anyHeading `next`
headingWithName "Project14" `withFailMsg` "couldn't find p14" `next`
paragraphMatchingRegex "Description" `withFailMsg` "no desc")
org `output` paragraphText


would return a String
OR
select (anyHeading `next` headingWithName "Project2" `next`
paragraphMatchingRegex "Tag:") org `modify` paragraphText (++ ",Hard")


would return an OrgFile, with the new Hard tag added.


However, I don't know if this is even possible, how to do it, or if there is a better alternative to this. I would really apreciate any hints with regards to this. It would be useful to know if there are other libraries that also face this problem, and how they solved it.


Finally, I would be grateful for any other advice regarding my code. One thing that has bugged me is my solution for having file position info - my solution never seemed very elegant.


Thanks,
David