pax_global_header00006660000000000000000000000064111024276270014514gustar00rootroot0000000000000052 comment=fbae00e39008824596270d074b5eedeede67c267 mnemosyne-blog-0.12/000077500000000000000000000000001110242762700143715ustar00rootroot00000000000000mnemosyne-blog-0.12/LICENSE000066400000000000000000000013701110242762700153770ustar00rootroot00000000000000Copyright © 2006-2008 Decklin Foster . Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. mnemosyne-blog-0.12/NEWS000066400000000000000000000101061110242762700150660ustar00rootroot000000000000000.12 (2008-10-30): ================== * Fix mixins. The config interface has changed! You must now define a list ``mixins`` containing your classes (there can be more than one, and they can have any names you want). See the example config. * When a layout error occurs, continue running (a la make -k). I might make this an option. * Provide tracebacks for more errors. * Properly decode (and flatten) multipart text messages. * Write Unicode subject slugs to the filesystem as UTF-8. 0.11 (2008-10-27): ================== * Greatly simplify attribute caching stuff. All attribute methods in your mixins should be renamed to get_ATTR, and just return a value (no more cache() method). This will break your code. * Update our URL. * Hack around lossage in the 2.5 mailbox module (which wraps the file object for no good reason). 0.10 (2008-10-12): ================== * Switch to ISC license (33% less fat!) * Changed default config dir to ~/.mnemosyne. * Fixed cleaning of Unicode subjects. 0.9 (2006-02-28): ================= * Fixed the "simple" example, which has a new name, some better default variables, and an updated mixin demo. Its ``config.py`` also lists the default values in a hopefully more readable way. (Thanks to Aigars Mahinovs for reporting this, and writing the Expat formatter.) 0.8 (2006-02-21): ================= * Don't skip entries with empty subjects when deciding what pages to output. * Use Kid's xhtml-strict serializer so that we get the correct XHTML doctype. Layouts should now simply print the template object rather than calling serialize directly. * Split the module into a package. If you have an old version installed you'll want to remove it from site-packages. 0.7 (2006-02-10): ================= * Removed EmPy entirely. While we could still plug something else in with not too much hassle, I don't think it's worth keeping if we have to work around broken Unicode support. * Layouts now just print their serialized pages; we capture sys.stdout and save it. 0.6 (2006-02-10): ================= * Added a cache interface so that any different properties that want to cache themselves can do so in a consistent way. We may also want to save this cache to disk at some point in the futre. * Added a method to expand Kid templates from your layout instead of EmPy. At the moment, both work, but the Kid examples are really clunky since they are just a straight port. Instead of building pages in the same way, we could make use of Kid's much greater capabilities. I will have to consider if EmPy should stay or go first. (Thanks to Erich Schubert for the suggestion!) 0.5 (2006-02-08): ================= * Switch back to ``email`` for parsing messages, so we can support encoded messages (quoted-printable, charset RFC 2047 headers, whatever), and add a ``charset`` option in the configuration to specify the default encoding for data we send to the templates. * Make ``mime-gpg-decode`` actually decode the message body in the same manner, rather than merely pulling the literal signed data out. This is not required, but is very convenient if you want to edit messages later. This also makes it congruous with ``mimedecode``, which you could use on unsigned mail. * Renamed the EntryMixin interfaces to ``_prop_*`` and ``_init_*``, to better reflect what they really mean. In particular, ``_prop_*`` attributes are now implemented as real properties. This means it is up to each method to decide when and how to cache property values. 0.4 (2006-02-04): ================= * Added contrib directory with ``automnemosyne``, ``mime-gpg-decode``, and example procmail recipe. * Don't copy ordinary files from layout unless they have been modified. Do update variable files if their layouts have been modified. 0.3 (2006-02-03): ================= * Make the default Atom style mark content as HTML rather than XHTML (since the rest of the example layout is HTML). 0.2 (2006-02-03): ================= * Add a --help option, better error messages, and more documentation. 0.1 (2006-02-02): ================= * Initial release. mnemosyne-blog-0.12/README000066400000000000000000000165151110242762700152610ustar00rootroot00000000000000Introduction ============ Mnemosyne is a simple blogging system which generates static files. You provide it with some entries, which are stored in an ordinary Maildir, a layout, which is a directory tree indicating where these entries should go, and some styles, which are Kid templates that the layout can use to put pages together. The layout tree may contain ordinary files, such as images, stylesheets, or constant pages, and Python scripts, which will be used to generate variable pages, XML feeds, or anything else you want. Requirements ============ * Python, probably at least version 2.2. I use 2.3. * If you want to use reStructuredText to format entries as HTML, docutils. This is the default configuration, but you could also replace this with PyMarkdown, PyTextile, or nothing at all. * Kid, available at http://kid.lesscode.org/. This could probably be replaced as well, but it would be more work than swapping out reST. * Something that will allow you to create, add to and edit your Maildir. I use procmail and Mutt. Configuration ============= A Mnemosyne blog starts with a configuration file, which is a Python script. The example provided in ``examples/simple/config.py`` explains what it can do and what variables can be set. By default, Mnemosyne looks for this file in ``~/.mnemosyne/config.py``. Usage ===== To regenerate your blog, simply run ``mnemosyne``, or, if your config lives in a different location, ``mnemosyne path/to/config.py``. By default, only pages for which the applicable entries or layout have been changed since the page was last written will be evaluated; you can override this and rebuild the entire blog with the ``--force`` option. (At the moment, this is your only recourse if you change your styles.) Layout ====== Layout scripts are also written in Python; any file with the executable bit set will be executed and its output will be used to produce the corresponding output file. Apart from any local variables defined in the blog's configuration, layouts will be given the following: * ``muse``: a ``mnemosyne.Muse`` object, the thing that generates the blog. It contains these attributes (among others, of course, but you can safely ignore the rest): - ``muse.entries``: the list of all entries in the blog, not just the ones you are considering for this page. - ``muse.template(name, ...)``: a method that will look up the style ``name.kid`` in your configuration's style directory and initialize it for you with any keyword arguments provided; in almost all cases, you will want to give the style all your local variables by specifying ``**locals()``. * ``entries``: the subset of entries that apply to the particular file being generated. In an archive page, for example, this would be only the entries in that particular year, week, or whatever. This means all such entries, so you will want to choose a subset of them in most cases. Files in the top level directory of your layout will be evaluated with all the entries in the blog. However, you can create files or directories with a name like ``__year__``, and it will produce files (or, in this case, directories) named ``2006``, ``2007``, etc, which will be evaluated in turn with only the entries for that year. You can do the same thing for attributes which have multiple values, as well: for example, a directory named ``__tags__`` will be evaluated for each tag that appears on an entry, even if the tags are stored as a list. The directory tree in ``examples/simple/layout`` illustrates how to do each of these, and a basic Atom feed; you can of course be more creative. Entries ======= The values for all these expansions come from the attributes of mnemosyne's Entry class. Several useful ones, such as ``subject`` and ``tags``, are provided by default: if you wish to define more, you may add a mixin class to your configuration, with some specially-named methods. Attributes may be calculated at initialization or only when needed. When generating file or directory names, the attributes' repr() is used, so that dates, subject lines, and so on may be appropriately formatted and linked to. The ``mnemosyne.utils`` module provides the functions ``clean`` and ``cook``, to make creating useful values easier. This is just a matter of convention; it is not required, but it does ensure that all the formatting logic is in one place. Entry Details ============= For each entry ``e``, the following attributes (plus any you define) are available: * ``msg``: the message that the entry was created from. Initially, only the headers have been read; the rest is read when you access ``e.content``. * ``date``: a time-tuple indicating the date on the Date: header. * ``mtime``: a time-tuple indicating when the message's Maildir file was created. This is not the mtime of the file itself! Messing with Maildirs by hand is not recommended. To edit an entry, open the Maildir in mutt and hit the "e" key on its message. * ``content``: the body of the entry, processed with reStructuredText. Its repr() is the first 100 characters of the unprocessed body (suitable for perhaps a summary feed). * ``subject``: The contents of the Subject header. Its repr() is a URL-cleaned (at most 3 words) version of same, with a number appended to make each one unique within a given day: 'subject', 'subject-1', 'subject-2', etc. These are calculated on startup in chronological order so that they are always the same. * ``id``: The Message-Id: header. Its repr() is a tag: URL suitable for an Atom feed, as described in http://diveintomark.org/archives/2004/05/28/howto-atom-id. * ``author``: Name from the From: header. Its repr() is the URL-cleaned version, in case you want to categorize a multi-author blog by author name. * ``email``: Email address from the From: header. Its repr() is something like 'decklin at red dash bean dot com'. * ``tags``: A list of the contents of the X-Tags: header, split by commas. URL-cleaned repr()s for tag categorization. * ``year``, ``month``, ``day``: What the names imply. The values are ints; the repr()s are formatted with ``time.strftime``. Updating ======== The ``contrib`` directory contains some small scripts to facilitate updating your blog, and an example of how to put them together so that your blog can pick up PGP-signed mail at a public email address (or perhaps ordinary mail at an address internal to your own network) and automatically rebuild itself upon receipt. Future and Caveats ================== I intend to write an SQLite backend once someone complains that it takes too long to load their 10,000 entries. Be loquacious. At the moment, however, Mnemosyne depends on the Maildir really being a Maildir (for reliable modification times) and the standard ``email`` and ``mailbox`` modules, which are not perfect. A lot of the more magic stuff may not be particularly good style; where some clarity or brevity in Mnemosyne itself could be sacrificed to make the layout and styles easier to work with, it has been. About the Name ============== Mnemosyne, in Greek mythology, is the personification of memory and mother of the muses, goddesses whom poets would invoke to help them tell a story. The metaphor, hopefully, should be obvious. Author and Copyright ==================== Copyright © 2007 Decklin Foster . This program is free software; please see LICENSE for terms and conditions. mnemosyne-blog-0.12/contrib/000077500000000000000000000000001110242762700160315ustar00rootroot00000000000000mnemosyne-blog-0.12/contrib/automnemosyne000077500000000000000000000020151110242762700206600ustar00rootroot00000000000000#!/usr/bin/python import sys import os import email import time # automnemosyne -- Decklin Foster # # If you deliver messages to your entry_dir via procmail, use a carbon copy # recipe for the Maildir and a pipe to this program as the final destination. # Your blog will then be updated automatically. Be sure to redirect output to # a suitable log file. def fmt_time(): return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()) if __name__ == '__main__': m = email.message_from_file(sys.stdin) print "Automatically rebuilding at %s" % fmt_time() print "on receipt of '%s' from %s:" % (m['Subject'], m['From']) print; sys.stdout.flush() # If we were invoked from procmail, then our umask is probably 077. This # would be a bad idea, as we want all output files to be world-readable. os.umask(022) argv = ['mnemosyne'] + sys.argv[1:] status = os.spawnvp(os.P_WAIT, argv[0], argv) print print "Finished at %s with status %d" % (fmt_time(), status) print mnemosyne-blog-0.12/contrib/mime-gpg-decode000077500000000000000000000040631110242762700207050ustar00rootroot00000000000000#!/usr/bin/python import sys import os import email import tempfile # mime-gpg-decode -- Decklin Foster # # If you send your entries by mail to a public address, you will want to check # on the receiving end that messages are not spam or malicious before # delivering them. This filter will take a multipart/signed mail, run it # through gpg --verify, and discard the signature part. The exit status can # then be used to deliver valid signed messages to one destination and invalid # or unsigned messages to another. def validate(msg): try: parent, body, sig = [part for part in msg.walk()] except ValueError: raise RuntimeError('message must have exactly two body parts') try: assert parent.get_content_type() == 'multipart/signed' assert parent.get_param('protocol') == 'application/pgp-signature' assert parent.get_param('micalg') in ('pgp-sha1') # XXX: or...? assert sig.get_content_type() == 'application/pgp-signature' except AssertionError: raise RuntimeError('message must be PGP-SHA1 multipart/signed') return parent, body, sig def writetmp(s): tmpf = tempfile.NamedTemporaryFile() tmpf.write(s) tmpf.flush() return tmpf if __name__ == '__main__': msg = email.message_from_file(sys.stdin) try: parent, body, sig = validate(msg) except RuntimeError, e: print >>sys.stderr, 'error: %s' % e sys.exit(1) bodylines = str(body).split('\n')[1:] bodyfile = writetmp('\015\012'.join(bodylines)) sigfile = writetmp(sig.get_payload()) status = os.system('gpg --verify %s %s >/dev/null 2>&1' % (sigfile.name, bodyfile.name)) # email.Message.__setitem__ can bite me def set_header(msg, k, v): del msg[k]; msg[k] = v # We'll be cheap and reuse parent for k, v in body.items(): set_header(parent, k, v) set_header(parent, 'Content-Transfer-Encoding', '8bit') parent.set_payload(body.get_payload(decode=True), body.get_charset()) print parent, sys.exit(os.WEXITSTATUS(status)) mnemosyne-blog-0.12/contrib/procmailrc.example000066400000000000000000000011141110242762700215360ustar00rootroot00000000000000# Here is an example of how to receive blog entries via a public email # address, and automatically update the blog on receipt of a valid signed # message. First, mime-gpg-decode verifies and discards the signature. If it # succeeds, the message is copied to the entry_dir and then delivered to # automnemosyne. If it fails, the message is sent to /dev/null. :0 * ^TO_blog@example.invalid { :0fW | mime-gpg-decode :0a { :0c $HOME/.mnemosyne/entries/ :0 | automnemosyne >>$HOME/.mnemosyne/automnemosyne.log } :0E /dev/null } mnemosyne-blog-0.12/examples/000077500000000000000000000000001110242762700162075ustar00rootroot00000000000000mnemosyne-blog-0.12/examples/simple/000077500000000000000000000000001110242762700175005ustar00rootroot00000000000000mnemosyne-blog-0.12/examples/simple/config.py000066400000000000000000000110521110242762700213160ustar00rootroot00000000000000# Example Mnemosyne configuration # =============================== # # This file is a Python script. The configuration defined here will # generate a very simple blog, somewhat like PyBlosxom's defaults. from mnemosyne import get_conf, cook, clean # File locations # -------------- # # * ``entry_dir``: a Maildir containing all the blog entries. # * ``layout_dir``: the blog's layout, as a skeleton directory tree. # * ``style_dir``: Kid templates used for filling the layouts. # * ``output_dir``: location where we will write the generated pages. # # All these directories must already exist. They default to the following # subdirectories of ~/.mnemosyne: entry_dir = get_conf('entries') layout_dir = get_conf('layout') style_dir = get_conf('style') output_dir = get_conf('htdocs') # Custom Variables # ---------------- # # * ``locals``: a dict of local variables passed to all templates. # # This is initially empty; add anything you want to use in all layouts. # Our example uses: locals['blogname'] = 'Simple Example' locals['blogroot'] = 'http://blog.example.invalid' locals['authname'] = 'Melete' locals['authemail'] = 'melete@example.invalid' locals['authhome'] = 'http://www.example.invalid/~melete/' # Ignore list # ----------- # # * ``ignore``: a list of file names in the layout tree to ignore. # # This defaults to: ignore = ('.hg', '_darcs', '.git', 'MT', '.svn', 'CVS') # Creating and redefining attributes # ---------------------------------- # # * ``mixins``: a list of classes to search for attribute methods. # # You can add attributes to the Entry class by defining your own classes # here. Any methods named ``get_ATTRIBUTE`` will be used to provide # ``entry.ATTRIBUTE``, and that value will be automatically cached. To # use one of these example classes, uncomment it and then add its name to # the list at the end. # # We use the repr() of each attribute when putting it in a URL. For # example, if you wanted to have a tag called 'My Tag', you would add a # ``__repr__`` method that returned 'my-tag', so that you could use it # in a link such as ``My Tag``. # # To easily create objects that work like this, we import ``cook``, # which takes two arguments: a object and a value to override its # repr(). It does this by defining a new singleton class behind the # scenes. #class FooMixin: # # Pull anything you want out of self.msg (an email.Message.Message # # object), and use it to provide a new attribute. If there is no # # usable value, make sure there is some default for the repr() so # # that we don't accidentally create an invalid URL when we use it. # # def get_foo(self): # try: # foo = self.msg['X-Foo'] # return cook(foo, clean(foo, 3)) # except KeyError: # return cook('', 'xxx-foo-missing') #class MarkdownMixin: # # You could use Markdown instead of ReST to write entries; you'll # # need to install it from http://err.no/pymarkdown/. # # import pymarkdown # # def get_content(self): # s = self.msg.get_body() # return cook(pymarkdown.Markdown(s), s[:80]) #class RawMarkupMixin: # # If you would occasionally like to paste a chunk of HTML or # # XML from elsewhere into a message, you could use a different # # formatter depending on the value of an ``X-Format:`` header. This # # uses Expat to ensure that the output is valid XHTML; you could # # also use Tidy. Contributed by Aigars Mahinovs. # # import xml.dom # import xml.parsers.expat # # def get_content(self): # """Read in the message's body, strip any signature, and format using # reStructedText unless X-Format=='html'.""" # # s = self.msg.get_body(decode=True) # body = False # # try: # if self.msg['X-Format'] == "html": # body = s.replace(" ", " ") # body = re.sub(r'&(?!\w{1,10};)',r'&',body) # body = xml.dom.minidom.parseString("
" + body + # "
").toxml() # except KeyError: # pass # except xml.parsers.expat.ExpatError, e: # print "W: Parse failed for " + self.msg['Subject'] + " at " + \ # self.msg['Date'] + " from " + \ # str(int(time.mktime(time.strptime(self.msg["Date"], # "%a %b %d %H:%M:%S %Y")))) # print xml.parsers.expat.ErrorString(e.code), e.lineno, e.offset # # if not body: # parts = docutils.core.publish_parts(s, writer_name='html') # body = parts['body'] # # return body mixins = [] mnemosyne-blog-0.12/examples/simple/layout/000077500000000000000000000000001110242762700210155ustar00rootroot00000000000000mnemosyne-blog-0.12/examples/simple/layout/__year__/000077500000000000000000000000001110242762700225515ustar00rootroot00000000000000mnemosyne-blog-0.12/examples/simple/layout/__year__/__month__/000077500000000000000000000000001110242762700244725ustar00rootroot00000000000000mnemosyne-blog-0.12/examples/simple/layout/__year__/__month__/__day__/000077500000000000000000000000001110242762700260435ustar00rootroot00000000000000mnemosyne-blog-0.12/examples/simple/layout/__year__/__month__/__day__/__subject__.xhtml000077500000000000000000000002231110242762700313540ustar00rootroot00000000000000title = '%s - %s' % (entries[0].subject, blogname) body = muse.template('econtent', locals()) page = muse.template('xhtml', locals()) print page mnemosyne-blog-0.12/examples/simple/layout/__year__/__month__/__day__/index.xhtml000077500000000000000000000002231110242762700302300ustar00rootroot00000000000000title = 'Day %s - %s' % (muse.where[-2], blogname) body = muse.template('econtent', locals()) page = muse.template('xhtml', locals()) print page mnemosyne-blog-0.12/examples/simple/layout/__year__/__month__/index.xhtml000077500000000000000000000002251110242762700266610ustar00rootroot00000000000000title = 'Month %s - %s' % (muse.where[-2], blogname) body = muse.template('esummary', locals()) page = muse.template('xhtml', locals()) print page mnemosyne-blog-0.12/examples/simple/layout/__year__/index.xhtml000077500000000000000000000002241110242762700247370ustar00rootroot00000000000000title = 'Year %s - %s' % (muse.where[-2], blogname) body = muse.template('calendar', locals()) page = muse.template('xhtml', locals()) print page mnemosyne-blog-0.12/examples/simple/layout/feed/000077500000000000000000000000001110242762700217205ustar00rootroot00000000000000mnemosyne-blog-0.12/examples/simple/layout/feed/atom.xml000077500000000000000000000003271110242762700234070ustar00rootroot00000000000000import time def newer(e, ago): return time.mktime(e.mtime) > (time.time() - ago) entries = [e for e in entries if newer(e, 86400*7)][-10:] entries.reverse() atom = muse.template('atom', locals()) print atom mnemosyne-blog-0.12/examples/simple/layout/index.xhtml000077500000000000000000000002331110242762700232030ustar00rootroot00000000000000title = blogname entries = entries[-10:] entries.reverse() body = muse.template('econtent', locals()) page = muse.template('xhtml', locals()) print page mnemosyne-blog-0.12/examples/simple/layout/tag/000077500000000000000000000000001110242762700215705ustar00rootroot00000000000000mnemosyne-blog-0.12/examples/simple/layout/tag/__tags__/000077500000000000000000000000001110242762700233225ustar00rootroot00000000000000mnemosyne-blog-0.12/examples/simple/layout/tag/__tags__/index.xhtml000077500000000000000000000002771110242762700255200ustar00rootroot00000000000000title = 'Tag "%s" - %s' % (muse.where[-2], blogname) entries = entries[-50:] entries.reverse() body = muse.template('econtent', locals()) page = muse.template('xhtml', locals()) print page mnemosyne-blog-0.12/examples/simple/layout/tag/index.xhtml000077500000000000000000000001761110242762700237640ustar00rootroot00000000000000title = 'Tags - %s' % blogname body = muse.template('taglist', locals()) page = muse.template('xhtml', locals()) print page mnemosyne-blog-0.12/examples/simple/style/000077500000000000000000000000001110242762700206405ustar00rootroot00000000000000mnemosyne-blog-0.12/examples/simple/style/atom.kid000066400000000000000000000025511110242762700222740ustar00rootroot00000000000000 <id py:content="blogroot" /> <link rel="self" type="application/atom+xml" href="${'/'.join([blogroot]+muse.where)}" /> <link rel="alternate" type="application/xhtml+xml" href="${blogroot}/" /> <updated>${rfc3339(muse.entries[-1].date)}</updated> <generator uri="${__url__}" version="${__version__}"> Mnemosyne </generator> <author> <name py:content="authname" /> <email py:content="authemail" /> <uri py:content="authhome" /> </author> <entry py:for="e in entries"> <title type="text" py:content="e.subject" /> <link rel="alternate" type="application/xhtml+xml" href="${blogroot}/${`e.year`}/${`e.month`}/${`e.day`}/${`e.subject`}.xhtml" /> <id py:content="`e.id`" /> <published py:content="rfc3339(e.date)" /> <updated py:content="rfc3339(e.mtime)" /> <content type="xhtml"> <div xmlns="http://www.w3.org/1999/xhtml" py:content="XML(e.content)" /> </content> </entry> </feed> �������������������������������������������������������������������������������������������������������������������������������������������������������mnemosyne-blog-0.12/examples/simple/style/calendar.kid����������������������������������������������0000664�0000000�0000000�00000002232�11102427627�0023101�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?xml version='1.0' encoding='utf-8'?> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"> <div py:def="contents()"> <?python import time import calendar months = {} stub = {} for e in entries: months.setdefault(e.month, {}).setdefault(e.day, []).append(e) stub[e.month] = e keys = months.keys() keys.sort() ?> <div py:strip="True" py:for="m in keys"> <?python monthname = time.strftime('%B', months[m].values()[0][0].date) cal = calendar.monthcalendar(entries[0].year, m) ?> <h3><a href="${blogroot}/${`stub[m].year`}/${`stub[m].month`}/">${time.strftime('%B', stub[m].date)}</a></h3> <table class="cal"> <tr class="week" py:for="week in cal"> <td class="day" py:for="day in week"> <p> <?python e = months[m].get(day, [None])[0] ?> <a py:if="months[m].has_key(day)" href="${blogroot}/${`e.year`}/${`e.month`}/${`e.day`}/">${day}</a> <span py:if="not months[m].has_key(day)" py:strip="True"> <span py:if="day" py:strip="True">${day}</span> </span> </p> </td> </tr> </table> </div> </div> </html> ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mnemosyne-blog-0.12/examples/simple/style/econtent.kid����������������������������������������������0000664�0000000�0000000�00000001603�11102427627�0023150�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?xml version='1.0' encoding='utf-8'?> <?python import time ?> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"> <div py:def="contents()"> <div py:for="e in entries"> <h2><a href="${blogroot}/${`e.year`}/${`e.month`}/${`e.day`}/${`e.subject`}.xhtml">${e.subject}</a></h2> <div py:replace="XML(e.content)" /> <p>Posted at <a href="${blogroot}/${`e.year`}/">${`e.year`}</a>-<a href="${blogroot}/${`e.year`}/${`e.month`}/">${`e.month`}</a>-<a href="${blogroot}/${`e.year`}/${`e.month`}/${`e.day`}/">${`e.day`}</a> ${time.strftime('%H:%M:%S', e.date)} by <a href="mailto:${e.email}">${e.author}</a><br /> Tags: <span py:for="i, t in enumerate(e.tags)" py:strip="True"><span py:if="i > 0" py:strip="True">, </span><a href="${blogroot}/tag/${`t`}" py:content="t" /></span> </p> </div> </div> </html> �����������������������������������������������������������������������������������������������������������������������������mnemosyne-blog-0.12/examples/simple/style/esummary.kid����������������������������������������������0000664�0000000�0000000�00000000562�11102427627�0023176�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?xml version='1.0' encoding='utf-8'?> <?python import time ?> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"> <div py:def="contents()"> <p py:for="e in entries"> ${time.strftime('%a %d %b, %H:%M:%S', e.date)}: <a href="${blogroot}/${`e.year`}/${`e.month`}/${`e.day`}/${`e.subject`}.xhtml">${e.subject}</a> </p> </div> </html> ����������������������������������������������������������������������������������������������������������������������������������������������mnemosyne-blog-0.12/examples/simple/style/taglist.kid�����������������������������������������������0000664�0000000�0000000�00000000543�11102427627�0023002�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?xml version='1.0' encoding='utf-8'?> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"> <div py:def="contents()"> <?python tags = {} for e in entries: for t in e.tags: tags[t] = tags.get(t, 0) + 1 ?> <p py:for="t, n in tags.iteritems()"> <a href="${`t`}">${t} (${n})</a> </p> </div> </html> �������������������������������������������������������������������������������������������������������������������������������������������������������������mnemosyne-blog-0.12/examples/simple/style/xhtml.kid�������������������������������������������������0000664�0000000�0000000�00000001057�11102427627�0022470�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?xml version='1.0' encoding='utf-8'?> <?python from mnemosyne import __url__, __version__ ?> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:py="http://purl.org/kid/ns#"> <head> <title py:content="title" /> <link rel="alternate" type="application/atom+xml" title="Atom" href="${blogroot}/feed/atom.xml" /> </head> <body> <h1><a href="${blogroot}/">${blogname}</a></h1> <div class="main" py:content="body.contents()" /> <hr /> <p>Generated by <a href="${__url__}">Mnemosyne ${__version__}</a>.</p> </body> </html> ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mnemosyne-blog-0.12/lib/����������������������������������������������������������������������������0000775�0000000�0000000�00000000000�11102427627�0015137�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mnemosyne-blog-0.12/lib/mnemosyne/������������������������������������������������������������������0000775�0000000�0000000�00000000000�11102427627�0017151�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mnemosyne-blog-0.12/lib/mnemosyne/__init__.py�������������������������������������������������������0000664�0000000�0000000�00000003230�11102427627�0021260�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������"""Mnemosyne -- a static weblog generator.""" import os __version__ = '0.12' __author__ = 'Decklin Foster' __email__ = 'decklin@red-bean.com' __url__ = 'http://www.red-bean.com/decklin/mnemosyne/' __all__ = ['muse', 'entry'] def get_conf(s): return os.path.expanduser('~/.mnemosyne/%s' % s) def cook(obj, rep): """Create an object exactly like obj, except its repr() is rep. This will allow layouts to use the "cooked" rep (by convention, this is how we format stuff for URLs etc.) without caring how or when or why it was set.""" _class = type("Cooked", (type(obj),), {'__repr__': lambda self: rep}) return _class(obj) def clean(s, maxwords=None): """Split the given string into words, lowercase and strip all non-alphanumerics from them, and join them with '-'. If maxwords is given, limit the returned string to that many words. If the string is None, return None.""" try: words = s.strip().lower().split()[:maxwords] words = [filter(lambda c: c.isalnum(), w) for w in words] return '-'.join(words) or '-' except AttributeError: return None def cheapiter(x): """DWIM-style iterator which, if given a sequence, will iterate over that sequence, unless it is a string type. For a string or any other atomic type, create an iterator which will return the given value once and then stop. Unless it's None. This is a horrible, horrible kludge.""" try: if isinstance(x, basestring): return iter((x,)) else: return iter(x) except TypeError: if x != None: return iter((x,)) else: return iter(()) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mnemosyne-blog-0.12/lib/mnemosyne/entry.py����������������������������������������������������������0000664�0000000�0000000�00000015160�11102427627�0020667�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import os import time import docutils.core import email, email.Message, email.Header, email.Utils from mnemosyne import cook, clean class BaseEntry: """Base class for all entries. Initialized with an open file object, so it may be passed to maildir.Maildir as a factory class. Parses the file's contents as a Message object, setting a date attribute from the parsed date and an mtime attribute from the Maildir filename.""" def __init__(self, fp): def fixdate(d): # For some bizarre reason, parsedate doesn't set wday/yday/isdst. return time.localtime(time.mktime(d)) def getstamp(fp): # _ProxyFile is a disgusting kludge. I take no responsibility. try: path = fp.name # 2.4 or earlier except AttributeError: path = fp._file.name stamp, id, host = os.path.split(path)[1].split('.', 2) return int(stamp) self.msg = email.message_from_file(fp, Message) self.date = fixdate(email.Utils.parsedate(self.msg['Date'])) self.mtime = time.localtime(getstamp(fp)) def __cmp__(self, other): if other: return cmp(time.mktime(self.date), time.mktime(other.date)) else: return 1 def get_content(self): """Read in the message's body, strip any signature, and format using reStructedText.""" s = self.msg.get_body() parts = docutils.core.publish_parts(s, writer_name='html') return parts['body'] byday = {} def get_subject(self): """Get the contents of the Subject: header and a cleaned, uniq'd version of same.""" try: subject = self.msg['Subject'] cleaned = clean(subject, 3) except KeyError: subject = '' cleaned = 'entry' # Grab the namespace for the day of this entry day = self.byday.setdefault(self.date[0:3], UniqueDict()) # This is not quite right. I think maybe it should be cook's # reponsibility to encode things. slug = day.setdefault(hash(self.msg), cleaned) return cook(subject, slug.encode('utf-8', 'replace')) def get_id(self): """Get the Message-ID and a globally unique tag: URL based on it, for use in feeds.""" try: id = self.msg['Message-Id'][1:-1] local, host = id.split('@') date = time.strftime('%Y-%m-%d', self.date) return cook(id, 'tag:%s,%s:%s' % (host, date, local)) except KeyError: return '' def get_author(self): """Get the real name portion of the From: address.""" author, addr = email.Utils.parseaddr(self.msg.get('From')) return cook(author, clean(author)) def get_email(self): """Get the author's email address and a trivially spam-protected version of same.""" try: author, addr = email.Utils.parseaddr(self.msg['From']) cleaned = addr.replace('@', ' at ') cleaned = cleaned.replace('.', ' dot ') cleaned = cleaned.replace('-', ' dash ') return cook(addr, cleaned) except KeyError: return '' def get_tags(self): """Get a list of tags from the comma-delimited X-Tags: header.""" try: tags = [t.strip() for t in self.msg['X-Tags'].split(',')] return [cook(t, clean(t)) for t in tags] except KeyError: return [] def get_year(self): """Extract the year from the Date: header.""" return cook(self.date[0], time.strftime('%Y', self.date)) def get_month(self): """Extract the month from the Date: header.""" return cook(self.date[1], time.strftime('%m', self.date)) def get_day(self): """Extract the day of the month from the Date: header.""" return cook(self.date[2], time.strftime('%d', self.date)) class Entry(BaseEntry): """Actual entry class. To look up an attribute, will search the user-provided mixin classes and then BaseEntry for methods of the form get_*, caching the results (it is assumed that values are referentially transparent).""" def __init__(self, fp): # might want to load this from disk, keyed on hash(self.msg) self.cache = {} for _class in self.__class__.__bases__: try: _class.__init__(self, fp) except AttributeError: pass def __getattr__(self, attr): try: return self.cache[attr] except KeyError: for _class in self.__class__.__bases__: try: method = getattr(_class, 'get_'+attr) except AttributeError: continue return self.cache.setdefault(attr, method(self)) else: raise AttributeError("Entry has no attribute '%s'" % attr) class Message(email.Message.Message): """Non-broken version of email's Message class. Returns unicode headers when necessary and raises KeyError when appropriate.""" def __getitem__(self, item): header = email.Message.Message.__getitem__(self, item) if not header: raise KeyError def actually_decode(s, e): try: return s.decode(e) except: return s.decode('utf-8', 'replace') parts = email.Header.decode_header(header) parts = [actually_decode(s, encoding) for s, encoding in parts] return ' '.join(parts) def get_body(self): """Returns the message payload with any signature stripped.""" body = self.get_payload(decode=True) or self.get_payload(decode=False) if isinstance(body, list): return ''.join([payload.get_body() for payload in body]) else: return body[:body.rfind('-- \n')].decode('utf-8', 'replace') class UniqueDict(dict): """A read-only dict which munges its values so that they are unique. If an existing key has the value 'foo', attempting to set another key to 'foo' will cause it to become 'foo-1', then 'foo-2', etc. These numberings are stable as long as each key is assigned to in the same order; attempting to set an existing key will cause a ValueError. """ def __getitem__(self, k): k, i = dict.__getitem__(self, k) if i: return '%s-%d' % (k, i) else: return k def __setitem__(self, k, v): if k in self: raise ValueError n = len([x for x, y in self.iteritems() if y[0] == v]) dict.__setitem__(self, k, (v, n)) # Yes, we must. Le sigh. def setdefault(self, key, failobj=None): if not self.has_key(key): self[key] = failobj return self[key] ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mnemosyne-blog-0.12/lib/mnemosyne/muse.py�����������������������������������������������������������0000664�0000000�0000000�00000013574�11102427627�0020506�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import os import sys import mailbox import time import stat import shutil import kid import StringIO from entry import Entry from mnemosyne import get_conf, cheapiter class Muse: def __init__(self, config, force): self.force = force self.where = [] self.conf = { 'entry_dir': get_conf('entries'), 'layout_dir': get_conf('layout'), 'style_dir': get_conf('style'), 'output_dir': get_conf('htdocs'), 'ignore': ('.hg', '_darcs', '.git', 'MT', '.svn', 'CVS'), 'locals': {}, 'mixins': [], } try: exec file(config) in self.conf except Exception, e: raise RuntimeError("Error running config: %s" % e) Entry.__bases__ = tuple(self.conf['mixins']) + Entry.__bases__ for d in ('entry_dir', 'layout_dir', 'style_dir', 'output_dir'): if not os.path.exists(self.conf[d]): raise RuntimeError("%s %s does not exist" % (d, self.conf[d])) self.box = mailbox.Maildir(self.conf['entry_dir'], Entry) self.entries = [e for e in self.box] print 'Sorting %d entries...' % len(self.entries) self.entries.sort() def sing(self, entries=None, spath=None, dpath=None, what=None): """From the contents of spath, build output in dpath, based on the provided entries. For each entry in spath, will be called recursively with a tuple what representing the source and dest file. For any source files starting with __attr__ will recur several times based on which entries match each value of that attribute. For regularly named files, evaluate them as layout scripts if they are executable and simply copy them if they are not.""" if not entries: entries = self.entries if not spath: spath = self.conf['layout_dir'] if not dpath: dpath = self.conf['output_dir'] def stale(dpath, spath, entries=None): """Test if the file named by dpath is nonexistent or older than either the file named by spath or any entry in the given list of entries. If --force has been turned on, always return True.""" if self.force or not os.path.exists(dpath): return True else: dmtime = os.path.getmtime(dpath) smtimes = [os.path.getmtime(spath)] if entries: smtimes += [time.mktime(e.mtime) for e in entries] return dmtime < max(smtimes) if what: source, dest = what spath = os.path.join(spath, source) dpath = os.path.join(dpath, dest) if source not in self.conf['ignore']: if os.path.isfile(spath): if os.stat(spath).st_mode & stat.S_IXUSR: if stale(dpath, spath, entries): self.sing_file(entries, spath, dpath) else: if stale(dpath, spath): shutil.copyfile(spath, dpath) print 'Copied %s' % dpath elif os.path.isdir(spath): self.sing(entries, spath, dpath) else: if not os.path.isdir(dpath): os.makedirs(dpath) for f in os.listdir(spath): if f.startswith('__'): self.sing_instances(entries, spath, dpath, f) else: self.where.append(f) self.sing(entries, spath, dpath, (f, f)) self.where.pop() def sing_instances(self, entries, spath, dpath, what): """Given a source and dest file in the tuple what, where the source starts with __attr__, group the provided entries by the values of that attribute over all the provided entries. For an entry e and attribute attr, e.attr may be an atomic value or a sequence of values. For each value so encountered, evaluate the source file given all entries in entries that match that value.""" subst = what[:what.rindex('__')+2] inst = {} for e in entries: mv = getattr(e, subst[2:-2]) for m in cheapiter(mv): inst.setdefault(repr(m), []).append(e) for k, entries in inst.iteritems(): self.where.append(k) self.sing(entries, spath, dpath, (what, what.replace(subst, k))) self.where.pop() def template(self, name, kwargs): """Open a Kid template in the configuration's style directory, and initialize it with any given keyword arguments.""" path = os.path.join(self.conf['style_dir'], '%s.kid' % name) return KidTemplate(path, kwargs) def sing_file(self, entries, spath, dpath): """Given an source layout and and dest file, exec it with the locals from config plus muse (ourself) and entries (the ones we're actually looking at).""" locals = self.conf['locals'].copy() locals['muse'] = self locals['entries'] = entries stdout = sys.stdout sys.stdout = StringIO.StringIO() try: exec file(spath) in globals(), locals except Exception, e: print >>sys.stderr, "Error running layout %s: %s" % (spath, e) else: print >>stdout, 'Wrote %s' % dpath try: file(dpath, 'w').write(sys.stdout.getvalue()) except Exception, e: print >>sys.stderr, "Error writing file: %s" % e sys.stdout = stdout class KidTemplate: def __init__(self, filename, kwargs): module = kid.load_template(filename) self.template = module.Template(assume_encoding='utf-8', **kwargs) def __str__(self): return self.template.serialize(output='xhtml-strict') def __getattr__(self, attr): return getattr(self.template, attr) ������������������������������������������������������������������������������������������������������������������������������������mnemosyne-blog-0.12/mnemosyne�����������������������������������������������������������������������0000775�0000000�0000000�00000001501�11102427627�0016326�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/python import sys import getopt from mnemosyne import get_conf from mnemosyne.muse import Muse if __name__ == '__main__': shortopts = 'fh' longopts = ['force', 'help'] try: opts, args = getopt.getopt(sys.argv[1:], shortopts, longopts) except getopt.GetoptError, e: print >>sys.stderr, 'error: %s' % e sys.exit(1) force=False for opt, arg in opts: if opt in ('--force', '-f'): force = True if opt in ('--help', '-h'): print "usage: mnemosyne [--force] [configfile]" sys.exit(0) try: config = args[0] except IndexError: config = get_conf('config.py') try: muse = Muse(config, force) muse.sing() except RuntimeError, e: print >>sys.stderr, e sys.exit(1) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������mnemosyne-blog-0.12/setup.py������������������������������������������������������������������������0000775�0000000�0000000�00000001460�11102427627�0016107�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/env python from distutils.core import setup setup( name='Mnemosyne', version='0.12', description='Static blog generation system', author='Decklin Foster', author_email='decklin@red-bean.com', url='http://www.red-bean.com/decklin/mnemosyne/', classifiers=[ 'Intended Audience :: Advanced End Users', 'Development Status :: 4 - Beta', 'License :: MIT/X Consortium License', 'Internet :: WWW/HTTP :: Dynamic Content :: News/Diary', 'Operating System :: POSIX', 'Environment :: Console (Text Based)', 'Programming Language :: Python', ], package_dir = {'': 'lib'}, packages = ['mnemosyne'], scripts = ['mnemosyne'], data_files=[ # ('share/man1', ['mnemosyne.1', 'etc...']), ], ) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������