--- mnemosyne-blog-0.12.orig/debian/README.Debian +++ mnemosyne-blog-0.12/debian/README.Debian @@ -0,0 +1,10 @@ +mnemosyne-blog for Debian +------------------------- + +This package is named ``mnemosyne'' upstream. Because that package name was +already used in Debian, the script and user configuration directory have been +renamed (put your config in ~/.mnemosyne-blog, and run mnemosyne-blog). The +Python module is mnemosyne_blog (with an underscore) in order to be a legal +identifier. + + -- Decklin Foster Sat, 11 Oct 2008 21:43:46 -0400 --- mnemosyne-blog-0.12.orig/debian/changelog +++ mnemosyne-blog-0.12/debian/changelog @@ -0,0 +1,61 @@ +mnemosyne-blog (0.12-3) unstable; urgency=medium + + * QA upload. + - Set Maintainer to Debian QA Group. + * Switch to dh_python2 (Closes: #786128). + + -- Luca Falavigna Sun, 14 Jun 2015 18:59:21 +0200 + +mnemosyne-blog (0.12-2) unstable; urgency=low + + * Don't stupidly install byte-compiled files (Closes: #516313) + * Change user config dir to ~/.mnemosyne-blog (Closes: #515882) + * Fix import error when `mnemosyne' package (not us) is installed + (Closes: #515895) + + -- Decklin Foster Thu, 26 Feb 2009 14:24:50 -0500 + +mnemosyne-blog (0.12-1) unstable; urgency=low + + * New Upstream Version + + -- Decklin Foster Thu, 30 Oct 2008 18:05:11 -0400 + +mnemosyne-blog (0.11-2) unstable; urgency=low + + * Add missing maintainer scripts + + -- Decklin Foster Thu, 30 Oct 2008 18:02:25 -0400 + +mnemosyne-blog (0.11-1) unstable; urgency=low + + * New upstream version + * Use python-support + + -- Decklin Foster Tue, 28 Oct 2008 22:58:40 -0400 + +mnemosyne-blog (0.10-3) unstable; urgency=low + + * Install manual page + + -- Decklin Foster Thu, 23 Oct 2008 23:37:49 -0400 + +mnemosyne-blog (0.10-2) unstable; urgency=low + + * Backport fix for python 2.5 mailbox module from upstream + + -- Decklin Foster Mon, 13 Oct 2008 18:37:11 -0400 + +mnemosyne-blog (0.10-1) unstable; urgency=low + + * New upstream version + * Update license in copyright + + -- Decklin Foster Mon, 13 Oct 2008 17:11:18 -0400 + +mnemosyne-blog (0.9-1) unstable; urgency=low + + * Initial release (Closes: #489059) + + -- Decklin Foster Sat, 11 Oct 2008 21:43:46 -0400 + --- mnemosyne-blog-0.12.orig/debian/compat +++ mnemosyne-blog-0.12/debian/compat @@ -0,0 +1 @@ +7 --- mnemosyne-blog-0.12.orig/debian/control +++ mnemosyne-blog-0.12/debian/control @@ -0,0 +1,25 @@ +Source: mnemosyne-blog +Section: web +Priority: extra +Maintainer: Debian QA Group +Build-Depends: debhelper (>= 7), python (>= 2.6.6-3~), dh-python +Standards-Version: 3.8.0 +Homepage: http://www.red-bean.com/decklin/mnemosyne/ + +Package: mnemosyne-blog +Architecture: all +Depends: ${python:Depends}, python-kid, ${misc:Depends} +Provides: ${python:Provides} +Recommends: python-docutils | python-markdown +Description: Maildir-to-blog compiler with XML templating and Python extensions + Mnemosyne is a simple blogging system which generates static files. + Instead of using a database or filesystem hierarchy, you store your + entries in a Maildir. Writing a blog entry is thus as easy as sending + an email, and rebuilding the blog can be automated with mail filters, + cron, etc. + . + XHTML and XML are generated with Kid templates; a bare-bones web view + and an Atom feed are included as examples. Mnemosyne is extensible in + Python to add features such as input preprocessing (reStructuredText + is used by default), metadata ("tags" are standard) and filtering + entries for custom feeds. --- mnemosyne-blog-0.12.orig/debian/copyright +++ mnemosyne-blog-0.12/debian/copyright @@ -0,0 +1,31 @@ +This package was debianized by Decklin Foster on +Sat, 11 Oct 2008 21:43:46 -0400. + +It was downloaded from http://www.red-bean.com/decklin/mnemosyne/ + +Upstream Author: + + Decklin Foster + +Copyright: + + Copyright (C) 2006 Decklin Foster + +License: + + Copyright © 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. + +The Debian packaging is (C) 2008, Decklin Foster and +is licensed under the same terms as above. --- mnemosyne-blog-0.12.orig/debian/mnemosyne-blog.1 +++ mnemosyne-blog-0.12/debian/mnemosyne-blog.1 @@ -0,0 +1,29 @@ +.TH MNEMOSYNE 1 "October 11, 2008" +.SH NAME +mnemosyne-blog \- Weblog compiler +.SH SYNOPSIS +.B mnemosyne-blog +.I [CONFIG] +.SH DESCRIPTION +This manual page documents briefly the +.B mnemosyne-blog +command. +.PP +.B mnemosyne-blog +compiles XML and XHTML files comprising a weblog from the contents of +a Maildir. The locations of each are read from the specified +.IR CONFIG . +If no configuration is specified, +.I ~/.mnemosyne/config.py +is used by default. +.SH OPTIONS +.TP +.B \-f, \-\-force +Rebuild all files, whether sources have changed or not. +.TP +.B \-h, \-\-help +Show summary of options. +.SH SEE ALSO +.I /usr/share/doc/mnemosyne-blog/README.gz +.SH AUTHOR +mnemosyne-blog was written by Decklin Foster . --- mnemosyne-blog-0.12.orig/debian/mnemosyne-blog.changelogs +++ mnemosyne-blog-0.12/debian/mnemosyne-blog.changelogs @@ -0,0 +1 @@ +NEWS --- mnemosyne-blog-0.12.orig/debian/mnemosyne-blog.dirs +++ mnemosyne-blog-0.12/debian/mnemosyne-blog.dirs @@ -0,0 +1 @@ +usr/bin --- mnemosyne-blog-0.12.orig/debian/mnemosyne-blog.docs +++ mnemosyne-blog-0.12/debian/mnemosyne-blog.docs @@ -0,0 +1 @@ +README --- mnemosyne-blog-0.12.orig/debian/mnemosyne-blog.manpages +++ mnemosyne-blog-0.12/debian/mnemosyne-blog.manpages @@ -0,0 +1 @@ +debian/mnemosyne-blog.1 --- mnemosyne-blog-0.12.orig/debian/rules +++ mnemosyne-blog-0.12/debian/rules @@ -0,0 +1,7 @@ +#!/usr/bin/make -f + +binary: binary-indep +binary-indep: install + +%: + dh $@ --with python2 --- mnemosyne-blog-0.12.orig/lib/mnemosyne_blog/__init__.py +++ mnemosyne-blog-0.12/lib/mnemosyne_blog/__init__.py @@ -0,0 +1,52 @@ +"""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-blog/%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.orig/lib/mnemosyne_blog/entry.py +++ mnemosyne-blog-0.12/lib/mnemosyne_blog/entry.py @@ -0,0 +1,182 @@ +import os +import time +import docutils.core +import email, email.Message, email.Header, email.Utils + +from mnemosyne_blog 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.orig/lib/mnemosyne_blog/muse.py +++ mnemosyne-blog-0.12/lib/mnemosyne_blog/muse.py @@ -0,0 +1,155 @@ +import os +import sys +import mailbox +import time +import stat +import shutil +import kid +import StringIO + +from entry import Entry +from mnemosyne_blog 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.orig/mnemosyne-blog +++ mnemosyne-blog-0.12/mnemosyne-blog @@ -0,0 +1,37 @@ +#!/usr/bin/python + +import sys +import getopt + +from mnemosyne_blog import get_conf +from mnemosyne_blog.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-blog [--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.orig/setup.py +++ mnemosyne-blog-0.12/setup.py @@ -19,9 +19,9 @@ 'Programming Language :: Python', ], package_dir = {'': 'lib'}, - packages = ['mnemosyne'], - scripts = ['mnemosyne'], + packages = ['mnemosyne_blog'], + scripts = ['mnemosyne-blog'], data_files=[ - # ('share/man1', ['mnemosyne.1', 'etc...']), + # ('share/man1', ['mnemosyne-blog.1', 'etc...']), ], )