Code archives/File Utilities/Dictnode type, for parsing things
This code has been declared by its author to be Public Domain code.
Download source code
| |||||
You'll need the pine.BinTree module, which is here: http://blitzbasic.com/Community/posts.php?topic=97992 And can be downloaded directly from: http://dl.dropbox.com/u/10116881/blitz/pine.bintree/pine.bintree.mod.zip The big Rem block at the top of the code explains everything. Here's a file to use for the included example code: #root[ < This is a comment! > #rectangle[ x: 16; y: 16; width: 32; height: 24; ]; #rectangle[ x: 128; y: 48; width: 8; height: 64; ]; #rectangle[ x: 56; y: 72; width: 8; height: 8; ]; ]; | |||||
' --+-----------------------------------------------------------------------------------------+-- ' | This code was originally written by Sophie Kirschner (sophiek@pineapplemachine.com) | ' | It is released as public domain. Please don't interpret that as liberty to claim credit | ' | that isn't yours, or to sell this code when it could otherwise be obtained for free | ' | because that would be a really shitty thing of you to do. | ' --+-----------------------------------------------------------------------------------------+-- SuperStrict Import pine.BinTree ' http://blitzbasic.com/Community/posts.php?topic=97992 DIRECT: http://dl.dropbox.com/u/10116881/blitz/pine.bintree/pine.bintree.mod.zip Import brl.filesystem Import brl.stream Import brl.retro Rem The dictnode object is relatively straightforward to use. It's very much like XML, but with a lot fewer characters. --- A node is defined like so: # node [ ] ; The hash designates the beginning of a definition, "node" gives a name for the node, the data for the node belongs inside the "[ ]", and the ";" is an (optional) termination character. Note - while it's optional here (since the parser is smart enough to just end on a "]") it isn't optional in most other places. Also note that whitespace is ALWAYS optional. --- We can add some information to the node: # node [ stuff : "foo" ; stuff : "bar" ; ] ; This gives the node two values that can be accessed as members of a list with the getvalues("stuff") method, or the first one created with the getvalue("stuff") method. Values are always represented with and returned as Strings, so you might need to cast things to Ints or Doubles sometimes. Of course, you can call the values whatever you like; "stuff" is only one example. You can even include funky characters! Try using something like "S"#"uff" if you're really inclined to put a hash into a value name. Quotation marks are ALWAYS optional. However, leading and trailing whitespace that is not enclosed in quotes will be disregarded by the parser. If you want to put quotes in your values without them being parsed as quotes, you have a couple options. You can alter the constant in the dictnode type to give the parser the idea that some other character, say "`", should be used as quotes instead. You could also decide on some sequence of characters like "\q" and use Replace to turn occurrences of it into quotation marks after all the parsing is done. --- Nodes can also be nested: # node [ # another node [ foo : "hello" ; bar : "world" ; ] ; # so many nodes oh my gosh [ foobar : hi ; ] ; ] ; You can get a list of nodes with the same name using the method getchildren(), and you can get the first one with some name with getchild(). In this example, getchild("another node") would return the first nested dictnode. You can't play tricks with special characters and quotation marks in node names because I'm too lazy to implement it. If you really, really, really want to put open brackets or something in the names of your nodes then the source code is just below here a bit, and you're welcome to put it in yourself. --- Comments! # node < this is a comment > [ fo< this is a comment, too. >o : "hello, < this isn't a comment. because it's inside quotation marks. >" ; bar: 123< comments can go ANYWHERE >456 ; ] < except in quotation marks > ; << you can nest comments, too! > this is still a comment! > You can interrupt anything you like with comments. Note that comments are never terminated by a newline. --- Finally, you can also include additional files. node1.txt: # node1 [ foo : bar ; include : "node2.txt" ; ] ; node2.txt: # node2[ foobar : "where do we even go from here?" ; ] ; The result would be like this: # node1 [ foo : bar ; #node2 [ foobar : "where do we even go from here?" ; ] ; ] ; You can't properly read or include files that aren't all contained within a single root node, sorry. --- And that's that! Have fun with it. It's been my go-to method of defining game data outside the code for a matter of years now. EndRem ' Example program Rem ' Simple rectangle type. We'll be reading these from the example file. Type rect ' A global list containing all the rects we create. Global list:TList=CreateList() ' Position and dimensions, pretty straightforward here. Field x%,y%,width%,height% ' Create a rectangle using the information in a dictnode. Function Create:rect(node:dictnode) Local n:rect=New rect n.x=Int(node.getvalue("x")) n.y=Int(node.getvalue("y")) n.width=Int(node.getvalue("width")) n.height=Int(node.getvalue("height")) list.addlast n Return n End Function End Type ' Read the root dictnode object from a file. Global path$="dictnode_test.txt" Local root:dictnode=dictnode.parsefile(path) If Not root Then Print "Couldn't read file "+path;End ' Dump it to the console, just to show how pretty it is. Print root.dump() ' Now go through all the children of the root node and turn them into rectangle objects. For Local node:dictnode=EachIn root.children If Lower(node.name)="rectangle" Then rect.Create(node) Next ' Finally, display the result! Graphics 256,256 For Local r:rect=EachIn rect.list DrawRect r.x,r.y,r.width,r.height Next Repeat Flip Delay 100 If KeyDown(27) Or AppTerminate() Then End Forever EndRem ' Node type. Type dictnode ' The name of the node, as defined after the "#" and before the "[". Field name$="" ' The path to the file the node was read from. This can be useful if your dictnode file is listing off file paths and you want them to be ' relative the dictnode file they were defined in. This becomes really useful when you're including lots of files. Field path$ ' Positions in the file stream where the node was defined (the "#"), where the body started (the "["), and where it ended (the "]"). Field nodestartpos%,nodebodypos%,nodebodyend% ' BinTree containing all the values as Strings and linked to their names as keys Field values:BinTree=CreateTree() ' BinTree containing all the children dictnode objects and linked to their names as keys Field children:BinTree=CreateTree() ' Consts tell the parser all about special characters Const nodedefine% =Asc("#") Const nodeopen% =Asc("[") Const nodeclose% =Asc("]") Const lineend% =Asc(";") Const assignment% =Asc(":") Const quote% =Asc("~q") Const commentopen% =Asc("<") Const commentclose% =Asc(">") ' Returns true if there's at least one value with the specific key, false otherwise. Method hasvalue%(key$) Return TreeContains(values,key) End Method ' Returns true if there's at least one child node with the specific key, false otherwise. Method haschild%(key$) Return TreeContains(children,key) End Method ' Returns the first occurence of a value with a specific key. Method getvalue$(key$) Return String(TreeFind(values,key)) End Method ' Returns the first child node with a specific key. Method getchild:dictnode(key$) Return dictnode(TreeFind(children,key)) End Method ' Returns a list of all values with a specific key. Method getvalues:TList(key$) Return TreeFindAll(values,key) End Method ' Returns a list of all child nodes with a specific key. Method getchildren:TList(key$) Return TreeFindAll(children,key) End Method ' Like ToString() except badass, you could write this string to a file if you wanted to save the dictnode in addition to just reading it. Const tabstr$=" " Method dump$(tabs$="") Local str$=tabs+"#"+name+"[~n" Local ttabs$=tabs+tabstr For Local valnode:BinNode=EachIn TreeNodes(values) For Local val$=EachIn valnode.values() str:+ttabs+valnode.key+": ~q"+val+"~q;~n" Next Next For Local d:dictnode=EachIn children str:+d.dump(ttabs) Next str:+tabs+"];~n" Return str End Method ' Takes a file and spits out its root dictnode. Function parsefile:dictnode(path$) Local f:TStream=ReadFile(path) If Not f Then Return Null Local node:dictnode=parse(f,path) CloseFile f Return node End Function ' Takes a stream and spits out the root dictnode. Function parse:dictnode(f:TStream,path$,immediatelydefined%=False) ' Make a new dictnode object. Local n:dictnode=New dictnode Local char@,incomment%=0 n.path=path ' Look for the "#". If Not immediatelydefined Then Repeat char=ReadByte(f) If char=commentopen incomment:+1 ElseIf char=commentclose incomment=Max(0,incomment-1) ElseIf incomment=0 And char=nodedefine Exit EndIf If Eof(f) Then DebugLog " dictnode: Encountered unexpected end-of-file while looking for node definition." Return Null EndIf Forever EndIf n.nodestartpos=StreamPos(f) ' Now look for the "[". Repeat char=ReadByte(f) If char=commentopen incomment:+1 ElseIf char=commentclose incomment=Max(0,incomment-1) ElseIf incomment=0 If char=nodeopen Then Exit Else n.name:+Chr(char) EndIf EndIf If Eof(f) Then DebugLog " dictnode: Encountered unexpected end-of-file while looking for node opening." Return Null EndIf Forever n.name=Trim(n.name) If Not n.name Then DebugLog " dictnode: Encountered a node without a name. That could get a mite confusing." n.nodebodypos=StreamPos(f) ' Read the values and the children until "]". Local value$[2],valon%=0 Local inquote%=0,hitquote%=-1 Local hitnotwhitespace%=0 Repeat char=ReadByte(f) If inquote If char=quote inquote=Not inquote hitquote=value[valon].length Else value[valon]:+Chr(char) EndIf ElseIf char=commentopen incomment:+1 ElseIf char=commentclose incomment=Max(0,incomment-1) ElseIf char=quote inquote=Not inquote hitquote=value[valon].length ElseIf incomment=0 ' This is where the real magic happens. If char=nodedefine Then Local child:dictnode=parse(f,path,True) If child Then TreeInsert n.children,child.name,child Else DebugLog " dictnode: Encountered node definition but there was an error reading the child node." DebugLog " dictnode: Encoutered with node: "+n.tostring() Exit EndIf ElseIf char=assignment value[0]=TrimRightString(value[0],hitquote) valon=1 hitnotwhitespace=0 hitquote=-1 ElseIf char=lineend If Lower(value[0])="include" Then Local cpath$=ExtractDir(path)+"/"+value[1] Local child:dictnode=parsefile(cpath) If child Then TreeInsert n.children,child.name,child Else DebugLog " dictnode: Failed to read child dictnode from included file ~q"+cpath+"~q." DebugLog " dictnode: Encoutered with node: "+n.tostring() EndIf Else If valon Then TreeInsert n.values,Trim(value[0]),TrimRightString(value[1],hitquote) EndIf value[0]=Null;value[1]=Null;valon=0 hitnotwhitespace=0 ElseIf char=nodeclose Exit Else If hitnotwhitespace Or hitquote>=0 Or (Not IsWhiteSpace(char)) Then hitnotwhitespace=1 value[valon]:+Chr(char) EndIf EndIf EndIf If Eof(f) Then DebugLog " dictnode: Encountered unexpected end-of-file while looking for node closing." DebugLog " dictnode: Encoutered with node: "+n.tostring() Exit EndIf Forever If valon Then TreeInsert n.values,Trim(value[0]),TrimRightString(value[1],hitquote) n.nodebodyend=StreamPos(f) ' And now return the dictnode, of course. Return n ' A really specific function that gets rid of trailing whitespace but also considering if and where there was a ' final, closing quotation mark. Function TrimRightString$(str$,hitquote%) For Local i%=str.length-1 To 0 Step -1 If i=hitquote-1 Or Not(IsWhitespace(str[i])) Then Return Left(str,i+1) EndIf Next End Function End Function End Type ' This function just returns whether a given character is whitespace or not. Private Const whitespace_space%=Asc(" ") Const whitespace_newl%=Asc("~n") Const whitespace_return%=Asc("~r") Const whitespace_tab%=Asc(" ") Function IsWhitespace%(char%) Return char=whitespace_space Or char=whitespace_newl Or char=whitespace_return Or char=whitespace_tab End Function |
Comments
None.
Code Archives Forum