diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,340 @@
+            GNU GENERAL PUBLIC LICENSE
+               Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+    51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+            GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+             END OF TERMS AND CONDITIONS
+
+        How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year  name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/README.markdown b/README.markdown
new file mode 100644
--- /dev/null
+++ b/README.markdown
@@ -0,0 +1,496 @@
+`yst` - static websites from YAML and string templates
+======================================================
+
+`yst` is a tool for generating a static website by filling [string
+template][]s with data taken from [YAML][] or [CSV][] text files. This approach
+combines the speed, security, and ease of deployment of a static
+website with the flexibility and maintainability of a dynamic site that
+separates presentation and data.
+
+Installing yst
+--------------
+
+`yst` is written in Haskell. The easiest way to install `yst` is by using
+Haskell's [cabal install][] tool. The best way to get this tool is to
+install the [Haskell platform][], which includes a complete installation
+of the [GHC][] compiler and the `cabal` executable.
+
+Once you have `cabal`, you can install yst with two commands:
+
+    cabal update
+    cabal install yst
+
+(Note that by default, `cabal` installs the `yst` executable into
+a special directory:  `~\.cabal\bin` on unix systems.  You will need
+to make sure that directory is in your system path.)
+
+Getting started
+---------------
+
+To get started with `yst`, use the command:
+
+    yst create mysite
+
+This will create a directory `mysite` and populate it with the files
+needed for a sample site.  Change to this directory and run `yst` with
+no arguments to create the site:
+
+    cd mysite
+    yst
+
+The site will be created in the `site` directory.  Open up `site/index.html`
+to take a look.
+
+The configuration file `index.yaml` tells `yst` which pages to build,
+and from which templates and data files.  Let's take a look, so we can see
+what went into `index.html`. The file is a YAML list of YAML hashes
+(name/value pairs). The first item is
+
+    - url      : index.html
+      title    : Home
+      template : index.st
+      requires : event.st
+      data     :
+        recentevents : events.yaml ORDER BY date DESC LIMIT 2
+
+This says:  build the page `index.html` from the string template `index.st`
+(and subtemplate `event.st`) and data from `events.yaml`. Sort this data
+(a list of events) by date in descending order, and discard all but the
+first two items. Put the resulting data in the string template attribute
+`recentevents`. Give the page the title "Home."
+
+Now take a look at `events.yaml`, the data source. Again it is a YAML
+list of YAML hashes, each item in the list representing one event.
+The first is:
+
+    - date: 2009-06-28
+      speaker:  Sam Smith
+      title: Building a static website
+
+Pretty self-explanatory!  Try adding a new event, then rebuild the
+site by typing `yst` and see how it looks.
+
+If you have problems, make sure `events.yaml` is a valid YAML file.
+Structure is shown through indentation, so make sure things line
+up right.  And occasionally you may need to use quotation marks
+around string values---for example, when a title contains a colon.
+
+There's one more ingredient in our recipe---the string templates.
+Take a look at `index.st`, the template from which `index.html`
+is built:
+
+    # Welcome
+    
+    Here's our website. Have a look around.
+    
+    Our last two events:
+    
+    $recentevents:event()$
+    
+    For a complete list of events, see the [events](events.html) page.
+
+The first thing to notice is that this is in [markdown][] format (or,
+to be precise, markdown with [pandoc][] extensions).  So, for example,
+the first line is a level-one header, and there is a hyperlink
+to the events page on the last line.
+
+The second thing to notice is the bit between dollar signs.
+This is a placeholder for some formatted data.  The rendered
+template will include the list `recentevents` (remember, this
+was populated from `events.yaml` after some transformations---see
+above).  And each element of this list will be formatted by
+a subtemplate called `event.st`:
+
+    - $if(it.speaker)$$it.speaker; separator=" and "$, $endif$ *$it.title$*.
+
+Let's break this down.  The whole line will print a bulleted list
+item. `it` here refers to the event that is being processed by the
+template. So the first part says: if this event has a `speaker` field,
+print the speaker, or, if the `speaker` field is a list, print all the
+speakers separated by the word "and", followed by a comma. And the
+second part says, print the contents of the `title` field, surrounding
+it with asterisks (which is the markdown way of making it *emphasized*).
+
+(See the [string template][] documentation for details of template syntax,
+and examples.) 
+
+If you look at `index.html`, you'll see this rough structure, but in
+an HTML file, not a markdown file. `yst` converts the rendered markdown
+template to HTML (using pandoc), and inserts it into a "layout" file
+called `layout.html.st`.  If you look at this file, you'll see that it's
+an HTML file with blanks for `$contents$` and `$nav$`.  The `$contents$`
+blank gets filled by the rendered template, converted to HTML, and
+the `$nav$` blank gets filled by an HTML navigation menu (an unordered
+list with links).
+
+To summarize our example, then: `yst` sorts and filters the data in
+`events.yaml` according to our instructions; inserts this data into
+the `events.st` template, formatting each item using the `event.st`
+template; uses pandoc to convert the rendered template to HTML;
+constructs a navigation menu; and puts the contents and navigation
+menu in the layout template `layout.html.st`. The result is our page,
+`index.html`.
+
+Reference
+---------
+
+### The `yst` command
+
+Synopsis:
+
+    yst                    # rebuilds site, using default config.yaml
+    yst -f myconf.yaml     # rebuilds site, using myconf.yaml as config
+    yst create newsite     # creates a starter (demo) site in newsite directory
+
+When run without arguments, `yst` looks at `index.yaml` to determine
+the dependencies of each page, and rebuilds only the pages whose
+dependencies have changed since the last build.
+
+In order for this to work properly, you must be sure to list all
+subtemplates included recursively in the main page template using
+the `requires` field.  This field takes a single filename or a
+YAML list, which may be in either of two formats:
+
+    requires: [event.st, date.st]
+
+or
+
+    requires:
+      - event.st
+      - date.st
+
+If you don't list all the subtemplates needed to render a page
+under `requires`, `yst` will still work, but it might sometimes
+fail to rebuild a page when one of these subtemplates has been changed.
+
+### `config.yaml`
+
+The configuration file specifies the following:
+
+- `indexfile`: the filename of the index file (default: `index.yaml`)
+- `title`:  the title of the whole site
+- `templatedir`:  the directory containing templates (default: `.`)
+- `datadir`:  the directory containing yaml data files (default: `.`)
+- `filesdir`: the directory containing static files (default: `files`)
+- `layout`: the default layout template for the site (default:
+  `layout.html.st`)
+
+### `index.yaml` and submenus
+
+The index file is a YAML list of pages. Each page may have the following
+fields:
+
+- `url`:   the relative URL of the page to be built
+- `title`:   the title of the page
+- `template`:   the main string template from which the page will be built
+- `source`:   the markdown source from which the page will be built
+- `requires`:   other files changes to which should trigger a page rebuild
+  (primarily subtemplates of the main page template)
+- `data`:   string template attributes, data sources, and transformations
+  (see below)
+- `layout`:   a layout template to use, if other than the site default
+- `inmenu`:   if 'no', the page will not be included in the site navigation
+   menu
+
+Each page must have at least `url`, `title`, and either `template` or
+`source`.
+
+The pages may be organized into a tree-like hierarchy, which will be
+reflected in the site navigation menu.  It is easiest to see how this
+works by example:
+
+    - Rooms:
+      - url      : room101.html
+        title    : Room 101
+        source   : room101.txt
+    
+      - url      : room203.html
+        title    : Room 203
+        source   : room203.txt
+
+Here we have a subtree called "Rooms" with two pages under it.
+Subtrees can contain other subtrees.  Just be consistent about indentation.
+
+### The `data` field
+
+The `data` field in `index.yaml` can populate any number of stringtemplate
+attributes with data from YAML or CSV files.  The syntax is easiest to
+explain by example (note that the keywords do not have to be in ALL CAPS,
+although they may, and the query doesn't have to end with a semicolon,
+though it may):
+
+    data:
+        events:  events.yaml order by date desc group by title then location
+        people:  people.csv order by birthday then lastname where
+                  birthstate = 'CA' limit 5
+
+First we have the name of the stringtemplate attribute to be populated
+(say, `events`).  Then, after the colon, we have the data source
+(`events.yaml`) followed by one or more *transformations*, which will
+be applied in order.  Here are the possible transformations.  In
+what follows, brackets denote an optional component, `|` denotes
+alternatives, and `*` indicates that the component may be repeated
+several times:
+
+`ORDER BY field [ASC|DESC] [THEN field [ASC|DESC]]*`
+
+>   Sorts a list by comparing the value of `field`.  `ASC`
+    (the default) means "ascending", and `DESC` means "descending".
+    The keyword `THEN` is used to separate fields that will be
+    compared in order.  So, if we are ordering by `birthday then lastname`,
+    we will compare birthdays, and if these are equal, we will break
+    the tie by comparing last names. 
+
+`GROUP BY field [THEN field]*`
+
+>   Converts a list into a list of lists, where each sublist contains
+    only items with the same value for `field`.  So, for example,
+    `group by date` takes a list of events and produces a list of
+    lists of items, where each sublist contains events occuring at
+    a single date.  `GROUP BY date THEN venue` would produce a list
+    of lists of lists, and so on.
+
+`LIMIT n`
+
+>   Removes all but the *n* top items from a list.  *n* must be a number.
+
+`WHERE condition`
+
+>   Selects only items that meet a condition.
+
+>   A *condition* in a `WHERE` statement is a Boolean combination (using
+    `NOT`, `AND`, `OR`, and parentheses for disambiguation) of *basic
+    conditions*.  A *basic condition* is of the form `value op value`,
+    where `value` may be either a fieldname or a constant.  Note that
+    all constants must be enclosed in quotes.  `op` may be one of the
+    following:  `=`, `>=`, `<=`, `>`, `<`.
+
+Note that the order of transformations is significant.  You can get
+different results if you use `LIMIT` before or after `ORDER BY`,
+for example.
+
+### Static files
+
+Any file or subdirectory in the `files` directory (or whatever is
+the value of `filesdir` in `config.yaml`) will be copied verbatim to
+the site.  So this is the place to put javascripts, css files, images,
+PDFs, and the like.
+
+### Date fields
+
+`yst` will recognize date fields in data files automatically, if the
+dates are in one of the following formats:
+
+    - the locale's standard date format
+    - MM/DD/YYYY (e.g. 04/28/1953)
+    - MM/DD/YY (e.g. 04/28/53)
+    - YYYY-MM-DD (e.g. 1953-04-28)
+    - DD MON YYYY (e.g. 28 Apr 1953)
+
+Dates may be formatted in templates using a stringtemplate "format"
+directive. There's an example in the demo file `date.st`:
+
+    $it; format="%B %d, %Y"$
+
+The following codes may be used in format strings (taken from
+Haskell's `Date.Time.Format` documentation):
+
+- `%D` :   same as `%m/%d/%y`
+- `%F` :   same as `%Y-%m-%d`
+- `%x` :   as dateFmt locale (e.g. `%m/%d/%y`)
+- `%Y` :   year
+- `%y` :   last two digits of year, 00 - 99
+- `%C` :   century (being the first two digits of the year), 00 - 99
+- `%B` :   month name, long form (fst from months locale), January - December
+- `%b, %h` :   month name, short form (snd from months locale), Jan - Dec
+- `%m` :   month of year, leading 0 as needed, 01 - 12
+- `%d` :   day of month, leading 0 as needed, 01 - 31
+- `%e` :   day of month, leading space as needed, 1 - 31
+- `%j` :   day of year for Ordinal Date format, 001 - 366
+- `%G` :   year for Week Date format
+- `%g` :   last two digits of year for Week Date format, 00 - 99
+- `%f` :   century (first two digits of year) for Week Date format, 00 - 99
+- `%V` :   week for Week Date format, 01 - 53
+- `%u` :   day for Week Date format, 1 - 7
+- `%a` :   day of week, short form (snd from wDays locale), Sun - Sat
+- `%A` :   day of week, long form (fst from wDays locale), Sunday - Saturday
+- `%U` :   week number of year, where weeks start on Sunday (as sundayStartWeek), 00 - 53
+- `%w` :   day of week number, 0 (= Sunday) - 6 (= Saturday)
+- `%W` :   week number of year, where weeks start on Monday (as mondayStartWeek), 00 - 53
+
+### Lists as values
+
+In some cases, a field may have one or several values.  For example, an
+event might occur at a date or a date range, and an article may have one
+author or a list of authors.
+
+An elegant way to handle these cases is to let the field take either a
+scalar or a list value, and use stringtemplate's "separator" directive
+to format the result appropriately.  So, for example, in our `events.yaml`
+we have:
+
+    - date: 2009-06-28
+      speaker:  Sam Smith
+      title: Building a static website
+    
+    - date:  2009-04-15
+      speaker:
+        - Sam Smith
+        - '[Jim Jones](http://foo.bar/baz)'
+      title: Advantages of static websites
+    
+    - date:
+        - 2009-04-20
+        - 2009-04-22
+      title: Seminar on web security
+    
+    - date: 2009-04-15
+      speaker: Jim Jones
+      title:  XSS attacks for dummies
+
+Note that the `date` field is sometimes a single date, sometimes a
+list (with start and end date of a range), and the `speaker` field is
+sometimes a single speaker, and sometimes a range.
+
+Here is how we handle the date in `eventgroup.st`:
+
+    **$first(it).date:date(); separator=" - "$**
+
+Here `first(it).date` is the raw data, which may be a single date
+or a list.  `first(it).date:date()` is the result of formatting each
+date using the `date.st` template (discussed above).  And
+`first(it).date:date; separator=" - "` is the result of taking this
+list of formatted dates and concatenating them, separated by a hyphen.
+When there is just one date, we just get a date. When there are two,
+we get a date range.
+
+We can use the same trick in the case of `speaker`.  If `it` is
+an event record, then `it.speaker; separator=" and "` will be
+either a single speaker (if the value is not a list) or a list
+of speakers separated by "and" (if it is a list).
+
+In sorting lists with `order by`, `yst` compares two lists by
+comparing the first members, then (in case of a tie) the second
+members, and so on.  If one item is a list and the other a scalar,
+the scalar is compared to the first item of the list.  So, in
+the example above, `Seminar on web security` will be sorted
+an earlier than an event with date `2009-04-21`, and later than
+an event with date range `2009-04-20 - 2009-04-21`.
+
+### YAML gotchas
+
+If you have a colon in a YAML value, be sure to enclose it in quotes,
+or you'll get an error.  So,
+
+    title:  "Cheever: A Life"
+
+not
+
+    title:  Cheever: A Life
+
+Or (especially if the string is long), use `>` or `|` for a wrapped
+or unwrapped multiline string literal:
+
+    title: |
+      A very long string that
+      goes on and on.
+
+      You can even have blank lines,
+      but be sure to maintain indentation.
+
+### Using CSV files instead of YAML
+
+If you like, you can use a CSV file instead of YAML for your data source.
+Just give it the extension `.csv`.  In `index.yaml`, you'd have:
+
+    data:
+      events:  events.csv order by date desc
+
+This can be handy if you're using existing data, because spreadsheets
+and databases can easily be dumped to CSV. In the case of a SQL
+database, you can use a query like this to get the CSV:
+
+    SELECT * INTO OUTFILE 'result.csv'
+    FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'
+    LINES TERMINATED BY '\n'
+    FROM my_table;
+
+(Thanks to
+<http://www.terminally-incoherent.com/blog/2006/07/20/dump-mysql-table-into-csv-file/>.)
+
+### Using HTML in the templates
+
+Markdown allows raw HTML to be used, so you can embed HTML in templates.
+Pandoc's extended markdown is different from standard markdown in that
+it parses text within HTML block elements as markdown.  So, for example,
+you can include a section in `<div>` tags, or use raw `<ul>` and `<li>`
+tags instead of markdown formatting.
+
+### Stringtemplate gotchas
+
+Sometimes you want to create a span-level template---something like
+`date.st`, which just formats a date:
+
+    $it; format="%B %d, %Y"$
+
+If you create a file containing this line with a regular text editor,
+the file will end with a newline character.  This will be treated
+as a newline in the template, so you'll get a new line after every
+date in a list of dates: not what you want.
+
+The solution is to set up your text editor so that it doesn't
+add the newline at the end of the file.  Using [vim][], you can do this
+with the commands `set binary` and `set noeol`.  To make this the
+default for `.st` files, add this line to `augroup filetypedetect`
+in `~\.vim\filetype.vim`:
+
+    au! BufRead,BufNewFile *.st setfiletype stringtemplate
+
+Then add a file `stringtemplate.st` in `~/.vim/ftplugin` with
+the following contents:
+
+    set binary
+    set noeol
+
+### Layout templates
+
+Layout files are also string templates, but they are not treated as
+markdown by default.  They should use a double extension to indicate
+the format.  So, for example, an HTML layout could be `standard.html.st`,
+and a LaTeX layout could be `printed.tex.st`.  `yst` will convert the
+page contents appropriately for the format of the layout template.
+Here are the supported formats and extensions:
+
+- HTML:   `.html.st`, `.xhtml.st`
+- LaTeX:   `.tex.st`, `.latex.st`
+- ConTeXt:   `.context.st`
+- Groff man:   `.`1`.st`
+- Rich text format:   `.rtf.st`
+- Texinfo:   `.texi.st`
+- DocBook:   `.db.st`
+- OpenDocument XML:   `.fodt.st`
+- Plain text (markdown):   `.txt.st`, `.markdown.st`
+
+The demo site shows how you can use `yst` to produce a LaTeX document
+from the same data sources you use to produce HTML pages.
+
+The following stringtemplate attributes are defined when layouts
+are rendered:
+
+- `$contents$`:   the result of rendering the page and converting to the layout's format
+- `$nav$`:   an HTML navigation menu created from `index.yaml`
+- `$gendate$`:   the date the page was generated
+- `$sitetitle$`:   the site title from `config.yaml`
+- `$pagetitle$`:   the page title as defined in `index.yaml`
+
+
+[string template]: http://www.stringtemplate.org/
+[YAML]: http://www.yaml.org/
+[CSV]: http://en.wikipedia.org/wiki/Comma-separated_values
+[cabal install]: http://hackage.haskell.org/trac/hackage/wiki/CabalInstall
+[Haskell platform]: http://hackage.haskell.org/platform/
+[markdown]: http://daringfireball.com/markdown
+[pandoc]: http://johnmacfarlane.net/pandoc/
+[vim]: http://www.vim.org/
+
+
diff --git a/Setup.lhs b/Setup.lhs
new file mode 100644
--- /dev/null
+++ b/Setup.lhs
@@ -0,0 +1,3 @@
+#!/usr/bin/env runhaskell
+> import Distribution.Simple
+> main = defaultMain
diff --git a/Yst/Build.hs b/Yst/Build.hs
new file mode 100644
--- /dev/null
+++ b/Yst/Build.hs
@@ -0,0 +1,94 @@
+{-
+Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+-}
+
+module Yst.Build (buildSite)
+where
+import Yst.Types
+import Yst.Util
+import Yst.Render
+import qualified Data.Map as M
+import Data.Maybe (fromMaybe)
+import Data.List
+import System.FilePath
+import System.Directory
+import System.Exit
+import System.Time (ClockTime(..))
+import System.IO.UTF8
+import System.IO (stderr)
+import Prelude hiding (readFile, putStrLn, print, writeFile)
+import Control.Monad
+
+dependencies :: Site -> String -> [FilePath]
+dependencies site url =
+  let page = case M.lookup url (pageIndex site) of
+                  Nothing   -> error $ "Tried to get dependencies for nonexistent page: " ++ url
+                  Just pg   -> pg
+      layout = sourceDir site </> stripStExt (fromMaybe (defaultLayout site) $ layoutFile page) <.> "st"
+      requires = map (sourceDir site </>) $ requiresFiles page
+      srcdir = sourceDir site </>
+                 case sourceFile page of
+                       TemplateFile f -> stripStExt f <.> "st"
+                       SourceFile f   -> f
+      dataFiles = map (\(_,(f,_)) -> dataDir site </> f) $ pageData page
+  in  indexFile site : layout : srcdir : (requires ++ dataFiles)
+
+buildSite :: Site -> IO ()
+buildSite site = do
+  files <- liftM (filter (/=".") . map (makeRelative $ filesDir site)) $ getDirectoryContentsRecursive $ filesDir site
+  let pages = M.keys $ pageIndex site
+  let overlap = files `intersect` pages
+  unless (null overlap) $ forM_ overlap
+    (\f -> hPutStrLn stderr $ "Warning: the page '" ++ f ++ "' will overwrite the file by the same name.")
+  forM_ files $ \file ->
+     updateFile site file
+  forM_ pages $ \page ->
+     case M.lookup page (pageIndex site) of
+          Just  pg -> updatePage site pg
+          Nothing  -> error $ "Couldn't find page " ++ page
+
+updateFile :: Site -> FilePath -> IO ()
+updateFile site file = do
+  let destpath = deployDir site </> file
+  let srcpath = filesDir site </> file
+  srcmod <- getModificationTime srcpath
+  destmod <- catch (getModificationTime destpath) (\_ -> return $ TOD 0 0)
+  if srcmod > destmod
+     then do
+       createDirectoryIfMissing True $ takeDirectory destpath
+       hPutStrLn stderr $ "Updating " ++ destpath
+       copyFile srcpath destpath
+     else return ()
+
+updatePage :: Site -> Page -> IO ()
+updatePage site page = do
+  let destpath = deployDir site </> pageUrl page
+  let deps = dependencies site $ pageUrl page
+  forM_ deps $ \dep -> do
+    exists <- doesFileExist dep
+    unless exists $ do
+      hPutStrLn stderr $ "Missing dependency: " ++ dep
+      hPutStrLn stderr $ "Aborting!  Cannot build " ++ destpath
+      exitWith $ ExitFailure 3
+  depsmod <- mapM getModificationTime deps
+  destmod <- catch (getModificationTime destpath) (\_ -> return $ TOD 0 0)
+  if maximum depsmod > destmod
+     then do
+       createDirectoryIfMissing True $ takeDirectory destpath
+       hPutStrLn stderr $ "Updating " ++ destpath
+       renderPage site page >>= writeFile destpath
+     else return ()
diff --git a/Yst/CSV.hs b/Yst/CSV.hs
new file mode 100644
--- /dev/null
+++ b/Yst/CSV.hs
@@ -0,0 +1,45 @@
+{-
+Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+-}
+
+module Yst.CSV (readCSVFile)
+where
+import Yst.Types
+import Yst.Util
+import Text.CSV
+import Prelude hiding (readFile)
+import System.IO.UTF8
+
+readCSVFile :: FilePath -> IO Node
+readCSVFile f = catch (readFile f >>= return . csvToNode . parseCSV' f . stripBlanks)
+                   (\e -> errorExit 11 ("Error parsing " ++ f ++ ": " ++ show e) >> return NNil)
+
+parseCSV' :: FilePath -> String -> CSV
+parseCSV' f s = case parseCSV f s of
+                     Left e    -> error $ "Error parsing " ++ f ++ ": " ++ show e
+                     Right c   -> c
+
+csvToNode :: CSV -> Node
+csvToNode [] = NNil
+csvToNode (fieldNames : records) =
+  NList $ map (\record -> NMap $ zip fieldNames $ map fieldToNode record) records 
+
+fieldToNode :: Field -> Node
+fieldToNode s =
+  case parseAsDate s of
+       Nothing -> NString s
+       Just d  -> NDate d
diff --git a/Yst/Config.hs b/Yst/Config.hs
new file mode 100644
--- /dev/null
+++ b/Yst/Config.hs
@@ -0,0 +1,95 @@
+{-
+Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+-}
+
+module Yst.Config (parseConfigFile, parseIndexFile) where
+import System.FilePath
+import Yst.Types
+import Yst.Util
+import Yst.Yaml
+import Yst.Data
+import Data.Char
+import qualified Data.Map as M
+
+parseConfigFile :: FilePath -> IO Site
+parseConfigFile configfile = do
+  node <- readYamlFile configfile
+  case node of
+       NMap xs -> do
+         let indexfile = getStrAttrWithDefault "indexfile" "index.yaml" xs
+         (ind, nav) <- parseIndexFile indexfile
+         return Site{ siteTitle     = getStrAttrWithDefault "title" "" xs
+                    , sourceDir     = getStrAttrWithDefault "sourcedir" "." xs
+                    , dataDir       = getStrAttrWithDefault "datadir" "." xs
+                    , filesDir      = getStrAttrWithDefault "filesdir" "files" xs
+                    , deployDir     = getStrAttrWithDefault "deploydir" "site" xs
+                    , defaultLayout = getStrAttrWithDefault "layout" "layout" xs
+                    , indexFile     = indexfile
+                    , pageIndex     = M.fromList $ map (\pg -> (pageUrl pg, pg)) ind
+                    , navigation    = nav
+                    }
+       _       -> errorExit 7 "Configuration file must be a YAML hash." >> return undefined
+
+parseIndexFile :: FilePath -> IO ([Page], [NavNode])
+parseIndexFile indexfile = do
+  node <- readYamlFile indexfile
+  case node of
+        NList xs -> return $ foldr processItem ([], []) xs
+        _        -> errorExit 9 "Index file must be a YAML list." >> return undefined
+
+processItem :: Node -> ([Page], [NavNode]) -> ([Page], [NavNode])
+processItem (NMap [(s, NList xs)]) (index, nav) = -- a submenu
+  (pages ++ index, (NavMenu s navnodes) : nav)
+    where (pages, navnodes) = foldr processItem ([], []) xs
+processItem (NMap xs) (index, nav) =  -- a page
+  (page : index, newnav)
+    where page = processPage xs
+          newnav = if pageInMenu page
+                      then NavPage (pageTitle page) (pageUrl page) : nav
+                      else nav
+processItem _ _ = error "processItem encountered a non-NMap node."
+
+processPage :: [(String, Node)] -> Page
+processPage xs =
+  Page { pageData     = case lookup "data" xs of
+                             Nothing        -> []
+                             Just (NMap ds) -> map (\(k,v) -> (k, parseDataField v)) ds
+                             Just _         -> error "data must be a YAML map"
+       , layoutFile   = fmap fromNString $ lookup "layout" xs
+       , sourceFile   = case (lookup "source" xs, lookup "template" xs) of
+                             (Nothing, Nothing)  -> error "No 'source' or 'template' found for page."
+                             (Nothing, Just f)   -> TemplateFile $ fromNString f
+                             (Just f, Nothing)   -> SourceFile $ fromNString f
+                             (Just _, Just _)    -> error "Both 'source' and 'template' specified for the same page."
+       , requiresFiles = case lookup "requires" xs of
+                              Nothing          -> []
+                              Just (NList fs)  -> map getStrAttr fs
+                              Just (NString f) -> [f]
+                              Just _           -> error "'requires' must be scalar or list"
+       , pageUrl      = url
+       , pageTitle    = getStrAttrWithDefault "title" (dropExtension url) xs
+       , pageInMenu   = (map toLower $ getStrAttrWithDefault "inmenu" "yes" xs) `notElem`
+                          ["no","false"]
+       }
+    where getPageField f = case (fmap fromNString $ lookup f xs) of
+                             Just s  -> s
+                             Nothing -> error $ "Missing required " ++ f ++ " field in page definition"
+          url = getPageField "url"
+          getStrAttr (NString s) = s
+          getStrAttr x           = error $ "expected string, got " ++ show x
+
+
diff --git a/Yst/Data.hs b/Yst/Data.hs
new file mode 100644
--- /dev/null
+++ b/Yst/Data.hs
@@ -0,0 +1,255 @@
+{-# LANGUAGE FlexibleContexts #-}
+{-
+Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+-}
+
+module Yst.Data (getData, parseDataField)
+where
+import Yst.Types
+import Yst.Util
+import Yst.Yaml
+import Yst.CSV
+import Control.Monad
+import Data.Char
+import Data.Maybe (fromMaybe)
+import Data.List (sortBy, nub)
+import Text.ParserCombinators.Parsec
+import System.FilePath (takeExtension)
+
+getData :: (FilePath, [DataOption]) -> IO Node
+getData (file, opts) = do
+  raw <- catch (readDataFile file)
+          (\e -> errorExit 15 ("Error reading data from " ++ file ++ ": " ++ show e) >> return undefined)
+  return $ foldl applyDataOption raw opts
+
+readDataFile :: FilePath -> IO Node
+readDataFile f =
+  case (map toLower $ takeExtension f) of
+       ".yaml"      -> readYamlFile f
+       ".csv"       -> readCSVFile f
+       _            -> readYamlFile f
+
+applyDataOption :: Node -> DataOption -> Node
+applyDataOption (NList ns) (Limit lim) =
+  NList $ take lim ns
+applyDataOption (NList ns) (Where cond) =
+  NList $ filter (satisfiesCond cond) ns
+applyDataOption (NList ns) (GroupBy []) = NList ns
+applyDataOption (NList ns) (GroupBy (k:ks)) =
+  NList sorted
+    where sorted             = map (\x -> applyDataOption (NList $ filter (\n -> getAttr k n == x) ns) (GroupBy ks)) keys
+          keys               = nub $ map (getAttr k) ns
+          getAttr x (NMap m) = lookup x m
+          getAttr _ _        = error "Can't get an attribute from a non-map"
+applyDataOption (NList ns) (OrderBy xs) =
+  NList $ sortBy (compareNodeAt xs) ns
+applyDataOption _ _ = error "order by and group by can be used only on lists"
+
+satisfiesCond :: FilterCond -> Node -> Bool
+satisfiesCond (And c1 c2) n = satisfiesCond c1 n && satisfiesCond c2 n
+satisfiesCond (Or  c1 c2) n = satisfiesCond c1 n || satisfiesCond c2 n
+satisfiesCond (Not c1)    n = not (satisfiesCond c1 n)
+satisfiesCond (Filter test arg1 arg2) n =
+  (filterTestPred test) (filterArgToNode arg1 n) (filterArgToNode arg2 n)
+
+filterTestPred :: FilterTest -> (Node -> Node -> Bool)
+filterTestPred TestEq    = (==)
+filterTestPred TestGt    = (>)
+filterTestPred TestLt    = (<)
+filterTestPred TestGtEq  = (>=)
+filterTestPred TestLtEq  = (<=)
+
+filterArgToNode :: FilterArg -> Node -> Node
+filterArgToNode (AttrValue attr) (NMap ns) = fromMaybe NNil (lookup attr ns)
+filterArgToNode (AttrValue _) x = error $ "Cannot lookup attribute in non-map node: " ++ show x
+filterArgToNode (StringConstant s) _ = NString s
+filterArgToNode (DateConstant d) _ = NDate d
+
+compareNodeAt :: [(String,SortDirection)] -> Node -> Node -> Ordering
+compareNodeAt ((a,dir'):as) (NMap xs) (NMap ys) = reverseIfDescending dir' $
+  case ((lookup a xs), (lookup a ys)) of
+       (Just x, Just y)   -> case compare x y of
+                                   EQ -> compareNodeAt as (NMap xs) (NMap ys)
+                                   z  -> z
+       (Just _, Nothing)  -> GT
+       (Nothing, Just _)  -> LT
+       (Nothing, Nothing) -> EQ
+compareNodeAt [] (NMap _) (NMap _) = EQ
+compareNodeAt _ _ _ = error "sortby and groupby can be used only on lists of maps"
+
+reverseIfDescending :: SortDirection -> Ordering -> Ordering
+reverseIfDescending Ascending o = o
+reverseIfDescending Descending EQ = EQ
+reverseIfDescending Descending LT = GT
+reverseIfDescending Descending GT = LT
+
+parseDataField :: Node -> (FilePath, [DataOption])
+parseDataField (NString s) = case parse pDataField s s of
+  Right res   -> res
+  Left err    -> error $ show err
+parseDataField x = error $ "Expected string node, got " ++ show x
+
+pDataField :: GenParser Char st ([Char], [DataOption])
+pDataField = do
+  fname <- pIdentifier <?> "name of YAML or CSV file"
+  opts <- many $ (pOptWhere <?> "where [CONDITION]")
+              <|> (pOptLimit <?> "limit [NUMBER]")
+              <|> (pOptOrderBy <?> "order by [CONDITION]")
+              <|> (pOptGroupBy <?> "group by [CONDITION]")
+  spaces
+  optional $ char ';'
+  spaces
+  eof
+  return (fname, opts)
+
+pIdentifier :: GenParser Char st [Char]
+pIdentifier = spaces >> (pQuoted '\'' <|> pQuoted '"' <|> many (noneOf " \t\n;,'\""))
+
+-- | Case-insensitive string parser.
+pString :: String -> GenParser Char st String
+pString s = do
+  s' <- count (length s) anyChar
+  if map toLower s == map toLower s'
+     then return s
+     else mzero
+
+pQuoted :: Char -> GenParser Char st [Char]
+pQuoted delim = try $ do
+  char delim
+  res <- many (noneOf [delim] <|> (try $ char '\\' >> char delim))
+  char delim
+  return res
+
+pOptLimit :: GenParser Char st DataOption
+pOptLimit = try $ do
+  optional $ oneOf ",;"
+  spaces
+  pString "limit"
+  pSpace
+  lim <- many1 digit
+  return $ Limit $ read lim
+
+pOptOrderBy :: GenParser Char st DataOption
+pOptOrderBy = try $ do
+  optional $ oneOf ",;"
+  spaces
+  pString "order"
+  pSpace
+  pString "by"
+  pSpace
+  keys <- spaces >> sepBy1 pSortKey (try $ pSpace >> spaces >> pString "then" >> pSpace)
+  return $ OrderBy keys
+
+pSortKey :: GenParser Char st ([Char], SortDirection)
+pSortKey = do
+  res <- pIdentifier
+  dir' <- option Ascending pAscDesc
+  return (res, dir')
+
+pAscDesc :: GenParser Char st SortDirection
+pAscDesc = (try $ pSpace >> pString "desc" >> return Descending)
+       <|> (try $ pSpace >> pString "asc" >> return Ascending)
+
+pOptGroupBy :: GenParser Char st DataOption
+pOptGroupBy = try $ do
+  optional $ oneOf ",;"
+  spaces
+  pString "group"
+  pSpace 
+  pString "by"
+  pSpace
+  keys <- spaces >> sepBy1 pIdentifier (try $ pSpace >> spaces >> pString "then" >> pSpace)
+  return $ GroupBy keys
+
+pOptWhere :: GenParser Char st DataOption
+pOptWhere = try $ do
+  optional $ oneOf ",;"
+  spaces
+  pString "where"
+  pSpace
+  cond <- pBooleanCondition
+  return $ Where cond
+
+pBooleanCondition :: GenParser Char st FilterCond
+pBooleanCondition = spaces >> (pNot <|> pAnd <|> pOr <|> pInParens pBooleanCondition <|> pBasicCond)
+
+pInParens :: GenParser Char st a -> GenParser Char st a
+pInParens innerParser = try $ do
+  char '('
+  spaces
+  res <- innerParser
+  spaces
+  char ')'
+  return res 
+
+pNot :: GenParser Char st FilterCond
+pNot = try $ pString "not" >> pSpace >> liftM Not pBooleanCondition
+
+pAnd :: GenParser Char st FilterCond
+pAnd = try $ do
+  x <- pNot <|> pInParens pBooleanCondition <|> pBasicCond
+  pSpace
+  pString "and"
+  pSpace
+  y <- pBooleanCondition
+  return $ And x y
+
+pOr :: GenParser Char st FilterCond
+pOr = try $ do
+  x <- pNot <|> pAnd <|> pInParens pBooleanCondition <|> pBasicCond
+  pSpace
+  pString "or"
+  pSpace
+  y <- pBooleanCondition
+  return $ Or x y
+
+pBasicCond :: GenParser Char st FilterCond
+pBasicCond = try $ do
+  x <- pFilterArg
+  t <- pFilterTest
+  y <- pFilterArg
+  return $ Filter t x y
+
+pFilterArg :: GenParser Char st FilterArg
+pFilterArg = spaces >> (pStringOrDateConstant <|> pAttrValue)
+
+pStringOrDateConstant :: GenParser Char st FilterArg
+pStringOrDateConstant = do
+  str <- pQuoted '"' <|> pQuoted '\''
+  case parseAsDate str of
+       Just d    -> return $ DateConstant d
+       Nothing   -> return $ StringConstant str
+
+pAttrValue :: GenParser Char st FilterArg
+pAttrValue = liftM AttrValue pIdentifier
+
+pFilterTest :: GenParser Char st FilterTest
+pFilterTest = do
+  spaces
+  choice $ map (\(s,t) -> try $ string s >> return t)
+         [ ("==",TestEq)
+         , ("=",TestEq)
+         , (">=",TestGtEq)
+         , ("<=",TestLtEq)
+         , (">",TestGt)
+         , ("<",TestLt)
+         ]
+
+pSpace :: GenParser Char st ()
+pSpace = oneOf " \t\n" >> spaces
+
+
diff --git a/Yst/Render.hs b/Yst/Render.hs
new file mode 100644
--- /dev/null
+++ b/Yst/Render.hs
@@ -0,0 +1,121 @@
+{-
+Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+-}
+
+module Yst.Render (renderPage)
+where
+import Yst.Types
+import Yst.Util
+import Yst.Data
+import System.Directory
+import Text.Pandoc
+import Text.XHtml hiding (option, (</>))
+import Data.Char
+import Data.List (intercalate)
+import Text.StringTemplate
+import Data.Maybe (fromMaybe)
+import System.FilePath
+import System.IO.UTF8
+import Prelude hiding (readFile, putStrLn, print, writeFile)
+import Data.Time
+import Control.Monad
+
+renderNav :: String -> [NavNode] -> String
+renderNav targeturl nodes = renderHtmlFragment $
+  ulist ! [theclass "nav"] << map (renderNavNode targeturl) nodes
+
+renderNavNode :: String -> NavNode -> Html
+renderNavNode targeturl (NavPage tit pageurl) =
+  li ! attrs << hotlink pageurl << tit
+    where attrs = if pageurl == targeturl
+                     then [theclass "current"]
+                     else []
+renderNavNode targeturl (NavMenu tit nodes) =
+  li ! attrs << [ toHtml $ hotlink "#" << (tit ++ " »")
+                , ulist ! attrs << map (renderNavNode targeturl) nodes ]
+    where active = targeturl `isInNavNodes` nodes
+          attrs = if active then [theclass "active"] else []
+          isInNavNodes u = any (isInNavNode u)
+          isInNavNode u (NavPage _ u') = u == u'
+          isInNavNode u (NavMenu _ ns) = u `isInNavNodes` ns
+
+formatFromExtension :: FilePath -> Format
+formatFromExtension f = case (map toLower $ takeExtension f) of
+                             ".html"  -> HtmlFormat
+                             ".xhtml" -> HtmlFormat
+                             ".latex" -> LaTeXFormat
+                             ".tex"   -> LaTeXFormat
+                             ".context" -> ConTeXtFormat
+                             ".1"     -> ManFormat
+                             ".rtf"   -> RTFFormat
+                             ".texi"  -> TexinfoFormat
+                             ".db"    -> DocBookFormat
+                             ".fodt"  -> OpenDocumentFormat
+                             ".txt"   -> PlainFormat
+                             ".markdown" -> PlainFormat
+                             _       -> HtmlFormat
+renderPage :: Site -> Page -> IO String
+renderPage site page = do
+  let menuHtml = renderNav (pageUrl page) (navigation site)
+  let layout = fromMaybe (defaultLayout site) $ layoutFile page
+  srcDir <- canonicalizePath $ sourceDir site
+  g <- directoryGroup srcDir
+  attrs <- forM (pageData page) $ \(k, v) -> getData v >>= \n -> return (k,n)
+  todaysDate <- liftM utctDay getCurrentTime
+  rawContents <-
+    case sourceFile page of
+          SourceFile sf   -> readFile (srcDir </> sf)
+          TemplateFile tf -> do
+            templ <- getTemplate tf g
+            return $ render (setManyAttrib attrs templ)
+  layoutTempl <- getTemplate layout g
+  let format = formatFromExtension (stripStExt layout)
+  let contents = converterForFormat format rawContents
+  return $ render
+         . setAttribute "sitetitle" (siteTitle site)
+         . setAttribute "pagetitle" (pageTitle page)
+         . setAttribute "gendate" todaysDate 
+         . setAttribute "contents" contents
+         . setAttribute "nav" menuHtml
+         $ layoutTempl
+
+converterForFormat :: Format -> String -> String
+converterForFormat f =
+  let reader = readMarkdown defaultParserState{stateSmart = True}
+  in  case f of
+       HtmlFormat          -> writeHtmlString defaultWriterOptions . reader
+       LaTeXFormat         -> writeLaTeX defaultWriterOptions . reader
+       PlainFormat         -> id
+       ConTeXtFormat       -> writeConTeXt defaultWriterOptions . reader
+       ManFormat           -> writeMan defaultWriterOptions . reader
+       RTFFormat           -> writeRTF defaultWriterOptions . reader
+       DocBookFormat       -> writeDocbook defaultWriterOptions . reader
+       TexinfoFormat       -> writeTexinfo defaultWriterOptions . reader
+       OpenDocumentFormat  -> writeOpenDocument defaultWriterOptions . reader
+
+getTemplate :: Stringable a => String -> STGroup a -> IO (StringTemplate a)
+getTemplate templateName templateGroup = do
+  let template = case getStringTemplate (stripStExt templateName) templateGroup of
+                       Just pt  -> pt
+                       Nothing  -> error $ "Could not load template: " ++ templateName
+  case checkTemplate template of
+       (Just parseErrors, _, _ )       -> errorExit 17 $ "Error in template '" ++ templateName ++
+                                             "': " ++ parseErrors
+       (_, _, Just templatesNotFound)  -> errorExit 21 $ "Templates referenced in template '" ++ templateName ++
+                                             "' not found: " ++ (intercalate ", " templatesNotFound)
+       (_, _, _)                       -> return ()
+  return template
diff --git a/Yst/Types.hs b/Yst/Types.hs
new file mode 100644
--- /dev/null
+++ b/Yst/Types.hs
@@ -0,0 +1,132 @@
+{-# LANGUAGE FlexibleInstances, TypeSynonymInstances #-}
+{-
+Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+-}
+
+module Yst.Types
+where
+import System.FilePath
+import Data.Char
+import Data.Time
+import Text.StringTemplate
+import qualified Data.Map as M
+
+data Site = Site {
+    siteTitle     :: String
+  , sourceDir     :: FilePath
+  , dataDir       :: FilePath
+  , filesDir      :: FilePath
+  , deployDir     :: FilePath
+  , defaultLayout :: FilePath
+  , indexFile     :: FilePath
+  , pageIndex     :: M.Map String Page
+  , navigation    :: [NavNode]
+  } deriving (Show, Read, Eq)
+
+data Source = TemplateFile FilePath
+            | SourceFile FilePath
+            deriving (Show, Read, Eq)
+
+data Page = Page {
+    pageData      :: [(String, (FilePath, [DataOption]))]
+  , layoutFile    :: Maybe FilePath
+  , sourceFile    :: Source
+  , requiresFiles :: [FilePath]
+  , pageUrl       :: String
+  , pageTitle     :: String
+  , pageInMenu    :: Bool
+  } deriving (Show, Read, Eq)
+
+data NavNode = NavPage String String
+             | NavMenu String [NavNode]
+             deriving (Show, Read, Eq)
+
+data Node = NString String
+          | NDate Day
+          | NList [Node]
+          | NMap [(String, Node)]
+          | NNil
+          deriving (Show, Read, Eq)
+
+instance Ord Node where
+  compare (NString x) (NString y)   = compare x y
+  compare (NDate x) (NDate y)       = compare x y
+  compare (NList x) (NList y)       = compare x y
+  compare (NMap x) (NMap y)         = compare x y
+  compare NNil NNil                 = EQ
+  compare (NList x) y               = compare (NList x) (NList [y])
+  compare x (NList y)               = compare (NList [x]) (NList y)
+  compare (NString _) _             = GT
+  compare (NDate _) _               = GT
+  compare _ _                       = GT
+
+data DataOption = OrderBy [(String, SortDirection)]
+                | GroupBy [String]
+                | Where FilterCond
+                | Limit Int 
+                deriving (Show, Read, Eq)
+
+data FilterCond = Filter FilterTest FilterArg FilterArg
+                | And FilterCond FilterCond
+                | Or  FilterCond FilterCond
+                | Not FilterCond
+                deriving (Show, Read, Eq)
+
+data FilterArg = AttrValue String
+               | StringConstant String
+               | DateConstant Day
+               deriving (Show, Read, Eq) 
+
+data FilterTest = TestEq
+                | TestGt
+                | TestLt
+                | TestGtEq
+                | TestLtEq
+                deriving (Show, Read, Eq)
+
+data SortDirection = Ascending | Descending deriving (Show, Read, Eq)
+
+data Format = HtmlFormat
+            | LaTeXFormat
+            | ConTeXtFormat
+            | PlainFormat
+            | ManFormat
+            | RTFFormat
+            | TexinfoFormat
+            | DocBookFormat
+            | OpenDocumentFormat
+            deriving (Show, Read, Eq)
+
+instance StringTemplateShows String
+  where stringTemplateShow s = s
+        stringTemplateFormattedShow "uppercase" s = map toUpper s
+        stringTemplateFormattedShow "lowercase" s = map toUpper s
+        stringTemplateFormattedShow "capitalize" s = if null s
+                                                        then ""
+                                                        else toUpper (head s) : tail s
+        stringTemplateFormattedShow f _ = error $ "Unknown format: " ++ f
+
+instance ToSElem String
+ where toSElem = stShowsToSE
+
+instance ToSElem Node
+  where toSElem x = case x of
+         NString s   -> toSElem s
+         NDate d     -> toSElem d
+         NList xs    -> toSElem xs
+         NMap xs     -> toSElem $ M.fromList xs
+         NNil        -> toSElem ""
diff --git a/Yst/Util.hs b/Yst/Util.hs
new file mode 100644
--- /dev/null
+++ b/Yst/Util.hs
@@ -0,0 +1,72 @@
+{-
+Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+-}
+
+module Yst.Util (stripBlanks, parseAsDate, stripStExt, getStrAttrWithDefault, fromNString, getDirectoryContentsRecursive, errorExit)
+where
+import Yst.Types
+import System.Exit
+import System.FilePath
+import System.IO (stderr)
+import System.IO.UTF8 (hPutStrLn)
+import System.Directory
+import Control.Monad
+import Data.Time
+import Data.List (intercalate)
+import Data.Char (isSpace)
+import System.Locale (defaultTimeLocale)
+
+-- | Strip blank lines from a file.
+stripBlanks :: String -> String
+stripBlanks = intercalate "\n" . filter (not . all isSpace) . lines
+
+parseAsDate :: (ParseTime t) => String -> Maybe t
+parseAsDate s =
+  msum $ map (\fs -> parsetimeWith fs s) formats
+   where parsetimeWith = parseTime defaultTimeLocale
+         formats = ["%x","%m/%d/%Y", "%D","%F", "%d %b %Y"]
+
+stripStExt :: FilePath -> FilePath
+stripStExt f =
+  if (takeExtension f == ".st")
+     then dropExtension f
+     else f
+
+getStrAttrWithDefault :: String -> String -> [(String, Node)] -> String
+getStrAttrWithDefault attr def xs =
+  case lookup attr xs of
+        Just (NString s)   -> s
+        Just _             -> error $ attr ++ " must have string value."
+        Nothing            -> def
+
+fromNString :: Node -> String
+fromNString (NString s) = s
+fromNString x = error $ "Expected string value, got " ++ show x
+
+getDirectoryContentsRecursive :: FilePath -> IO [FilePath]
+getDirectoryContentsRecursive path = do
+  isDir <- doesDirectoryExist path
+  if isDir
+     then do
+       contents <- getDirectoryContents path
+       let contents' = map (path </>) $ filter (`notElem` ["..","."]) contents
+       children <- mapM getDirectoryContentsRecursive contents'
+       return (concat children)
+     else return [path]
+
+errorExit :: Int -> String -> IO ()
+errorExit lvl msg = hPutStrLn stderr msg >> exitWith (ExitFailure lvl)
diff --git a/Yst/Yaml.hs b/Yst/Yaml.hs
new file mode 100644
--- /dev/null
+++ b/Yst/Yaml.hs
@@ -0,0 +1,63 @@
+{-
+Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+-}
+
+module Yst.Yaml (readYamlFile, nodeToYamlNode)
+where
+import Yst.Types
+import Yst.Util
+import Data.Yaml.Syck
+import Data.Time
+import System.Locale (defaultTimeLocale)
+import Prelude hiding (readFile)
+import System.IO.UTF8
+
+readYamlFile :: FilePath -> IO Node
+readYamlFile f = catch (readFile f >>= parseYaml >>= return . yamlNodeToNode)
+                   (\e -> errorExit 11 ("Error parsing " ++ f ++ ": " ++ show e) >> return NNil)
+
+yamlNodeToNode :: YamlNode -> Node
+yamlNodeToNode n =
+  case n_elem n of
+        EStr s  -> case parseAsDate (unpackBuf s) of
+                        Nothing  -> NString (unpackBuf s)
+                        Just d   -> NDate d
+        EMap xs | all (\(k,_) -> isStrNode k) xs -> NMap pairs
+                     where pairs = map mkPair xs
+                           mkPair (k,v) = (strFrom k, yamlNodeToNode v)
+        EMap _  -> error "Map keys must all be strings."
+        ESeq xs -> NList $ map yamlNodeToNode xs
+        ENil    -> NNil
+
+nodeToYamlNode :: Node -> YamlNode
+nodeToYamlNode n =
+  case n of
+       NString s -> mkNode (EStr $ packBuf s)
+       NDate s   -> mkNode (EStr $ packBuf $ formatTime defaultTimeLocale "%x" s)
+       NMap xs   -> mkNode (EMap $ map (\(k,v) -> (nodeToYamlNode (NString k), nodeToYamlNode v)) xs)
+       NList xs  -> mkNode (ESeq $ map nodeToYamlNode xs)
+       NNil      -> mkNode ENil
+
+isStrNode :: YamlNode -> Bool
+isStrNode x = case n_elem x of
+                   EStr _ -> True
+                   _      -> False
+
+strFrom :: YamlNode -> String
+strFrom x = case n_elem x of
+                 EStr z   -> unpackBuf z
+                 _        -> error "expected EStr node"
diff --git a/demo/april_events.st b/demo/april_events.st
new file mode 100644
--- /dev/null
+++ b/demo/april_events.st
@@ -0,0 +1,3 @@
+Here's what happened in April:
+
+$april:event()$
diff --git a/demo/config.yaml b/demo/config.yaml
new file mode 100644
--- /dev/null
+++ b/demo/config.yaml
@@ -0,0 +1,6 @@
+indexfile: index.yaml
+title: My Website
+templatedir: .
+datadir: .
+filesdir: files
+layout: layout.html.st
diff --git a/demo/date.st b/demo/date.st
new file mode 100644
--- /dev/null
+++ b/demo/date.st
@@ -0,0 +1,1 @@
+$it; format="%B %d, %Y"$
diff --git a/demo/event.st b/demo/event.st
new file mode 100644
--- /dev/null
+++ b/demo/event.st
@@ -0,0 +1,2 @@
+- $if(it.speaker)$$it.speaker; separator=" and "$, $endif$ *$it.title$*.
+
diff --git a/demo/eventgroup.st b/demo/eventgroup.st
new file mode 100644
--- /dev/null
+++ b/demo/eventgroup.st
@@ -0,0 +1,3 @@
+**$first(it).date:date(); separator=" - "$**
+
+$it:event()$
diff --git a/demo/events.st b/demo/events.st
new file mode 100644
--- /dev/null
+++ b/demo/events.st
@@ -0,0 +1,11 @@
+# Events
+
+Here is a list of recent events:
+
+<dl>
+$eventsbydate:eventgroup()$
+</dl>
+
+Click [here](april_events.tex) for a LaTeX version
+of the list of April events, generated form the same
+YAML data file.
diff --git a/demo/events.yaml b/demo/events.yaml
new file mode 100644
--- /dev/null
+++ b/demo/events.yaml
@@ -0,0 +1,19 @@
+- date: 2009-06-28
+  speaker:  Sam Smith
+  title: Building a static website
+
+- date:  2009-04-15
+  speaker:
+    - Sam Smith
+    - '[Jim Jones](http://foo.bar/baz)'
+  title: Advantages of static websites
+
+- date:
+    - 2009-04-20
+    - 2009-04-22
+  title: Seminar on web security
+
+- date: 2009-04-15
+  speaker: Jim Jones
+  title:  XSS attacks for dummies
+
diff --git a/demo/files/css/base-min.css b/demo/files/css/base-min.css
new file mode 100644
--- /dev/null
+++ b/demo/files/css/base-min.css
@@ -0,0 +1,7 @@
+/*
+Copyright (c) 2009, Yahoo! Inc. All rights reserved.
+Code licensed under the BSD License:
+http://developer.yahoo.net/yui/license.txt
+version: 2.7.0
+*/
+body{margin:10px;}h1{font-size:138.5%;}h2{font-size:123.1%;}h3{font-size:108%;}h1,h2,h3{margin:1em 0;}h1,h2,h3,h4,h5,h6,strong,dt{font-weight:bold;}optgroup{font-weight:normal;}abbr,acronym{border-bottom:1px dotted #000;cursor:help;}em{font-style:italic;}del{text-decoration:line-through;}blockquote,ul,ol,dl{margin:1em;}ol,ul,dl{margin-left:2em;}ol li{list-style:decimal outside;}ul li{list-style:disc outside;}dl dd{margin-left:1em;}th,td{border:1px solid #000;padding:.5em;}th{font-weight:bold;text-align:center;}caption{margin-bottom:.5em;text-align:center;}sup{vertical-align:super;}sub{vertical-align:sub;}p,fieldset,table,pre{margin-bottom:1em;}button,input[type="checkbox"],input[type="radio"],input[type="reset"],input[type="submit"]{padding:1px;}
diff --git a/demo/files/css/hk-pyg.css b/demo/files/css/hk-pyg.css
new file mode 100644
--- /dev/null
+++ b/demo/files/css/hk-pyg.css
@@ -0,0 +1,20 @@
+/* Loosely based on pygment's default colors */
+table.sourceCode, tr.sourceCode, td.lineNumbers, td.sourceCode, table.sourceCode pre 
+   { margin: 0; padding: 0; border: 0; vertical-align: baseline; border: none; }
+td.lineNumbers { border-right: 1px solid #AAAAAA; text-align: right; color: #AAAAAA; padding-right: 5px; padding-left: 5px; } 
+td.sourceCode { padding-left: 5px; }
+pre.sourceCode { }
+pre.sourceCode span.Normal { }
+pre.sourceCode span.Keyword { color: #007020; font-weight: bold; } 
+pre.sourceCode span.DataType { color: #902000; }
+pre.sourceCode span.DecVal { color: #40a070; }
+pre.sourceCode span.BaseN { color: #40a070; }
+pre.sourceCode span.Float { color: #40a070; }
+pre.sourceCode span.Char { color: #4070a0; }
+pre.sourceCode span.String { color: #4070a0; }
+pre.sourceCode span.Comment { color: #60a0b0; font-style: italic; }
+pre.sourceCode span.Others { color: #007020; }
+pre.sourceCode span.Alert { color: red; font-weight: bold; }
+pre.sourceCode span.Function { color: #06287e; }
+pre.sourceCode span.RegionMarker { }
+pre.sourceCode span.Error { color: red; font-weight: bold; }
diff --git a/demo/files/css/ie.css b/demo/files/css/ie.css
new file mode 100644
--- /dev/null
+++ b/demo/files/css/ie.css
@@ -0,0 +1,20 @@
+/* ie.css */
+body {text-align:center;}
+.container {text-align:left;}
+* html .column {overflow-x:hidden;}
+* html legend {margin:-18px -8px 16px 0;padding:0;}
+ol {margin-left:2em;}
+sup {vertical-align:text-top;}
+sub {vertical-align:text-bottom;}
+html>body p code {*white-space:normal;}
+hr {margin:-8px auto 11px;}
+.container ul { list-style: disc outside; margin-left: 2em; } /* IE can't handle :before and :after */
+.container ul li { text-indent: 0; margin-left: 0; }
+.container legend { margin-bottom: 1.6em; } /* IE form margin bug */
+sup, sub { font-size: 100%; } /* IE superscript & subscript bug */
+.container blockquote p, #content blockquote ul, #content blockquote ol, #content blockquote dl, #content blockquote pre, #content blockquote address, 
+.container blockquote table, #content blockquote form, #content blockquote h1, #content blockquote h2, #content blockquote h3, #content blockquote h4, #content blockquote h5, #content blockquote h6 { margin-top: .8em; margin-bottom: .8em; } /* IE can't handle :first-child */
+* html .container textarea, * html .container input { padding: 0; } /* IE < 7 form fix */
+.container input[type='submit'], .container input[type='button'] { padding: 0; } /* IE 7 button fix */
+.container legend+* { margin-top: 0; } /* we already added legend margin */
+a abbr, a acronym { text-decoration: underline; } /* IE 7 bug */
diff --git a/demo/files/css/print.css b/demo/files/css/print.css
new file mode 100644
--- /dev/null
+++ b/demo/files/css/print.css
@@ -0,0 +1,39 @@
+body { 
+width:100% !important;
+margin:0 !important;
+padding:0 !important;
+line-height: 1.4;
+word-spacing:1.1pt;
+letter-spacing:0.2pt; font-family: "Times New Roman", serif; color: #000; background: none; font-size: 12pt; }
+
+/*Headings */
+h1,h2,h3,h4,h5,h6 { font-family: Helvetica, Arial, sans-serif; }
+h1{font-size:19pt;}
+h2{font-size:17pt;}
+h3{font-size:15pt;}
+h4,h5,h6{font-size:12pt;}
+
+pre, code { font: 10pt Courier, monospace; } 
+blockquote { margin: 1.3em; padding: 1em;  font-size: 10pt; }
+hr { background-color: #ccc; }
+
+/* Images */
+img { float: left; margin: 1em 1.5em 1.5em 0; }
+a img { border: none; }
+
+/* Links */
+a:link, a:visited { background: transparent; font-weight: normal; text-decoration: underline; color:#333; }
+a:link[href^="http://"]:after, a[href^="http://"]:visited:after { content: " (" attr(href) ")"; font-size: 90%; }
+a[href^="http://"] {color:#000; }
+
+/* Table */
+table { margin: 1px; text-align:left; }
+th { font-weight: bold; }
+th,td { padding: 4px 10px 4px 0; }
+tfoot { font-style: italic; }
+caption { background: #fff; margin-bottom:2em; text-align:left; }
+thead {display: table-header-group;}
+tr {page-break-inside: avoid;} 
+
+/*hide various parts from the site*/
+
diff --git a/demo/files/css/reset-fonts-grids.css b/demo/files/css/reset-fonts-grids.css
new file mode 100644
--- /dev/null
+++ b/demo/files/css/reset-fonts-grids.css
@@ -0,0 +1,7 @@
+/*
+Copyright (c) 2009, Yahoo! Inc. All rights reserved.
+Code licensed under the BSD License:
+http://developer.yahoo.net/yui/license.txt
+version: 2.7.0
+*/
+html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,strong,th,var,optgroup{font-style:inherit;font-weight:inherit;}del,ins{text-decoration:none;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym{border:0;font-variant:normal;}sup{vertical-align:baseline;}sub{vertical-align:baseline;}legend{color:#000;}input,button,textarea,select,optgroup,option{font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;}input,button,textarea,select{*font-size:100%;}body{font:13px/1.231 arial,helvetica,clean,sans-serif;*font-size:small;*font:x-small;}select,input,button,textarea,button{font:99% arial,helvetica,clean,sans-serif;}table{font-size:inherit;font:100%;}pre,code,kbd,samp,tt{font-family:monospace;*font-size:108%;line-height:100%;}body{text-align:center;}#doc,#doc2,#doc3,#doc4,.yui-t1,.yui-t2,.yui-t3,.yui-t4,.yui-t5,.yui-t6,.yui-t7{margin:auto;text-align:left;width:57.69em;*width:56.25em;}#doc2{width:73.076em;*width:71.25em;}#doc3{margin:auto 10px;width:auto;}#doc4{width:74.923em;*width:73.05em;}.yui-b{position:relative;}.yui-b{_position:static;}#yui-main .yui-b{position:static;}#yui-main,.yui-g .yui-u .yui-g{width:100%;}.yui-t1 #yui-main,.yui-t2 #yui-main,.yui-t3 #yui-main{float:right;margin-left:-25em;}.yui-t4 #yui-main,.yui-t5 #yui-main,.yui-t6 #yui-main{float:left;margin-right:-25em;}.yui-t1 .yui-b{float:left;width:12.30769em;*width:12.00em;}.yui-t1 #yui-main .yui-b{margin-left:13.30769em;*margin-left:13.05em;}.yui-t2 .yui-b{float:left;width:13.8461em;*width:13.50em;}.yui-t2 #yui-main .yui-b{margin-left:14.8461em;*margin-left:14.55em;}.yui-t3 .yui-b{float:left;width:23.0769em;*width:22.50em;}.yui-t3 #yui-main .yui-b{margin-left:24.0769em;*margin-left:23.62em;}.yui-t4 .yui-b{float:right;width:13.8456em;*width:13.50em;}.yui-t4 #yui-main .yui-b{margin-right:14.8456em;*margin-right:14.55em;}.yui-t5 .yui-b{float:right;width:18.4615em;*width:18.00em;}.yui-t5 #yui-main .yui-b{margin-right:19.4615em;*margin-right:19.125em;}.yui-t6 .yui-b{float:right;width:23.0769em;*width:22.50em;}.yui-t6 #yui-main .yui-b{margin-right:24.0769em;*margin-right:23.62em;}.yui-t7 #yui-main .yui-b{display:block;margin:0 0 1em 0;}#yui-main .yui-b{float:none;width:auto;}.yui-gb .yui-u,.yui-g .yui-gb .yui-u,.yui-gb .yui-g,.yui-gb .yui-gb,.yui-gb .yui-gc,.yui-gb .yui-gd,.yui-gb .yui-ge,.yui-gb .yui-gf,.yui-gc .yui-u,.yui-gc .yui-g,.yui-gd .yui-u{float:left;}.yui-g .yui-u,.yui-g .yui-g,.yui-g .yui-gb,.yui-g .yui-gc,.yui-g .yui-gd,.yui-g .yui-ge,.yui-g .yui-gf,.yui-gc .yui-u,.yui-gd .yui-g,.yui-g .yui-gc .yui-u,.yui-ge .yui-u,.yui-ge .yui-g,.yui-gf .yui-g,.yui-gf .yui-u{float:right;}.yui-g div.first,.yui-gb div.first,.yui-gc div.first,.yui-gd div.first,.yui-ge div.first,.yui-gf div.first,.yui-g .yui-gc div.first,.yui-g .yui-ge div.first,.yui-gc div.first div.first{float:left;}.yui-g .yui-u,.yui-g .yui-g,.yui-g .yui-gb,.yui-g .yui-gc,.yui-g .yui-gd,.yui-g .yui-ge,.yui-g .yui-gf{width:49.1%;}.yui-gb .yui-u,.yui-g .yui-gb .yui-u,.yui-gb .yui-g,.yui-gb .yui-gb,.yui-gb .yui-gc,.yui-gb .yui-gd,.yui-gb .yui-ge,.yui-gb .yui-gf,.yui-gc .yui-u,.yui-gc .yui-g,.yui-gd .yui-u{width:32%;margin-left:1.99%;}.yui-gb .yui-u{*margin-left:1.9%;*width:31.9%;}.yui-gc div.first,.yui-gd .yui-u{width:66%;}.yui-gd div.first{width:32%;}.yui-ge div.first,.yui-gf .yui-u{width:74.2%;}.yui-ge .yui-u,.yui-gf div.first{width:24%;}.yui-g .yui-gb div.first,.yui-gb div.first,.yui-gc div.first,.yui-gd div.first{margin-left:0;}.yui-g .yui-g .yui-u,.yui-gb .yui-g .yui-u,.yui-gc .yui-g .yui-u,.yui-gd .yui-g .yui-u,.yui-ge .yui-g .yui-u,.yui-gf .yui-g .yui-u{width:49%;*width:48.1%;*margin-left:0;}.yui-g .yui-g .yui-u{width:48.1%;}.yui-g .yui-gb div.first,.yui-gb .yui-gb div.first{*margin-right:0;*width:32%;_width:31.7%;}.yui-g .yui-gc div.first,.yui-gd .yui-g{width:66%;}.yui-gb .yui-g div.first{*margin-right:4%;_margin-right:1.3%;}.yui-gb .yui-gc div.first,.yui-gb .yui-gd div.first{*margin-right:0;}.yui-gb .yui-gb .yui-u,.yui-gb .yui-gc .yui-u{*margin-left:1.8%;_margin-left:4%;}.yui-g .yui-gb .yui-u{_margin-left:1.0%;}.yui-gb .yui-gd .yui-u{*width:66%;_width:61.2%;}.yui-gb .yui-gd div.first{*width:31%;_width:29.5%;}.yui-g .yui-gc .yui-u,.yui-gb .yui-gc .yui-u{width:32%;_float:right;margin-right:0;_margin-left:0;}.yui-gb .yui-gc div.first{width:66%;*float:left;*margin-left:0;}.yui-gb .yui-ge .yui-u,.yui-gb .yui-gf .yui-u{margin:0;}.yui-gb .yui-gb .yui-u{_margin-left:.7%;}.yui-gb .yui-g div.first,.yui-gb .yui-gb div.first{*margin-left:0;}.yui-gc .yui-g .yui-u,.yui-gd .yui-g .yui-u{*width:48.1%;*margin-left:0;}.yui-gb .yui-gd div.first{width:32%;}.yui-g .yui-gd div.first{_width:29.9%;}.yui-ge .yui-g{width:24%;}.yui-gf .yui-g{width:74.2%;}.yui-gb .yui-ge div.yui-u,.yui-gb .yui-gf div.yui-u{float:right;}.yui-gb .yui-ge div.first,.yui-gb .yui-gf div.first{float:left;}.yui-gb .yui-ge .yui-u,.yui-gb .yui-gf div.first{*width:24%;_width:20%;}.yui-gb .yui-ge div.first,.yui-gb .yui-gf .yui-u{*width:73.5%;_width:65.5%;}.yui-ge div.first .yui-gd .yui-u{width:65%;}.yui-ge div.first .yui-gd div.first{width:32%;}#hd:after,#bd:after,#ft:after,.yui-g:after,.yui-gb:after,.yui-gc:after,.yui-gd:after,.yui-ge:after,.yui-gf:after{content:".";display:block;height:0;clear:both;visibility:hidden;}#hd,#bd,#ft,.yui-g,.yui-gb,.yui-gc,.yui-gd,.yui-ge,.yui-gf{zoom:1;}
diff --git a/demo/files/css/reset-min.css b/demo/files/css/reset-min.css
new file mode 100644
--- /dev/null
+++ b/demo/files/css/reset-min.css
@@ -0,0 +1,7 @@
+/*
+Copyright (c) 2009, Yahoo! Inc. All rights reserved.
+Code licensed under the BSD License:
+http://developer.yahoo.net/yui/license.txt
+version: 2.7.0
+*/
+html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,strong,th,var,optgroup{font-style:inherit;font-weight:inherit;}del,ins{text-decoration:none;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym{border:0;font-variant:normal;}sup{vertical-align:baseline;}sub{vertical-align:baseline;}legend{color:#000;}input,button,textarea,select,optgroup,option{font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;}input,button,textarea,select{*font-size:100%;}
diff --git a/demo/files/css/screen.css b/demo/files/css/screen.css
new file mode 100644
--- /dev/null
+++ b/demo/files/css/screen.css
@@ -0,0 +1,51 @@
+@import url("reset-fonts-grids.css");
+@import url("base-min.css");
+@import url("hk-pyg.css");
+
+html { background: #f9f9f9; color: black; } 
+
+hr { height: 1px; color: #aaa; background-color: #aaa; border: 0; margin: .2em 0 .2em 0; }
+
+h1, h2, h3, h4, h5, h6 { font-weight: normal; border-bottom: 1px solid black; }
+
+h1 { font-size: 153.9%; margin: 1.07em 0 .535em; }
+h2 { font-size: 138.5%; margin: 1.14em 0 .57em; }
+h3 { font-size: 123.1%; margin: 1.23em 0 .615em; }
+h4 { font-size: 116%; margin: 1.33em 0 .67em; }
+h5 { font-size: 108%; margin: 1.6em 0 .8em; }
+h6 { font-size: 100%; margin: 1.6em 0 .8em; }
+
+ul { list-style-type: square; }
+dt { font-weight: bold; margin-bottom: .1em; }
+
+table { border-top: 1px solid #ccc;  border-left: 1px solid #ccc; }
+th, td { border-bottom: 1px solid #ddd; border-right: 1px solid #ccc; }
+
+blockquote { padding: 0 1.6em; color: #666; }
+
+/* COLORS */
+
+a:link { text-decoration: underline; color: #36c; }
+a:visited { text-decoration: underline; color: #99c; }
+a:hover { text-decoration: underline; color: #c33; }
+a:active,  a:focus { text-decoration: underline; color: #000; }
+/* code,  pre { color: #c33; } /* very optional, but still useful. W3C uses about the same colors for codes */
+
+h1 > a:link, h1 > a:active, h1 > a:hover, h1 > a:focus, h1 > a:visited,
+h2 > a:link, h2 > a:active, h2 > a:hover, h2 > a:focus, h2 > a:visited,
+h3 > a:link, h3 > a:active, h3 > a:hover, h3 > a:focus, h3 > a:visited,
+h4 > a:link, h4 > a:active, h4 > a:hover, h4 > a:focus, h4 > a:visited,
+h5 > a:link, h5 > a:active, h5 > a:hover, h5 > a:focus, h5 > a:visited,
+h6 > a:link, h6 > a:active, h6 > a:hover, h6 > a:focus, h6 > a:visited {
+        color: black;
+        text-decoration: none;
+}
+
+div#maincol { max-width: 46em; }
+
+#footer { padding: 1em; color: #888; text-align: center; font-size: 93%; }
+
+div#nav { margin-top: 2em; }
+div#nav ul { margin-top: 0; margin-left: 1em; margin-bottom: 0; }
+div#nav ul li { list-style: none; padding-bottom: 0.1em; }
+div#nav ul li.current { list-style: square; }
diff --git a/demo/files/js/nav.js b/demo/files/js/nav.js
new file mode 100644
--- /dev/null
+++ b/demo/files/js/nav.js
@@ -0,0 +1,7 @@
+$(document).ready(function(){
+  $("div#nav li > ul").hide();
+  $("div#nav li > ul.active").show();
+  $("div#nav ul li a").click(function(event){
+    $(event.target).parent().children("ul").slideToggle("fast");
+  });
+});
diff --git a/demo/index.st b/demo/index.st
new file mode 100644
--- /dev/null
+++ b/demo/index.st
@@ -0,0 +1,9 @@
+# Welcome
+
+Here's our website. Have a look around.
+
+Our last two events:
+
+$recentevents:event()$
+
+For a complete list of events, see the [events](events.html) page.
diff --git a/demo/index.yaml b/demo/index.yaml
new file mode 100644
--- /dev/null
+++ b/demo/index.yaml
@@ -0,0 +1,32 @@
+- url      : index.html
+  title    : Home
+  template : index.st
+  requires : event.st
+  data     :
+    recentevents : events.yaml ORDER BY date DESC LIMIT 2
+
+- url      : events.html
+  title    : Events 
+  template : events.st
+  requires : [event.st, eventgroup.st, date.st]
+  data     :
+    eventsbydate : events.yaml ORDER BY date DESC GROUP BY date
+
+- url      : april_events.tex
+  title    : April Events
+  template : april_events.st
+  requires : event.st
+  data     :
+    april  : events.yaml WHERE date >= '2009-04-01' AND date < '2009-05-01' ORDER BY date
+  layout   : layout.tex.st
+  inmenu   : no 
+
+- Rooms:
+  - url      : room101.html
+    title    : Room 101
+    source   : room101.txt
+
+  - url      : room203.html
+    title    : Room 203
+    source   : room203.txt
+
diff --git a/demo/layout.html.st b/demo/layout.html.st
new file mode 100644
--- /dev/null
+++ b/demo/layout.html.st
@@ -0,0 +1,22 @@
+<html>
+<head>
+   <title>$sitetitle$ - $pagetitle$</title>
+   <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+   <link rel="stylesheet" type="text/css" href="css/screen.css" />
+   <link rel="stylesheet" type="text/css" media="print" href="css/print.css" />
+   <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js" type="text/javascript"></script>
+   <script src="js/nav.js" type="text/javascript"></script>
+</head>
+<body>
+  <div id="doc3" class="yui-t1">
+    <div id="yui-main">
+      <div id="maincol" class="yui-b">
+$contents$
+      </div>
+    </div>
+    <div id="nav" class="yui-b first">
+$nav$
+    </div>
+  </div>
+</body>
+</html>
diff --git a/demo/layout.tex.st b/demo/layout.tex.st
new file mode 100644
--- /dev/null
+++ b/demo/layout.tex.st
@@ -0,0 +1,20 @@
+\documentclass{article}
+\usepackage{amsmath}
+\usepackage[mathletters]{ucs}
+\usepackage[utf8x]{inputenc}
+\usepackage{hyperref}
+\usepackage{listings}
+
+\lstnewenvironment{code}{\lstset{language=Haskell,basicstyle=\small\ttfamily}}{}
+
+\setlength{\parindent}{0pt}
+\setlength{\parskip}{6pt plus 2pt minus 1pt}
+
+\title{$pagetitle$}
+
+\begin{document}
+\maketitle
+
+$contents$
+
+\end{document}
diff --git a/demo/room101.txt b/demo/room101.txt
new file mode 100644
--- /dev/null
+++ b/demo/room101.txt
@@ -0,0 +1,9 @@
+# Room 101
+
+Room 101 holds 60 people.
+
+Equipment:
+
+  - blackboard
+  - overhead projector
+
diff --git a/demo/room203.txt b/demo/room203.txt
new file mode 100644
--- /dev/null
+++ b/demo/room203.txt
@@ -0,0 +1,9 @@
+# Room 203
+
+Room 203 is accessible only via rope ladders.
+
+Equipment:
+
+  - Manual typewriters
+  - Chalk
+
diff --git a/yst.cabal b/yst.cabal
new file mode 100644
--- /dev/null
+++ b/yst.cabal
@@ -0,0 +1,53 @@
+name:                yst
+version:             0.1
+Cabal-version:       >= 1.2
+build-type:          Simple
+synopsis:            Builds a static website from templates and data in YAML or
+                     CSV files.
+description:         `yst` is a tool for generating a static website
+                     by filling string templates with data taken from
+                     YAML text files (or, alternatively, CSV files).
+                     This approach combines the speed, security, and
+                     ease of deployment of a static website with the
+                     flexibility and maintainability of a dynamic site
+                     that separates presentation and data.
+
+category:            Text
+license:             GPL
+license-file:        LICENSE
+author:              John MacFarlane
+maintainer:          jgm@berkeley.edu
+homepage:            http://github.com/jgm/yst
+stability:           experimental
+data-files:          README.markdown
+                     demo/config.yaml
+                     demo/date.st
+                     demo/eventgroup.st
+                     demo/april_events.st
+                     demo/events.st
+                     demo/event.st
+                     demo/events.yaml
+                     demo/index.st
+                     demo/index.yaml
+                     demo/layout.html.st
+                     demo/layout.tex.st
+                     demo/room101.txt
+                     demo/room203.txt
+                     demo/files/css/base-min.css
+                     demo/files/css/hk-pyg.css
+                     demo/files/css/ie.css
+                     demo/files/css/print.css
+                     demo/files/css/reset-fonts-grids.css
+                     demo/files/css/reset-min.css
+                     demo/files/css/screen.css
+                     demo/files/js/nav.js
+Executable           yst
+  hs-source-dirs:    .
+  main-is:           yst.hs
+  other-modules:     Yst.Types, Yst.Yaml, Yst.Util, Yst.Data, Yst.Config,
+                     Yst.Render, Yst.Build, Yst.CSV
+  build-depends:     base >=3 && < 5, HStringTemplate >= 0.6.1, HsSyck, csv,
+                     filepath, containers, directory, utf8-string, time,
+                     old-locale, old-time, parsec, xhtml, pandoc
+  ghc-options:       -Wall -threaded -fno-warn-orphans
+  ghc-prof-options:  -auto-all -caf-all
diff --git a/yst.hs b/yst.hs
new file mode 100644
--- /dev/null
+++ b/yst.hs
@@ -0,0 +1,67 @@
+{-
+Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+-}
+
+module Main
+where
+import Paths_yst
+import Yst.Util
+import Yst.Config
+import Yst.Build
+import System.FilePath
+import System.Environment
+import System.Directory
+import System.Exit
+import System.IO.UTF8
+import System.IO (stderr)
+import Prelude hiding (readFile, putStrLn, print, writeFile)
+import Control.Monad
+
+createSite :: FilePath -> IO ()
+createSite path = do
+  existsd <- doesDirectoryExist path
+  existsf <- doesFileExist path
+  when (existsd || existsf) $ do
+    hPutStrLn stderr $ "Aborting!  " ++ path ++ " already exists."
+    exitWith $ ExitFailure 5
+  demoDir <- getDataFileName "demo"
+  contents <- liftM (filter (/=".") . map (makeRelative demoDir)) $ getDirectoryContentsRecursive demoDir
+  forM_ contents $ \file -> do
+    let dest = path </> file 
+    createDirectoryIfMissing True $ takeDirectory dest
+    copyFile (demoDir </> file) dest
+  readme <- getDataFileName "README.markdown"
+  copyFile readme (path </> "README")
+  hPutStrLn stderr $ "Created starter site in " ++ path
+
+usageMessage :: IO ()
+usageMessage = hPutStrLn stderr $
+  "yst - create static website from string templates and YAML data\n" ++
+  "Usage:\n" ++
+  "yst [-f CONFIGFILE]        # build website\n" ++
+  "yst create DIRECTORY       # create starter site in DIRECTORY"
+
+main :: IO ()
+main = do
+  args <- getArgs
+  site <- case args of
+               ["-f",x]     -> parseConfigFile x
+               []           -> parseConfigFile "config.yaml"
+               ["create",d] -> createSite d >> exitWith ExitSuccess
+               _            -> usageMessage >> exitWith (ExitFailure 1)
+  buildSite site
+
