weblog-1.0/.hg_archival.txt0000644000000000000000000000013611060362141016053 0ustar00usergroup00000000000000repo: 00ecfb3367fecf6d8ba7a94c3f995bc789c18d1e node: 1ed0521ffc52335e6560d2135b0f85dc4aab01b2 weblog-1.0/.hgignore0000644000000000000000000000006411060362141014570 0ustar00usergroup00000000000000syntax: glob *.pyc *~ .*.swp build weblog.egg-info weblog-1.0/.hgtags0000644000000000000000000000116011060362141014241 0ustar00usergroup000000000000009bafbb9dfb928172d988390ea61932b610278ea3 WEBLOG_0_1 7053f6c08fab7af1c5b76d78a9bb6e41fe6a8b5a WEBLOG_0_2 ebe752dc0a655b451babdc2acb6027a523ac8474 WEBLOG_0_3 9cc6b91a2fb95946b5443461201c8a57ad301a53 WEBLOG_0_4 85db8e1cb11890a15f38b3d161dc59962e00b135 WEBLOG_0_5 fcd5f323c67112916c0d43d776f52a96064bef53 WEBLOG_0_5 44abff8da985b456545fa393a2f634932400476b WEBLOG_0_5 a0a1dd94b8b2371f45536e90ee03074dae314f71 WEBLOG_0_6 657340b5fa4b2a1747e139809da6e576d7699290 WEBLOG_0_7 f4075497305adf1cada74fa556a227049a2ccae5 WEBLOG_0_8 8377a76875c476b8697cbfc25be7b3d1fe961028 WEBLOG_0_9 2b7a9f4e897d42683ac16491822eddeab8f5b3b7 WEBLOG_1_0 weblog-1.0/COPYING0000644000000000000000000000467011060362141014027 0ustar00usergroup00000000000000Copyright (c) 2007, Henry Precheur 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. --- Also the file PyRSS2Gen.py is covered by the BSD license: (This is the BSD license, based on the template at http://www.opensource.org/licenses/bsd-license.php ) Copyright (c) 2003, Dalke Scientific Software, LLC All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the Dalke Scientific Softare, LLC, Andrew Dalke, nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. weblog-1.0/README0000644000000000000000000000074111060362141013647 0ustar00usergroup00000000000000Weblog is a web log or blog publisher. It takes structured text files as input and outputs static HTML / RSS files. Weblog aims to be simple and robust. to learn how to install and use weblog please read the text file: doc/weblog.rst If you have docutils installed you can turn it into a HTML file: $ rst2html.py weblog.py > weblog.html Jinja is needed to use Weblog (http://jinja.pocoo.org) Weblog includes PyRSS2Gen (http://www.dalkescientific.com/Python/PyRSS2Gen.html) weblog-1.0/TODO0000644000000000000000000000017711060362141013462 0ustar00usergroup00000000000000--- v1.0 --- Stable release: - more unit tests - more docs! 1.1 --- Add Markdown support Add Atom 1.0 Remove RSS 2.0 (?) weblog-1.0/bin/weblog0000755000000000000000000000524711060362141014752 0ustar00usergroup00000000000000#!/usr/bin/env python # vim:set filetype=python: import os import sys import datetime import logging from shutil import copy from optparse import OptionParser, SUPPRESS_HELP from weblog import command_publish, command_date _COMMANDS = ('publish', 'date') def main(): parser = OptionParser() parser.add_option("-s", "--source_dir", dest="source_dir", default='.', help='The source directory where the blog posts are ' 'located. [default: \'%default\']', metavar="DIR") parser.add_option("-o", "--output_dir", dest="output_dir", default='output', help='The directory where all the generated files are ' 'written. If it does not exist it is created.' '[default: \'%default\']', metavar="DIR") parser.add_option('-c', '--conf', dest='configuration_file', help='The configuration file to use. If the file is not ' 'present in the current directory, the source directory ' 'is searched.' ' [default: \'%default\']', metavar='FILE', default='weblog.ini') parser.add_option('-q', '--quiet', dest='quiet', default=False, action='store_true', help='Do not output anything except critical error ' 'messages') parser.add_option('--debug', dest='debug', default=False, action='store_true', help=SUPPRESS_HELP) parser.set_usage('%%prog [option] command\n\nCommands:\n %s' % \ '\n '.join(_COMMANDS)) (options, args) = parser.parse_args() if options.debug: logging.basicConfig(level=logging.DEBUG, format='%(levelname)s %(message)s') elif options.quiet: logging.basicConfig(level=logging.ERROR, format='%(message)s') else: logging.basicConfig(level=logging.INFO, format='%(message)s') if not args: logging.warning('Warning: No command specified, assuming \'publish\'.\n' ' To remove this warning, type: %s publish' % sys.argv[0]) command = 'publish' else: command = args.pop(0) if command not in _COMMANDS: parser.error('invalid command \'%s\'' % command) elif command == 'publish': command_publish(args, options) elif command == 'date': command_date(args, options) if __name__ == '__main__': main() weblog-1.0/doc/style.rst0000644000000000000000000001051611060362141015427 0ustar00usergroup00000000000000Customizing Weblog's appearance =============================== By default Weblog does not have a style-sheet thus looks raw. It is possible to make it more appealing by adding a style-sheet. This document details the possibilities of customizing Weblog's visual appearance. Note to the user ~~~~~~~~~~~~~~~~ On Internet content is more important than appearance. Even with the best graphics and the fanciest website possible, if you don't have content your site will be worthless and nobody will look at it. An interesting post will drive people to your Blog. Your choice of color or a custom logo will not. Don't overspent time on design! Getting started --------------- External CSS ~~~~~~~~~~~~ The recommended way of customizing Weblog visual appearance. Is via an external CSS style-sheet. Add the following line to ``weblog.ini``:: html_head: extra_files: style.css Create a file named ``style.css`` in the source directory and generate a temporary blog to tweak CSS file:: $ cd source/directory $ touch style.css $ weblog -s . -o temporary_blog Open ``temporary_blog/index.html`` in your browser and change the visual appearance by editing ``temporary_blog/style.css``. Inline CSS ~~~~~~~~~~ This method is also valid, but it makes HTML files bigger. The "External CSS" method is prefered over this one. To have the CSS stylesheet embedded into the pages, create a file named ``style.css`` containing:: Pages structure --------------- Most of Weblog HTML tags are associated with an `id` or a `class`. The following tables show the different tags and class associated with it. Base structure ~~~~~~~~~~~~~~ The structure common to all pages. `header` and `footer` are user-defined. +--------------+ | Body | | | | +----------+ | | | header | | | +----------+ | | | div#main | | | +----------+ | | | footer | | | +----------+ | +--------------+ Listing structure ~~~~~~~~~~~~~~~~~ The structure of a listing page contained in the `main div`. +----------------------+ | h1#title | +----------------------+ | p#description | +----------------------+ | List of posts | | | | +------------------+ | | | h2.post-title | | | +------------------+ | | | p.post-header | | | | | | | | +-------------+ | | | | | span.date | | | | | +-------------+ | | | | | span.author | | | | | +-------------+ | | | +------------------+ | | | div.post-content | | | +------------------+ | | | +----------------------+ | hr.footer-ruler | +----------------------+ | div.paginator | | | | +------------------+ | | | a or span + | | | .paginator-link + | | +------------------+ | +----------------------+ Post structure ~~~~~~~~~~~~~~ +------------------+ | h1.post-title | +------------------+ | p.post-header | | | | +-------------+ | | | span.date | | | +-------------+ | | | span.author | | | +-------------+ | +------------------+ | div.post-content | +------------------+ Custom header & footer ---------------------- The custom header and footer make it possible to add a menu bar or logo. To add a custom logo at the top of the blog, create a directory ``html`` in the source directory, and create a file named ``header.html`` in this new directory:: Then edit ``weblog.ini`` and add the following lines:: html_header = html/header.html extra_files = my_fancy_logo.png This insert the content of the file ``html/header.html`` before the blog's title, and copy the file ``my_fancy_logo.png``. CSS resources ------------- Learning and developing with CSS is hard. The CSS syntax tend to be confusing for beginners. The numerous browser incompatibilities makes the designer's work even more complicated. Here is a list of useful resources regarding this subject: * SitePoint_ CSS Reference is helpful if you are a beginner with CSS. It lists all CSS properties and document how well they are supported by the different browsers. * HtmlHelp_ contains a complete HTML 4 reference. .. _HtmlHelp: http://htmlhelp.com/reference/html40/ .. _SitePoint: http://reference.sitepoint.com/css .. vim:se tw=80 sw=2 ts=2 et encoding=utf-8: weblog-1.0/doc/weblog.rst0000644000000000000000000002520211060362141015544 0ustar00usergroup00000000000000Weblog manual ============= :Author: Henry Prêcheur :Reviewers: Anis Kadri, Bastien Simondi, Eric Salama Abstract -------- Simple blog publisher. It reads structured text files and generates static HTML / RSS files. Weblog aims to be simple and robust. In this document *Weblog* is the name of the software. The *web log* concept is referred as the more common term *blog*. According to Wikipedia_: A *blog* (a portmanteau of *web log*) is a website where entries are written in chronological order and commonly displayed in reverse chronological order. .. _Wikipedia: http://en.wikipedia.org/wiki/Blog Pre-requirements ~~~~~~~~~~~~~~~~ - Python version 2.5+ - Jinja version 1.1+ or Jinja 2.0+. Learn how to install Jinja at http://jinja.pocoo.org/2/documentation/intro#installation or http://jinja.pocoo.org/documentation/installation. Installation ------------ Download Weblog's latest version at http://henry.precheur.org/weblog/. Extract it:: tar zxf weblog.tar.gz It can be used right away using the helper script ``weblog_run.py``. Or install it using the supplied ``setup.py`` script. Run ``python setup.py --help`` to learn how to use it. Alternatively if easy_install is present, simply type:: easy_install weblog It fetches the latest version of Weblog and installs it. Quick Start ----------- In the following examples ``weblog/`` represents Weblog's installation directory. If you downloaded the source tarball without installing Weblog; Use the helper script ``weblog_run.py`` instead of the ``weblog`` command:: $ python /path/to/weblog/weblog_run.py --help Create a new directory named ``my_blog``. The $ sign represents the shell prompt, do not type it!:: $ mkdir my_blog Copy from the Weblog installation directory the file ``weblog.ini`` into ``my_blog``:: $ cp weblog/examples/weblog.ini my_blog ``weblog.ini`` is the configuration file of the blog. Check the configuration file section for more information. Do not worry about it now, no modification is required to get the following examples working. Create a file named ``first_post.html`` in the ``my_blog`` directory:: title: First post author: Me date: 2007-08-25 Hello world! Actually all the post filenames must end with ``.html``. Go in the ``my_blog`` directory and run the Weblog using the publish command:: $ cd my_blog/ $ weblog publish It should create a directory named ``output`` containing the generated files. Look at the results by opening the file ``output/index.html`` in your web-browser. The first 3 lines of the file ``first_post.html`` define the post's parameters. These are standard :RFC:`2822` headers (the headers used in Emails). Only ``title`` is mandatory. ``date`` and ``author`` are optional. If you don't fill these fields, the author is the one specified in ``weblog.ini``, and the post's date is the post file's last modification date. The line ``Hello world!`` is the actual content of the post. Note that a blank line is required between the headers and the content. The content is an HTML block. Use the HTML syntax to format your post content. For example create a second file named ``second_post.html``:: title: Second post author: Me (again!) date: 2007-08-26 Second test post!

© 2007 Me

Regenerate the blog files:: $ weblog publish Reload the page in your browser. You should see a second post with some formating. The default post file encoding is ASCII. To use a different encoding specify it via the field ``encoding``:: title: Encoding test date: 2007-11-5 encoding: latin-1 Here you can put some ISO-8856-1 text ... Specify the default encoding in ``weblog.ini``, to avoid setting the encoding field for every file. While writing your blog post, don't bother about the ``date`` field immediately. Weblog automatically sets the date to the filename's last modification time. A good practice though is to set the date when the post gets published. By doing so the date won't get changed if the file gets copied. To set the date of a post, use the command ``date``:: $ date Mon Apr 14 00:10:44 PDT 2008 $ cat my_blog_post.html title: My blog post This is a blog post without any date. $ weblog date my_blog_post.html Setting date to 2008-04-14 00:12:22 in file my_blog_post.html $ cat my_blog_post.html title: My blog post date: 2008-04-14 00:12:22 This is a blog post without any date. $ weblog date my_blog_post 2008-5-15 Setting date to 2008-05-15 in file my_blog_post.html $ cat my_blog_post.html title: My blog post date: 2008-05-15 This is a blog post without any date. The ``date`` command accepts 3 formats as argument: - YEAR-MONTH-DAY (2008-01-31) - YEAR-MONTH-DAY HOUR:MINUTE (2008-01-31 16:45) - YEAR-MONTH-DAY HOUR:MINUTE:SECONDS (2008-01-31 16:45:14) For conciseness the ``date`` command uses aliases to specify commonly used date: - now - today (like now but only set the date, not the time) - tomorrow (now + 24 hours) - next_day (like tomorrow but only sets the date, not the time) Encoding and escaping --------------------- Weblog tries to make sure its output is always *correct*. Non-ASCII characters, are converted to HTML entities so you don't have to worry about it. The output is *never* encoded into ISO-8856-1, UTF-8 or another non-ASCII encoding. Encoding conversions are not so simple in practice. By doing only one conversion to the simplest encoding possible, a lot of problems are solved. The content of the post is not escaped. The title and the date of the post are escaped. The title ``Hello World`` is escaped. HTML tags appear, and no formating is applied to ``world``. The original text "Hello World" appears instead of "Hello *World*", It is possible to override this by specifying ``raw`` as the encoding. Using the ``raw`` encoding nothing is escaped or converted, but you must make sure all characters are ASCII characters:: title: Non-escaped title author: Me <me@my_weblog.org> encoding: raw If the ``raw`` encoding is used, all the characters must be ASCII characters. Otherwise an error is reported. Attaching a file to a post -------------------------- To attach files like images to a blog post, use the field ``files``:: title: Attach a file files: picture.png directory/file a picture a file It will copy ``picture.png`` and ``directory/file``. If ``directory`` does not exist, it will be created. How URI's are handled --------------------- Relative links (````) are rewritten in the RSS file and in some HTML files. In the RSS file ``base_url`` is prepended to the link to make sure it always points to the correct URI. Absolute links (````) are not rewritten. It should always point to the correct location regardless of the context. Note that Weblog considers ``/`` as the root directory. If ``base_url`` is ``http://example.com/``; ``test.html`` and ``/test.html`` are both rewritten to ``http://example.com/test.html``. Command line parameters ----------------------- Usage: weblog [options] Options: -h, --help show this help message and exit -s DIR, --source-dir=DIR The source directory where the blog posts and the file weblog.ini are located -o DIR, --output-dir=DIR The directory where all the generated files are written. If it does not exist it is created. -q, --quiet Do not output anything except critical error messages Configuration file ------------------ All configuration options are in the ``weblog`` section. Learn more about the format of the configuration file: http://docs.python.org/lib/module-ConfigParser.html. A sample configuration file:: [weblog] title: Blog's title url: http://example.com/ description: A sample blog. source_dir: path/to/my/posts output_dir: path/to/output/directory encoding: latin-1 author: Me Fields description ~~~~~~~~~~~~~~~~~~ title The blog's title. It appears at the top of the homepage and in the page's title. This field is mandatory. url The base URL of your blog. For example ``http://my-host.com/my-weblog/``. It is used to generate the absolute URL's to your blog. This field is mandatory. description A short description of your blog. Like "My favorite books reviews", or "Dr. Spock, publications about electronics". Note that it is possible to use multiple lines:: description: My blog about configuration files. The description is merged to a single line; ``My blog about configuration files.``. This field is mandatory. source_dir The directory containing the file ``weblog.ini``, the post files and possibly the ``templates`` directory. By default the current directory. output_dir The output directory. Generated files are put there. By default ``output``. encoding The default post file encoding. Default ``ASCII``. It is overridden by the ``encoding`` field in the post file. author The default author. It is overridden by the ``author`` field in the post file. post_per_page The number of post displayed per listing page. Default is 10. rss_limit The maximum number of post to be included in the RSS file. The most recent posts are the ones included. Default is 10. html_head Additional information for the ```` section. Useful to add custom CSS style sheets. Can be a string or a filename. If a file with this name exists in the source directory then it is read. Else it is considered as a string. The result is processed using Jinja. Use the variable ``top_dir`` to link to external files. It contains the path to the top directory of the blog. Examples:: html_head= html_head={{ top_dir }}my_stylesheet.css html_header Additional content located just before the blog content. Can be a string or a filename. (See html_head above) Useful to add a logo or a search box at the top. html_footer Additional content located just after the blog content. Can be a string or a filename. (See html_head above) Useful to add ... A footer! Tips on Uploading ----------------- rsync_ is a useful tool to upload files generated by Weblog. To make sure rsync does not change the last modification time of the files that did not change, use the following:: rsync --compress --checksum --recursive path/to/blog remote_host:public/dir/ Accurate last modification time makes efficient caching possible. .. _rsync: http://samba.anu.edu.au/rsync/ .. vim:se tw=80 sw=2 ts=2 et encoding=utf-8: weblog-1.0/examples/enconding.html0000644000000000000000000000055111060362141017436 0ustar00usergroup00000000000000title: Weblog encode le français! author: Henry Prêcheur encoding: latin-1 date: 2007-10-01

Weblog encode maintenant le texte correctement! Des caractéres tels que: È, Õ ou Ä sont maintenant bien encodés!
Français, Español & Deutsh :)

The encoding of the file is ISO-8859-1 or latin-1.

weblog-1.0/examples/first_post.html0000644000000000000000000000007411060362141017666 0ustar00usergroup00000000000000title: First post author: Me date: 2007-08-25 Hello world! weblog-1.0/examples/second_post.html0000644000000000000000000000020711060362141020010 0ustar00usergroup00000000000000title: Second post date: 2007-08-26 Second test post!

The author lastname is Prêcheur

weblog-1.0/examples/utf-8.html0000644000000000000000000000016111060362141016432 0ustar00usergroup00000000000000title: Some UTF-8, ç ä é ö ó date: 2008-1-1 encoding: UTF-8 Test post with UTF-8 inside ... ç ä é ö ó weblog-1.0/examples/w3_steely_style.css0000644000000000000000000000036011060362141020452 0ustar00usergroup00000000000000body { text-align: center; /* for IE 4+ */ } div#main { margin: 0 auto; text-align: left; /* counter the body center */ width: 42em; max-width: 90%; } p.weblog-ad { margin: 0 auto; text-align: left !important; } weblog-1.0/examples/weblog.ini0000644000000000000000000000027011060362141016562 0ustar00usergroup00000000000000[weblog] title=Sample blog url=http://blog.sample.org description=Brief description of this sample blog. Do multiline, this way! encoding=UTF-8 author=Me weblog-1.0/examples/weblog_w3_steely_css.ini0000644000000000000000000000051611060362141021433 0ustar00usergroup00000000000000[weblog] title=Sample blog url=http://blog.sample.org description=Brief description of this sample blog. author=Me html_head= extra_files=w3_steely_style.css weblog-1.0/setup.cfg0000644000000000000000000000004711060362141014607 0ustar00usergroup00000000000000[nosetests] verbosity=3 with-doctest=1 weblog-1.0/setup.py0000644000000000000000000000262611060362141014505 0ustar00usergroup00000000000000try: from setuptools import setup except: from distutils.core import setup import os version = '1.0' f = open(os.path.join(os.path.dirname(__file__), 'doc', 'weblog.rst')) # The long description has to be ascii encoded ... long_description = f.read().strip().decode('utf-8').encode('ascii', 'replace') f.close() setup(name="weblog", version=version, packages=['weblog'], package_data={'weblog': ['templates/*.tmpl']}, scripts=['bin/weblog'], requires=['Jinja2 (>=2.0)'], install_requires=['Jinja2'], data_files=[('doc', ['doc/weblog.rst', 'doc/style.rst'])], # unzip the egg so we can access to documentation & templates zip_safe = False, # metadata for upload to PyPI author = 'Henry Precheur', author_email = 'henry@precheur.org', description = ('Simple blog publisher. It reads structured text ' 'files and generates static HTML / RSS files. Weblog aims ' 'to be simple and robust.'), long_description=long_description, license = "ISC", keywords = "weblog blog journal diary rss", url = "http://henry.precheur.org/weblog/", classifiers=[ 'Development Status :: 4 - Beta', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content :: News/Diary', 'Intended Audience :: End Users/Desktop', 'Programming Language :: Python', ]) weblog-1.0/test.py0000644000000000000000000001537611060362141014332 0ustar00usergroup00000000000000import os import shutil import tempfile import unittest import StringIO import email import datetime from optparse import Values from weblog import Post, PostError, jinja_environment from weblog.publish import load_post_list, generate_rss, generate_index_listing from weblog.date import command_date from weblog.publish import command_publish class TestSimpleLoad(unittest.TestCase): def test_load_post_list(self): post_list = load_post_list('test/simple/') self.assertEqual(len(post_list), 3) sorted_list = sorted(post_list) self.assertEqual(sorted_list[0].title, 'post1') self.assertEqual(sorted_list[1].title, 'post2') self.assertEqual(sorted_list[2].title, 'post3') def test_load_post_list_encoding_failure(self): Post.DEFAULT_ENCODING = 'ascii' self.assertRaises(PostError, load_post_list, 'test/encoding/') def test_load_post_list_encoding(self): Post.DEFAULT_ENCODING = 'UTF-8' post_list = load_post_list('test/encoding/') self.assertEqual(len(post_list), 2) sorted_list = sorted(post_list) self.assertEqual(sorted_list[0].title, 'UTF-8 post ÖÉÈÄ ...') self.assertEqual(sorted_list[0].content, 'Öéèä\n') self.assertEqual(sorted_list[1].title, 'latin post ÖÉÈÄ ...') self.assertEqual(sorted_list[1].content, 'Öéèä\n') class TestJinja(unittest.TestCase): env = jinja_environment(os.path.dirname(__file__)) def test_renderstring(self): template = self.env.\ from_string('Hello {{ string_template|renderstring }}!') self.assertEqual(template.render(dict(string_template='{{ foo }} world', foo='crazy')), u'Hello crazy world!') def test_renderstring_empty(self): template = self.env.\ from_string('Hello {{ string_template|renderstring }}!') self.assertEqual(template.render(dict(string_template='', foo='crazy')), u'Hello !') def test_format_date_(self): template = self.env.from_string('{{ d|format_date }}') self.assertEqual(template.render(dict(d=datetime.date(2008, 7, 21))), '2008-07-21') self.assertEqual(template.render(dict(d=datetime.datetime(2008, 7, 21, 21, 42, 12, 123))), '2008-07-21 21:42:12') self.assertRaises(TypeError, template.render, dict(d=12)) class TestGeneration(unittest.TestCase): env = jinja_environment(os.path.dirname(__file__)) def setUp(self): self.tempdir = tempfile.mkdtemp() def tearDown(self): shutil.rmtree(self.tempdir) def test_generate_listing_empty(self): generate_index_listing(10, self.tempdir, self.env.get_template('index.html.tmpl'), list(), dict(title='test', url='http://test.net', description='test')) def test_generate_listing(self): post1 = '''title: post1 date: 2008-02-04 post 1''' post2 = '''title: post2 date: 2008-01-18 author: test@test.com post 2''' post_list = [Post(StringIO.StringIO(post1)), Post(StringIO.StringIO(post2))] generate_index_listing(10, self.tempdir, self.env.get_template('index.html.tmpl'), post_list, dict(title='test', url='http://test.net', description='test')) def test_generate_rss(self): post1 = '''title: post1 date: 2008-02-04 post 1''' post2 = '''title: post2 date: 2008-01-18 author: test@test.com post 2
''' post_list = [Post(StringIO.StringIO(post1)), Post(StringIO.StringIO(post2))] generate_rss(post_list, os.path.join(self.tempdir, 'rss.xml'), dict(title='test', url='http://test.net', description='test')) def test_generate_rss_empty(self): generate_rss(list(), os.path.join(self.tempdir, 'rss.xml'), dict(title='test', url='http://test.net', description='test')) def test_date(self): filename = os.path.join(self.tempdir, 'set_date.html') # First test a message without any date defined def file_without_date(): file(filename, 'w').write('title: Some title\n\nSome content') file_without_date() command_date([filename, '2008-1-1'], None) message = email.message_from_file(file(filename)) self.assert_('date' in message) self.assertEqual(message['date'], str(datetime.date(2008, 1, 1))) # Then test a file which has already a date def file_with_date(): file(filename, 'w').write('title: Some title\ndate: 2008-12-31\n' '\nSome content') file_with_date() command_date([filename, '2008-1-1'], None) message = email.message_from_file(file(filename)) self.assert_('date' in message) self.assertEqual(message['date'], str(datetime.date(2008, 1, 1))) # Test aliases for alias in ('now', 'today', 'tomorrow', 'next_day'): file_without_date() command_date([filename, alias], None) message = email.message_from_file(file(filename)) self.assert_('date' in message) def _test_publish(self, dirname): options = Values(dict(source_dir=os.path.join(os.path.dirname(__file__), 'test', dirname), output_dir=self.tempdir, configuration_file='weblog.ini', debug=False)) command_publish(None, options) def test_publish_empty(self): self._test_publish('empty') def test_publish_encoding(self): self._test_publish('encoding') def test_publish_full_url(self): self._test_publish('full_url') def test_publish_simple(self): self._test_publish('simple') if __name__ == '__main__': import nose nose.main() weblog-1.0/test/empty/weblog.ini0000644000000000000000000000014411060362141017061 0ustar00usergroup00000000000000[weblog] title=Test blog url=http://blog.test.org description=Test blog author=test weblog-1.0/test/encoding/latin-1.html0000644000000000000000000000010411060362141017660 0ustar00usergroup00000000000000title: latin post ÖÉÈÄ ... date: 2008-02-04 encoding: latin-1 Öéèä weblog-1.0/test/encoding/utf-8.html0000644000000000000000000000007211060362141017362 0ustar00usergroup00000000000000title: UTF-8 post ÖÉÈÄ ... date: 2008-02-03 Öéèä weblog-1.0/test/encoding/weblog.ini0000644000000000000000000000016311060362141017512 0ustar00usergroup00000000000000[weblog] title=Test blog url=http://blog.test.org description=Test blog author=test encoding=utf-8 weblog-1.0/test/full_url/utf-8.html0000644000000000000000000000025011060362141017416 0ustar00usergroup00000000000000title: UTF-8 post ÖÉÈÄ ... date: 2008-02-03 Öéèä äyÔÀ Weblog weblog-1.0/test/full_url/weblog.ini0000644000000000000000000000016311060362141017550 0ustar00usergroup00000000000000[weblog] title=Test blog url=http://blog.test.org description=Test blog author=test encoding=utf-8 weblog-1.0/test/simple/post1.html0000644000000000000000000000004511060362141017170 0ustar00usergroup00000000000000title: post1 date: 2007-01-01 post1 weblog-1.0/test/simple/post2.html0000644000000000000000000000004411060362141017170 0ustar00usergroup00000000000000title: post2 date: 2007-6-15 post2 weblog-1.0/test/simple/post3.html0000644000000000000000000000004511060362141017172 0ustar00usergroup00000000000000title: post3 date: 2007-12-31 post3 weblog-1.0/test/simple/weblog.ini0000644000000000000000000000014411060362141017214 0ustar00usergroup00000000000000[weblog] title=Test blog url=http://blog.test.org description=Test blog author=test weblog-1.0/weblog/PyRSS2Gen.py0000644000000000000000000003432411060362141016320 0ustar00usergroup00000000000000"""PyRSS2Gen - A Python library for generating RSS 2.0 feeds.""" __name__ = "PyRSS2Gen" __version__ = (1, 0, 0) __author__ = "Andrew Dalke " _generator_name = __name__ + "-" + ".".join(map(str, __version__)) import datetime # Could make this the base class; will need to add 'publish' class WriteXmlMixin: def write_xml(self, outfile, encoding = "iso-8859-1"): from xml.sax import saxutils handler = saxutils.XMLGenerator(outfile, encoding) handler.startDocument() self.publish(handler) handler.endDocument() def to_xml(self, encoding = "iso-8859-1"): try: import cStringIO as StringIO except ImportError: import StringIO f = StringIO.StringIO() self.write_xml(f, encoding) return f.getvalue() def _element(handler, name, obj, d = {}): if isinstance(obj, basestring) or obj is None: # special-case handling to make the API easier # to use for the common case. handler.startElement(name, d) if obj is not None: handler.characters(obj) handler.endElement(name) else: # It better know how to emit the correct XML. obj.publish(handler) def _opt_element(handler, name, obj): if obj is None: return _element(handler, name, obj) def _format_date(dt): """convert a datetime into an RFC 822 formatted date Input date must be in GMT. """ # Looks like: # Sat, 07 Sep 2002 00:00:01 GMT # Can't use strftime because that's locale dependent # # Isn't there a standard way to do this for Python? The # rfc822 and email.Utils modules assume a timestamp. The # following is based on the rfc822 module. if isinstance(dt, datetime.datetime): hour = dt.hour minute = dt.minute second = dt.second elif isinstance(dt, datetime.date): hour = minute = second = 0 return "%s, %02d %s %04d %02d:%02d:%02d GMT" % ( ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][dt.weekday()], dt.day, ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][dt.month-1], dt.year, hour, minute, second) ## # A couple simple wrapper objects for the fields which # take a simple value other than a string. class IntElement: """implements the 'publish' API for integers Takes the tag name and the integer value to publish. (Could be used for anything which uses str() to be published to text for XML.) """ element_attrs = {} def __init__(self, name, val): self.name = name self.val = val def publish(self, handler): handler.startElement(self.name, self.element_attrs) handler.characters(str(self.val)) handler.endElement(self.name) class DateElement: """implements the 'publish' API for a datetime.date Takes the tag name and the datetime to publish. Converts the datetime to RFC 2822 timestamp (4-digit year). """ def __init__(self, name, dt): self.name = name self.dt = dt def publish(self, handler): _element(handler, self.name, _format_date(self.dt)) #### class Category: """Publish a category element""" def __init__(self, category, domain = None): self.category = category self.domain = domain def publish(self, handler): d = {} if self.domain is not None: d["domain"] = self.domain _element(handler, "category", self.category, d) class Cloud: """Publish a cloud""" def __init__(self, domain, port, path, registerProcedure, protocol): self.domain = domain self.port = port self.path = path self.registerProcedure = registerProcedure self.protocol = protocol def publish(self, handler): _element(handler, "cloud", None, { "domain": self.domain, "port": str(self.port), "path": self.path, "registerProcedure": self.registerProcedure, "protocol": self.protocol}) class Image: """Publish a channel Image""" element_attrs = {} def __init__(self, url, title, link, width = None, height = None, description = None): self.url = url self.title = title self.link = link self.width = width self.height = height self.description = description def publish(self, handler): handler.startElement("image", self.element_attrs) _element(handler, "url", self.url) _element(handler, "title", self.title) _element(handler, "link", self.link) width = self.width if isinstance(width, int): width = IntElement("width", width) _opt_element(handler, "width", width) height = self.height if isinstance(height, int): height = IntElement("height", height) _opt_element(handler, "height", height) _opt_element(handler, "description", self.description) handler.endElement("image") class Guid: """Publish a guid Defaults to being a permalink, which is the assumption if it's omitted. Hence strings are always permalinks. """ def __init__(self, guid, isPermaLink = 1): self.guid = guid self.isPermaLink = isPermaLink def publish(self, handler): d = {} if self.isPermaLink: d["isPermaLink"] = "true" else: d["isPermaLink"] = "false" _element(handler, "guid", self.guid, d) class TextInput: """Publish a textInput Apparently this is rarely used. """ element_attrs = {} def __init__(self, title, description, name, link): self.title = title self.description = description self.name = name self.link = link def publish(self, handler): handler.startElement("textInput", self.element_attrs) _element(handler, "title", self.title) _element(handler, "description", self.description) _element(handler, "name", self.name) _element(handler, "link", self.link) handler.endElement("textInput") class Enclosure: """Publish an enclosure""" def __init__(self, url, length, type): self.url = url self.length = length self.type = type def publish(self, handler): _element(handler, "enclosure", None, {"url": self.url, "length": str(self.length), "type": self.type, }) class Source: """Publish the item's original source, used by aggregators""" def __init__(self, name, url): self.name = name self.url = url def publish(self, handler): _element(handler, "source", self.name, {"url": self.url}) class SkipHours: """Publish the skipHours This takes a list of hours, as integers. """ element_attrs = {} def __init__(self, hours): self.hours = hours def publish(self, handler): if self.hours: handler.startElement("skipHours", self.element_attrs) for hour in self.hours: _element(handler, "hour", str(hour)) handler.endElement("skipHours") class SkipDays: """Publish the skipDays This takes a list of days as strings. """ element_attrs = {} def __init__(self, days): self.days = days def publish(self, handler): if self.days: handler.startElement("skipDays", self.element_attrs) for day in self.days: _element(handler, "day", day) handler.endElement("skipDays") class RSS2(WriteXmlMixin): """The main RSS class. Stores the channel attributes, with the "category" elements under ".categories" and the RSS items under ".items". """ rss_attrs = {"version": "2.0"} element_attrs = {} def __init__(self, title, link, description, language = None, copyright = None, managingEditor = None, webMaster = None, pubDate = None, # a datetime, *in* *GMT* lastBuildDate = None, # a datetime categories = None, # list of strings or Category generator = _generator_name, docs = "http://blogs.law.harvard.edu/tech/rss", cloud = None, # a Cloud ttl = None, # integer number of minutes image = None, # an Image rating = None, # a string; I don't know how it's used textInput = None, # a TextInput skipHours = None, # a SkipHours with a list of integers skipDays = None, # a SkipDays with a list of strings items = None, # list of RSSItems ): self.title = title self.link = link self.description = description self.language = language self.copyright = copyright self.managingEditor = managingEditor self.webMaster = webMaster self.pubDate = pubDate self.lastBuildDate = lastBuildDate if categories is None: categories = [] self.categories = categories self.generator = generator self.docs = docs self.cloud = cloud self.ttl = ttl self.image = image self.rating = rating self.textInput = textInput self.skipHours = skipHours self.skipDays = skipDays if items is None: items = [] self.items = items def publish(self, handler): handler.startElement("rss", self.rss_attrs) handler.startElement("channel", self.element_attrs) _element(handler, "title", self.title) _element(handler, "link", self.link) _element(handler, "description", self.description) self.publish_extensions(handler) _opt_element(handler, "language", self.language) _opt_element(handler, "copyright", self.copyright) _opt_element(handler, "managingEditor", self.managingEditor) _opt_element(handler, "webMaster", self.webMaster) pubDate = self.pubDate if isinstance(pubDate, (datetime.datetime, datetime.date)): pubDate = DateElement("pubDate", pubDate) _opt_element(handler, "pubDate", pubDate) lastBuildDate = self.lastBuildDate if isinstance(lastBuildDate, (datetime.datetime, datetime.date)): lastBuildDate = DateElement("lastBuildDate", lastBuildDate) _opt_element(handler, "lastBuildDate", lastBuildDate) for category in self.categories: if isinstance(category, basestring): category = Category(category) category.publish(handler) _opt_element(handler, "generator", self.generator) _opt_element(handler, "docs", self.docs) if self.cloud is not None: self.cloud.publish(handler) ttl = self.ttl if isinstance(self.ttl, int): ttl = IntElement("ttl", ttl) _opt_element(handler, "tt", ttl) if self.image is not None: self.image.publish(handler) _opt_element(handler, "rating", self.rating) if self.textInput is not None: self.textInput.publish(handler) if self.skipHours is not None: self.skipHours.publish(handler) if self.skipDays is not None: self.skipDays.publish(handler) for item in self.items: item.publish(handler) handler.endElement("channel") handler.endElement("rss") def publish_extensions(self, handler): # Derived classes can hook into this to insert # output after the three required fields. pass class RSSItem(WriteXmlMixin): """Publish an RSS Item""" element_attrs = {} def __init__(self, title = None, # string link = None, # url as string description = None, # string author = None, # email address as string categories = None, # list of string or Category comments = None, # url as string enclosure = None, # an Enclosure guid = None, # a unique string pubDate = None, # a datetime source = None, # a Source ): if title is None and description is None: raise TypeError( "must define at least one of 'title' or 'description'") self.title = title self.link = link self.description = description self.author = author if categories is None: categories = [] self.categories = categories self.comments = comments self.enclosure = enclosure self.guid = guid self.pubDate = pubDate self.source = source # It sure does get tedious typing these names three times... def publish(self, handler): handler.startElement("item", self.element_attrs) _opt_element(handler, "title", self.title) _opt_element(handler, "link", self.link) self.publish_extensions(handler) _opt_element(handler, "description", self.description) _opt_element(handler, "author", self.author) for category in self.categories: if isinstance(category, basestring): category = Category(category) category.publish(handler) _opt_element(handler, "comments", self.comments) if self.enclosure is not None: self.enclosure.publish(handler) _opt_element(handler, "guid", self.guid) pubDate = self.pubDate if isinstance(pubDate, (datetime.datetime, datetime.date)): pubDate = DateElement("pubDate", pubDate) _opt_element(handler, "pubDate", pubDate) if self.source is not None: self.source.publish(handler) handler.endElement("item") def publish_extensions(self, handler): # Derived classes can hook into this to insert # output after the title and link elements pass weblog-1.0/weblog/__init__.py0000644000000000000000000000142011060362141016352 0ustar00usergroup00000000000000from load import load_configuration, load_post_list from post import Post, PostError from _jinja_environment import jinja_environment from html_full_url import html_full_url from publish import command_publish from date import command_date import listing __all__ = ('Post', 'PostError', 'listing', 'jinja_environment', 'load_configuration', 'load_post_list', 'html_full_url', 'command_publish', 'command_date', 'command_check_url') def main(): import doctest import utils import post import listing import html_full_url import date doctest.testmod(utils) doctest.testmod(post) doctest.testmod(listing) doctest.testmod(html_full_url) doctest.testmod(date) doctest.testmod() if __name__ == '__main__': main() weblog-1.0/weblog/_jinja_environment.py0000644000000000000000000000620511060362141020477 0ustar00usergroup00000000000000import os import sys import datetime from utils import format_date try: from jinja2 import Environment, FileSystemLoader, ChoiceLoader from jinja2 import environmentfilter, contextfilter, Markup @contextfilter def renderstring(context, value): ''' Render the passed string. It is similar to the tag rendertemplate, except it uses the passed string as the template. Example: The template 'Hello {{ string_template|renderstring }}!'; Called with the following context: dict(string_template='{{ foo }} world', foo='crazy') Renders to: 'Hello crazy world!' ''' if value: env = context.environment result = env.from_string(value).render(context.get_all()) if env.autoescape: result = Markup(result) return result else: return '' def format_date_(value): return format_date(value) except ImportError: try: from jinja import Environment, FileSystemLoader, ChoiceLoader def renderstring(): def wrapped(env, context, value): ''' Render the passed string. It is similar to the tag rendertemplate, except it uses the passed string as the template. Example: The template 'Hello {{ string_template|renderstring }}!'; Called with the following context: dict(string_template='{{ foo }} world', foo='crazy') Renders to: 'Hello crazy world!' ''' if value: return env.from_string(value).render(context.to_dict()) else: return '' return wrapped def format_date_(): def wrapped(env, context, value): ''' Format the passed. ''' return format_date(value) return wrapped except ImportError: exit('Please install Jinja or Jinja 2 (http://jinja.pocoo.org/)' ' to use Weblog') def jinja_environment(source_dir): """ Build the Jinja environment. Setup all template loaders. """ TEMPLATE_DIR = 'templates' fs_loader = FileSystemLoader(os.path.join(source_dir, TEMPLATE_DIR)) fs_app_loader = FileSystemLoader(os.path.join(sys.path[0], 'weblog', TEMPLATE_DIR)) # if setuptools is present use the loader else fake it. try: import pkg_resources from jinja import PackageLoader except ImportError: pkg_loader = FileSystemLoader(os.path.join(os.path.dirname(__file__), TEMPLATE_DIR)) else: pkg_loader = PackageLoader('weblog', TEMPLATE_DIR) choice_loader = ChoiceLoader([fs_loader, fs_app_loader, pkg_loader]) env = Environment(loader=choice_loader, trim_blocks=True) env.filters['renderstring'] = renderstring env.filters['format_date'] = format_date_ return env weblog-1.0/weblog/date.py0000644000000000000000000000362711060362141015543 0ustar00usergroup00000000000000import sys import logging import datetime import email from utils import format_date from post import Post def command_date(args, options): ''' Execute the 'date' command, which set the date to the specified filename. The command need at least one parameter. The remaining parameters are the date to be set in the file. >>> command_date(None, None) # doctest: +ELLIPSIS Traceback (most recent call last): ... SystemExit: No file specified: ... >>> command_date(['/dev/null', '2008-1000-10'], None) Traceback (most recent call last): ... SystemExit: Unable to parse date '2008-1000-10' (Use YYYY-MM-DD [[HH:MM]:SS] format) ''' if not args: raise SystemExit('No file specified:\n' '%s date filename [date]' % sys.argv[0]) filename = args.pop(0) if args: if len(args) == 1 and args[0] == 'today': date = datetime.date.today() elif len(args) == 1 and args[0] == 'next_day': date = datetime.date.today() + datetime.timedelta(days=1) elif len(args) == 1 and args[0] == 'tomorrow': date = datetime.datetime.now() + datetime.timedelta(days=1) elif len(args) == 1 and args[0] == 'now': date = datetime.datetime.now() else: try: date = Post.parse_date(' '.join(args)) except ValueError, error: raise SystemExit(error) else: date = datetime.datetime.now() logging.info('Setting date to %s in file %s', format_date(date), filename) try: post_file = email.message_from_file(file(filename)) if 'date' in post_file: post_file.replace_header('date', format_date(date)) else: post_file.add_header('date', format_date(date)) file(filename, 'w').write(post_file.as_string()) except IOError, error: raise SystemExit(error) weblog-1.0/weblog/html_full_url.py0000644000000000000000000001274211060362141017474 0ustar00usergroup00000000000000import re # Ignore http:// ftp:// mailto: javascript: ... _scheme_regex = re.compile(r'\w+:') def internal_url(url): ''' Returns True if ``url`` refers to an external resource. >>> internal_url('http://www.google.ca/') False >>> internal_url('mailto:me@example.com') False >>> internal_url('javascript:return false;') False >>> internal_url('/pic.jpg') True >>> internal_url('') True ''' if _scheme_regex.match(url): return False else: return True from HTMLParser import HTMLParser from cStringIO import StringIO class FullUrlHtmlParser(HTMLParser): ''' Parse an HTML document and transform relative URI to absolute URI. Prepending ``base_url`` to them. >>> p = FullUrlHtmlParser('http://www.example.com') >>> p.feed('') >>> print p.buffer.getvalue() Non-external resource are ignored:: >>> p = FullUrlHtmlParser('http://www.example.com') >>> p.feed('') >>> print p.buffer.getvalue() A more complex example:: >>> p.reset() >>> p.feed(r""" ... ... foo ... ... some random text. ... bar ... »~ ... ... ... More ..........""") >>> print p.buffer.getvalue() #doctest: +NORMALIZE_WHITESPACE foo some random text. bar »~ More .......... ''' def __init__(self, base_url): HTMLParser.__init__(self) self.buffer = StringIO() self.base_url = base_url.rstrip('/') def reset(self): HTMLParser.reset(self) if hasattr(self, 'buffer'): del self.buffer self.buffer = StringIO() @staticmethod def html_attrs(attrs): ''' >>> FullUrlHtmlParser.html_attrs(dict(src='pic.jpg', alt='pic')) "src='pic.jpg' alt='pic'" >>> FullUrlHtmlParser.html_attrs(dict()) '' ''' return ' '.join('%s=\'%s\'' % (k, v) for k, v in attrs.iteritems()) def make_full_url(self, attr, attrs): ''' Change ``attrs[attr]`` from a relative URI to an absolute URI. >>> p = FullUrlHtmlParser('http://www.example.com') >>> d = dict(src='pic.jpg') >>> p.make_full_url('src', d) >>> d {'src': 'http://www.example.com/pic.jpg'} >>> d = dict(src='http://www.example2.com') >>> p.make_full_url('src', d) >>> d {'src': 'http://www.example2.com'} >>> d = dict() >>> p.make_full_url('src', d) >>> d {} ''' if attr in attrs and internal_url(attrs[attr]): attrs[attr] = '/'.join((self.base_url, attrs[attr].lstrip('/'))) def check_and_rewrite_tag(self, tag, attrs, endtag=''): if attrs: attrs = dict(attrs) if tag == 'a': self.make_full_url('href', attrs) elif tag == 'img': self.make_full_url('src', attrs) elif tag == 'object': self.make_full_url('data', attrs) self.make_full_url('codebase', attrs) elif tag == 'script': self.make_full_url('src', attrs) self.buffer.write('<%s %s%s>' % (tag, self.html_attrs(attrs), endtag)) else: self.buffer.write('<%s%s>' % (tag, endtag)) def handle_starttag(self, tag, attrs): self.check_and_rewrite_tag(tag, attrs) def handle_startendtag(self, tag, attrs): self.check_and_rewrite_tag(tag, attrs, endtag='/') def handle_endtag(self, tag): self.buffer.write('' % tag) def handle_data(self, data): self.buffer.write(data) def handle_charref(self, name): self.buffer.write('&#%s;' % name) def handle_entityref(self, name): self.buffer.write('&%s;' % name) def handle_comment(self, comment): self.buffer.write('' % comment) def handle_decl(self, decl): self.buffer.write('' % decl) def handle_pi(self, pi): self.buffer.write('' % pi) def html_full_url(base_url, text): ''' Appends ``base_url`` to relative uri's in the HTML document ``text``. Example with ``base_url=http://example.com``:: '' becomes '' '' becomes '' but ' is not changed since it is an *absolute* URI. >>> html_full_url('http://example.com', '') "" >>> html_full_url('http://example.com', '') "" ''' p = FullUrlHtmlParser(base_url) p.feed(text) return p.buffer.getvalue() if __name__ == '__main__': import doctest doctest.testmod() weblog-1.0/weblog/listing.py0000644000000000000000000001122411060362141016267 0ustar00usergroup00000000000000from os.path import join class Page(object): ''' Page contains a `string key` named title used to compare against other `Page`s and strings. It is used for the pagination. The item can be whatever you want. >>> page_list = sorted([Page('2', 2), Page('3', None), Page('1', '1')]) >>> page_list [, , ] >>> for i in page_list: ... print i.title, i.item 1 1 2 2 3 None >>> '2' in page_list True >>> '5' in page_list False >>> page_list.index('2') 1 ''' def __init__(self, title, item): ''' >>> Page('foo', 'bar') ''' super(Page, self).__init__() self.title = title self.item = item def filename(self): return self.title + '.html' def url(self): ''' >>> Page('foo', 'bar').url() 'foo.html' ''' return self.filename() def __repr__(self): return '<%s(%r, %r)>' % \ (self.__class__.__name__, self.title, self.item) def __cmp__(self, other): ''' >>> Page('1', '') > '2' False >>> Page('foo', 'string') == 'foo' True >>> Page('2', '') > Page('4', '') False >>> Page('bar', '') == Page('bar', '') True >>> Page('1', '') == None # base object comparison False ''' if isinstance(other, Page): return cmp(self.title, other.title) elif isinstance(other, self.title.__class__): return cmp(self.title, other) else: return cmp(id(self), id(other)) class PageIndex(Page): ''' Special case to have a page that returns 'index.html' as filename. Used for the first page of the listing. ''' def filename(self): return 'index.html' def slice_list(full_list, limit): ''' Return an iterable containing the given list sliced in sub-lists of size `limit`. >>> list(slice_list(range(10), 3)) [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]] >>> list(slice_list([], 10)) [] >>> list(slice_list([1, 2, 3], 5)) [[1, 2, 3]] >>> list(slice_list([1, 2, 3], 3)) [[1, 2, 3]] ''' it = iter(full_list) result = list() try: while True: for c in xrange(limit): result.append(it.next()) yield result result = list() except StopIteration: if result: yield result raise StopIteration def slice_list_groupby(full_list, func): ''' >>> slice_list_groupby(range(10), lambda x: 'even' if x % 2 == 0 else 'odd') {'even': [0, 2, 4, 6, 8], 'odd': [1, 3, 5, 7, 9]} >>> slice_list_groupby([1, 2, 3], lambda x: x) {1: [1], 2: [2], 3: [3]} >>> slice_list_groupby([dict(key='1'), dict(key='2'), dict(key='3')], ... lambda x: x['key']) {'1': [{'key': '1'}], '3': [{'key': '3'}], '2': [{'key': '2'}]} ''' d = dict() for i in full_list: key = func(i) if key in d: d[key].append(i) else: d[key] = [i] return d def generate_list(output_dir, template, page_list, template_params): for page in page_list: filename = join(output_dir, page.filename()) output_file = file(filename, 'w') output_file.write(template.render(post_list=page.item, page=page, pages=page_list, **template_params)) output_file.close() def generate_index_listing(limit, output_dir, template, post_list, template_params): ''' Generate a listing containing at most `limit` post per page. ''' # list() needed since pages is subscripted later pages = list(slice_list(post_list, limit)) if pages: pages = [PageIndex('0', pages[0])] + \ [Page(str(k + 1), v) for k, v in enumerate(pages[1:])] else: # If there is no page generates an 'empty' page pages = [PageIndex('0', list())] generate_list(output_dir, template, pages, template_params) def generate_monthly_listing(output_dir, template, post_list, template_params): pages = slice_list_groupby(post_list, lambda x: str(x.date.year) + '-' + str(x.date.month)) pages = list(Page(str(k), pages[k]) for k in (sorted(pages.keys()))) generate_list(output_dir, template, pages, template_params) if __name__ == '__main__': import doctest doctest.testmod() weblog-1.0/weblog/load.py0000644000000000000000000001275711060362141015551 0ustar00usergroup00000000000000import os import logging from utils import encode, load_if_filename from ConfigParser import SafeConfigParser from post import Post def load_configuration(configuration_file, source_dir=None): ''' Read the file ``config_file`` and sanitise it. Returns a dictionnary containing the parameters from the [weblog] section. >>> from StringIO import StringIO >>> config_file = StringIO("""[weblog] ... title = Test title ... url = http://example.com ... description = Example blog""") >>> load_configuration(config_file) == {'url': 'http://example.com/', ... 'rss_limit': 10, 'description': 'Example blog', ... 'post_per_page': 10, 'title': 'Test title'} True The configuration file must have a `weblog` section containing at lease: - `title` - `url` - `description` >>> # Configuration without title >>> config_file = StringIO("""[weblog] ... url = http://example.com ... description = example""") >>> load_configuration(config_file) Traceback (most recent call last): ... KeyError: "Unable to find 'title' in configuration file 'unknown filename'" >>> # Configuration without url >>> config_file = StringIO("""[weblog] ... title = Example blog ... description = example""") >>> load_configuration(config_file) Traceback (most recent call last): ... KeyError: "Unable to find 'url' in configuration file 'unknown filename'" >>> # Configuration without description >>> config_file = StringIO("""[weblog] ... title = Example blog ... url = http://example.com""") >>> load_configuration(config_file) Traceback (most recent call last): ... KeyError: "Unable to find 'description' in configuration file 'unknown \ filename'" Also some field must be integer: - rss_limit - post_per_page >>> config_file = StringIO("""[weblog] ... title = Test title ... url = http://example.com ... description = Example blog ... rss_limit = not_a_number""") >>> load_configuration(config_file) Traceback (most recent call last): ... ValueError: Error in configuration file 'unknown filename' 'rss_limit': \ invalid literal for int() with base 10: 'not_a_number' >>> config_file = StringIO("""[weblog] ... title = Test title ... url = http://example.com ... description = Example blog ... post_per_page = not_a_number""") >>> load_configuration(config_file) Traceback (most recent call last): ... ValueError: Error in configuration file 'unknown filename' \ 'post_per_page': invalid literal for int() with base 10: 'not_a_number' ''' config = SafeConfigParser() if isinstance(configuration_file, basestring): try: f = file(configuration_file) except IOError: # The file was not found try to load it from the source directory if # it is just a filename. if os.path.basename(configuration_file) == configuration_file: f = file(os.path.join(source_dir, configuration_file)) else: raise else: f = configuration_file configuration_file = 'unknown filename' config.readfp(f) config_dict = dict(config.items('weblog')) try: config_dict['title'] = encode(config_dict['title'], config_dict.get('encoding', 'ascii')) config_dict['description'] = encode(config_dict['description'], config_dict.get('encoding', 'ascii')) if not config_dict['url'].endswith('/'): config_dict['url'] += '/' def _load_if_filename(key): if key in config_dict: config_dict[key] = load_if_filename(source_dir, config_dict[key]) _load_if_filename('html_head') _load_if_filename('html_header') _load_if_filename('html_footer') def config_set_int(key, default): try: config_dict[key] = int(config_dict.get(key, default)) except ValueError, e: raise ValueError('Error in configuration file \'%s\' \'%s\': %s' % (configuration_file, key, e)) config_set_int('post_per_page', 10) config_set_int('rss_limit', 10) except KeyError, e: raise KeyError('Unable to find %s in configuration file \'%s\'' % (e, configuration_file)) else: return config_dict def load_post_list(path): ''' List and load all the files ending with '.html' in the passed directory. Returns a list containing ``Post`` objects created using the loaded files. ''' post_list = set() for filename in os.listdir(path): if filename.endswith('.html'): logging.debug('Loading \'%s\'', filename) p = Post(os.path.join(path, filename)) if p in post_list: logging.debug('%r is duplicated', p) for duplicated_post in post_list: if duplicated_post == p: break raise IOError('"%s", there is already a post ' 'with this title and date ("%s")' % \ (filename, duplicated_post.get_filename())) else: post_list.add(p) else: logging.debug('Ignoring \'%s\'', filename) return post_list weblog-1.0/weblog/post.py0000644000000000000000000002124711060362141015611 0ustar00usergroup00000000000000import email import codecs import logging from os import stat from datetime import datetime, date from urllib import quote from cgi import escape from utils import encode, escape_and_encode class PostError(Exception): ''' Error in post file ''' def __init__(self, message, filename, line=None): super(PostError, self).__init__(self, message) self.message = message self.filename = filename self.line = line def __repr__(self): return '%s(%r, %r, %r)' % (self.__class__.__name__, self.exception, self.filename, self.line) def __str__(self): if self.line is None: return 'Error in post file %s: %s' % (self.filename, self.message) else: return 'Error in post file %s line %d: %s' % (self.filename, self.line, self.message) class Post(object): DEFAULT_ENCODING = 'ascii' DEFAULT_AUTHOR = 'unknown author' def __init__(self, f): ''' >>> file_content = """title: test ... date: 2008-1-1 ... author: test author ... encoding: utf-8 ... ... test.""" >>> from StringIO import StringIO >>> p = Post(StringIO(file_content)) >>> p >>> p.title == 'test' True >>> import datetime >>> p.date == datetime.date(2008, 1, 1) True >>> p.content == 'test.' True >>> p.encoding == 'utf-8' True >>> p.author == 'test author' True >>> Post(StringIO('title: no payload\\ndate: 2008-1-1')) >>> Post(StringIO('title: no date')) Traceback (most recent call last): ... PostError: Error in post file : No date defined >>> Post(StringIO("""title: bad encoding ... date: 2008-1-1 ... encoding: bad-encoding""")) Traceback (most recent call last): ... PostError: Error in post file : unknown encoding: \ bad-encoding >>> Post(StringIO("""title: bad date ... date: 200008-101-10""")) Traceback (most recent call last): ... PostError: Error in post file : Unable to parse \ date '200008-101-10' (Use YYYY-MM-DD [[HH:MM]:SS] format) ''' if isinstance(f, (str, basestring)): self._filename = f input_file = open(f) else: self._filename = None input_file = f post_file = email.message_from_file(input_file) self.__dict__.update((key.lower(), value) for (key, value) in post_file.items()) if not hasattr(self, 'encoding'): self.encoding = self.DEFAULT_ENCODING if self.encoding.lower() != 'raw': try: codecs.lookup(self.encoding) except LookupError, e: raise PostError(e.message, self.get_filename()) if not hasattr(self, 'author'): self.author = self.DEFAULT_AUTHOR # Handle the date. If no date was specified use the file's modification # time. if not hasattr(self, 'date'): # Get the date from file's mtime and issue a warning if not self._filename: raise PostError('No date defined', self.get_filename()) else: self.date = datetime.\ fromtimestamp(stat(self._filename).st_mtime) logging.warning('No date defined in \'%s\', using the ' \ 'file\'s last modification time instead.' % \ self._filename) else: try: self.date = self.parse_date(self.date) except ValueError, e: raise PostError(e.message, self.get_filename()) try: self.ascii_title = self.title.decode('ascii' if self.encoding == 'raw' else self.encoding).\ encode('ascii', 'replace') self.title = escape_and_encode(self.title, self.encoding) except UnicodeDecodeError, e: raise PostError('Bad encoding in title', self.get_filename()) try: self.author = escape_and_encode(self.author, self.encoding) except UnicodeDecodeError, e: raise PostError('Bad encoding in author', self.get_filename()) try: self.content = encode(post_file.get_payload(), self.encoding) except UnicodeDecodeError, e: # find error line number for line_number, line in enumerate(post_file.as_string().\ splitlines()): try: line.decode('ascii' if self.encoding == 'raw' else self.encoding) except UnicodeDecodeError, e: break # line_number starts at 0, real line number == line_number + 1 raise PostError('Bad encoding in content line %d, %s' % \ (line_number + 1, e), self.get_filename()) # Transform the 'files' field into a list of string if hasattr(self, 'files'): self.files = self.files.split() else: self.files = list() # FIXME prefix & suffix param or members of the class ? def url(self, prefix=''): ''' >>> file_content = """title: test ... date: 2008-1-1 ... ... test""" >>> from StringIO import StringIO >>> Post(StringIO(file_content)).url() '2008/1/1/test.html' >>> Post(StringIO(file_content)).url('prefix/') 'prefix/2008/1/1/test.html' >>> file_content = """title: Weird @!% filename ... date: 2008-1-1 ... ... test""" >>> Post(StringIO(file_content)).url() '2008/1/1/Weird%20%40%21%25%20filename.html' ''' return '%s%d/%d/%d/%s.html' % \ (prefix, self.date.year, self.date.month, self.date.day, quote(self.ascii_title)) _DATE_FORMAT_LIST = ('%Y-%m-%d', '%y-%m-%d') _DATETIME_FORMAT_LIST = \ tuple('%s %%H:%%M' % f for f in _DATE_FORMAT_LIST) + \ tuple('%s %%H:%%M:%%S' % f for f in _DATE_FORMAT_LIST) @staticmethod def parse_date(date_): """ >>> Post.parse_date('2006-1-1') datetime.date(2006, 1, 1) >>> Post.parse_date('2007-12-31') datetime.date(2007, 12, 31) >>> Post.parse_date('2008-4-05 12:35') datetime.datetime(2008, 4, 5, 12, 35) >>> Post.parse_date('10000-1-1') Traceback (most recent call last): ... ValueError: Unable to parse date '10000-1-1' (Use YYYY-MM-DD [[HH:MM]:SS] format) >>> Post.parse_date(2007) Traceback (most recent call last): ... TypeError: strptime() argument 1 must be string, not int """ for date_format in Post._DATE_FORMAT_LIST: try: return datetime.strptime(date_, date_format).date() except ValueError: continue for date_format in Post._DATETIME_FORMAT_LIST: try: return datetime.strptime(date_, date_format) except ValueError: continue raise ValueError('Unable to parse date %r\n' '(Use YYYY-MM-DD [[HH:MM]:SS] format)' % (date_)) def get_filename(self): if not self._filename: return '' else: return self._filename def __cmp__(self, other): ''' >>> file1 = """title: 1 ... date: 2008-1-1 ... ... test""" >>> file2 = """title: 2 ... date: 2007-12-31""" >>> from StringIO import StringIO >>> Post(StringIO(file1)) > Post(StringIO(file2)) True >>> Post(StringIO(file1)) == Post(StringIO(file2)) False >>> Post(StringIO(file1)) == Post(StringIO(file1)) True >>> l = [Post(StringIO(file2)), Post(StringIO(file1))] >>> l.index(Post(StringIO(file1))) 1 ''' return cmp(str(self.date) + str(self.title), str(other.date) + str(self.title)) def __hash__(self): return hash(str(self.date) + self.title) def __repr__(self): return '<%s(%r, %r)>' % (self.__class__.__name__, self.title, self.date) if __name__ == '__main__': import doctest doctest.testmod() weblog-1.0/weblog/publish.py0000644000000000000000000001230511060362141016265 0ustar00usergroup00000000000000import os import datetime import logging from shutil import copy from _jinja_environment import jinja_environment from PyRSS2Gen import RSS2, RSSItem from html_full_url import html_full_url from post import Post, PostError from load import load_post_list, load_configuration from listing import generate_index_listing def generate_post_html(post_list, output_dir, post_tmpl, params): for post in post_list: logging.debug('Generating HTML file for %r', post) dir = os.path.join(output_dir, str(post.date.year), str(post.date.month), str(post.date.day)) if not os.path.exists(dir): logging.debug('Creating \'%s\'', dir) os.makedirs(dir) elif not os.path.isdir(dir): raise IOError('\'%s\' already exists and is not a directory' % dir) filename = os.path.join(dir, post.ascii_title + '.html') output = file(filename, 'w') top_dir = '../../../' output.write(post_tmpl.render(title=post.title, date=post.date, author=post.author, content=html_full_url(top_dir, post.content), top_dir=top_dir, **dict(((k, v) for k, v in params.iteritems() if k != 'title')))) def generate_rss(post_list, filename, params): def make_rss_item(post): return RSSItem(title=post.title, link=post.url(prefix=params['url']), description=html_full_url(params['url'], post.content), guid=post.url(prefix=params['url']), pubDate=post.date) rss = RSS2(title = params['title'], link = params['url'], description = params['description'], lastBuildDate = datetime.datetime.now(), items=(make_rss_item(post) for post in post_list)) rss.write_xml(open(filename, "w")) def command_publish(args, options): source_dir = options.source_dir output_dir = options.output_dir try: config = load_configuration(options.configuration_file, source_dir or '.') except (KeyError, ValueError, IOError), error: logging.error('Error while loading configuration file \'%s\'' % options.configuration_file) raise SystemExit(error) source_dir = source_dir or config.get('source_dir', '.') output_dir = output_dir or config.get('output_dir', 'output') # add the default author & encoding constant to the post class if 'encoding' in config: Post.DEFAULT_ENCODING = config['encoding'] author = config.get('author', None) if author: Post.DEFAULT_AUTHOR = author if not os.path.exists(output_dir): os.mkdir(output_dir) env = jinja_environment(source_dir) try: post_list = list(reversed(sorted(load_post_list(source_dir)))) except (IOError, PostError), e: logging.error('Error while loading post files.') exit(e) def generate_all(): params = dict(title=config['title'], description=config['description'], url=config['url'], html_head=config.get('html_head'), html_header=config.get('html_header'), html_footer=config.get('html_footer')) # generate the main index page logging.debug('Generating HTML listings') index_template = env.get_template('index.html.tmpl') generate_index_listing(config['post_per_page'], output_dir, index_template, post_list, params) logging.debug('Generating HTML posts files') post_tmpl = env.get_template('post.html.tmpl') generate_post_html(post_list, output_dir, post_tmpl, params) # Copy all 'attached' files for post in post_list: for filename in post.files: destination = os.path.join(output_dir, filename) # Create the destination directory if it does not exist destination_dir = os.path.dirname(destination) # isdir returns False if the passed file does not exist if not os.path.isdir(destination_dir): os.makedirs(destination_dir) copy(os.path.join(source_dir, filename), destination) generate_rss(post_list[:config['rss_limit']], os.path.join(output_dir, 'rss.xml'), params) for f in config.get('extra_files', '').split(): copy(os.path.join(source_dir, f), output_dir) if options.debug: generate_all() else: try: generate_all() except IOError, e: logging.error('Error while generating files ...') exit(e) else: logging.info('Successfully generated weblog.') weblog-1.0/weblog/templates/base.html.tmpl0000644000000000000000000000152711060362141021022 0ustar00usergroup00000000000000 {% block title %}{{ title }}{% endblock %} {% block rss %} {% endblock %} {{ html_head|renderstring }} {% block extrahead %} {% endblock %} {% block header %} {{ html_header|renderstring }} {% endblock %}
{% block content %}{% endblock %}
{% block footer %} {% if html_footer %} {{ html_footer|renderstring }} {% else %}

Published using Weblog

{% endif %} {% endblock %} {# vim:set ft=htmljinja: #} weblog-1.0/weblog/templates/index.html.tmpl0000644000000000000000000000210511060362141021210 0ustar00usergroup00000000000000{% extends 'base.html.tmpl' %} {% block content %}

{{ title|e }}

{% if description %}

{{ description|e }}

{% endif %} {% for post in post_list %}

{{ post.title }}

{{ post.date|format_date }}, by {{ post.author|urlize }}

{{ post.content }}
{% endfor %} {% if pages|length > 1 %}
{% if pages.index(page) > 0 %} « prev {% endif %} {% for p in pages %} {% if p == page %} {{ p.title }} {% else %} {{ p.title }} {% endif %} {% endfor %} {% if pages.index(page) + 1 < pages|length %} next » {% endif %}
{% endif %} {% endblock %} {# vim:set ft=htmljinja: #} weblog-1.0/weblog/templates/post.html.tmpl0000644000000000000000000000064711060362141021077 0ustar00usergroup00000000000000{% extends 'base.html.tmpl' %} {% block content %}

{{ title }}

{{ date|format_date }}, by {{ author|urlize }}

{{ content }}
{% endblock %} {# vim:set ft=htmljinja: #} weblog-1.0/weblog/utils.py0000644000000000000000000000572711060362141015771 0ustar00usergroup00000000000000import os import logging import datetime from cgi import escape def encode(text, encoding, errors='xmlcharrefreplace'): ''' >>> encode('foo & bar', 'ascii') 'foo & bar' >>> encode('\\xdcTF-8 ?', 'raw') Traceback (most recent call last): ... UnicodeDecodeError: 'ascii' codec can't decode byte 0xdc in position 0: \ ordinal not in range(128) >>> encode('\\xdcTF-8 ?', 'latin-1') 'ÜTF-8 ?' >>> encode(u'\\xdcTF-8 ?', 'UTF-8') 'ÜTF-8 ?' ''' if encoding.lower() == 'raw': return text.encode('ascii') elif isinstance(text, unicode): return text.encode('ascii', errors) else: return text.decode(encoding).encode('ascii', errors) def escape_and_encode(text, encoding, errors='xmlcharrefreplace'): ''' Escapes '&', '<' and '>' to HTML-safe sequences and encode the text to the specified encoding. >>> escape_and_encode('<>&', 'ascii') '<>&' >>> escape_and_encode(u'\\xdcTF-8', 'utf-8') 'ÜTF-8' >>> escape_and_encode('\\xdcTF-8', 'latin-1') 'ÜTF-8' >>> escape_and_encode('\\xdcTF-8 &<>', 'raw') Traceback (most recent call last): ... UnicodeDecodeError: 'ascii' codec can't decode byte 0xdc in position 0: \ ordinal not in range(128) >>> escape_and_encode('UTF-8 &<>', 'raw') 'UTF-8 &<>' ''' if encoding.lower() == 'raw': return text.encode('ascii') else: return encode(escape(text), encoding, errors) def load_if_filename(source_dir, f): ''' If ``f`` is a filename. Read it and returns the content. Else return ``f``. If ``bool(f)`` is false returns ``None``. # Assumes that there is no file named 'This is not a file' in the current # directory ;-) >>> load_if_filename('.', 'This is not a file') 'This is not a file' >>> load_if_filename('.', '') >>> load_if_filename('.', list()) >>> load_if_filename('.', None) ''' if not f: return full = os.path.join(source_dir, f) if os.path.exists(full): return file(full).read() else: return f def format_date(date): ''' Return a string representing a ``date`` or a ``datetime``. >>> format_date(datetime.datetime(2008, 1, 1, 20, 40, 23, 345)) '2008-01-01 20:40:23' >>> format_date(datetime.datetime(2008, 1, 1)) '2008-01-01 00:00:00' >>> format_date(datetime.date(2008, 1, 1)) '2008-01-01' >>> format_date(datetime.time()) Traceback (most recent call last): ... TypeError: expected date or datetime, got time instead ''' if isinstance(date, datetime.datetime): return date.strftime('%Y-%m-%d %H:%M:%S') elif isinstance(date, datetime.date): return str(date) else: raise TypeError('expected %s or %s, got %s instead' % (datetime.date.__name__, datetime.datetime.__name__, date.__class__.__name__)) if __name__ == '__main__': import doctest doctest.testmod() weblog-1.0/weblog_run.py0000755000000000000000000000037511060362141015512 0ustar00usergroup00000000000000#!/usr/bin/env python import imp from os import path filename = path.join(path.dirname(__file__), 'bin', 'weblog') module = imp.load_module('weblog_executable', file(filename), filename, ('', 'r', imp.PY_SOURCE)) module.main()