./weblog+jinja-1.0004075500017500000000000000000001106036251300130355ustar00henrywheel./weblog+jinja-1.0/bin004075500017500000000000000000001106036251300136055ustar00henrywheel./weblog+jinja-1.0/bin/weblog010075500017500000000000000052471106036251300150760ustar00henrywheel#!/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+jinja-1.0/doc004075500017500000000000000000001106036251300136025ustar00henrywheel./weblog+jinja-1.0/doc/style.rst010064400017500000000000000105161106036251300155530ustar00henrywheelCustomizing 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+jinja-1.0/doc/weblog.rst010064400017500000000000000252021106036251300156700ustar00henrywheelWeblog 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+jinja-1.0/COPYING010064400017500000000000000046701106036251300141530ustar00henrywheelCopyright (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+jinja-1.0/README010064400017500000000000000007411106036251300137730ustar00henrywheelWeblog 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+jinja-1.0/TODO010064400017500000000000000001771106036251300136060ustar00henrywheel--- v1.0 --- Stable release: - more unit tests - more docs! 1.1 --- Add Markdown support Add Atom 1.0 Remove RSS 2.0 (?) ./weblog+jinja-1.0/examples004075500017500000000000000000001106036251300146535ustar00henrywheel./weblog+jinja-1.0/examples/second_post.html010064400017500000000000000002071106036251300201340ustar00henrywheeltitle: Second post date: 2007-08-26 Second test post!

The author lastname is Prêcheur

./weblog+jinja-1.0/examples/enconding.html010064400017500000000000000005511106036251300175620ustar00henrywheeltitle: 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+jinja-1.0/examples/first_post.html010064400017500000000000000000741106036251300200120ustar00henrywheeltitle: First post author: Me date: 2007-08-25 Hello world! ./weblog+jinja-1.0/examples/w3_steely_style.css010064400017500000000000000003601106036251300205760ustar00henrywheelbody { 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+jinja-1.0/examples/utf-8.html010064400017500000000000000001611106036251300165560ustar00henrywheeltitle: Some UTF-8, ç ä é ö ó date: 2008-1-1 encoding: UTF-8 Test post with UTF-8 inside ... ç ä é ö ó ./weblog+jinja-1.0/examples/weblog.ini010064400017500000000000000002701106036251300167060ustar00henrywheel[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+jinja-1.0/examples/weblog_w3_steely_css.ini010064400017500000000000000005161106036251300215570ustar00henrywheel[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+jinja-1.0/test004075500017500000000000000000001106036247400140225ustar00henrywheel./weblog+jinja-1.0/test/empty004075500017500000000000000000001106036251300151525ustar00henrywheel./weblog+jinja-1.0/test/empty/weblog.ini010064400017500000000000000001441106036251300172050ustar00henrywheel[weblog] title=Test blog url=http://blog.test.org description=Test blog author=test ./weblog+jinja-1.0/test/encoding004075500017500000000000000000001106036251300156025ustar00henrywheel./weblog+jinja-1.0/test/encoding/latin-1.html010064400017500000000000000001041106036251300200040ustar00henrywheeltitle: latin post ÖÉÈÄ ... date: 2008-02-04 encoding: latin-1 Öéèä ./weblog+jinja-1.0/test/encoding/utf-8.html010064400017500000000000000000721106036251300175060ustar00henrywheeltitle: UTF-8 post ÖÉÈÄ ... date: 2008-02-03 Öéèä ./weblog+jinja-1.0/test/encoding/weblog.ini010064400017500000000000000001631106036251300176360ustar00henrywheel[weblog] title=Test blog url=http://blog.test.org description=Test blog author=test encoding=utf-8 ./weblog+jinja-1.0/test/full_url004075500017500000000000000000001106036251300156405ustar00henrywheel./weblog+jinja-1.0/test/full_url/utf-8.html010064400017500000000000000002501106036251300175420ustar00henrywheeltitle: UTF-8 post ÖÉÈÄ ... date: 2008-02-03 Öéèä
äyÔÀ Weblog ./weblog+jinja-1.0/test/full_url/weblog.ini010064400017500000000000000001631106036251300176740ustar00henrywheel[weblog] title=Test blog url=http://blog.test.org description=Test blog author=test encoding=utf-8 ./weblog+jinja-1.0/test/simple004075500017500000000000000000001106036251300153055ustar00henrywheel./weblog+jinja-1.0/test/simple/post1.html010064400017500000000000000000451106036251300173140ustar00henrywheeltitle: post1 date: 2007-01-01 post1 ./weblog+jinja-1.0/test/simple/post2.html010064400017500000000000000000441106036251300173140ustar00henrywheeltitle: post2 date: 2007-6-15 post2 ./weblog+jinja-1.0/test/simple/post3.html010064400017500000000000000000451106036251300173160ustar00henrywheeltitle: post3 date: 2007-12-31 post3 ./weblog+jinja-1.0/test/simple/weblog.ini010064400017500000000000000001441106036251300173400ustar00henrywheel[weblog] title=Test blog url=http://blog.test.org description=Test blog author=test ./weblog+jinja-1.0/setup.cfg010064400017500000000000000000471106036251300147330ustar00henrywheel[nosetests] verbosity=3 with-doctest=1 ./weblog+jinja-1.0/setup.py010064400017500000000000000026261106036251300146310ustar00henrywheeltry: 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+jinja-1.0/test.py010064400017500000000000000153761106036251300144560ustar00henrywheelimport 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+jinja-1.0/weblog004075500017500000000000000000001106036251300143145ustar00henrywheel./weblog+jinja-1.0/weblog/_jinja_environment.py010064400017500000000000000062051106036251300206230ustar00henrywheelimport 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+jinja-1.0/weblog/PyRSS2Gen.py010064400017500000000000000343241106036251300164440ustar00henrywheel"""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+jinja-1.0/weblog/__init__.py010064400017500000000000000014201106036251300164760ustar00henrywheelfrom 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+jinja-1.0/weblog/listing.py010064400017500000000000000112241106036251300164130ustar00henrywheelfrom 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+jinja-1.0/weblog/date.py010064400017500000000000000036271106036251300156670ustar00henrywheelimport 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+jinja-1.0/weblog/templates004075500017500000000000000000001106036251300163125ustar00henrywheel./weblog+jinja-1.0/weblog/templates/base.html.tmpl010064400017500000000000000015271106036251300211460ustar00henrywheel {% 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+jinja-1.0/weblog/templates/index.html.tmpl010064400017500000000000000021051106036251300213340ustar00henrywheel{% 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+jinja-1.0/weblog/templates/post.html.tmpl010064400017500000000000000006471106036251300212230ustar00henrywheel{% extends 'base.html.tmpl' %} {% block content %}

{{ title }}

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

{{ content }}
{% endblock %} {# vim:set ft=htmljinja: #} ./weblog+jinja-1.0/weblog/html_full_url.py010064400017500000000000000127421106036251300176200ustar00henrywheelimport 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+jinja-1.0/weblog/load.py010064400017500000000000000127571106036251300156750ustar00henrywheelimport 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+jinja-1.0/weblog/post.py010064400017500000000000000212471106036251300157350ustar00henrywheelimport 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+jinja-1.0/weblog/publish.py010064400017500000000000000123051106036251300164110ustar00henrywheelimport 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+jinja-1.0/weblog/utils.py010064400017500000000000000057271106036251300161150ustar00henrywheelimport 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+jinja-1.0/jinja2004075500017500000000000000000001106036250300142115ustar00henrywheel./weblog+jinja-1.0/jinja2/visitor.py010064400017500000000000000063721106036251300163470ustar00henrywheel# -*- coding: utf-8 -*- """ jinja2.visitor ~~~~~~~~~~~~~~ This module implements a visitor for the nodes. :copyright: Copyright 2008 by Armin Ronacher. :license: BSD. """ from jinja2.nodes import Node class NodeVisitor(object): """Walks the abstract syntax tree and call visitor functions for every node found. The visitor functions may return values which will be forwarded by the `visit` method. Per default the visitor functions for the nodes are ``'visit_'`` + class name of the node. So a `TryFinally` node visit function would be `visit_TryFinally`. This behavior can be changed by overriding the `get_visitor` function. If no visitor function exists for a node (return value `None`) the `generic_visit` visitor is used instead. """ def get_visitor(self, node): """Return the visitor function for this node or `None` if no visitor exists for this node. In that case the generic visit function is used instead. """ method = 'visit_' + node.__class__.__name__ return getattr(self, method, None) def visit(self, node, *args, **kwargs): """Visit a node.""" f = self.get_visitor(node) if f is not None: return f(node, *args, **kwargs) return self.generic_visit(node, *args, **kwargs) def generic_visit(self, node, *args, **kwargs): """Called if no explicit visitor function exists for a node.""" for node in node.iter_child_nodes(): self.visit(node, *args, **kwargs) class NodeTransformer(NodeVisitor): """Walks the abstract syntax tree and allows modifications of nodes. The `NodeTransformer` will walk the AST and use the return value of the visitor functions to replace or remove the old node. If the return value of the visitor function is `None` the node will be removed from the previous location otherwise it's replaced with the return value. The return value may be the original node in which case no replacement takes place. """ def generic_visit(self, node, *args, **kwargs): for field, old_value in node.iter_fields(): if isinstance(old_value, list): new_values = [] for value in old_value: if isinstance(value, Node): value = self.visit(value, *args, **kwargs) if value is None: continue elif not isinstance(value, Node): new_values.extend(value) continue new_values.append(value) old_value[:] = new_values elif isinstance(old_value, Node): new_node = self.visit(old_value, *args, **kwargs) if new_node is None: delattr(node, field) else: setattr(node, field, new_node) return node def visit_list(self, node, *args, **kwargs): """As transformers may return lists in some places this method can be used to enforce a list as return value. """ rv = self.visit(node, *args, **kwargs) if not isinstance(rv, list): rv = [rv] return rv ./weblog+jinja-1.0/jinja2/_speedups.c010064400017500000000000000132611106036251300164240ustar00henrywheel/** * jinja2._speedups * ~~~~~~~~~~~~~~~~ * * This module implements functions for automatic escaping in C for better * performance. Additionally it defines a `tb_set_next` function to patch the * debug traceback. If the speedups module is not compiled a ctypes * implementation of `tb_set_next` and Python implementations of the other * functions are used. * * :copyright: 2008 by Armin Ronacher, Mickaël Guérin. * :license: BSD. */ #include #define ESCAPED_CHARS_TABLE_SIZE 63 #define UNICHR(x) (((PyUnicodeObject*)PyUnicode_DecodeASCII(x, strlen(x), NULL))->str); #if PY_VERSION_HEX < 0x02050000 && !defined(PY_SSIZE_T_MIN) typedef int Py_ssize_t; #define PY_SSIZE_T_MAX INT_MAX #define PY_SSIZE_T_MIN INT_MIN #endif static PyObject* markup; static Py_ssize_t escaped_chars_delta_len[ESCAPED_CHARS_TABLE_SIZE]; static Py_UNICODE *escaped_chars_repl[ESCAPED_CHARS_TABLE_SIZE]; static int init_constants(void) { PyObject *module; /* happing of characters to replace */ escaped_chars_repl['"'] = UNICHR("""); escaped_chars_repl['\''] = UNICHR("'"); escaped_chars_repl['&'] = UNICHR("&"); escaped_chars_repl['<'] = UNICHR("<"); escaped_chars_repl['>'] = UNICHR(">"); /* lengths of those characters when replaced - 1 */ memset(escaped_chars_delta_len, 0, sizeof (escaped_chars_delta_len)); escaped_chars_delta_len['"'] = escaped_chars_delta_len['\''] = \ escaped_chars_delta_len['&'] = 4; escaped_chars_delta_len['<'] = escaped_chars_delta_len['>'] = 3; /* import markup type so that we can mark the return value */ module = PyImport_ImportModule("jinja2.utils"); if (!module) return 0; markup = PyObject_GetAttrString(module, "Markup"); Py_DECREF(module); return 1; } static PyObject* escape_unicode(PyUnicodeObject *in) { PyUnicodeObject *out; Py_UNICODE *inp = in->str; const Py_UNICODE *inp_end = in->str + in->length; Py_UNICODE *next_escp; Py_UNICODE *outp; Py_ssize_t delta=0, erepl=0, delta_len=0; /* First we need to figure out how long the escaped string will be */ while (*(inp) || inp < inp_end) { if (*inp < ESCAPED_CHARS_TABLE_SIZE && escaped_chars_delta_len[*inp]) { delta += escaped_chars_delta_len[*inp]; ++erepl; } ++inp; } /* Do we need to escape anything at all? */ if (!erepl) { Py_INCREF(in); return (PyObject*)in; } out = (PyUnicodeObject*)PyUnicode_FromUnicode(NULL, in->length + delta); if (!out) return NULL; outp = out->str; inp = in->str; while (erepl-- > 0) { /* look for the next substitution */ next_escp = inp; while (next_escp < inp_end) { if (*next_escp < ESCAPED_CHARS_TABLE_SIZE && (delta_len = escaped_chars_delta_len[*next_escp])) { ++delta_len; break; } ++next_escp; } if (next_escp > inp) { /* copy unescaped chars between inp and next_escp */ Py_UNICODE_COPY(outp, inp, next_escp-inp); outp += next_escp - inp; } /* escape 'next_escp' */ Py_UNICODE_COPY(outp, escaped_chars_repl[*next_escp], delta_len); outp += delta_len; inp = next_escp + 1; } if (inp < inp_end) Py_UNICODE_COPY(outp, inp, in->length - (inp - in->str)); return (PyObject*)out; } static PyObject* escape(PyObject *self, PyObject *text) { PyObject *s = NULL, *rv = NULL, *html; /* we don't have to escape integers, bools or floats */ if (PyInt_CheckExact(text) || PyLong_CheckExact(text) || PyFloat_CheckExact(text) || PyBool_Check(text) || text == Py_None) return PyObject_CallFunctionObjArgs(markup, text, NULL); /* if the object has an __html__ method that performs the escaping */ html = PyObject_GetAttrString(text, "__html__"); if (html) { rv = PyObject_CallObject(html, NULL); Py_DECREF(html); return rv; } /* otherwise make the object unicode if it isn't, then escape */ PyErr_Clear(); if (!PyUnicode_Check(text)) { PyObject *unicode = PyObject_Unicode(text); if (!unicode) return NULL; s = escape_unicode((PyUnicodeObject*)unicode); Py_DECREF(unicode); } else s = escape_unicode((PyUnicodeObject*)text); /* convert the unicode string into a markup object. */ rv = PyObject_CallFunctionObjArgs(markup, (PyObject*)s, NULL); Py_DECREF(s); return rv; } static PyObject* soft_unicode(PyObject *self, PyObject *s) { if (!PyUnicode_Check(s)) return PyObject_Unicode(s); Py_INCREF(s); return s; } static PyObject* tb_set_next(PyObject *self, PyObject *args) { PyTracebackObject *tb, *old; PyObject *next; if (!PyArg_ParseTuple(args, "O!O:tb_set_next", &PyTraceBack_Type, &tb, &next)) return NULL; if (next == Py_None) next = NULL; else if (!PyTraceBack_Check(next)) { PyErr_SetString(PyExc_TypeError, "tb_set_next arg 2 must be traceback or None"); return NULL; } else Py_INCREF(next); old = tb->tb_next; tb->tb_next = (PyTracebackObject*)next; Py_XDECREF(old); Py_INCREF(Py_None); return Py_None; } static PyMethodDef module_methods[] = { {"escape", (PyCFunction)escape, METH_O, "escape(s) -> markup\n\n" "Convert the characters &, <, >, ', and \" in string s to HTML-safe\n" "sequences. Use this if you need to display text that might contain\n" "such characters in HTML. Marks return value as markup string."}, {"soft_unicode", (PyCFunction)soft_unicode, METH_O, "soft_unicode(object) -> string\n\n" "Make a string unicode if it isn't already. That way a markup\n" "string is not converted back to unicode."}, {"tb_set_next", (PyCFunction)tb_set_next, METH_VARARGS, "Set the tb_next member of a traceback object."}, {NULL, NULL, 0, NULL} /* Sentinel */ }; #ifndef PyMODINIT_FUNC /* declarations for DLL import/export */ #define PyMODINIT_FUNC void #endif PyMODINIT_FUNC init_speedups(void) { if (!init_constants()) return; Py_InitModule3("jinja2._speedups", module_methods, ""); } ./weblog+jinja-1.0/jinja2/compiler.py010064400017500000000000001454621106036251300164660ustar00henrywheel# -*- coding: utf-8 -*- """ jinja2.compiler ~~~~~~~~~~~~~~~ Compiles nodes into python code. :copyright: Copyright 2008 by Armin Ronacher. :license: BSD. """ from copy import copy from keyword import iskeyword from cStringIO import StringIO from itertools import chain from jinja2 import nodes from jinja2.visitor import NodeVisitor, NodeTransformer from jinja2.exceptions import TemplateAssertionError from jinja2.utils import Markup, concat, escape operators = { 'eq': '==', 'ne': '!=', 'gt': '>', 'gteq': '>=', 'lt': '<', 'lteq': '<=', 'in': 'in', 'notin': 'not in' } try: exec '(0 if 0 else 0)' except SyntaxError: have_condexpr = False else: have_condexpr = True def generate(node, environment, name, filename, stream=None): """Generate the python source for a node tree.""" if not isinstance(node, nodes.Template): raise TypeError('Can\'t compile non template nodes') generator = CodeGenerator(environment, name, filename, stream) generator.visit(node) if stream is None: return generator.stream.getvalue() def has_safe_repr(value): """Does the node have a safe representation?""" if value is None or value is NotImplemented or value is Ellipsis: return True if isinstance(value, (bool, int, long, float, complex, basestring, xrange, Markup)): return True if isinstance(value, (tuple, list, set, frozenset)): for item in value: if not has_safe_repr(item): return False return True elif isinstance(value, dict): for key, value in value.iteritems(): if not has_safe_repr(key): return False if not has_safe_repr(value): return False return True return False def find_undeclared(nodes, names): """Check if the names passed are accessed undeclared. The return value is a set of all the undeclared names from the sequence of names found. """ visitor = UndeclaredNameVisitor(names) try: for node in nodes: visitor.visit(node) except VisitorExit: pass return visitor.undeclared class Identifiers(object): """Tracks the status of identifiers in frames.""" def __init__(self): # variables that are known to be declared (probably from outer # frames or because they are special for the frame) self.declared = set() # undeclared variables from outer scopes self.outer_undeclared = set() # names that are accessed without being explicitly declared by # this one or any of the outer scopes. Names can appear both in # declared and undeclared. self.undeclared = set() # names that are declared locally self.declared_locally = set() # names that are declared by parameters self.declared_parameter = set() def add_special(self, name): """Register a special name like `loop`.""" self.undeclared.discard(name) self.declared.add(name) def is_declared(self, name, local_only=False): """Check if a name is declared in this or an outer scope.""" if name in self.declared_locally or name in self.declared_parameter: return True if local_only: return False return name in self.declared def find_shadowed(self): """Find all the shadowed names.""" return (self.declared | self.outer_undeclared) & \ (self.declared_locally | self.declared_parameter) class Frame(object): """Holds compile time information for us.""" def __init__(self, parent=None): self.identifiers = Identifiers() # a toplevel frame is the root + soft frames such as if conditions. self.toplevel = False # the root frame is basically just the outermost frame, so no if # conditions. This information is used to optimize inheritance # situations. self.rootlevel = False # inside some tags we are using a buffer rather than yield statements. # this for example affects {% filter %} or {% macro %}. If a frame # is buffered this variable points to the name of the list used as # buffer. self.buffer = None # the name of the block we're in, otherwise None. self.block = parent and parent.block or None # the parent of this frame self.parent = parent if parent is not None: self.identifiers.declared.update( parent.identifiers.declared | parent.identifiers.declared_locally | parent.identifiers.declared_parameter | parent.identifiers.undeclared ) self.identifiers.outer_undeclared.update( parent.identifiers.undeclared - self.identifiers.declared ) self.buffer = parent.buffer def copy(self): """Create a copy of the current one.""" rv = copy(self) rv.identifiers = copy(self.identifiers) return rv def inspect(self, nodes, hard_scope=False): """Walk the node and check for identifiers. If the scope is hard (eg: enforce on a python level) overrides from outer scopes are tracked differently. """ visitor = FrameIdentifierVisitor(self.identifiers, hard_scope) for node in nodes: visitor.visit(node) def inner(self): """Return an inner frame.""" return Frame(self) def soft(self): """Return a soft frame. A soft frame may not be modified as standalone thing as it shares the resources with the frame it was created of, but it's not a rootlevel frame any longer. """ rv = copy(self) rv.rootlevel = False return rv class VisitorExit(RuntimeError): """Exception used by the `UndeclaredNameVisitor` to signal a stop.""" class DependencyFinderVisitor(NodeVisitor): """A visitor that collects filter and test calls.""" def __init__(self): self.filters = set() self.tests = set() def visit_Filter(self, node): self.generic_visit(node) self.filters.add(node.name) def visit_Test(self, node): self.generic_visit(node) self.tests.add(node.name) def visit_Block(self, node): """Stop visiting at blocks.""" class UndeclaredNameVisitor(NodeVisitor): """A visitor that checks if a name is accessed without being declared. This is different from the frame visitor as it will not stop at closure frames. """ def __init__(self, names): self.names = set(names) self.undeclared = set() def visit_Name(self, node): if node.ctx == 'load' and node.name in self.names: self.undeclared.add(node.name) if self.undeclared == self.names: raise VisitorExit() else: self.names.discard(node.name) def visit_Block(self, node): """Stop visiting a blocks.""" class FrameIdentifierVisitor(NodeVisitor): """A visitor for `Frame.inspect`.""" def __init__(self, identifiers, hard_scope): self.identifiers = identifiers self.hard_scope = hard_scope def visit_Name(self, node): """All assignments to names go through this function.""" if node.ctx == 'store': self.identifiers.declared_locally.add(node.name) elif node.ctx == 'param': self.identifiers.declared_parameter.add(node.name) elif node.ctx == 'load' and not \ self.identifiers.is_declared(node.name, self.hard_scope): self.identifiers.undeclared.add(node.name) def visit_Macro(self, node): self.identifiers.declared_locally.add(node.name) def visit_Import(self, node): self.generic_visit(node) self.identifiers.declared_locally.add(node.target) def visit_FromImport(self, node): self.generic_visit(node) for name in node.names: if isinstance(name, tuple): self.identifiers.declared_locally.add(name[1]) else: self.identifiers.declared_locally.add(name) def visit_Assign(self, node): """Visit assignments in the correct order.""" self.visit(node.node) self.visit(node.target) def visit_For(self, node): """Visiting stops at for blocks. However the block sequence is visited as part of the outer scope. """ self.visit(node.iter) def visit_CallBlock(self, node): for child in node.iter_child_nodes(exclude=('body',)): self.visit(child) def visit_FilterBlock(self, node): self.visit(node.filter) def visit_Block(self, node): """Stop visiting at blocks.""" class CompilerExit(Exception): """Raised if the compiler encountered a situation where it just doesn't make sense to further process the code. Any block that raises such an exception is not further processed. """ class CodeGenerator(NodeVisitor): def __init__(self, environment, name, filename, stream=None): if stream is None: stream = StringIO() self.environment = environment self.name = name self.filename = filename self.stream = stream # aliases for imports self.import_aliases = {} # a registry for all blocks. Because blocks are moved out # into the global python scope they are registered here self.blocks = {} # the number of extends statements so far self.extends_so_far = 0 # some templates have a rootlevel extends. In this case we # can safely assume that we're a child template and do some # more optimizations. self.has_known_extends = False # the current line number self.code_lineno = 1 # registry of all filters and tests (global, not block local) self.tests = {} self.filters = {} # the debug information self.debug_info = [] self._write_debug_info = None # the number of new lines before the next write() self._new_lines = 0 # the line number of the last written statement self._last_line = 0 # true if nothing was written so far. self._first_write = True # used by the `temporary_identifier` method to get new # unique, temporary identifier self._last_identifier = 0 # the current indentation self._indentation = 0 # -- Various compilation helpers def fail(self, msg, lineno): """Fail with a `TemplateAssertionError`.""" raise TemplateAssertionError(msg, lineno, self.name, self.filename) def temporary_identifier(self): """Get a new unique identifier.""" self._last_identifier += 1 return 't_%d' % self._last_identifier def buffer(self, frame): """Enable buffering for the frame from that point onwards.""" frame.buffer = self.temporary_identifier() self.writeline('%s = []' % frame.buffer) def return_buffer_contents(self, frame): """Return the buffer contents of the frame.""" if self.environment.autoescape: self.writeline('return Markup(concat(%s))' % frame.buffer) else: self.writeline('return concat(%s)' % frame.buffer) def indent(self): """Indent by one.""" self._indentation += 1 def outdent(self, step=1): """Outdent by step.""" self._indentation -= step def start_write(self, frame, node=None): """Yield or write into the frame buffer.""" if frame.buffer is None: self.writeline('yield ', node) else: self.writeline('%s.append(' % frame.buffer, node) def end_write(self, frame): """End the writing process started by `start_write`.""" if frame.buffer is not None: self.write(')') def simple_write(self, s, frame, node=None): """Simple shortcut for start_write + write + end_write.""" self.start_write(frame, node) self.write(s) self.end_write(frame) def blockvisit(self, nodes, frame, force_generator=True): """Visit a list of nodes as block in a frame. If the current frame is no buffer a dummy ``if 0: yield None`` is written automatically unless the force_generator parameter is set to False. """ if frame.buffer is None and force_generator: self.writeline('if 0: yield None') try: for node in nodes: self.visit(node, frame) except CompilerExit: pass def write(self, x): """Write a string into the output stream.""" if self._new_lines: if not self._first_write: self.stream.write('\n' * self._new_lines) self.code_lineno += self._new_lines if self._write_debug_info is not None: self.debug_info.append((self._write_debug_info, self.code_lineno)) self._write_debug_info = None self._first_write = False self.stream.write(' ' * self._indentation) self._new_lines = 0 self.stream.write(x) def writeline(self, x, node=None, extra=0): """Combination of newline and write.""" self.newline(node, extra) self.write(x) def newline(self, node=None, extra=0): """Add one or more newlines before the next write.""" self._new_lines = max(self._new_lines, 1 + extra) if node is not None and node.lineno != self._last_line: self._write_debug_info = node.lineno self._last_line = node.lineno def signature(self, node, frame, extra_kwargs=None): """Writes a function call to the stream for the current node. A leading comma is added automatically. The extra keyword arguments may not include python keywords otherwise a syntax error could occour. The extra keyword arguments should be given as python dict. """ # if any of the given keyword arguments is a python keyword # we have to make sure that no invalid call is created. kwarg_workaround = False for kwarg in chain((x.key for x in node.kwargs), extra_kwargs or ()): if iskeyword(kwarg): kwarg_workaround = True break for arg in node.args: self.write(', ') self.visit(arg, frame) if not kwarg_workaround: for kwarg in node.kwargs: self.write(', ') self.visit(kwarg, frame) if extra_kwargs is not None: for key, value in extra_kwargs.iteritems(): self.write(', %s=%s' % (key, value)) if node.dyn_args: self.write(', *') self.visit(node.dyn_args, frame) if kwarg_workaround: if node.dyn_kwargs is not None: self.write(', **dict({') else: self.write(', **{') for kwarg in node.kwargs: self.write('%r: ' % kwarg.key) self.visit(kwarg.value, frame) self.write(', ') if extra_kwargs is not None: for key, value in extra_kwargs.iteritems(): self.write('%r: %s, ' % (key, value)) if node.dyn_kwargs is not None: self.write('}, **') self.visit(node.dyn_kwargs, frame) self.write(')') else: self.write('}') elif node.dyn_kwargs is not None: self.write(', **') self.visit(node.dyn_kwargs, frame) def pull_locals(self, frame): """Pull all the references identifiers into the local scope.""" for name in frame.identifiers.undeclared: self.writeline('l_%s = context.resolve(%r)' % (name, name)) def pull_dependencies(self, nodes): """Pull all the dependencies.""" visitor = DependencyFinderVisitor() for node in nodes: visitor.visit(node) for dependency in 'filters', 'tests': mapping = getattr(self, dependency) for name in getattr(visitor, dependency): if name not in mapping: mapping[name] = self.temporary_identifier() self.writeline('%s = environment.%s[%r]' % (mapping[name], dependency, name)) def collect_shadowed(self, frame): """This function returns all the shadowed variables in a dict in the form name: alias and will write the required assignments into the current scope. No indentation takes place. """ aliases = {} for name in frame.identifiers.find_shadowed(): aliases[name] = ident = self.temporary_identifier() self.writeline('%s = l_%s' % (ident, name)) return aliases def restore_shadowed(self, aliases): """Restore all aliases.""" for name, alias in aliases.iteritems(): self.writeline('l_%s = %s' % (name, alias)) def function_scoping(self, node, frame, children=None, find_special=True): """In Jinja a few statements require the help of anonymous functions. Those are currently macros and call blocks and in the future also recursive loops. As there is currently technical limitation that doesn't allow reading and writing a variable in a scope where the initial value is coming from an outer scope, this function tries to fall back with a common error message. Additionally the frame passed is modified so that the argumetns are collected and callers are looked up. This will return the modified frame. """ # we have to iterate twice over it, make sure that works if children is None: children = node.iter_child_nodes() children = list(children) func_frame = frame.inner() func_frame.inspect(children, hard_scope=True) # variables that are undeclared (accessed before declaration) and # declared locally *and* part of an outside scope raise a template # assertion error. Reason: we can't generate reasonable code from # it without aliasing all the variables. XXX: alias them ^^ overriden_closure_vars = ( func_frame.identifiers.undeclared & func_frame.identifiers.declared & (func_frame.identifiers.declared_locally | func_frame.identifiers.declared_parameter) ) if overriden_closure_vars: self.fail('It\'s not possible to set and access variables ' 'derived from an outer scope! (affects: %s' % ', '.join(sorted(overriden_closure_vars)), node.lineno) # remove variables from a closure from the frame's undeclared # identifiers. func_frame.identifiers.undeclared -= ( func_frame.identifiers.undeclared & func_frame.identifiers.declared ) # no special variables for this scope, abort early if not find_special: return func_frame func_frame.accesses_kwargs = False func_frame.accesses_varargs = False func_frame.accesses_caller = False func_frame.arguments = args = ['l_' + x.name for x in node.args] undeclared = find_undeclared(children, ('caller', 'kwargs', 'varargs')) if 'caller' in undeclared: func_frame.accesses_caller = True func_frame.identifiers.add_special('caller') args.append('l_caller') if 'kwargs' in undeclared: func_frame.accesses_kwargs = True func_frame.identifiers.add_special('kwargs') args.append('l_kwargs') if 'varargs' in undeclared: func_frame.accesses_varargs = True func_frame.identifiers.add_special('varargs') args.append('l_varargs') return func_frame def macro_body(self, node, frame, children=None): """Dump the function def of a macro or call block.""" frame = self.function_scoping(node, frame, children) args = frame.arguments self.writeline('def macro(%s):' % ', '.join(args), node) self.indent() self.buffer(frame) self.pull_locals(frame) self.blockvisit(node.body, frame) self.return_buffer_contents(frame) self.outdent() return frame def macro_def(self, node, frame): """Dump the macro definition for the def created by macro_body.""" arg_tuple = ', '.join(repr(x.name) for x in node.args) name = getattr(node, 'name', None) if len(node.args) == 1: arg_tuple += ',' self.write('Macro(environment, macro, %r, (%s), (' % (name, arg_tuple)) for arg in node.defaults: self.visit(arg, frame) self.write(', ') self.write('), %r, %r, %r)' % ( bool(frame.accesses_kwargs), bool(frame.accesses_varargs), bool(frame.accesses_caller) )) def position(self, node): """Return a human readable position for the node.""" rv = 'line %d' % node.lineno if self.name is not None: rv += ' in' + repr(self.name) return rv # -- Statement Visitors def visit_Template(self, node, frame=None): assert frame is None, 'no root frame allowed' from jinja2.runtime import __all__ as exported self.writeline('from __future__ import division') self.writeline('from jinja2.runtime import ' + ', '.join(exported)) # do we have an extends tag at all? If not, we can save some # overhead by just not processing any inheritance code. have_extends = node.find(nodes.Extends) is not None # find all blocks for block in node.find_all(nodes.Block): if block.name in self.blocks: self.fail('block %r defined twice' % block.name, block.lineno) self.blocks[block.name] = block # find all imports and import them for import_ in node.find_all(nodes.ImportedName): if import_.importname not in self.import_aliases: imp = import_.importname self.import_aliases[imp] = alias = self.temporary_identifier() if '.' in imp: module, obj = imp.rsplit('.', 1) self.writeline('from %s import %s as %s' % (module, obj, alias)) else: self.writeline('import %s as %s' % (imp, alias)) # add the load name self.writeline('name = %r' % self.name) # generate the root render function. self.writeline('def root(context, environment=environment):', extra=1) # process the root frame = Frame() frame.inspect(node.body) frame.toplevel = frame.rootlevel = True self.indent() if have_extends: self.writeline('parent_template = None') if 'self' in find_undeclared(node.body, ('self',)): frame.identifiers.add_special('self') self.writeline('l_self = TemplateReference(context)') self.pull_locals(frame) self.pull_dependencies(node.body) self.blockvisit(node.body, frame) self.outdent() # make sure that the parent root is called. if have_extends: if not self.has_known_extends: self.indent() self.writeline('if parent_template is not None:') self.indent() self.writeline('for event in parent_template.' 'root_render_func(context):') self.indent() self.writeline('yield event') self.outdent(2 + (not self.has_known_extends)) # at this point we now have the blocks collected and can visit them too. for name, block in self.blocks.iteritems(): block_frame = Frame() block_frame.inspect(block.body) block_frame.block = name self.writeline('def block_%s(context, environment=environment):' % name, block, 1) self.indent() undeclared = find_undeclared(block.body, ('self', 'super')) if 'self' in undeclared: block_frame.identifiers.add_special('self') self.writeline('l_self = TemplateReference(context)') if 'super' in undeclared: block_frame.identifiers.add_special('super') self.writeline('l_super = context.super(%r, ' 'block_%s)' % (name, name)) self.pull_locals(block_frame) self.pull_dependencies(block.body) self.blockvisit(block.body, block_frame) self.outdent() self.writeline('blocks = {%s}' % ', '.join('%r: block_%s' % (x, x) for x in self.blocks), extra=1) # add a function that returns the debug info self.writeline('debug_info = %r' % '&'.join('%s=%s' % x for x in self.debug_info)) def visit_Block(self, node, frame): """Call a block and register it for the template.""" level = 1 if frame.toplevel: # if we know that we are a child template, there is no need to # check if we are one if self.has_known_extends: return if self.extends_so_far > 0: self.writeline('if parent_template is None:') self.indent() level += 1 self.writeline('for event in context.blocks[%r][0](context):' % node.name, node) self.indent() self.simple_write('event', frame) self.outdent(level) def visit_Extends(self, node, frame): """Calls the extender.""" if not frame.toplevel: self.fail('cannot use extend from a non top-level scope', node.lineno) # if the number of extends statements in general is zero so # far, we don't have to add a check if something extended # the template before this one. if self.extends_so_far > 0: # if we have a known extends we just add a template runtime # error into the generated code. We could catch that at compile # time too, but i welcome it not to confuse users by throwing the # same error at different times just "because we can". if not self.has_known_extends: self.writeline('if parent_template is not None:') self.indent() self.writeline('raise TemplateRuntimeError(%r)' % 'extended multiple times') # if we have a known extends already we don't need that code here # as we know that the template execution will end here. if self.has_known_extends: raise CompilerExit() self.outdent() self.writeline('parent_template = environment.get_template(', node) self.visit(node.template, frame) self.write(', %r)' % self.name) self.writeline('for name, parent_block in parent_template.' 'blocks.iteritems():') self.indent() self.writeline('context.blocks.setdefault(name, []).' 'append(parent_block)') self.outdent() # if this extends statement was in the root level we can take # advantage of that information and simplify the generated code # in the top level from this point onwards if frame.rootlevel: self.has_known_extends = True # and now we have one more self.extends_so_far += 1 def visit_Include(self, node, frame): """Handles includes.""" if node.with_context: self.writeline('template = environment.get_template(', node) self.visit(node.template, frame) self.write(', %r)' % self.name) self.writeline('for event in template.root_render_func(' 'template.new_context(context.parent, True)):') else: self.writeline('for event in environment.get_template(', node) self.visit(node.template, frame) self.write(', %r).module._body_stream:' % self.name) self.indent() self.simple_write('event', frame) self.outdent() def visit_Import(self, node, frame): """Visit regular imports.""" self.writeline('l_%s = ' % node.target, node) if frame.toplevel: self.write('context.vars[%r] = ' % node.target) self.write('environment.get_template(') self.visit(node.template, frame) self.write(', %r).' % self.name) if node.with_context: self.write('make_module(context.parent, True)') else: self.write('module') if frame.toplevel and not node.target.startswith('_'): self.writeline('context.exported_vars.discard(%r)' % node.target) def visit_FromImport(self, node, frame): """Visit named imports.""" self.newline(node) self.write('included_template = environment.get_template(') self.visit(node.template, frame) self.write(', %r).' % self.name) if node.with_context: self.write('make_module(context.parent, True)') else: self.write('module') var_names = [] discarded_names = [] for name in node.names: if isinstance(name, tuple): name, alias = name else: alias = name self.writeline('l_%s = getattr(included_template, ' '%r, missing)' % (alias, name)) self.writeline('if l_%s is missing:' % alias) self.indent() self.writeline('l_%s = environment.undefined(%r %% ' 'included_template.__name__, ' 'name=%r)' % (alias, 'the template %%r (imported on %s) does ' 'not export the requested name %s' % ( self.position(node), repr(name) ), name)) self.outdent() if frame.toplevel: var_names.append(alias) if not alias.startswith('_'): discarded_names.append(alias) if var_names: if len(var_names) == 1: name = var_names[0] self.writeline('context.vars[%r] = l_%s' % (name, name)) else: self.writeline('context.vars.update({%s})' % ', '.join( '%r: l_%s' % (name, name) for name in var_names )) if discarded_names: if len(discarded_names) == 1: self.writeline('context.exported_vars.discard(%r)' % discarded_names[0]) else: self.writeline('context.exported_vars.difference_' 'update((%s))' % ', '.join(map(repr, discarded_names))) def visit_For(self, node, frame): # when calculating the nodes for the inner frame we have to exclude # the iterator contents from it children = node.iter_child_nodes(exclude=('iter',)) if node.recursive: loop_frame = self.function_scoping(node, frame, children, find_special=False) else: loop_frame = frame.inner() loop_frame.inspect(children) # try to figure out if we have an extended loop. An extended loop # is necessary if the loop is in recursive mode if the special loop # variable is accessed in the body. extended_loop = node.recursive or 'loop' in \ find_undeclared(node.iter_child_nodes( only=('body',)), ('loop',)) # make sure the loop variable is a special one and raise a template # assertion error if a loop tries to write to loop loop_frame.identifiers.add_special('loop') for name in node.find_all(nodes.Name): if name.ctx == 'store' and name.name == 'loop': self.fail('Can\'t assign to special loop variable ' 'in for-loop target', name.lineno) # if we don't have an recursive loop we have to find the shadowed # variables at that point if not node.recursive: aliases = self.collect_shadowed(loop_frame) # otherwise we set up a buffer and add a function def else: self.writeline('def loop(reciter, loop_render_func):', node) self.indent() self.buffer(loop_frame) aliases = {} self.pull_locals(loop_frame) if node.else_: iteration_indicator = self.temporary_identifier() self.writeline('%s = 1' % iteration_indicator) # Create a fake parent loop if the else or test section of a # loop is accessing the special loop variable and no parent loop # exists. if 'loop' not in aliases and 'loop' in find_undeclared( node.iter_child_nodes(only=('else_', 'test')), ('loop',)): self.writeline("l_loop = environment.undefined(%r, name='loop')" % ("'loop' is undefined. the filter section of a loop as well " "as the else block doesn't have access to the special 'loop'" " variable of the current loop. Because there is no parent " "loop it's undefined. Happened in loop on %s" % self.position(node))) self.writeline('for ', node) self.visit(node.target, loop_frame) self.write(extended_loop and ', l_loop in LoopContext(' or ' in ') # if we have an extened loop and a node test, we filter in the # "outer frame". if extended_loop and node.test is not None: self.write('(') self.visit(node.target, loop_frame) self.write(' for ') self.visit(node.target, loop_frame) self.write(' in ') if node.recursive: self.write('reciter') else: self.visit(node.iter, loop_frame) self.write(' if (') test_frame = loop_frame.copy() self.visit(node.test, test_frame) self.write('))') elif node.recursive: self.write('reciter') else: self.visit(node.iter, loop_frame) if node.recursive: self.write(', recurse=loop_render_func):') else: self.write(extended_loop and '):' or ':') # tests in not extended loops become a continue if not extended_loop and node.test is not None: self.indent() self.writeline('if not ') self.visit(node.test, loop_frame) self.write(':') self.indent() self.writeline('continue') self.outdent(2) self.indent() self.blockvisit(node.body, loop_frame, force_generator=True) if node.else_: self.writeline('%s = 0' % iteration_indicator) self.outdent() if node.else_: self.writeline('if %s:' % iteration_indicator) self.indent() self.blockvisit(node.else_, loop_frame, force_generator=False) self.outdent() # reset the aliases if there are any. self.restore_shadowed(aliases) # if the node was recursive we have to return the buffer contents # and start the iteration code if node.recursive: self.return_buffer_contents(loop_frame) self.outdent() self.start_write(frame, node) self.write('loop(') self.visit(node.iter, frame) self.write(', loop)') self.end_write(frame) def visit_If(self, node, frame): if_frame = frame.soft() self.writeline('if ', node) self.visit(node.test, if_frame) self.write(':') self.indent() self.blockvisit(node.body, if_frame) self.outdent() if node.else_: self.writeline('else:') self.indent() self.blockvisit(node.else_, if_frame) self.outdent() def visit_Macro(self, node, frame): macro_frame = self.macro_body(node, frame) self.newline() if frame.toplevel: if not node.name.startswith('_'): self.write('context.exported_vars.add(%r)' % node.name) self.writeline('context.vars[%r] = ' % node.name) self.write('l_%s = ' % node.name) self.macro_def(node, macro_frame) def visit_CallBlock(self, node, frame): children = node.iter_child_nodes(exclude=('call',)) call_frame = self.macro_body(node, frame, children) self.writeline('caller = ') self.macro_def(node, call_frame) self.start_write(frame, node) self.visit_Call(node.call, call_frame, forward_caller=True) self.end_write(frame) def visit_FilterBlock(self, node, frame): filter_frame = frame.inner() filter_frame.inspect(node.iter_child_nodes()) aliases = self.collect_shadowed(filter_frame) self.pull_locals(filter_frame) self.buffer(filter_frame) self.blockvisit(node.body, filter_frame, force_generator=False) self.start_write(frame, node) self.visit_Filter(node.filter, filter_frame) self.end_write(frame) self.restore_shadowed(aliases) def visit_ExprStmt(self, node, frame): self.newline(node) self.visit(node.node, frame) def visit_Output(self, node, frame): # if we have a known extends statement, we don't output anything if self.has_known_extends and frame.toplevel: return if self.environment.finalize: finalize = lambda x: unicode(self.environment.finalize(x)) else: finalize = unicode self.newline(node) # if we are in the toplevel scope and there was already an extends # statement we have to add a check that disables our yield(s) here # so that they don't appear in the output. outdent_later = False if frame.toplevel and self.extends_so_far != 0: self.writeline('if parent_template is None:') self.indent() outdent_later = True # try to evaluate as many chunks as possible into a static # string at compile time. body = [] for child in node.nodes: try: const = child.as_const() except nodes.Impossible: body.append(child) continue try: if self.environment.autoescape: if hasattr(const, '__html__'): const = const.__html__() else: const = escape(const) const = finalize(const) except: # if something goes wrong here we evaluate the node # at runtime for easier debugging body.append(child) continue if body and isinstance(body[-1], list): body[-1].append(const) else: body.append([const]) # if we have less than 3 nodes or a buffer we yield or extend/append if len(body) < 3 or frame.buffer is not None: if frame.buffer is not None: # for one item we append, for more we extend if len(body) == 1: self.writeline('%s.append(' % frame.buffer) else: self.writeline('%s.extend((' % frame.buffer) self.indent() for item in body: if isinstance(item, list): val = repr(concat(item)) if frame.buffer is None: self.writeline('yield ' + val) else: self.writeline(val + ', ') else: if frame.buffer is None: self.writeline('yield ', item) else: self.newline(item) close = 1 if self.environment.autoescape: self.write('escape(') else: self.write('unicode(') if self.environment.finalize is not None: self.write('environment.finalize(') close += 1 self.visit(item, frame) self.write(')' * close) if frame.buffer is not None: self.write(', ') if frame.buffer is not None: # close the open parentheses self.outdent() self.writeline(len(body) == 1 and ')' or '))') # otherwise we create a format string as this is faster in that case else: format = [] arguments = [] for item in body: if isinstance(item, list): format.append(concat(item).replace('%', '%%')) else: format.append('%s') arguments.append(item) self.writeline('yield ') self.write(repr(concat(format)) + ' % (') idx = -1 self.indent() for argument in arguments: self.newline(argument) close = 0 if self.environment.autoescape: self.write('escape(') close += 1 if self.environment.finalize is not None: self.write('environment.finalize(') close += 1 self.visit(argument, frame) self.write(')' * close + ', ') self.outdent() self.writeline(')') if outdent_later: self.outdent() def visit_Assign(self, node, frame): self.newline(node) # toplevel assignments however go into the local namespace and # the current template's context. We create a copy of the frame # here and add a set so that the Name visitor can add the assigned # names here. if frame.toplevel: assignment_frame = frame.copy() assignment_frame.assigned_names = set() else: assignment_frame = frame self.visit(node.target, assignment_frame) self.write(' = ') self.visit(node.node, frame) # make sure toplevel assignments are added to the context. if frame.toplevel: public_names = [x for x in assignment_frame.assigned_names if not x.startswith('_')] if len(assignment_frame.assigned_names) == 1: name = iter(assignment_frame.assigned_names).next() self.writeline('context.vars[%r] = l_%s' % (name, name)) else: self.writeline('context.vars.update({') for idx, name in enumerate(assignment_frame.assigned_names): if idx: self.write(', ') self.write('%r: l_%s' % (name, name)) self.write('})') if public_names: if len(public_names) == 1: self.writeline('context.exported_vars.add(%r)' % public_names[0]) else: self.writeline('context.exported_vars.update((%s))' % ', '.join(map(repr, public_names))) # -- Expression Visitors def visit_Name(self, node, frame): if node.ctx == 'store' and frame.toplevel: frame.assigned_names.add(node.name) self.write('l_' + node.name) def visit_Const(self, node, frame): val = node.value if isinstance(val, float): self.write(str(val)) else: self.write(repr(val)) def visit_TemplateData(self, node, frame): self.write(repr(node.as_const())) def visit_Tuple(self, node, frame): self.write('(') idx = -1 for idx, item in enumerate(node.items): if idx: self.write(', ') self.visit(item, frame) self.write(idx == 0 and ',)' or ')') def visit_List(self, node, frame): self.write('[') for idx, item in enumerate(node.items): if idx: self.write(', ') self.visit(item, frame) self.write(']') def visit_Dict(self, node, frame): self.write('{') for idx, item in enumerate(node.items): if idx: self.write(', ') self.visit(item.key, frame) self.write(': ') self.visit(item.value, frame) self.write('}') def binop(operator): def visitor(self, node, frame): self.write('(') self.visit(node.left, frame) self.write(' %s ' % operator) self.visit(node.right, frame) self.write(')') return visitor def uaop(operator): def visitor(self, node, frame): self.write('(' + operator) self.visit(node.node, frame) self.write(')') return visitor visit_Add = binop('+') visit_Sub = binop('-') visit_Mul = binop('*') visit_Div = binop('/') visit_FloorDiv = binop('//') visit_Pow = binop('**') visit_Mod = binop('%') visit_And = binop('and') visit_Or = binop('or') visit_Pos = uaop('+') visit_Neg = uaop('-') visit_Not = uaop('not ') del binop, uaop def visit_Concat(self, node, frame): self.write('%s((' % (self.environment.autoescape and 'markup_join' or 'unicode_join')) for arg in node.nodes: self.visit(arg, frame) self.write(', ') self.write('))') def visit_Compare(self, node, frame): self.visit(node.expr, frame) for op in node.ops: self.visit(op, frame) def visit_Operand(self, node, frame): self.write(' %s ' % operators[node.op]) self.visit(node.expr, frame) def visit_Getattr(self, node, frame): self.write('environment.getattr(') self.visit(node.node, frame) self.write(', %r)' % node.attr) def visit_Getitem(self, node, frame): # slices or integer subscriptions bypass the getitem # method if we can determine that at compile time. if isinstance(node.arg, nodes.Slice) or \ (isinstance(node.arg, nodes.Const) and isinstance(node.arg.value, (int, long))): self.visit(node.node, frame) self.write('[') self.visit(node.arg, frame) self.write(']') else: self.write('environment.getitem(') self.visit(node.node, frame) self.write(', ') self.visit(node.arg, frame) self.write(')') def visit_Slice(self, node, frame): if node.start is not None: self.visit(node.start, frame) self.write(':') if node.stop is not None: self.visit(node.stop, frame) if node.step is not None: self.write(':') self.visit(node.step, frame) def visit_Filter(self, node, frame): self.write(self.filters[node.name] + '(') func = self.environment.filters.get(node.name) if func is None: self.fail('no filter named %r' % node.name, node.lineno) if getattr(func, 'contextfilter', False): self.write('context, ') elif getattr(func, 'environmentfilter', False): self.write('environment, ') # if the filter node is None we are inside a filter block # and want to write to the current buffer if node.node is not None: self.visit(node.node, frame) elif self.environment.autoescape: self.write('Markup(concat(%s))' % frame.buffer) else: self.write('concat(%s)' % frame.buffer) self.signature(node, frame) self.write(')') def visit_Test(self, node, frame): self.write(self.tests[node.name] + '(') if node.name not in self.environment.tests: self.fail('no test named %r' % node.name, node.lineno) self.visit(node.node, frame) self.signature(node, frame) self.write(')') def visit_CondExpr(self, node, frame): def write_expr2(): if node.expr2 is not None: return self.visit(node.expr2, frame) self.write('environment.undefined(%r)' % ('the inline if-' 'expression on %s evaluated to false and ' 'no else section was defined.' % self.position(node))) if not have_condexpr: self.write('((') self.visit(node.test, frame) self.write(') and (') self.visit(node.expr1, frame) self.write(',) or (') write_expr2() self.write(',))[0]') else: self.write('(') self.visit(node.expr1, frame) self.write(' if ') self.visit(node.test, frame) self.write(' else ') write_expr2() self.write(')') def visit_Call(self, node, frame, forward_caller=False): if self.environment.sandboxed: self.write('environment.call(context, ') else: self.write('context.call(') self.visit(node.node, frame) extra_kwargs = forward_caller and {'caller': 'caller'} or None self.signature(node, frame, extra_kwargs) self.write(')') def visit_Keyword(self, node, frame): self.write(node.key + '=') self.visit(node.value, frame) # -- Unused nodes for extensions def visit_MarkSafe(self, node, frame): self.write('Markup(') self.visit(node.expr, frame) self.write(')') def visit_EnvironmentAttribute(self, node, frame): self.write('environment.' + node.name) def visit_ExtensionAttribute(self, node, frame): self.write('environment.extensions[%r].%s' % (node.identifier, node.name)) def visit_ImportedName(self, node, frame): self.write(self.import_aliases[node.importname]) def visit_InternalName(self, node, frame): self.write(node.name) def visit_ContextReference(self, node, frame): self.write('context') def visit_Continue(self, node, frame): self.writeline('continue', node) def visit_Break(self, node, frame): self.writeline('break', node) ./weblog+jinja-1.0/jinja2/constants.py010064400017500000000000000031261106036251300166560ustar00henrywheel# -*- coding: utf-8 -*- """ jinja.constants ~~~~~~~~~~~~~~~ Various constants. :copyright: 2007 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ #: list of lorem ipsum words used by the lipsum() helper function LOREM_IPSUM_WORDS = u'''\ a ac accumsan ad adipiscing aenean aliquam aliquet amet ante aptent arcu at auctor augue bibendum blandit class commodo condimentum congue consectetuer consequat conubia convallis cras cubilia cum curabitur curae cursus dapibus diam dictum dictumst dignissim dis dolor donec dui duis egestas eget eleifend elementum elit enim erat eros est et etiam eu euismod facilisi facilisis fames faucibus felis fermentum feugiat fringilla fusce gravida habitant habitasse hac hendrerit hymenaeos iaculis id imperdiet in inceptos integer interdum ipsum justo lacinia lacus laoreet lectus leo libero ligula litora lobortis lorem luctus maecenas magna magnis malesuada massa mattis mauris metus mi molestie mollis montes morbi mus nam nascetur natoque nec neque netus nibh nisi nisl non nonummy nostra nulla nullam nunc odio orci ornare parturient pede pellentesque penatibus per pharetra phasellus placerat platea porta porttitor posuere potenti praesent pretium primis proin pulvinar purus quam quis quisque rhoncus ridiculus risus rutrum sagittis sapien scelerisque sed sem semper senectus sit sociis sociosqu sodales sollicitudin suscipit suspendisse taciti tellus tempor tempus tincidunt torquent tortor tristique turpis ullamcorper ultrices ultricies urna ut varius vehicula vel velit venenatis vestibulum vitae vivamus viverra volutpat vulputate''' ./weblog+jinja-1.0/jinja2/debug.py010064400017500000000000000137071106036251300157360ustar00henrywheel# -*- coding: utf-8 -*- """ jinja2.debug ~~~~~~~~~~~~ Implements the debug interface for Jinja. This module does some pretty ugly stuff with the Python traceback system in order to achieve tracebacks with correct line numbers, locals and contents. :copyright: Copyright 2008 by Armin Ronacher. :license: BSD. """ import sys from types import CodeType def translate_exception(exc_info): """If passed an exc_info it will automatically rewrite the exceptions all the way down to the correct line numbers and frames. """ result_tb = prev_tb = None initial_tb = tb = exc_info[2].tb_next while tb is not None: template = tb.tb_frame.f_globals.get('__jinja_template__') if template is not None: lineno = template.get_corresponding_lineno(tb.tb_lineno) tb = fake_exc_info(exc_info[:2] + (tb,), template.filename, lineno, prev_tb)[2] if result_tb is None: result_tb = tb prev_tb = tb tb = tb.tb_next return exc_info[:2] + (result_tb or initial_tb,) def translate_syntax_error(error): """When passed a syntax error it will generate a new traceback with more debugging information. """ filename = error.filename if filename is None: filename = '