weblog+jinja2+markdown2-2.5004075500017500000000000000000001133707607500147045ustar00henrywheelweblog+jinja2+markdown2-2.5/contrib004075500017500000000000000000001133707606200163405ustar00henrywheelweblog+jinja2+markdown2-2.5/contrib/migrate.py010064400017500000000000000070221133707606200204170ustar00henrywheelimport os import sys import ConfigParser from optparse import OptionParser, SUPPRESS_HELP from weblog.publish import load_post_list from weblog.post import Post 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('-e', '--encoding', dest='encoding', default='utf-8') parser.add_option('--redirections-only', dest='redirection_only', default=False, action='store_true', help='Generate only redirection files.') (options, args) = parser.parse_args() if options.redirection_only: print 'Creating config.py ...' config = ConfigParser.SafeConfigParser() filename = os.path.join(options.source_dir, 'weblog.ini') if os.path.isfile(filename): config.read(filename) else: raise SystemExit(filename + " doesn't exist") string_values = ('title', 'url', 'description', 'source_dir', 'output_dir', 'encoding', 'author') integer_values = ('post_per_page', 'feed_limit') obsolete_values = ('html_head', 'html_header', 'html_footer') output = open(os.path.join(options.output_dir, 'config.py'), 'w') def _config_get(key): try: return config.get('weblog', key) except ConfigParser.NoOptionError: return for key in string_values: value = _config_get(key) if value: output.write('%s = %r\n' % (key, value)) for key in integer_values: value = _config_get(key) if value is not None: output.write('%s = %d\n' % (key, int(value))) obsoletes = list() for key in obsolete_values: if _config_get(key): print ('Warning: %s are now obsolete. Check documentation.' % ', '.join(obsolete_values)) break print 'done' if options.encoding: Post.DEFAULT_ENCODING = options.encoding print 'Creating redirections ...' _REDIRECTION_FILE = \ ('\n' '\n' '\n' 'click to get redirected') posts = load_post_list(options.source_dir) for post in posts: dir = os.path.join(options.output_dir, str(post.date.year), str(post.date.month), str(post.date.day)) if not os.path.isdir(dir): os.makedirs(dir) ascii_title = post.title.encode('ascii', 'replace') if ascii_title != post.slug: new_url = post.slug + '.html' redirection_file = _REDIRECTION_FILE % (new_url, new_url) open(os.path.join(dir, ascii_title + '.html'), 'w').\ write(redirection_file) print 'Done.' if __name__ == '__main__': main() weblog+jinja2+markdown2-2.5/contrib/dist.sh010064400017500000000000000037231133707606200177200ustar00henrywheel#!/bin/sh # # Create Weblog's distribution tarball # if [ -z ${1} ] then echo "Usage: ${0} version"; exit 1 else version=${1} fi dir='/tmp/weblog' mkdir -p ${dir} # Normal version hg archive -t tgz ${dir}/weblog-${version}.tar.gz # Standalone version standalone_basename="weblog+jinja2+markdown2-${version}" standalone_dir="${dir}/${standalone_basename}" license="${standalone_dir}/COPYING" rm -rf ${standalone_dir} hg archive -t files ${standalone_dir} jinja_version=2.3 jinja_basename=Jinja2-${jinja_version} jinja_filename=${jinja_basename}.tar.gz jinja_url=http://pypi.python.org/packages/source/J/Jinja2/${jinja_filename} if [ ! -r ${dir}/${jinja_filename} ] then ftp -o ${dir}/${jinja_filename} ${jinja_url} fi tar zxf ${dir}/${jinja_filename} -C ${dir} mkdir -p ${standalone_dir}/jinja2 cp -r ${dir}/${jinja_basename}/jinja2/*.py ${standalone_dir}/jinja2 echo >> ${license} echo 'Jinja 2 license:' >> ${license} # Jinja LICENSE is a dos file ... awk '{ sub("\r$", ""); print }' ${dir}/${jinja_basename}/LICENSE >> ${license} echo >> ${license} echo 'Jinja 2 authors:' >> ${license} cat ${dir}/${jinja_basename}/AUTHORS >> ${license} markdown2_version=1.0.1.16 markdown2_basename=markdown2-${markdown2_version} markdown2_filename=${markdown2_basename}.zip markdown2_url=http://pypi.python.org/packages/source/m/markdown2/${markdown2_filename} if [ ! -r ${dir}/${markdown2_filename} ] then ftp -o ${dir}/${markdown2_filename} ${markdown2_url} fi unzip -qo ${dir}/${markdown2_filename} -d ${dir} cp ${dir}/${markdown2_basename}/lib/markdown2.py ${standalone_dir}/ echo >> ${license} echo 'Markdown 2 license:' >> ${license} cat ${dir}/${markdown2_basename}/LICENSE.txt >> ${license} tar zcf ${dir}/${standalone_basename}.tar.gz -C ${dir} ${standalone_basename} echo "Don't forget to check: INSTALL, weblog/__init__.py" hg tags | fgrep -q "${version}" || echo "CHECK TAGS" echo 'To upload to PyPi: ~/env/weblog/bin/python setup.py sdist upload --sign' weblog+jinja2+markdown2-2.5/doc004075500017500000000000000000001133707606200154455ustar00henrywheelweblog+jinja2+markdown2-2.5/doc/index.rst010064400017500000000000000006631133707606200173670ustar00henrywheelWeblog's documentation ====================== .. include:: short_description.txt To get news and updates about Weblog check the author's blog: http://henry.precheur.org/ or check Weblog's homepage: http://henry.precheur.org/weblog/ Contents .. toctree:: :maxdepth: 2 install tutorial reference * Markdown_ markup language * :ref:`search` .. _Markdown: http://daringfireball.net/projects/markdown/syntax#overview weblog+jinja2+markdown2-2.5/doc/conf.py010064400017500000000000000131521133707606200170220ustar00henrywheel# -*- coding: utf-8 -*- # # Weblog documentation build configuration file, created by # sphinx-quickstart on Sun Nov 16 21:11:24 2008. # # This file is execfile()d with the current directory set to its containing dir. # # The contents of this file are pickled, so don't put values in the namespace # that aren't pickleable (module imports are okay, they're removed automatically). # # All configuration values have a default value; values that are commented out # serve to show the default value. import sys, os # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it # absolute, like shown here. #sys.path.append(os.path.abspath('some/directory')) sys.path.append('..') import weblog # General configuration # --------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [] # Add any paths that contain templates here, relative to this directory. templates_path = ['.templates'] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General substitutions. project = u'Weblog' copyright = u'2007-2009, ' + weblog.__author__ # The default replacements for |version| and |release|, also used in various # other places throughout the built documents. # # The short X.Y version. version = weblog.__version__ # The full version, including alpha/beta/rc tags. release = version # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. #unused_docs = [] # List of directories, relative to source directories, that shouldn't be searched # for source files. exclude_trees = ['.build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # Options for HTML output # ----------------------- # The style sheet to use for HTML and HTML Help pages. A file of that name # must exist either in Sphinx' static/ path, or in one of the custom paths # given in html_static_path. html_style = 'default.css' # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['.static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_use_modindex = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, the reST sources are included in the HTML build as _sources/. #html_copy_source = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'Weblogdoc' # Options for LaTeX output # ------------------------ # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, document class [howto/manual]). latex_documents = [ ('index', 'Weblog.tex', u'Weblog Documentation', weblog.__author__, 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True weblog+jinja2+markdown2-2.5/doc/reference.rst010064400017500000000000000273171133707606200202230ustar00henrywheel.. _reference_manual: Weblog's reference manual ========================= 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 Setting the publication date ---------------------------- A good practice is to set the date when the post gets published. By doing so the date won't get changed if the file gets copied or modified. 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 Some random content. $ 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 Some random content. $ 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. Without any argument the date is set the local time. Most of the time, you will only need this command:: $ weblog date path/to/my/post.txt The ``date`` command accepts 3 formats as optional 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) This way you can set a specific publication date for a post. Writing a Post -------------- Headers ~~~~~~~ Headers define everything that is not part of the post content: They are standard :RFC:`2822` headers (the headers used in Emails). Only ``title`` is mandatory. - Title: ``title`` - Author: ``author`` - File's encoding: ``encoding`` (see Encoding_) - Files attached to the post: ``files`` (see `Attaching a file to a post`_) - Slug: ``slug`` (See `Post's URL`_) A blank line must follow headers. Content ~~~~~~~ After the headers comes the content of post. You can write posts using 2 syntaxes: - Raw HTML syntax - Markdown_ The type of the post is determined by the post's file extension. - `.html` for HTML - `.txt` for Markdown .. _Markdown: http://en.wikipedia.org/wiki/Markdown Post's URL ~~~~~~~~~~ The URL of a post is determined by its date and its Slug_. For example:: title: test date: 2009-11-5 Example The URL will be http://.../2009/11/5/test.html. It is constructed this way:: ///.html `` is a label given to the post. By default, it is determined from the post's title, by replacing spaces with underscores. If the title is "Hello World", the slug will be Hello_World. You can change a post's Slug_ via the header `slug`:: title: My fancy blog post date: 2009-11-1 slug: fancy Example Here the URL will be http://.../2009/11/1/fancy.html. .. _Slug: http://en.wikipedia.org/wiki/Slug_%28production%29 Encoding and escaping --------------------- Encoding ~~~~~~~~ Weblog applies `Postel's law`_: Be conservative in what you do; be liberal in what you accept from others. It accepts files with different encoding as input but always output HTML files using ASCII encoding, non-ASCII characters being converted to HTML entities. The Atom feed is always encoded in UTF-8. You have 3 ways of specifying the input encoding: - The operation system's locale or system's encoding. - ``config.py``, via the field ``encoding``. This encoding becomes the default encoding for the post files and the configuration file ``config.py``. It overrides the system's encoding. - The post's header ``encoding``, example for UTF-8:: encoding: UTF-8 or latin-1:: encoding: latin-1 This override the encoding specified in ``config.py``. To get a list of supported encodings check `Python's documentation `_ .. _Postel's law: http://en.wikipedia.org/wiki/Postel's_law Escaping ~~~~~~~~ Weblog escapes strings to make sure everything displays smoothly. If you don't know what escaping is, you can probably skip this section. Everything is escaped except: - The content of a post if its syntax is HTML - HTML head, header, and footer Which means the title ``Me & You`` is converted to ``Me & you`` in HTML and Atom files. .. _attach_file: 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. You can specify multiple files like this:: files: image1.png image2.png Space characters act as the separators. This means that filenames cannot contain spaces. How URL's in content are handled -------------------------------- Sometime, URL's have to be changed to make sure they point to the correct location. Relative links (````) are rewritten in HTML files to make sure it always point to the root of the output directory. Absolute links (````) are not rewritten. It 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``. .. _style: Customizing Weblog's appearance ------------------------------- To customize Weblog's appearance you need to change the templates used to generate the pages. To learn how to modify the templates, check `Jinja 2`_ documentation, also a basic knowledge of HTML and CSS is needed. You can find the templates in ``weblog/templates`` in your Weblog's installation directory. Copy the files you want to modify into the ``templates`` directory inside of your source directory:: $ mkdir source/directory/templates $ cp /path/to/weblog/templates/base.html source/directory/templates ``base.html`` is probably the file you want to modify to customize Weblog's global appearance. All other templates extend it. ``index.html`` is the main page and ``archives.html`` is the archive page, listing all the posts on your blog. ``post.html`` is used to generate individual post's page. There is also a template named ``feed.atom`` you should not modify this one. It is used to generate the Atom feed. CSS and HTML resources ~~~~~~~~~~~~~~~~~~~~~~ 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 very helpful. It lists all CSS properties and document how well they are supported by the different browsers. - HtmlHelp_ contains a complete HTML 4 reference. .. _Jinja 2: http://jinja.pocoo.org/2/documentation/ .. _HtmlHelp: http://htmlhelp.com/reference/html40/ .. _SitePoint: http://reference.sitepoint.com/css Command line parameters ----------------------- Usage: weblog [option] command Commands: publish date Options: -h, --help show this help message and exit -s DIR, --source_dir=DIR The source directory where the blog posts are located. [default: '.'] -o DIR, --output_dir=DIR The directory where all the generated files are written. If it does not exist it is created.[default: 'output'] -c FILE, --conf=FILE The configuration file to use. If the file is not present in the current directory, the source directory is searched. [default: 'config.py'] -q, --quiet Do not output anything except critical error messages Configuration file ------------------ Weblog's configuration file is a Python script. If you don't know Python, don't worry, the syntax is straightforward and you need very little knowledge to get started with Weblog. Example ``config.py``:: title = "Blog's title" url = "http://example.com" description = "A sample blog" author = "Me " encoding = "latin-1" post_per_page = 10 source_dir = "path/to/my/posts" output_dir = "path/to/output/directory" To learn more about Python's syntax read the `Python tutorial`_. .. _Python tutorial: http://docs.python.org/tutorial/index.html All fields are optionals except `url` which is needed to generate Atom feed correctly. If the field is not present, you will just get a warning. This way you can start using Weblog without even having a configuration file. title The blog's title. It appears at the top of the homepage and in the page's title. 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. It should be present, otherwise Atom feed wont work correctly. description A short description of your blog. Like My "favorite books reviews", or "Dr. Spock, publications about electronics". source_dir The directory containing the post files and the ``templates`` directory. You can organize the files by creating subfolders in the source directory. Weblog visits and load files in all the subdirectories of ``source_dir``, execpt the one listed by ``ignore_dirs``. By default the current directory. ignore_dirs A list of directories to ignore when visiting ``source_dir``. The directory `templates` is always ignored and therefor you don't need to add it to ``ignore_dirs``. By default empty. output_dir The output directory. Generated files are put there. By default ``output``. encoding The default post file's encoding. It is overridden by the ``encoding`` field in the post file. By default it is the operating system's encoding. filesystem_encoding If you are using Microsoft or Mac OS X, you don't need to use this. If you are using an Unix based system, you might need to specify the filesystem's encoding to have proper filenames, for example if your operating system encoding is not the same as your filesystem. By default it is the operating system's encoding. author The default author. It is overridden by the ``author`` field in the post file. It can contain an Email address:: author = "An Example " post_per_page The number of post displayed on the listing page:: post_per_page = 42 Default is 10. feed_limit The maximum number of posts to be included in the Feed file. Default is 10. Note: rss_limit has been renamed to feed_limit. extra_files Additional files to be copied. Typically used to copy CSS style sheets and/or pictures. It is a list of string:: extra_files = ("style.css", "logo.png") Files are copied into ``output_dir``. The path is not preserved: ``style/weblog.css`` gets copied into ``output_dir/weblog.css``; not into ``output_dir/style/weblog.css``. This behavior is likely to change in the future. 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 modification time makes efficient caching possible. .. _rsync: http://samba.anu.edu.au/rsync/ Need more help? --------------- Don't hesitate to ask questions about Weblog: http://groups.google.com/group/weblog-users or weblog-users@googlegroups.com To report a bug, request a feature: http://bitbucket.org/henry/weblog/issues/ .. vim:se tw=79 sw=2 ts=2 et: weblog+jinja2+markdown2-2.5/doc/install.rst010064400017500000000000000000301133707606200177120ustar00henrywheel.. include:: ../INSTALL weblog+jinja2+markdown2-2.5/doc/short_description.txt010064400017500000000000000001761133707606200220300ustar00henrywheelSimple blog publisher. Read structured text files and generate static HTML / Feed files. Weblog aims to be simple and robust. weblog+jinja2+markdown2-2.5/doc/tutorial.rst010064400017500000000000000076531133707606200201310ustar00henrywheelTutorial ======== Weblog is a file system based Blog publisher. It works like a compiler. A compiler reads source files from the disk and produces an executable; Weblog reads structured text files and produces a Blog. Here is a quick overview of what is possible to do with Weblog. Create a Blog ------------- Before writing your first blog post, you must setup Weblog. Create a new directory which will contains all the files related to your Blog. Let's call it ``my_blog``. Inside ``my_blog`` create a file ``config.py`` containing:: title = 'My Blog' url = 'http://my_blog.example.com/' Change the values of ``title`` and ``url`` to whatever you like. You can already run Weblog from your blog's directory:: $ cd my_blog/ $ weblog publish Alternatively you can specify your blog directory via the option ``-s``, for example if the directory is in ``/path/to/``:: $ weblog -s /path/to/my_blog/ This will create a new directory ``output`` containing an empty blog. Preview it by opening the ``output/index.html`` with a browser. You can also specify the name and the location of the output directory via the ``-o`` option:: $ weblog -s /path/to/my_blog/ -o /tmp/weblog_output First post ---------- Let's publish something now. Create a file named ``first_post.txt`` in your blog directory containing:: title: My first post This is my very first post using Weblog. Re-run Weblog. Now the ``output/index.html`` page contains your new entry. You will see the title `My first post`, and below it the publication date. Post's structure ~~~~~~~~~~~~~~~~ The post file starts with a list of parameters. All the parameters are optional, except ``title``:: title: My first post You can also specify the author by adding an ``author`` parameter:: title: My first post author: Terry Scott You can add the author's Email after the author's name; it will automatically be recognised as an Email address, and the corresponding link will be added:: author: Terry Scott After these parameters, there is a blank line, which separate the parameters from the content. Don't forget it when composing! Formatting post content ----------------------- Let's add more content to the Blog. It is a little bit empty right now. Create a second file ``second_post.txt``:: title: A richer post This second post demonstrate the possibilities offered by Markdown, the markup language used in Weblog. Here is a second paragraph. *Emphased words*, and **Strong words**. - A list element - Another list element You can also quote text: > A silly quotation You can also have monospaced text, useful for code: print "Hello World" Now you have a second entry in your blog. This entry has some fancy formatting because the text above is using the Markdown_ syntax. Markdown_ markup is automatically turned into HTML. This way you can write your blog posts without learning HTML. But you can also use HTML if you want to:: title: HTML is available too

A paragraph

  • A list item
  • Antoher list item

Emphased words, and Strong words.

Adding a picture ---------------- Create a directory named ``images`` in your blog's directory. That's where the images will be stored. Copy a picture you would like to publish into ``images``. Let's call it ``weblog.png``. ``post_image.txt``:: title: Posting an image files: images/weblog.png ![A random image](images/weblog.png) The `files` parameter tells Weblog to copy `images/weblog.png` into the output directory. Note that the path is preserved; the file is copied to `output/images/weblog.png`. You can copy all kinds of files, not just images. What next? ---------- To learn more about Weblog and how to use it check :ref:`reference_manual` and how to customize its appearance check :ref:`style`. .. _Markdown: http://daringfireball.net/projects/markdown/syntax#overview .. vim:se tw=79 sw=2 ts=2 et: weblog+jinja2+markdown2-2.5/doc/update.sh010075500017500000000000000003151133707606200173410ustar00henrywheel#!/bin/ksh dst=${DST:-"${HOME}/henry.precheur.org/weblog/"} if [[ -d $dst ]]; then ${HOME}/env/sphinx/bin/sphinx-build -b html -E . $dst || echo 'Error' else echo "$dst doesn't exist" fi weblog+jinja2+markdown2-2.5/.hg_archival.txt010064400017500000000000000002231133707606200200370ustar00henrywheelrepo: 00ecfb3367fecf6d8ba7a94c3f995bc789c18d1e node: 35376e9bf8d1e47e534a8065262615d1c784d9e9 branch: default latesttag: 2.4 latesttagdistance: 16 weblog+jinja2+markdown2-2.5/.hgignore010064400017500000000000000001011133707606200165470ustar00henrywheelsyntax: glob *.pyc *~ .*.swp build weblog.egg-info doc/.build/* weblog+jinja2+markdown2-2.5/.hgtags010064400017500000000000000020701133707606200162310ustar00henrywheel9bafbb9dfb928172d988390ea61932b610278ea3 0.1 7053f6c08fab7af1c5b76d78a9bb6e41fe6a8b5a 0.2 ebe752dc0a655b451babdc2acb6027a523ac8474 0.3 9cc6b91a2fb95946b5443461201c8a57ad301a53 0.4 85db8e1cb11890a15f38b3d161dc59962e00b135 0.5 fcd5f323c67112916c0d43d776f52a96064bef53 0.5 44abff8da985b456545fa393a2f634932400476b 0.5 a0a1dd94b8b2371f45536e90ee03074dae314f71 0.6 657340b5fa4b2a1747e139809da6e576d7699290 0.7 f4075497305adf1cada74fa556a227049a2ccae5 0.8 8377a76875c476b8697cbfc25be7b3d1fe961028 0.9 2b7a9f4e897d42683ac16491822eddeab8f5b3b7 1.0 1ed0521ffc52335e6560d2135b0f85dc4aab01b2 1.0 e54c184ffac370252b0f34933a307cf741f92f97 1.1 96cef61f3ee4410144cb01064177b0fa1d03380f 1.2 09f6a45625e6b75335109406a7755b993cb137de 1.3 ef58aed7dbc44603f3ca10af11a74df407a1943d 2.0 8b9be5dcd7fac0a9b9a4e51a84314f7c6d3bd0f2 2.1 5f8669ae7a1aeabe763c0ac1c5d8a9a82c3d2c5a 2.1 57c5f28ca0550ec5ddebaadb559add2727e771aa 2.2 7d13ebee58c5b3b7934a6013a6361d8215f260ab 2.3 e71349aa1608fe904c79504d9b3117b8e38dc3aa 2.4 e71349aa1608fe904c79504d9b3117b8e38dc3aa 2.4 4a48bc0e343915c2ce760d9aad0bd0c8915a0cad 2.4 weblog+jinja2+markdown2-2.5/COPYING010064400017500000000000000120171133707607500160140ustar00henrywheelCopyright (c) 2007, 2008, 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. Jinja 2 license: Copyright (c) 2009 by the Jinja Team, see AUTHORS for more details. Some 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. * The names of the contributors may not 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. Jinja 2 authors: Jinja is written and maintained by the Jinja Team and various contributors: Lead Developer: - Armin Ronacher Developers: - Christoph Hack - Georg Brandl Contributors: - Bryan McLemore - Mickaël Guérin - Cameron Knight - Lawrence Journal-World. - David Cramer Patches and suggestions: - Ronny Pfannschmidt - Axel Böhm - Alexey Melchakov - Bryan McLemore - Clovis Fabricio (nosklo) - Cameron Knight - Peter van Dijk (Habbie) - Stefan Ebner - Rene Leonhardt Markdown 2 license: This implementation of Markdown is licensed under the MIT License: The MIT License Copyright (c) 2008 ActiveState Software Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. All files in a *source package* of markdown2 (i.e. those available on pypi.python.org and the Google Code project "downloads" page) are under the MIT license. However, in the *subversion repository* there are some files (used for performance and testing purposes) that are under different licenses as follows: - perf/recipes.pprint Python License. This file includes a number of real-world examples of Markdown from the ActiveState Python Cookbook, used for doing some performance testing of markdown2.py. - test/php-markdown-cases/... test/php-markdown-extra-cases/... GPL. These are from the MDTest package announced here: http://six.pairlist.net/pipermail/markdown-discuss/2007-July/000674.html - test/markdown.py GPL 2 or BSD. A copy (currently old) of Python-Markdown -- the other Python Markdown implementation. - test/markdown.php BSD-style. This is PHP Markdown (http://michelf.com/projects/php-markdown/). - test/Markdown.pl: BSD-style A copy of Perl Markdown (http://daringfireball.net/projects/markdown/). weblog+jinja2+markdown2-2.5/INSTALL010064400017500000000000000041631133707606200160110ustar00henrywheelInstallation ============ Requirements ------------ - Python 2.5+ - `Jinja 2.0+ `_ Optionally to use the `Markdown syntax `_, install `markdown2 `_, or for `reStructuredText `_, install `docutils `_. Download -------- `Download Weblog 2.5 `_ A standalone version of Weblog which includes Jinja2 and markdown2 is also available: `Download Weblog 2.5 standalone version `_ You can also get it from Weblog's page on the `Python package Index `_. You can get the development version of Weblog using the Mercurial_ repository http://bitbucket.org/henry/weblog/:: $ hg clone http://bitbucket.org/henry/weblog/ .. _Mercurial: http://www.selenic.com/mercurial/ How to install -------------- Download Weblog's tarball and 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. You can also use `easy_install` or `pip`, ``pip install weblog`` will install Weblog and all its dependencies. Optionally you can install markdown2 and docutils to use the Markdown and reStructuredText syntaxes: ``pip install markdown2 docutils``. Hacking Weblog -------------- Weblog comes with tests, you can run them from the root of the source directory:: python test.py If you plan to modify Weblog, I recommend to install `nose`_ a test runner and `coverage`_ a tool for measuring code coverage of Python programs. Like Weblog, you can install them with `pip` or `easy_install`:: pip install nose coverage To run the tests:: nosetests --with-doctest --with-coverage --cover-package=weblog .. _nose: http://somethingaboutorange.com/mrl/projects/nose/ .. _coverage: http://nedbatchelder.com/code/coverage/ weblog+jinja2+markdown2-2.5/MANIFEST.in010064400017500000000000000003611133707606200165120ustar00henrywheelinclude README include COPYING include INSTALL include test.py include weblog_run.py recursive-include weblog/templates *.tmpl recursive-include examples * recursive-include test * recursive-include doc * prune doc/.build prune doc/_build weblog+jinja2+markdown2-2.5/README010064400017500000000000000006201133707606200156320ustar00henrywheelWeblog 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.rst > weblog.html Jinja is needed to use Weblog (http://jinja.pocoo.org) weblog+jinja2+markdown2-2.5/test004075500017500000000000000000001133707606200156575ustar00henrywheelweblog+jinja2+markdown2-2.5/test/date004075500017500000000000000000001133707606200165745ustar00henrywheelweblog+jinja2+markdown2-2.5/test/date/no_date.txt010064400017500000000000000000541133707606200210210ustar00henrywheeltitle: Post with no date Post with no date weblog+jinja2+markdown2-2.5/test/duplicate004075500017500000000000000000001133707606200176315ustar00henrywheelweblog+jinja2+markdown2-2.5/test/duplicate/config.py010064400017500000000000000001531133707606200215230ustar00henrywheeltitle = 'Test blog' url = 'http://blog.test.org' description = 'Test blog' author = 'test ' weblog+jinja2+markdown2-2.5/test/duplicate/post1.html010064400017500000000000000000571133707606200216430ustar00henrywheeltitle: duplicate date: 2007-01-01 duplicate 1 weblog+jinja2+markdown2-2.5/test/duplicate/post2.html010064400017500000000000000000571133707606200216440ustar00henrywheeltitle: duplicate date: 2007-01-01 duplicate 1 weblog+jinja2+markdown2-2.5/test/empty004075500017500000000000000000001133707606200170155ustar00henrywheelweblog+jinja2+markdown2-2.5/test/empty/config.py010064400017500000000000000000001133707606200206760ustar00henrywheelweblog+jinja2+markdown2-2.5/test/encoding004075500017500000000000000000001133707606200174455ustar00henrywheelweblog+jinja2+markdown2-2.5/test/encoding/latin-1.html010064400017500000000000000001041133707606200216470ustar00henrywheeltitle: latin post ÖÉÈÄ ... date: 2008-02-04 encoding: latin-1 Öéèä weblog+jinja2+markdown2-2.5/test/encoding/config.py010064400017500000000000000001761133707606200213440ustar00henrywheeltitle = 'Test blog' url = 'http://blog.test.org' description = 'Test blog' author = 'test ' encoding = 'utf-8' weblog+jinja2+markdown2-2.5/test/encoding/utf-8.html010064400017500000000000000000721133707606200213510ustar00henrywheeltitle: UTF-8 post ÖÉÈÄ ... date: 2008-02-03 Öéèä weblog+jinja2+markdown2-2.5/test/full_url004075500017500000000000000000001133707606200175035ustar00henrywheelweblog+jinja2+markdown2-2.5/test/full_url/config.py010064400017500000000000000001761133707606200214020ustar00henrywheeltitle = 'Test blog' url = 'http://blog.test.org' description = 'Test blog' author = 'test ' encoding = 'utf-8' weblog+jinja2+markdown2-2.5/test/full_url/utf-8.html010064400017500000000000000002501133707606200214050ustar00henrywheeltitle: UTF-8 post ÖÉÈÄ ... date: 2008-02-03 Öéèä
äyÔÀ Weblog weblog+jinja2+markdown2-2.5/test/simple004075500017500000000000000000001133707606200171505ustar00henrywheelweblog+jinja2+markdown2-2.5/test/simple/config.py010064400017500000000000000001531133707606200210420ustar00henrywheeltitle = 'Test blog' url = 'http://blog.test.org' description = 'Test blog' author = 'test ' weblog+jinja2+markdown2-2.5/test/simple/post1.html010064400017500000000000000000451133707606200211570ustar00henrywheeltitle: post1 date: 2007-01-01 post1 weblog+jinja2+markdown2-2.5/test/simple/post2.html010064400017500000000000000000441133707606200211570ustar00henrywheeltitle: post2 date: 2007-6-15 post2 weblog+jinja2+markdown2-2.5/test/simple/post3.html010064400017500000000000000000451133707606200211610ustar00henrywheeltitle: post3 date: 2007-12-31 post3 weblog+jinja2+markdown2-2.5/test/template_encoding004075500017500000000000000000001133707606200213405ustar00henrywheelweblog+jinja2+markdown2-2.5/test/template_encoding/templates004075500017500000000000000000001133707606200233365ustar00henrywheelweblog+jinja2+markdown2-2.5/test/template_encoding/templates/base.html.tmpl010064400017500000000000000014001133707606200261600ustar00henrywheel {% block title %}{{ title|escape|decode }}{% endblock %} {% block feed %} {% endblock %} {% block head %} {% endblock %} {% block header %} {% endblock %}
{% block content %}{% endblock %}
{% block footer %} {% endblock %}

Some UTF-8 characters: ËÃØ ...

{# vim:set ft=htmljinja: #} weblog+jinja2+markdown2-2.5/test/template_encoding/config.py010064400017500000000000000000541133707606200232320ustar00henrywheelurl = 'http://test.org/' encoding = 'UTF-8' weblog+jinja2+markdown2-2.5/setup.cfg010064400017500000000000000000471133707606200165760ustar00henrywheel[nosetests] verbosity=3 with-doctest=1 weblog+jinja2+markdown2-2.5/setup.py010064400017500000000000000026061133707606200164720ustar00henrywheeltry: from setuptools import setup except: from distutils.core import setup from os.path import join, dirname import weblog short_description = open(join(dirname(__file__), 'doc', 'short_description.txt')).read() long_description = (short_description + '''\n To get news and updates about Weblog check the author's blog: http://henry.precheur.org/ or check Weblog's homepage: http://henry.precheur.org/weblog/''') setup(name="weblog", version=weblog.__version__, packages=['weblog'], package_dir={'weblog': 'weblog'}, package_data={'weblog': ['templates/*.tmpl']}, requires=['Jinja2'], install_requires=['Jinja2'], # 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=short_description, long_description=long_description, license="ISCL", keywords="weblog blog journal diary atom", url="http://henry.precheur.org/weblog/", classifiers=[ 'Development Status :: 5 - Production/Stable', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content :: News/Diary', 'Intended Audience :: End Users/Desktop', 'Programming Language :: Python'], entry_points=dict(console_scripts=('weblog = weblog:main',))) weblog+jinja2+markdown2-2.5/test.py010064400017500000000000000306541133707606200163150ustar00henrywheelfrom __future__ import with_statement import os import shutil import tempfile import unittest import email import datetime from optparse import Values from weblog.markup import markups from weblog.page import Page, Error, Author from weblog.publish import load_posts from weblog.date import command_date from weblog.publish import command_publish from weblog import configuration from weblog.html_full_url import FullUrlHtmlParser from weblog.utf8_html_parser import UTF8HTMLParser from weblog.rfc3339 import LocalTimeTestCase _DIRNAME = os.path.dirname(__file__) def _test_filename(filename): return os.path.join(_DIRNAME, 'test', filename) def _default_dict(**kwargs): d = dict(author=Author(''), url='/', encoding='ascii', filesystem_encoding='ascii', ignore_dirs=['templates'], post_per_page=10, feed_limit=10) d.update(kwargs) return d class TestSimpleLoad(unittest.TestCase): def test_load_post_list(self): post_list = load_posts(_test_filename('simple'), _default_dict()) 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): self.assertRaises(Error, load_posts, _test_filename('encoding'), _default_dict()) def test_load_post_list_encoding(self): post_list = load_posts(_test_filename('encoding'), _default_dict(encoding='UTF-8')) self.assertEqual(len(post_list), 2) sorted_list = sorted(post_list) self.assertEqual(sorted_list[0].title, u'UTF-8 post \xd6\xc9\xc8\xc4 ...') self.assertEqual(sorted_list[0].body, u'\xd6\xe9\xe8\xe4\n') self.assertEqual(sorted_list[1].title, u'latin post \xd6\xc9\xc8\xc4 ...') self.assertEqual(sorted_list[1].body, u'\xd6\xe9\xe8\xe4\n') def test_load_posts_duplicate(self): self.assertRaises(IOError, load_posts, _test_filename('duplicate'), _default_dict()) class TestPage(unittest.TestCase): def test_empty(self): self.assertRaises(ValueError, Page) def test_simple(self): sample_post = ('title: test\ndate: 2008-1-1\nauthor: test author\n' 'encoding: ascii\n\ntest.') post = Page(content=sample_post) self.assertEqual(post.title, u'test') self.assertEqual(post.date, datetime.date(2008, 1, 1)) self.assertEqual(post.author, u'test author') self.assertEqual(post.encoding, 'ascii') self.assertEqual(post.body, u'test.') def test_encoding(self): sample_post = (u'title: Test UTF-8 \xdcTF-8 ?\n' u'author: Henry Pr\xeacheur \n' u'encoding: utf8\n\n' u'blah \xdcTF-8.').encode('utf8') # convert to str post = Page(content=sample_post) self.assertEqual(post.title, u'Test UTF-8 \xdcTF-8 ?') self.assertEqual(post.author, u'Henry Pr\xeacheur ') self.assertEqual(post.encoding, u'utf8') self.assertEqual(post.body, u'blah \xdcTF-8.') def test_no_title(self): self.assertRaises(Error, Page, content='No title in this post') def test_no_payload(self): try: Page(content='title: no payload\ndate: 2008-1-1') except Error, e: self.assertEqual(e.args, (': no body',)) else: self.failUnless(False) # Should not be there def test_bad_encoding(self): sample_post = ('title: bad encoding\ndate: 2008-1-1\n' 'encoding: bad-encoding\n\ntest') try: Page(content=sample_post) except Error, e: self.assertEqual(e.args, (': unknown encoding: ' 'bad-encoding',)) else: self.failUnless(False) # Should not be there self.assertRaises(Error, Page, content=sample_post) def test_empty_author(self): post = Page(content='title: test\n\ntest') self.assertEqual(post.author, u'') self.assertEqual(post.author.name(), u'') self.assertEqual(post.author.email(), u'') def test_default_author(self): post = Page(content='title: test\n\ntest', default_author=u'Test ') self.assertEqual(post.author, u'Test ') self.assertEqual(post.author.name(), u'Test') self.assertEqual(post.author.email(), u'test@test.org') def test_author(self): post = Page(content='title:test\nauthor: Test \n\ntest') self.assertEqual(post.author, u'Test ') self.assertEqual(post.author.name(), u'Test') self.assertEqual(post.author.email(), u'test@test.org') def test_bad_date(self): sample_post = 'title: bad encoding\ndate: 20 bad date 08-1-1\n\ntest' try: Page(content=sample_post) except Error, e: self.assertEqual(e.args, (": Unable to parse date " "'20 bad date 08-1-1'\n" "(Use YYYY-MM-DD [[HH:MM]:SS] format)",)) else: self.failUnless(False) # Should not be there def test_file_no_date(self): p = Page(_test_filename('date/no_date.txt')) self.assert_(p.date) self.assert_(isinstance(p.date, datetime.date)) def test_html_markup(self): p = Page(content='title: html\nmarkup: html\n\n

html
test

') self.assertEqual(p.get_html(), u'

html
test

') self.assertEqual(p.get_xhtml(), u'

html
test

') if 'markdown' in markups: def test_markdown_markup(self): p = Page(content=('title: markdown\nmarkup: markdown\n\n' '*boo*\n\n---')) self.assertEqual(p.get_html(), u'

boo

\n\n
\n') self.assertEqual(p.get_xhtml(), u'

boo

\n\n
\n') if 'restructuredtext' in markups: def test_restructuredtext_markup(self): p = Page(content=('title: rst\nmarkup: restructuredtext\n\n' '*boo*\n\n.. image:: images/biohazard.png')) self.assertEqual(p.get_html(), u'

boo

\n' u'images/biohazard.png\n') self.assertEqual(p.get_xhtml(), u'

boo

\n' u'images/biohazard.png\n') def test_cmp(self): file1 = 'title: 1\ndate: 2008-1-1\n\ntest' post1 = Page(content=file1) post2 = Page(content='title: 2\ndate: 2007-12-31\n\ntest') self.assert_(post1 > post2) self.assertNotEqual(post1, post2) self.assertEqual(post1, post1) self.assertEqual(post1, Page(content=file1)) self.assertEqual([post2, post1].index(Page(content=file1)), 1) class TempDirMixin(object): def setUp(self): self.tempdir = tempfile.mkdtemp() def tearDown(self): shutil.rmtree(self.tempdir) class TestDate(TempDirMixin, unittest.TestCase): 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(): open(filename, 'w').write('title: Some title\n\nSome content') file_without_date() command_date([filename, '2008-1-1']) message = email.message_from_file(open(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(): open(filename, 'w').write('title: Some title\ndate: 2008-12-31\n' '\nSome content') file_with_date() command_date([filename, '2008-1-1']) message = email.message_from_file(open(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]) message = email.message_from_file(open(filename)) self.assert_('date' in message) def test_date_empty(self): filename = os.path.join(self.tempdir, 'empty_date.html') open(filename, 'w').write('title: title\n\ncontent') command_date([filename]) message = email.message_from_file(open(filename)) self.assert_('date' in message) self.assert_(message['date'] >= str(datetime.date.today())) open(filename, 'w').write('title: title\ndate: 2010-2-4\n\ncontent') command_date([filename]) message = email.message_from_file(open(filename)) self.assert_('date' in message) self.assert_(message['date'] >= str(datetime.date.today())) def test_format(self): from weblog.date import _format_date self.assertEqual(_format_date(datetime.\ datetime(2008, 1, 1, 20, 40, 23, 345)), '2008-01-01 20:40:23') self.assertEqual(_format_date(datetime.datetime(2008, 1, 1)), '2008-01-01 00:00:00') self.assertEqual(_format_date(datetime.date(2008, 1, 1)), '2008-01-01') self.assertRaises(TypeError, _format_date, datetime.time()) class TestPublish(TempDirMixin, unittest.TestCase): def _test(self, dirname): options = Values(dict(source_dir=_test_filename(dirname), output_dir=self.tempdir, configuration_file='config.py', debug=False)) command_publish(None, options) def test_empty(self): self._test('empty') def test_encoding(self): self._test('encoding') def test_full_url(self): self._test('full_url') def test_simple(self): self._test('simple') def test_template_encoding(self): self._test('template_encoding') class TestConfiguration(unittest.TestCase): @staticmethod def __config(string=None): f = tempfile.NamedTemporaryFile() if string: f.write(string) f.seek(0) return f _NEEDED_KEYS = ('author', 'url', 'ignore_dirs', 'encoding', 'filesystem_encoding', 'post_per_page', 'feed_limit') def test_empty(self): with self.__config() as f: conf = configuration.read(f.name) self.assert_(isinstance(conf, dict)) self.assert_(all(k in conf for k in self._NEEDED_KEYS)) def test_bad_encoding(self): with self.__config("encoding = 'DOES NOT EXIST'") as f: self.assertRaises(LookupError, configuration.read, f.name) def test_encoding(self): with self.__config("encoding = 'latin-1'") as f: self.assertEqual(configuration.read(f.name)['encoding'], 'latin-1') def test_non_existent(self): self.assertRaises(IOError, configuration.read, '') class TestUrlParser(unittest.TestCase): def test_full_url_parser_attrs(self): self.assertEqual(FullUrlHtmlParser.html_attrs([('href', 'foo?a=1&b=2')]), u'href="foo?a=1&b=2"') def test_anchor(self): '''Anchors shouldn't be rewritten.''' self.assertEqual(FullUrlHtmlParser.html_attrs([('href', '#foo')]), u'href="#foo"') def test_utf8_html_parser_attrs(self): self.assertEqual(UTF8HTMLParser.html_attrs([('alt', 'quote """')]), u'alt="quote """"') if __name__ == '__main__': try: import nose nose.main() except ImportError: unittest.main() weblog+jinja2+markdown2-2.5/weblog004075500017500000000000000000001133707606200161575ustar00henrywheelweblog+jinja2+markdown2-2.5/weblog/configuration.py010064400017500000000000000025361133707606200214620ustar00henrywheelimport logging import codecs import locale from weblog.page import Author def _default(config, key, default): if key not in config: config[key] = default def _encoding(key, config): if key in config: codecs.lookup(config[key]) # Check that the encoding exists else: config[key] = locale.getpreferredencoding() def read(filename): config = dict() try: execfile(filename, config) except Exception: logging.error('Unable to read configuration file "%s"' % filename) raise del config['__builtins__'] # clean-up the dictionnary config['author'] = Author(config.get('author', '')) if 'url' not in config: logging.warning('There is no url parameter in "%s". The atom feed ' 'will be incorrectly generated.' % filename) config['url'] = '/' _encoding('encoding', config) _encoding('filesystem_encoding', config) if 'ignore_dirs' in config: if not isinstance(config['ignore_dirs'], (tuple, list, set)): raise TypeError('ignore_dirs must be a list of strings') if 'templates' not in config['ignore_dirs']: config['ignore_dirs'].append('templates') else: config['ignore_dirs'] = ['templates'] _default(config, 'post_per_page', 10) _default(config, 'feed_limit', 10) return config weblog+jinja2+markdown2-2.5/weblog/__init__.py010064400017500000000000000047211133707606200203500ustar00henrywheel__author__ = u'Henry Pr\u00EAcheur ' __version__ = '2.5' __license__ = 'ISCL' def main(args=None): import logging from optparse import OptionParser, SUPPRESS_HELP from publish import command_publish from date import command_date _COMMANDS = ('publish', 'date') parser = OptionParser(usage=('%%prog [option] command\n\nCommands:\n ' + '\n '.join(_COMMANDS))) 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='', 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='config.py') 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) (options, args) = parser.parse_args(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: 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) weblog+jinja2+markdown2-2.5/weblog/markup.py010064400017500000000000000037651133707606200201170ustar00henrywheelfrom os.path import splitext _DEPENDENCIES = dict(markdown='markdown2', restructuredtext='docutils') markups = dict(html=lambda x: x) extensions = dict(html='html', htm='html', txt='markdown', mkd='markdown', rst='restructuredtext') try: import markdown2 def markdown(text): return markdown2.markdown(text, html4tags=True, extras={'demote-headers': 2}) markups['markdown'] = markdown except ImportError: pass try: import docutils.core def rst(text): '''Convert reST body to HTML chunk''' parts = docutils.core.publish_parts( source=text, reader_name='standalone', parser_name='restructuredtext', writer_name='html4css1', settings_overrides=dict(initial_header_level=2)) return parts['fragment'] markups['restructuredtext'] = rst except ImportError: pass def filename_extension(filename): '''Return `filename`'s extension without the dot. >>> filename_extension('foo.txt') 'txt' >>> filename_extension('foo..txt') 'txt' ''' return splitext(filename)[-1][1:] def html(text, filename=None, markup=None): '''Turn `text` into HTML. It determine the markup to via `markup`'s value or `filename`'s extensions if `markup` isn't specified. ''' # First determine the markup if not markup: if not filename: raise ValueError('markup or filename need to be specified') ext = filename_extension(filename) try: markup = extensions[ext] except KeyError: raise KeyError('Unable to find markup for file extension %r' % ext) if markup not in markups: msg = 'Unable to use the %s markup' % markup if markup in _DEPENDENCIES: msg += ', please install ' + _DEPENDENCIES[markup] raise ImportError(msg) else: return markups[markup](text) weblog+jinja2+markdown2-2.5/weblog/date.py010064400017500000000000000052671133707606200175340ustar00henrywheelimport sys import logging import datetime import email from page import Page 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__)) def command_date(args): ''' 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) # doctest: +ELLIPSIS Traceback (most recent call last): ... SystemExit: No file specified: ... >>> command_date(['/dev/null', '2008-1000-10']) 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 = Page.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+jinja2+markdown2-2.5/weblog/templates004075500017500000000000000000001133707606200201555ustar00henrywheelweblog+jinja2+markdown2-2.5/weblog/templates/archives.html.tmpl010064400017500000000000000032311133707606200236750ustar00henrywheel{% extends 'base.html.tmpl' %} {% block head %} {{ super() }} {% endblock %} {% block content %}

Archives

    {% for year, posts in posts|groupby('year')|reverse %}
  • {{ year }}
      {% for month, posts in posts|groupby('month')|reverse %}
    • {{ posts[0].date.strftime('%B') }}
        {% for day, posts in posts|groupby('day')|reverse %}
      • {{ day }}
        {% endfor %}
      {% endfor %}
    {% endfor %}
{% endblock %} {% block archives %}{% endblock %} {# vim:set ft=htmljinja sw=4 ts=4 et: #} weblog+jinja2+markdown2-2.5/weblog/templates/base.html.tmpl010064400017500000000000000037731133707606200230160ustar00henrywheel {% block title %}{{ title|escape|decode }}{% endblock %} {% block feed %} {% endblock %} {% block head %} {% endblock %} {% block header %} {% endblock %}
{% block content %}{% endblock %}
{% block footer %} {% endblock %} {# vim:set ft=htmljinja: #} weblog+jinja2+markdown2-2.5/weblog/templates/feed.atom.tmpl010064400017500000000000000023241133707606200227720ustar00henrywheel {{ url|escape }} {{ title|escape }} {% if description %} {{ description|escape }} {% endif %} {{ feed_updated|rfc3339 }} {% if author %} {{ author.name()|escape }} {{ author.email() }} {{ url|escape }} {% endif %} Weblog {% for post in posts %} {{ url }}{{ post.url() }} {{ post.title|escape }} {{ post.date|rfc3339 }} {{ post.author.name()|escape }} {{ post.author.email()|escape }} {{ url|escape }}
{{ post.get_xhtml() }}
{% endfor %}
{# vim: set filetype=jinja ts=4 sw=4 et: #} weblog+jinja2+markdown2-2.5/weblog/templates/index.html.tmpl010064400017500000000000000013671133707606200232100ustar00henrywheel{% extends 'base.html.tmpl' %} {% block content %}

{{ title|escape|decode }}

{% if description %}

{{ description|escape|decode }}

{% endif %} {% for post in posts[:(post_per_page or 10)] %}

{{ post.title|escape|decode }}

{{ post.date.isoformat(' ') if post.date.__class__.__name__ == 'datetime' else post.date.isoformat() }} {%- if post.author %} , by {{ post.author.name()|escape|decode }} <{{ post.author.email()|urlize }}> {% endif %}

{{ post.get_html()|decode }}
{% endfor %} {% endblock %} {# vim:set ft=htmljinja: #} weblog+jinja2+markdown2-2.5/weblog/templates/post.html.tmpl010064400017500000000000000007541133707606200230650ustar00henrywheel{% extends 'base.html.tmpl' %} {% block content %}

{{ title|escape|decode }}

{%- if date.__class__.__name__ == 'datetime' -%} {{ date.isoformat(' ') }} {%- else -%} {{ date.isoformat() }} {%- endif -%} {%- if author %} , by {{ author.name()|decode }} <{{ author.email()|urlize }}> {% endif %}

{{ content|decode }}
{% endblock %} {# vim:set ft=htmljinja: #} weblog+jinja2+markdown2-2.5/weblog/html_full_url.py010064400017500000000000000105351133707606200214610ustar00henrywheelfrom urlparse import urljoin from utf8_html_parser import UTF8HTMLParser class FullUrlHtmlParser(UTF8HTMLParser): ''' Parse an HTML document and transform relative URI to absolute URI. Prepending ``base_url`` to them:: >>> p = FullUrlHtmlParser('http://www.example.com') >>> p.feed(u'') >>> p.get_value() u'' Non-external resource are ignored:: >>> p = FullUrlHtmlParser('http://www.example.com') >>> p.feed('') >>> p.get_value() u'' A more complex example:: >>> p.reset() >>> p.feed(r""" ... ... foo ... ... some random text. ... bar ... »~ ... ... ... More ..........""") >>> print p.get_value() #doctest: +NORMALIZE_WHITESPACE foo some random text. bar »~ More .......... ''' def __init__(self, base_url): UTF8HTMLParser.__init__(self) self.base_url = base_url if base_url[-1] == '/' else base_url + '/' def make_full_url(self, attr, attrs): ''' Change ``attrs[attr]`` from a relative URI to an absolute URI. >>> p = FullUrlHtmlParser('http://www.example.com') >>> tuple(p.make_full_url('src', (('src', 'page'), ('foo', 'bar')))) (('src', 'http://www.example.com/page'), ('foo', 'bar')) >>> tuple(p.make_full_url('src', tuple())) () Note that anchors are not rewritten. >>> tuple(p.make_full_url('href', [('href', '#foo')])) (('href', '#foo'),) ''' for key, value in attrs: if key == attr and not value.startswith('#'): yield(key, urljoin(self.base_url, value)) else: yield (key, value) def rewrite_tag(self, tag, attrs, endtag=u''): ''' Rewrite URLs for tags a, img, object, script, area, & iframe. >>> p = FullUrlHtmlParser('http://www.example.com') >>> p.rewrite_tag('a', (('href', 'foo'),)) u'' >>> p.rewrite_tag('img', (('src', 'pic.png'), ('width', '100'))) u'' ''' if attrs: if tag in (u'img', u'script', u'iframe'): attrs = self.make_full_url(u'src', attrs) elif tag in (u'a', u'area'): attrs = self.make_full_url(u'href', attrs) elif tag == u'object': attrs = self.make_full_url(u'data', attrs) attrs = self.make_full_url(u'codebase', attrs) return u'<%s %s%s>' % (tag, self.html_attrs(attrs), endtag) else: return u'<%s%s>' % (tag, endtag) def handle_starttag(self, tag, attrs): self.output.append(self.rewrite_tag(tag, attrs)) def handle_startendtag(self, tag, attrs): self.output.append(self.rewrite_tag(tag, attrs, endtag=u'/')) 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', '') u'' >>> html_full_url('http://example.com', '') u'' ''' p = FullUrlHtmlParser(base_url) p.feed(text) return p.get_value() if __name__ == '__main__': import doctest doctest.testmod() weblog+jinja2+markdown2-2.5/weblog/html_to_xhtml.py010064400017500000000000000033371133707606200214750ustar00henrywheelimport logging from htmlentitydefs import name2codepoint, entitydefs from utf8_html_parser import UTF8HTMLParser class _Parser(UTF8HTMLParser): ''' Parse an HTML document and convert it to valid xhtml. ''' _EMPTY_HTML_TAGS = ('area', 'base', 'basefont', 'br', 'col', 'frame', 'hr', 'img', 'input', 'isindex', 'link', 'meta', 'param') _XML_ENTITIES = ('amp', 'gt', 'lt', 'quot') def handle_starttag(self, tag, attrs): if tag in self._EMPTY_HTML_TAGS: self.handle_startendtag(tag, attrs) elif attrs: self.output.append(u'<%s %s>' % (tag, self.html_attrs(attrs))) else: self.output.append(u'<%s>' % tag) def handle_startendtag(self, tag, attrs): if attrs: self.output.append(u'<%s %s />' % (tag, self.html_attrs(attrs))) else: self.output.append('<%s />' % tag) def handle_entityref(self, name): if name in self._XML_ENTITIES: self.output.append(u'&%s;' % name) elif name in name2codepoint: self.output.append(u'&#%d;' % name2codepoint[name]) else: logging.warning('Unknown XHTML entiry: &%s;' % name); def html_to_xhtml(html): ''' Convert html to xhtml >>> html_to_xhtml('

Hello
World

') u'

Hello
World

' >>> html_to_xhtml('Test & —') u'Test & —' >>> html_to_xhtml("
test") u'test' >>> html_to_xhtml("— > & &unknown;") u'— > & ' ''' p = _Parser() p.feed(html) return p.get_value() if __name__ == '__main__': import doctest doctest.testmod() weblog+jinja2+markdown2-2.5/weblog/page.py010064400017500000000000000217551133707606200175330ustar00henrywheelimport re import codecs import locale import logging import datetime from email import message_from_file, message_from_string from os import stat, path from urllib import quote from markup import html from html_to_xhtml import html_to_xhtml class Error(Exception): def __init__(self, filename, message): Exception.__init__(self, '%s: %s' % (filename, message)) class Author(unicode): ''' Extract the name and email from the passed string. The Email address must be between chevrons (< and >). >>> author = Author(u'User Name ') >>> author.name() u'User Name' >>> author.email() u'user@example.org' If the string doesn't contains an Email address, the name is the full string and the email address is an empty string. >>> author = Author(u'Hello World!').name() >>> author.name() u'Hello World!' >>> author.email() u'' ''' _AUTHOR_REGEX = re.compile(r''' ^ (?P.+[^<])\b \s* < (?P\S+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+) >$''', re.UNICODE|re.VERBOSE) def name(self): r = self._AUTHOR_REGEX.match(self) if r: return r.group('name') else: return self def email(self): r = self._AUTHOR_REGEX.match(self) if r: return r.group('email') else: return u'' class Page(object): def _error(self, *args, **kwargs): return Error(self.source_filename, *args, **kwargs) def __init__(self, filename=None, content=None, markup=None, default_encoding=u'UTF-8', default_author=u'', filesystem_encoding=locale.getpreferredencoding()): self._filename = filename if content: msg = message_from_string(content) elif filename: msg = message_from_file(open(filename)) else: raise ValueError('filename or content are required.') headers = dict(msg.items()) body = msg.get_payload() self.markup = headers.pop('markup', markup) self.encoding = unicode(headers.pop('encoding', default_encoding)) try: codecs.lookup(self.encoding) except LookupError, e: raise self._error(str(e)) if not body: raise self._error('no body') try: self.body = unicode(body, self.encoding) except UnicodeDecodeError, e: # find error line number for line_number, line in enumerate(msg.as_string().splitlines()): try: line.decode(self.encoding) except UnicodeDecodeError, e: break # line_number starts at 0, real line number == line_number + 1 raise self._error('Bad encoding line %d, %s' % (line_number + 1, e)) del msg # XXX # Copy all field "into the object" and convert string to unicode. for key, value in headers.iteritems(): if not key.islower(): logging.warning('%r will be renamed to %r' % (key, key.lower())) try: key = key.encode('ascii').lower() except UnicodeDecodeError, e: raise self._error("Page attributes can't contain " 'non-ascii characters: %r' % key) if hasattr(self, key): raise self._error('%s is reserved' % key) try: self.__dict__[key] = unicode(value, self.encoding) except UnicodeDecodeError, e: raise self._error("Unable to decode attribute's %s value" % key) # XXX title might not be required in future versions if not hasattr(self, 'title'): raise self._error('No title') if not hasattr(self, 'author'): self.author = Author(default_author) else: self.author = Author(self.author) # If no date was specified use the file's modification time. if not hasattr(self, 'date'): if not self._filename: self.date = None else: # Get the date from the file's mtime and issue a warning self.date = datetime.datetime.fromtimestamp(self.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 self._error(str(e)) if not hasattr(self, 'slug'): try: self.slug = self.title.encode(filesystem_encoding, 'replace') except UnicodeDecodeError, e: raise self._error('Bad encoding in title') for x in '/\\ ': self.slug = self.slug.replace(x, '_') # Transform the 'files' field into a list of string if hasattr(self, 'files'): self.files = self.files.split() else: self.files = list() def directories(self): r'''Return the list of directories where the page is stored. >>> p = Page(content='title: test\ndate: 2009-9-25\n\ntest') >>> p.directories() ('2009', '9', '25') ''' return (str(self.date.year), str(self.date.month), str(self.date.day)) def filename(self): r'''Return the filename where to page should be stored. >>> p = Page(content='title: test\ndate: 2009-9-25\n\ntest') >>> p.filename() '2009/9/25/test.html' ''' return path.join(*(self.directories() + (self.slug + '.html',))) def url(self): r'''Returns url of the page. >>> content = """title: test ... date: 2008-1-1 ... ... test""" >>> Page(content=content).url() '2008/1/1/test.html' The url is URL-quoted: >>> Page(content='title: @!%\ndate: 2009-10-1\n\ntest').url() '2009/10/1/%40%21%25.html' ''' return '/'.join(self.directories() + (quote(self.slug) + '.html',)) @property def mtime(self): if not hasattr(self, '_mtime'): self._mtime = stat(self._filename).st_mtime return self._mtime _DATE_FORMAT_LIST = ('%Y-%m-%d', '%y-%m-%d') _DATETIME_FORMAT_LIST = tuple(d + ' ' + t for d in _DATE_FORMAT_LIST for t in ('%H:%M', '%H:%M:%S')) @staticmethod def parse_date(date_): """ >>> Page.parse_date('2006-1-1') datetime.date(2006, 1, 1) >>> Page.parse_date('2007-12-31') datetime.date(2007, 12, 31) >>> Page.parse_date('2008-4-05 12:35') datetime.datetime(2008, 4, 5, 12, 35) >>> Page.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) >>> Page.parse_date(2007) Traceback (most recent call last): ... TypeError: strptime() argument 1 must be string, not int """ for date_format in Page._DATE_FORMAT_LIST: try: return datetime.datetime.strptime(date_, date_format).date() except ValueError: continue for date_format in Page._DATETIME_FORMAT_LIST: try: return datetime.datetime.strptime(date_, date_format) except ValueError: continue raise ValueError('Unable to parse date \'%s\'\n' '(Use YYYY-MM-DD [[HH:MM]:SS] format)' % (date_)) @property def source_filename(self): if not self._filename: return '' else: return self._filename def get_html(self): return html(self.body, filename=self._filename, markup=getattr(self, 'markup', None)) def get_xhtml(self): return html_to_xhtml(self.get_html()) @property def year(self): return self.date.year @property def month(self): return self.date.month @property def day(self): return self.date.day def __cmp__(self, other): return cmp(str(self.date) + self.title, str(other.date) + other.title) def __hash__(self): return hash(self.filename()) def __repr__(self): return '<%s(%r, %r)>' % (self.__class__.__name__, self.title, self.date) if __name__ == '__main__': import doctest doctest.testmod() weblog+jinja2+markdown2-2.5/weblog/publish.py010064400017500000000000000125101133707606200202520ustar00henrywheelimport os import datetime import logging import codecs from shutil import copy import weblog import template import configuration from markup import extensions, filename_extension from html_full_url import html_full_url from page import Page, Error def _check_duplicated(p, posts): if p in posts: logging.debug('%r is duplicated', p) for duplicated_post in posts: if duplicated_post == p: break raise IOError('%s, there is already a post ' 'with this title and date (%s)' % \ (p.source_filename, duplicated_post.source_filename)) def load_posts(source_dir, config): posts = set() ignore_dirs = config['ignore_dirs'] for root, dirs, files in os.walk(source_dir): # Remove ignored directories so walk doesn't visit them for d in tuple(dirs): if d.startswith('.') or d in ignore_dirs: del dirs[dirs.index(d)] for filename in files: if filename_extension(filename) in extensions: logging.debug('Loading %s', filename) p = Page(os.path.join(root, filename), default_encoding=config['encoding'], default_author=config['author'], filesystem_encoding=config['filesystem_encoding']) _check_duplicated(p, posts) posts.add(p) else: logging.debug('Ignoring %s', filename) return posts def generate_post_html(post_list, writer, config): for post in post_list: logging.debug('Generating HTML file for %r', post) top_dir = '../../../' params = dict(config) params.update(dict(title=post.title, date=post.date, author=post.author, content=html_full_url(top_dir, post.get_html()), top_dir=top_dir)) writer.write('post.html.tmpl', post.filename(), timestamp=post.mtime, **params) def command_publish(args, options): source_dir = options.source_dir output_dir = options.output_dir try: config = configuration.read(os.path.join(source_dir or '.', options.configuration_file)) except IOError, error: logging.error('Error while loading configuration file') raise SystemExit(error) source_dir = source_dir or config.get('source_dir', '.') output_dir = output_dir or config.get('output_dir', 'output') if not os.path.exists(output_dir): os.mkdir(output_dir) try: post_list = sorted(load_posts(source_dir, config), reverse=True) except (IOError, Error), e: logging.error('Error while loading post files.') raise SystemExit(e) writer = template.Writer(source_dir, output_dir, config.get('encoding')) def generate_all(): max_mtime = max(p.mtime for p in post_list) if post_list else 0 logging.debug('Generating archives page') writer.write('archives.html.tmpl', 'archives.html', posts=post_list, timestamp=max_mtime, **config) logging.debug('Generating main page') writer.write('index.html.tmpl', 'index.html', posts=post_list, timestamp=max_mtime, **config) logging.debug('Generating HTML posts files') generate_post_html(post_list, writer, config) # Copy all 'attached' files for post in post_list: for filename in post.files: src = os.path.join(source_dir, filename) dst = os.path.join(output_dir, filename) # Create the destination directory if it does not exist destination_dir = os.path.dirname(dst) if not os.path.isdir(destination_dir): os.makedirs(destination_dir) if (not os.path.isfile(dst) or os.stat(src).st_mtime >= os.stat(dst).st_mtime): copy(os.path.join(source_dir, filename), dst) # Generate Atom feed # Last time the feed was updated posts = post_list[:config.get('feed_limit', 10)] if posts: def _datetime(d): if isinstance(d, datetime.date): return datetime.datetime(d.year, d.month, d.day) else: return d feed_updated = max(_datetime(p.date) for p in posts) else: feed_updated = datetime.datetime.utcnow() writer.write('feed.atom.tmpl', 'feed.atom', url=config['url'], encoding='utf-8', posts=posts, feed_updated=feed_updated, weblog_version=weblog.__version__, timestamp=max(x.mtime for x in posts) if posts else 0, title=config.get('title', None)) for f in config.get('extra_files', tuple()): 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 ...') raise SystemExit(e) else: logging.info('Successfully generated weblog.') weblog+jinja2+markdown2-2.5/weblog/rfc3339.py010064400017500000000000000220341133707606200177020ustar00henrywheel#!/usr/bin/env python # # Copyright (c) 2009, 2010, 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. # ''' The function `rfc3339` formats dates according to the :RFC:`3339`. `rfc3339` tries to have as much as possible sensible defaults. ''' __author__ = 'Henry Precheur ' __license__ = 'ISCL' __version__ = '3' __all__ = ('rfc3339', ) import datetime import time import unittest def _timezone(utcoffset): ''' Return a string representing the timezone offset. >>> _timezone(0) '+00:00' >>> _timezone(3600) '+01:00' >>> _timezone(-28800) '-08:00' ''' # Python's division uses floor(), not round() like in other languages. # >>> -1 / 2 # -1 hours = int(float(utcoffset)) // 3600 minutes = abs(utcoffset) % 3600 // 60 return '%+03d:%02d' % (hours, minutes) def _timedelta_to_seconds(timedelta): ''' >>> _timedelta_to_seconds(datetime.timedelta(hours=3)) 10800 >>> _timedelta_to_seconds(datetime.timedelta(hours=3, minutes=15)) 11700 ''' return (timedelta.days * 86400 + timedelta.seconds + timedelta.microseconds // 1000) def _utc_offset(date, use_system_timezone): ''' Return the UTC offset of `date`. If `date` does not have any `tzinfo`, use the timezone informations stored locally on the system. >>> if time.localtime().tm_isdst: ... system_timezone = -time.altzone ... else: ... system_timezone = -time.timezone >>> _utc_offset(datetime.datetime.now(), True) == system_timezone True >>> _utc_offset(datetime.datetime.now(), False) 0 ''' if isinstance(date, datetime.datetime) and date.tzinfo is not None: return _timedelta_to_seconds(date.dst() or date.utcoffset()) elif use_system_timezone: t = time.mktime(date.timetuple()) if time.localtime(t).tm_isdst: # pragma: no cover return -time.altzone else: return -time.timezone else: return 0 def _utc_string(d): return d.strftime('%Y-%m-%dT%H:%M:%SZ') def rfc3339(date, utc=False, use_system_timezone=True): ''' Return a string formatted according to the :RFC:`3339`. If called with `utc=True`, it normalizes `date` to the UTC date. If `date` does not have any timezone information, uses the local timezone:: >>> date = datetime.datetime(2008, 4, 2, 20) >>> rfc3339(date, utc=True, use_system_timezone=False) '2008-04-02T20:00:00Z' >>> rfc3339(date) # doctest: +ELLIPSIS '2008-04-02T20:00:00...' If called with `user_system_time=False` don't use the local timezone if `date` does not have timezone informations and consider the offset to UTC to be zero:: >>> rfc3339(date, use_system_timezone=False) '2008-04-02T20:00:00+00:00' `date` must be a `datetime.datetime`, `datetime.date` or a timestamp as returned by `time.time()`:: >>> rfc3339(0, utc=True, use_system_timezone=False) '1970-01-01T00:00:00Z' >>> rfc3339(datetime.date(2008, 9, 6), utc=True, ... use_system_timezone=False) '2008-09-06T00:00:00Z' >>> rfc3339(datetime.date(2008, 9, 6), ... use_system_timezone=False) '2008-09-06T00:00:00+00:00' >>> rfc3339('foo bar') Traceback (most recent call last): ... TypeError: excepted datetime, got str instead ''' # Check if `date` is a timestamp. try: if utc: return _utc_string(datetime.datetime.utcfromtimestamp(date)) else: date = datetime.datetime.fromtimestamp(date) except TypeError: pass if isinstance(date, datetime.date): utcoffset = _utc_offset(date, use_system_timezone) if utc: if not isinstance(date, datetime.datetime): date = datetime.datetime(*date.timetuple()[:3]) return _utc_string(date + datetime.timedelta(seconds=utcoffset)) else: return date.strftime('%Y-%m-%dT%H:%M:%S') + _timezone(utcoffset) else: raise TypeError('excepted %s, got %s instead' % (datetime.datetime.__name__, date.__class__.__name__)) class LocalTimeTestCase(unittest.TestCase): ''' Test the use of the timezone saved locally. Since it is hard to test using doctest. ''' def setUp(self): local_utcoffset = _utc_offset(datetime.datetime.now(), True) self.local_utcoffset = datetime.timedelta(seconds=local_utcoffset) self.local_timezone = _timezone(local_utcoffset) def test_datetime(self): d = datetime.datetime.now() self.assertEqual(rfc3339(d), d.strftime('%Y-%m-%dT%H:%M:%S') + self.local_timezone) def test_datetime_timezone(self): class FixedNoDst(datetime.tzinfo): 'A timezone info with fixed offset, not DST' def utcoffset(self, dt): return datetime.timedelta(hours=2, minutes=30) def dst(self, dt): return None fixed_no_dst = FixedNoDst() class Fixed(FixedNoDst): 'A timezone info with DST' def dst(self, dt): return datetime.timedelta(hours=3, minutes=15) fixed = Fixed() d = datetime.datetime.now().replace(tzinfo=fixed_no_dst) timezone = _timezone(_timedelta_to_seconds(fixed_no_dst.\ utcoffset(None))) self.assertEqual(rfc3339(d), d.strftime('%Y-%m-%dT%H:%M:%S') + timezone) d = datetime.datetime.now().replace(tzinfo=fixed) timezone = _timezone(_timedelta_to_seconds(fixed.dst(None))) self.assertEqual(rfc3339(d), d.strftime('%Y-%m-%dT%H:%M:%S') + timezone) def test_datetime_utc(self): d = datetime.datetime.now() d_utc = d + self.local_utcoffset self.assertEqual(rfc3339(d, utc=True), d_utc.strftime('%Y-%m-%dT%H:%M:%SZ')) def test_date(self): d = datetime.date.today() self.assertEqual(rfc3339(d), d.strftime('%Y-%m-%dT%H:%M:%S') + self.local_timezone) def test_date_utc(self): d = datetime.date.today() # Convert `date` to `datetime`, since `date` ignores seconds and hours # in timedeltas: # >>> datetime.date(2008, 9, 7) + datetime.timedelta(hours=23) # datetime.date(2008, 9, 7) d_utc = datetime.datetime(*d.timetuple()[:3]) + self.local_utcoffset self.assertEqual(rfc3339(d, utc=True), d_utc.strftime('%Y-%m-%dT%H:%M:%SZ')) def test_timestamp(self): d = time.time() self.assertEqual(rfc3339(d), datetime.datetime.fromtimestamp(d).\ strftime('%Y-%m-%dT%H:%M:%S') + self.local_timezone) def test_timestamp_utc(self): d = time.time() d_utc = datetime.datetime.utcfromtimestamp(d) + self.local_utcoffset self.assertEqual(rfc3339(d), (d_utc.strftime('%Y-%m-%dT%H:%M:%S') + self.local_timezone)) # If these tests start failing it probably means there was a policy change # for the Pacific time zone. # See http://en.wikipedia.org/wiki/Pacific_Time_Zone. if 'PST' in time.tzname: def testPDTChange(self): '''Test Daylight saving change''' # PDT switch happens at 2AM on March 14, 2010 # 1:59AM PST self.assertEqual(rfc3339(datetime.datetime(2010, 3, 14, 1, 59)), '2010-03-14T01:59:00-08:00') # 3AM PDT self.assertEqual(rfc3339(datetime.datetime(2010, 3, 14, 3, 0)), '2010-03-14T03:00:00-07:00') def testPSTChange(self): '''Test Standard time change''' # PST switch happens at 2AM on November 6, 2010 # 0:59AM PDT self.assertEqual(rfc3339(datetime.datetime(2010, 11, 7, 0, 59)), '2010-11-07T00:59:00-07:00') # 1:00AM PST # There's no way to have 1:00AM PST without a proper tzinfo self.assertEqual(rfc3339(datetime.datetime(2010, 11, 7, 1, 0)), '2010-11-07T01:00:00-07:00') if __name__ == '__main__': # pragma: no cover import doctest doctest.testmod() unittest.main() weblog+jinja2+markdown2-2.5/weblog/template.py010064400017500000000000000037261133707606200204300ustar00henrywheelimport codecs import datetime import stat import sys from logging import debug from os import stat, makedirs from os.path import join, dirname, isdir, isfile import rfc3339 try: from jinja2 import Environment from jinja2 import FileSystemLoader, ChoiceLoader, PackageLoader from jinja2 import environmentfilter, contextfilter, Markup except ImportError: raise SystemExit('Please install Jinja 2 (http://jinja.pocoo.org/2/)' ' to use Weblog') def decode(value): if value: return value.encode('ascii', 'xmlcharrefreplace') else: return '' class Writer(object): def __init__(self, src_dir, dst_dir, encoding=None): self._dst_dir = dst_dir self._encoding = encoding TEMPLATE_DIR = 'templates' loaders = [FileSystemLoader(join(src_dir, TEMPLATE_DIR))] try: loaders.append(PackageLoader('weblog', TEMPLATE_DIR)) except ImportError: pass app_template_dir = join(dirname(__file__), TEMPLATE_DIR) if isdir(app_template_dir): loaders.append(FileSystemLoader(app_template_dir)) self._env = Environment(loader=ChoiceLoader(loaders), trim_blocks=True) self._env.filters['rfc3339'] = rfc3339.rfc3339 self._env.filters['decode'] = decode def write(self, template, filename, *args, **kwargs): timestamp = kwargs.pop('timestamp', None) encoding = kwargs.pop('encoding', self._encoding) p = join(self._dst_dir, filename) if timestamp and isfile(p) and stat(p).st_mtime > timestamp: debug('%r is up to date' % filename) else: debug('Updating %r' % p) d = dirname(p) if not isdir(d): makedirs(d) f = codecs.open(p, mode='w', encoding=encoding) t = self._env.get_template(template) try: f.write(t.render(*args, **kwargs)) finally: f.close() weblog+jinja2+markdown2-2.5/weblog/utf8_html_parser.py010064400017500000000000000045621133707606200221020ustar00henrywheelfrom cgi import escape from HTMLParser import HTMLParser class UTF8HTMLParser(HTMLParser): ''' Parse a HTML document and convert all nodes to UTF-8:: >>> parser = UTF8HTMLParser() >>> parser.feed("

Hello world

") >>> parser.get_value() u'

Hello world

' >>> parser.feed('

Another
sentence.

') >>> parser.get_value() u'

Hello world

Another
sentence.

' `reset()` resets the parser:: >>> parser.reset() >>> parser.get_value() u'' ''' def __init__(self): HTMLParser.__init__(self) self.output = list() def reset(self): HTMLParser.reset(self) self.output = list() def get_value(self): return u''.join(self.output) @staticmethod def html_attrs(attrs): ''' >>> UTF8HTMLParser.html_attrs((('src', 'pic.jpg'), ('alt', 'pic'))) u'src="pic.jpg" alt="pic"' >>> UTF8HTMLParser.html_attrs(list()) u'' >>> UTF8HTMLParser.html_attrs((('href', 'sample?foo=1&bar=2'),)) u'href="sample?foo=1&bar=2"' >>> UTF8HTMLParser.html_attrs((('alt', 'quote """'),)) u'alt="quote """"' ''' # HTMLParser unescape attributes values, we don't want that. return u' '.join(u'%s="%s"' % (k, escape(v, quote=True)) for k, v in attrs) def handle_starttag(self, tag, attrs): if attrs: self.output.append(u'<%s %s>' % (tag, self.html_attrs(attrs))) else: self.output.append(u'<%s>' % tag) def handle_startendtag(self, tag, attrs): self.handle_starttag(tag, attrs) def handle_endtag(self, tag): self.output.append(u'' % tag) def handle_data(self, data): self.output.append(data) def handle_charref(self, name): self.output.append(u'&#%s;' % name) def handle_entityref(self, name): self.output.append(u'&%s;' % name) def handle_comment(self, comment): self.output.append(u'' % comment) def handle_decl(self, decl): self.output.append(u'' % decl) def handle_pi(self, pi): self.output.append(u'' % pi) if __name__ == '__main__': import doctest doctest.testmod() weblog+jinja2+markdown2-2.5/jinja2004075500017500000000000000000001133707607400160605ustar00henrywheelweblog+jinja2+markdown2-2.5/jinja2/__init__.py010064400017500000000000000043021133707607400202440ustar00henrywheel# -*- coding: utf-8 -*- """ jinja2 ~~~~~~ Jinja2 is a template engine written in pure Python. It provides a Django inspired non-XML syntax but supports inline expressions and an optional sandboxed environment. Nutshell -------- Here a small example of a Jinja2 template:: {% extends 'base.html' %} {% block title %}Memberlist{% endblock %} {% block content %} {% endblock %} :copyright: (c) 2010 by the Jinja Team. :license: BSD, see LICENSE for more details. """ __docformat__ = 'restructuredtext en' try: __version__ = __import__('pkg_resources') \ .get_distribution('Jinja2').version except: __version__ = 'unknown' # high level interface from jinja2.environment import Environment, Template # loaders from jinja2.loaders import BaseLoader, FileSystemLoader, PackageLoader, \ DictLoader, FunctionLoader, PrefixLoader, ChoiceLoader # bytecode caches from jinja2.bccache import BytecodeCache, FileSystemBytecodeCache, \ MemcachedBytecodeCache # undefined types from jinja2.runtime import Undefined, DebugUndefined, StrictUndefined # exceptions from jinja2.exceptions import TemplateError, UndefinedError, \ TemplateNotFound, TemplatesNotFound, TemplateSyntaxError, \ TemplateAssertionError # decorators and public utilities from jinja2.filters import environmentfilter, contextfilter from jinja2.utils import Markup, escape, clear_caches, \ environmentfunction, contextfunction, is_undefined __all__ = [ 'Environment', 'Template', 'BaseLoader', 'FileSystemLoader', 'PackageLoader', 'DictLoader', 'FunctionLoader', 'PrefixLoader', 'ChoiceLoader', 'BytecodeCache', 'FileSystemBytecodeCache', 'MemcachedBytecodeCache', 'Undefined', 'DebugUndefined', 'StrictUndefined', 'TemplateError', 'UndefinedError', 'TemplateNotFound', 'TemplatesNotFound', 'TemplateSyntaxError', 'TemplateAssertionError', 'environmentfilter', 'contextfilter', 'Markup', 'escape', 'environmentfunction', 'contextfunction', 'clear_caches', 'is_undefined' ] weblog+jinja2+markdown2-2.5/jinja2/utils.py010064400017500000000000000614741133707607400176620ustar00henrywheel# -*- coding: utf-8 -*- """ jinja2.utils ~~~~~~~~~~~~ Utility functions. :copyright: (c) 2010 by the Jinja Team. :license: BSD, see LICENSE for more details. """ import re import sys import errno try: from thread import allocate_lock except ImportError: from dummy_thread import allocate_lock from collections import deque from itertools import imap _word_split_re = re.compile(r'(\s+)') _punctuation_re = re.compile( '^(?P(?:%s)*)(?P.*?)(?P(?:%s)*)$' % ( '|'.join(imap(re.escape, ('(', '<', '<'))), '|'.join(imap(re.escape, ('.', ',', ')', '>', '\n', '>'))) ) ) _simple_email_re = re.compile(r'^\S+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+$') _striptags_re = re.compile(r'(|<[^>]*>)') _entity_re = re.compile(r'&([^;]+);') _letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' _digits = '0123456789' # special singleton representing missing values for the runtime missing = type('MissingType', (), {'__repr__': lambda x: 'missing'})() # internal code internal_code = set() # concatenate a list of strings and convert them to unicode. # unfortunately there is a bug in python 2.4 and lower that causes # unicode.join trash the traceback. _concat = u''.join try: def _test_gen_bug(): raise TypeError(_test_gen_bug) yield None _concat(_test_gen_bug()) except TypeError, _error: if not _error.args or _error.args[0] is not _test_gen_bug: def concat(gen): try: return _concat(list(gen)) except: # this hack is needed so that the current frame # does not show up in the traceback. exc_type, exc_value, tb = sys.exc_info() raise exc_type, exc_value, tb.tb_next else: concat = _concat del _test_gen_bug, _error # for python 2.x we create outselves a next() function that does the # basics without exception catching. try: next = next except NameError: def next(x): return x.next() # if this python version is unable to deal with unicode filenames # when passed to encode we let this function encode it properly. # This is used in a couple of places. As far as Jinja is concerned # filenames are unicode *or* bytestrings in 2.x and unicode only in # 3.x because compile cannot handle bytes if sys.version_info < (3, 0): def _encode_filename(filename): if isinstance(filename, unicode): return filename.encode('utf-8') return filename else: def _encode_filename(filename): assert filename is None or isinstance(filename, str), \ 'filenames must be strings' return filename from keyword import iskeyword as is_python_keyword # common types. These do exist in the special types module too which however # does not exist in IronPython out of the box. Also that way we don't have # to deal with implementation specific stuff here class _C(object): def method(self): pass def _func(): yield None FunctionType = type(_func) GeneratorType = type(_func()) MethodType = type(_C.method) CodeType = type(_C.method.func_code) try: raise TypeError() except TypeError: _tb = sys.exc_info()[2] TracebackType = type(_tb) FrameType = type(_tb.tb_frame) del _C, _tb, _func def contextfunction(f): """This decorator can be used to mark a function or method context callable. A context callable is passed the active :class:`Context` as first argument when called from the template. This is useful if a function wants to get access to the context or functions provided on the context object. For example a function that returns a sorted list of template variables the current template exports could look like this:: @contextfunction def get_exported_names(context): return sorted(context.exported_vars) """ f.contextfunction = True return f def environmentfunction(f): """This decorator can be used to mark a function or method as environment callable. This decorator works exactly like the :func:`contextfunction` decorator just that the first argument is the active :class:`Environment` and not context. """ f.environmentfunction = True return f def internalcode(f): """Marks the function as internally used""" internal_code.add(f.func_code) return f def is_undefined(obj): """Check if the object passed is undefined. This does nothing more than performing an instance check against :class:`Undefined` but looks nicer. This can be used for custom filters or tests that want to react to undefined variables. For example a custom default filter can look like this:: def default(var, default=''): if is_undefined(var): return default return var """ from jinja2.runtime import Undefined return isinstance(obj, Undefined) def consume(iterable): """Consumes an iterable without doing anything with it.""" for event in iterable: pass def clear_caches(): """Jinja2 keeps internal caches for environments and lexers. These are used so that Jinja2 doesn't have to recreate environments and lexers all the time. Normally you don't have to care about that but if you are messuring memory consumption you may want to clean the caches. """ from jinja2.environment import _spontaneous_environments from jinja2.lexer import _lexer_cache _spontaneous_environments.clear() _lexer_cache.clear() def import_string(import_name, silent=False): """Imports an object based on a string. This use useful if you want to use import paths as endpoints or something similar. An import path can be specified either in dotted notation (``xml.sax.saxutils.escape``) or with a colon as object delimiter (``xml.sax.saxutils:escape``). If the `silent` is True the return value will be `None` if the import fails. :return: imported object """ try: if ':' in import_name: module, obj = import_name.split(':', 1) elif '.' in import_name: items = import_name.split('.') module = '.'.join(items[:-1]) obj = items[-1] else: return __import__(import_name) return getattr(__import__(module, None, None, [obj]), obj) except (ImportError, AttributeError): if not silent: raise def open_if_exists(filename, mode='rb'): """Returns a file descriptor for the filename if that file exists, otherwise `None`. """ try: return open(filename, mode) except IOError, e: if e.errno not in (errno.ENOENT, errno.EISDIR): raise def pformat(obj, verbose=False): """Prettyprint an object. Either use the `pretty` library or the builtin `pprint`. """ try: from pretty import pretty return pretty(obj, verbose=verbose) except ImportError: from pprint import pformat return pformat(obj) def urlize(text, trim_url_limit=None, nofollow=False): """Converts any URLs in text into clickable links. Works on http://, https:// and www. links. Links can have trailing punctuation (periods, commas, close-parens) and leading punctuation (opening parens) and it'll still do the right thing. If trim_url_limit is not None, the URLs in link text will be limited to trim_url_limit characters. If nofollow is True, the URLs in link text will get a rel="nofollow" attribute. """ trim_url = lambda x, limit=trim_url_limit: limit is not None \ and (x[:limit] + (len(x) >=limit and '...' or '')) or x words = _word_split_re.split(unicode(escape(text))) nofollow_attr = nofollow and ' rel="nofollow"' or '' for i, word in enumerate(words): match = _punctuation_re.match(word) if match: lead, middle, trail = match.groups() if middle.startswith('www.') or ( '@' not in middle and not middle.startswith('http://') and len(middle) > 0 and middle[0] in _letters + _digits and ( middle.endswith('.org') or middle.endswith('.net') or middle.endswith('.com') )): middle = '%s' % (middle, nofollow_attr, trim_url(middle)) if middle.startswith('http://') or \ middle.startswith('https://'): middle = '%s' % (middle, nofollow_attr, trim_url(middle)) if '@' in middle and not middle.startswith('www.') and \ not ':' in middle and _simple_email_re.match(middle): middle = '%s' % (middle, middle) if lead + middle + trail != word: words[i] = lead + middle + trail return u''.join(words) def generate_lorem_ipsum(n=5, html=True, min=20, max=100): """Generate some lorem impsum for the template.""" from jinja2.constants import LOREM_IPSUM_WORDS from random import choice, randrange words = LOREM_IPSUM_WORDS.split() result = [] for _ in xrange(n): next_capitalized = True last_comma = last_fullstop = 0 word = None last = None p = [] # each paragraph contains out of 20 to 100 words. for idx, _ in enumerate(xrange(randrange(min, max))): while True: word = choice(words) if word != last: last = word break if next_capitalized: word = word.capitalize() next_capitalized = False # add commas if idx - randrange(3, 8) > last_comma: last_comma = idx last_fullstop += 2 word += ',' # add end of sentences if idx - randrange(10, 20) > last_fullstop: last_comma = last_fullstop = idx word += '.' next_capitalized = True p.append(word) # ensure that the paragraph ends with a dot. p = u' '.join(p) if p.endswith(','): p = p[:-1] + '.' elif not p.endswith('.'): p += '.' result.append(p) if not html: return u'\n\n'.join(result) return Markup(u'\n'.join(u'

%s

' % escape(x) for x in result)) class Markup(unicode): r"""Marks a string as being safe for inclusion in HTML/XML output without needing to be escaped. This implements the `__html__` interface a couple of frameworks and web applications use. :class:`Markup` is a direct subclass of `unicode` and provides all the methods of `unicode` just that it escapes arguments passed and always returns `Markup`. The `escape` function returns markup objects so that double escaping can't happen. If you want to use autoescaping in Jinja just enable the autoescaping feature in the environment. The constructor of the :class:`Markup` class can be used for three different things: When passed an unicode object it's assumed to be safe, when passed an object with an HTML representation (has an `__html__` method) that representation is used, otherwise the object passed is converted into a unicode string and then assumed to be safe: >>> Markup("Hello World!") Markup(u'Hello World!') >>> class Foo(object): ... def __html__(self): ... return 'foo' ... >>> Markup(Foo()) Markup(u'foo') If you want object passed being always treated as unsafe you can use the :meth:`escape` classmethod to create a :class:`Markup` object: >>> Markup.escape("Hello World!") Markup(u'Hello <em>World</em>!') Operations on a markup string are markup aware which means that all arguments are passed through the :func:`escape` function: >>> em = Markup("%s") >>> em % "foo & bar" Markup(u'foo & bar') >>> strong = Markup("%(text)s") >>> strong % {'text': 'hacker here'} Markup(u'<blink>hacker here</blink>') >>> Markup("Hello ") + "" Markup(u'Hello <foo>') """ __slots__ = () def __new__(cls, base=u'', encoding=None, errors='strict'): if hasattr(base, '__html__'): base = base.__html__() if encoding is None: return unicode.__new__(cls, base) return unicode.__new__(cls, base, encoding, errors) def __html__(self): return self def __add__(self, other): if hasattr(other, '__html__') or isinstance(other, basestring): return self.__class__(unicode(self) + unicode(escape(other))) return NotImplemented def __radd__(self, other): if hasattr(other, '__html__') or isinstance(other, basestring): return self.__class__(unicode(escape(other)) + unicode(self)) return NotImplemented def __mul__(self, num): if isinstance(num, (int, long)): return self.__class__(unicode.__mul__(self, num)) return NotImplemented __rmul__ = __mul__ def __mod__(self, arg): if isinstance(arg, tuple): arg = tuple(imap(_MarkupEscapeHelper, arg)) else: arg = _MarkupEscapeHelper(arg) return self.__class__(unicode.__mod__(self, arg)) def __repr__(self): return '%s(%s)' % ( self.__class__.__name__, unicode.__repr__(self) ) def join(self, seq): return self.__class__(unicode.join(self, imap(escape, seq))) join.__doc__ = unicode.join.__doc__ def split(self, *args, **kwargs): return map(self.__class__, unicode.split(self, *args, **kwargs)) split.__doc__ = unicode.split.__doc__ def rsplit(self, *args, **kwargs): return map(self.__class__, unicode.rsplit(self, *args, **kwargs)) rsplit.__doc__ = unicode.rsplit.__doc__ def splitlines(self, *args, **kwargs): return map(self.__class__, unicode.splitlines(self, *args, **kwargs)) splitlines.__doc__ = unicode.splitlines.__doc__ def unescape(self): r"""Unescape markup again into an unicode string. This also resolves known HTML4 and XHTML entities: >>> Markup("Main » About").unescape() u'Main \xbb About' """ from jinja2.constants import HTML_ENTITIES def handle_match(m): name = m.group(1) if name in HTML_ENTITIES: return unichr(HTML_ENTITIES[name]) try: if name[:2] in ('#x', '#X'): return unichr(int(name[2:], 16)) elif name.startswith('#'): return unichr(int(name[1:])) except ValueError: pass return u'' return _entity_re.sub(handle_match, unicode(self)) def striptags(self): r"""Unescape markup into an unicode string and strip all tags. This also resolves known HTML4 and XHTML entities. Whitespace is normalized to one: >>> Markup("Main » About").striptags() u'Main \xbb About' """ stripped = u' '.join(_striptags_re.sub('', self).split()) return Markup(stripped).unescape() @classmethod def escape(cls, s): """Escape the string. Works like :func:`escape` with the difference that for subclasses of :class:`Markup` this function would return the correct subclass. """ rv = escape(s) if rv.__class__ is not cls: return cls(rv) return rv def make_wrapper(name): orig = getattr(unicode, name) def func(self, *args, **kwargs): args = _escape_argspec(list(args), enumerate(args)) _escape_argspec(kwargs, kwargs.iteritems()) return self.__class__(orig(self, *args, **kwargs)) func.__name__ = orig.__name__ func.__doc__ = orig.__doc__ return func for method in '__getitem__', 'capitalize', \ 'title', 'lower', 'upper', 'replace', 'ljust', \ 'rjust', 'lstrip', 'rstrip', 'center', 'strip', \ 'translate', 'expandtabs', 'swapcase', 'zfill': locals()[method] = make_wrapper(method) # new in python 2.5 if hasattr(unicode, 'partition'): partition = make_wrapper('partition'), rpartition = make_wrapper('rpartition') # new in python 2.6 if hasattr(unicode, 'format'): format = make_wrapper('format') # not in python 3 if hasattr(unicode, '__getslice__'): __getslice__ = make_wrapper('__getslice__') del method, make_wrapper def _escape_argspec(obj, iterable): """Helper for various string-wrapped functions.""" for key, value in iterable: if hasattr(value, '__html__') or isinstance(value, basestring): obj[key] = escape(value) return obj class _MarkupEscapeHelper(object): """Helper for Markup.__mod__""" def __init__(self, obj): self.obj = obj __getitem__ = lambda s, x: _MarkupEscapeHelper(s.obj[x]) __str__ = lambda s: str(escape(s.obj)) __unicode__ = lambda s: unicode(escape(s.obj)) __repr__ = lambda s: str(escape(repr(s.obj))) __int__ = lambda s: int(s.obj) __float__ = lambda s: float(s.obj) class LRUCache(object): """A simple LRU Cache implementation.""" # this is fast for small capacities (something below 1000) but doesn't # scale. But as long as it's only used as storage for templates this # won't do any harm. def __init__(self, capacity): self.capacity = capacity self._mapping = {} self._queue = deque() self._postinit() def _postinit(self): # alias all queue methods for faster lookup self._popleft = self._queue.popleft self._pop = self._queue.pop if hasattr(self._queue, 'remove'): self._remove = self._queue.remove self._wlock = allocate_lock() self._append = self._queue.append def _remove(self, obj): """Python 2.4 compatibility.""" for idx, item in enumerate(self._queue): if item == obj: del self._queue[idx] break def __getstate__(self): return { 'capacity': self.capacity, '_mapping': self._mapping, '_queue': self._queue } def __setstate__(self, d): self.__dict__.update(d) self._postinit() def __getnewargs__(self): return (self.capacity,) def copy(self): """Return an shallow copy of the instance.""" rv = self.__class__(self.capacity) rv._mapping.update(self._mapping) rv._queue = deque(self._queue) return rv def get(self, key, default=None): """Return an item from the cache dict or `default`""" try: return self[key] except KeyError: return default def setdefault(self, key, default=None): """Set `default` if the key is not in the cache otherwise leave unchanged. Return the value of this key. """ try: return self[key] except KeyError: self[key] = default return default def clear(self): """Clear the cache.""" self._wlock.acquire() try: self._mapping.clear() self._queue.clear() finally: self._wlock.release() def __contains__(self, key): """Check if a key exists in this cache.""" return key in self._mapping def __len__(self): """Return the current size of the cache.""" return len(self._mapping) def __repr__(self): return '<%s %r>' % ( self.__class__.__name__, self._mapping ) def __getitem__(self, key): """Get an item from the cache. Moves the item up so that it has the highest priority then. Raise an `KeyError` if it does not exist. """ rv = self._mapping[key] if self._queue[-1] != key: try: self._remove(key) except ValueError: # if something removed the key from the container # when we read, ignore the ValueError that we would # get otherwise. pass self._append(key) return rv def __setitem__(self, key, value): """Sets the value for an item. Moves the item up so that it has the highest priority then. """ self._wlock.acquire() try: if key in self._mapping: try: self._remove(key) except ValueError: # __getitem__ is not locked, it might happen pass elif len(self._mapping) == self.capacity: del self._mapping[self._popleft()] self._append(key) self._mapping[key] = value finally: self._wlock.release() def __delitem__(self, key): """Remove an item from the cache dict. Raise an `KeyError` if it does not exist. """ self._wlock.acquire() try: del self._mapping[key] try: self._remove(key) except ValueError: # __getitem__ is not locked, it might happen pass finally: self._wlock.release() def items(self): """Return a list of items.""" result = [(key, self._mapping[key]) for key in list(self._queue)] result.reverse() return result def iteritems(self): """Iterate over all items.""" return iter(self.items()) def values(self): """Return a list of all values.""" return [x[1] for x in self.items()] def itervalue(self): """Iterate over all values.""" return iter(self.values()) def keys(self): """Return a list of all keys ordered by most recent usage.""" return list(self) def iterkeys(self): """Iterate over all keys in the cache dict, ordered by the most recent usage. """ return reversed(tuple(self._queue)) __iter__ = iterkeys def __reversed__(self): """Iterate over the values in the cache dict, oldest items coming first. """ return iter(tuple(self._queue)) __copy__ = copy # register the LRU cache as mutable mapping if possible try: from collections import MutableMapping MutableMapping.register(LRUCache) except ImportError: pass class Cycler(object): """A cycle helper for templates.""" def __init__(self, *items): if not items: raise RuntimeError('at least one item has to be provided') self.items = items self.reset() def reset(self): """Resets the cycle.""" self.pos = 0 @property def current(self): """Returns the current item.""" return self.items[self.pos] def next(self): """Goes one item ahead and returns it.""" rv = self.current self.pos = (self.pos + 1) % len(self.items) return rv class Joiner(object): """A joining helper for templates.""" def __init__(self, sep=u', '): self.sep = sep self.used = False def __call__(self): if not self.used: self.used = True return u'' return self.sep # we have to import it down here as the speedups module imports the # markup type which is define above. try: from jinja2._speedups import escape, soft_unicode except ImportError: def escape(s): """Convert the characters &, <, >, ' and " in string s to HTML-safe sequences. Use this if you need to display text that might contain such characters in HTML. Marks return value as markup string. """ if hasattr(s, '__html__'): return s.__html__() return Markup(unicode(s) .replace('&', '&') .replace('>', '>') .replace('<', '<') .replace("'", ''') .replace('"', '"') ) def soft_unicode(s): """Make a string unicode if it isn't already. That way a markup string is not converted back to unicode. """ if not isinstance(s, unicode): s = unicode(s) return s # partials try: from functools import partial except ImportError: class partial(object): def __init__(self, _func, *args, **kwargs): self._func = _func self._args = args self._kwargs = kwargs def __call__(self, *args, **kwargs): kwargs.update(self._kwargs) return self._func(*(self._args + args), **kwargs) weblog+jinja2+markdown2-2.5/jinja2/tests.py010064400017500000000000000063611133707607400176560ustar00henrywheel# -*- coding: utf-8 -*- """ jinja2.tests ~~~~~~~~~~~~ Jinja test functions. Used with the "is" operator. :copyright: (c) 2010 by the Jinja Team. :license: BSD, see LICENSE for more details. """ import re from jinja2.runtime import Undefined # nose, nothing here to test __test__ = False number_re = re.compile(r'^-?\d+(\.\d+)?$') regex_type = type(number_re) try: test_callable = callable except NameError: def test_callable(x): return hasattr(x, '__call__') def test_odd(value): """Return true if the variable is odd.""" return value % 2 == 1 def test_even(value): """Return true if the variable is even.""" return value % 2 == 0 def test_divisibleby(value, num): """Check if a variable is divisible by a number.""" return value % num == 0 def test_defined(value): """Return true if the variable is defined: .. sourcecode:: jinja {% if variable is defined %} value of variable: {{ variable }} {% else %} variable is not defined {% endif %} See the :func:`default` filter for a simple way to set undefined variables. """ return not isinstance(value, Undefined) def test_undefined(value): """Like :func:`defined` but the other way round.""" return isinstance(value, Undefined) def test_none(value): """Return true if the variable is none.""" return value is None def test_lower(value): """Return true if the variable is lowercased.""" return unicode(value).islower() def test_upper(value): """Return true if the variable is uppercased.""" return unicode(value).isupper() def test_string(value): """Return true if the object is a string.""" return isinstance(value, basestring) def test_number(value): """Return true if the variable is a number.""" return isinstance(value, (int, long, float, complex)) def test_sequence(value): """Return true if the variable is a sequence. Sequences are variables that are iterable. """ try: len(value) value.__getitem__ except: return False return True def test_sameas(value, other): """Check if an object points to the same memory address than another object: .. sourcecode:: jinja {% if foo.attribute is sameas false %} the foo attribute really is the `False` singleton {% endif %} """ return value is other def test_iterable(value): """Check if it's possible to iterate over an object.""" try: iter(value) except TypeError: return False return True def test_escaped(value): """Check if the value is escaped.""" return hasattr(value, '__html__') TESTS = { 'odd': test_odd, 'even': test_even, 'divisibleby': test_divisibleby, 'defined': test_defined, 'undefined': test_undefined, 'none': test_none, 'lower': test_lower, 'upper': test_upper, 'string': test_string, 'number': test_number, 'sequence': test_sequence, 'iterable': test_iterable, 'callable': test_callable, 'sameas': test_sameas, 'escaped': test_escaped } weblog+jinja2+markdown2-2.5/jinja2/sandbox.py010064400017500000000000000223171133707607400201510ustar00henrywheel# -*- coding: utf-8 -*- """ jinja2.sandbox ~~~~~~~~~~~~~~ Adds a sandbox layer to Jinja as it was the default behavior in the old Jinja 1 releases. This sandbox is slightly different from Jinja 1 as the default behavior is easier to use. The behavior can be changed by subclassing the environment. :copyright: (c) 2010 by the Jinja Team. :license: BSD. """ import operator from jinja2.runtime import Undefined from jinja2.environment import Environment from jinja2.exceptions import SecurityError from jinja2.utils import FunctionType, MethodType, TracebackType, CodeType, \ FrameType, GeneratorType #: maximum number of items a range may produce MAX_RANGE = 100000 #: attributes of function objects that are considered unsafe. UNSAFE_FUNCTION_ATTRIBUTES = set(['func_closure', 'func_code', 'func_dict', 'func_defaults', 'func_globals']) #: unsafe method attributes. function attributes are unsafe for methods too UNSAFE_METHOD_ATTRIBUTES = set(['im_class', 'im_func', 'im_self']) import warnings # make sure we don't warn in python 2.6 about stuff we don't care about warnings.filterwarnings('ignore', 'the sets module', DeprecationWarning, module='jinja2.sandbox') from collections import deque _mutable_set_types = (set,) _mutable_mapping_types = (dict,) _mutable_sequence_types = (list,) # on python 2.x we can register the user collection types try: from UserDict import UserDict, DictMixin from UserList import UserList _mutable_mapping_types += (UserDict, DictMixin) _mutable_set_types += (UserList,) except ImportError: pass # if sets is still available, register the mutable set from there as well try: from sets import Set _mutable_set_types += (Set,) except ImportError: pass #: register Python 2.6 abstract base classes try: from collections import MutableSet, MutableMapping, MutableSequence _mutable_set_types += (MutableSet,) _mutable_mapping_types += (MutableMapping,) _mutable_sequence_types += (MutableSequence,) except ImportError: pass _mutable_spec = ( (_mutable_set_types, frozenset([ 'add', 'clear', 'difference_update', 'discard', 'pop', 'remove', 'symmetric_difference_update', 'update' ])), (_mutable_mapping_types, frozenset([ 'clear', 'pop', 'popitem', 'setdefault', 'update' ])), (_mutable_sequence_types, frozenset([ 'append', 'reverse', 'insert', 'sort', 'extend', 'remove' ])), (deque, frozenset([ 'append', 'appendleft', 'clear', 'extend', 'extendleft', 'pop', 'popleft', 'remove', 'rotate' ])) ) def safe_range(*args): """A range that can't generate ranges with a length of more than MAX_RANGE items. """ rng = xrange(*args) if len(rng) > MAX_RANGE: raise OverflowError('range too big, maximum size for range is %d' % MAX_RANGE) return rng def unsafe(f): """ Mark a function or method as unsafe:: @unsafe def delete(self): pass """ f.unsafe_callable = True return f def is_internal_attribute(obj, attr): """Test if the attribute given is an internal python attribute. For example this function returns `True` for the `func_code` attribute of python objects. This is useful if the environment method :meth:`~SandboxedEnvironment.is_safe_attribute` is overriden. >>> from jinja2.sandbox import is_internal_attribute >>> is_internal_attribute(lambda: None, "func_code") True >>> is_internal_attribute((lambda x:x).func_code, 'co_code') True >>> is_internal_attribute(str, "upper") False """ if isinstance(obj, FunctionType): if attr in UNSAFE_FUNCTION_ATTRIBUTES: return True elif isinstance(obj, MethodType): if attr in UNSAFE_FUNCTION_ATTRIBUTES or \ attr in UNSAFE_METHOD_ATTRIBUTES: return True elif isinstance(obj, type): if attr == 'mro': return True elif isinstance(obj, (CodeType, TracebackType, FrameType)): return True elif isinstance(obj, GeneratorType): if attr == 'gi_frame': return True return attr.startswith('__') def modifies_known_mutable(obj, attr): """This function checks if an attribute on a builtin mutable object (list, dict, set or deque) would modify it if called. It also supports the "user"-versions of the objects (`sets.Set`, `UserDict.*` etc.) and with Python 2.6 onwards the abstract base classes `MutableSet`, `MutableMapping`, and `MutableSequence`. >>> modifies_known_mutable({}, "clear") True >>> modifies_known_mutable({}, "keys") False >>> modifies_known_mutable([], "append") True >>> modifies_known_mutable([], "index") False If called with an unsupported object (such as unicode) `False` is returned. >>> modifies_known_mutable("foo", "upper") False """ for typespec, unsafe in _mutable_spec: if isinstance(obj, typespec): return attr in unsafe return False class SandboxedEnvironment(Environment): """The sandboxed environment. It works like the regular environment but tells the compiler to generate sandboxed code. Additionally subclasses of this environment may override the methods that tell the runtime what attributes or functions are safe to access. If the template tries to access insecure code a :exc:`SecurityError` is raised. However also other exceptions may occour during the rendering so the caller has to ensure that all exceptions are catched. """ sandboxed = True def __init__(self, *args, **kwargs): Environment.__init__(self, *args, **kwargs) self.globals['range'] = safe_range def is_safe_attribute(self, obj, attr, value): """The sandboxed environment will call this method to check if the attribute of an object is safe to access. Per default all attributes starting with an underscore are considered private as well as the special attributes of internal python objects as returned by the :func:`is_internal_attribute` function. """ return not (attr.startswith('_') or is_internal_attribute(obj, attr)) def is_safe_callable(self, obj): """Check if an object is safely callable. Per default a function is considered safe unless the `unsafe_callable` attribute exists and is True. Override this method to alter the behavior, but this won't affect the `unsafe` decorator from this module. """ return not (getattr(obj, 'unsafe_callable', False) or \ getattr(obj, 'alters_data', False)) def getitem(self, obj, argument): """Subscribe an object from sandboxed code.""" try: return obj[argument] except (TypeError, LookupError): if isinstance(argument, basestring): try: attr = str(argument) except: pass else: try: value = getattr(obj, attr) except AttributeError: pass else: if self.is_safe_attribute(obj, argument, value): return value return self.unsafe_undefined(obj, argument) return self.undefined(obj=obj, name=argument) def getattr(self, obj, attribute): """Subscribe an object from sandboxed code and prefer the attribute. The attribute passed *must* be a bytestring. """ try: value = getattr(obj, attribute) except AttributeError: try: return obj[attribute] except (TypeError, LookupError): pass else: if self.is_safe_attribute(obj, attribute, value): return value return self.unsafe_undefined(obj, attribute) return self.undefined(obj=obj, name=attribute) def unsafe_undefined(self, obj, attribute): """Return an undefined object for unsafe attributes.""" return self.undefined('access to attribute %r of %r ' 'object is unsafe.' % ( attribute, obj.__class__.__name__ ), name=attribute, obj=obj, exc=SecurityError) def call(__self, __context, __obj, *args, **kwargs): """Call an object from sandboxed code.""" # the double prefixes are to avoid double keyword argument # errors when proxying the call. if not __self.is_safe_callable(__obj): raise SecurityError('%r is not safely callable' % (__obj,)) return __context.call(__obj, *args, **kwargs) class ImmutableSandboxedEnvironment(SandboxedEnvironment): """Works exactly like the regular `SandboxedEnvironment` but does not permit modifications on the builtin mutable objects `list`, `set`, and `dict` by using the :func:`modifies_known_mutable` function. """ def is_safe_attribute(self, obj, attr, value): if not SandboxedEnvironment.is_safe_attribute(self, obj, attr, value): return False return not modifies_known_mutable(obj, attr) weblog+jinja2+markdown2-2.5/jinja2/runtime.py010064400017500000000000000426721133707607400202040ustar00henrywheel# -*- coding: utf-8 -*- """ jinja2.runtime ~~~~~~~~~~~~~~ Runtime helpers. :copyright: (c) 2010 by the Jinja Team. :license: BSD. """ import sys from itertools import chain, imap from jinja2.utils import Markup, partial, soft_unicode, escape, missing, \ concat, MethodType, FunctionType, internalcode, next from jinja2.exceptions import UndefinedError, TemplateRuntimeError, \ TemplateNotFound # these variables are exported to the template runtime __all__ = ['LoopContext', 'TemplateReference', 'Macro', 'Markup', 'TemplateRuntimeError', 'missing', 'concat', 'escape', 'markup_join', 'unicode_join', 'to_string', 'TemplateNotFound'] #: the types we support for context functions _context_function_types = (FunctionType, MethodType) #: the name of the function that is used to convert something into #: a string. 2to3 will adopt that automatically and the generated #: code can take advantage of it. to_string = unicode def markup_join(seq): """Concatenation that escapes if necessary and converts to unicode.""" buf = [] iterator = imap(soft_unicode, seq) for arg in iterator: buf.append(arg) if hasattr(arg, '__html__'): return Markup(u'').join(chain(buf, iterator)) return concat(buf) def unicode_join(seq): """Simple args to unicode conversion and concatenation.""" return concat(imap(unicode, seq)) def new_context(environment, template_name, blocks, vars=None, shared=None, globals=None, locals=None): """Internal helper to for context creation.""" if vars is None: vars = {} if shared: parent = vars else: parent = dict(globals or (), **vars) if locals: # if the parent is shared a copy should be created because # we don't want to modify the dict passed if shared: parent = dict(parent) for key, value in locals.iteritems(): if key[:2] == 'l_' and value is not missing: parent[key[2:]] = value return Context(environment, parent, template_name, blocks) class TemplateReference(object): """The `self` in templates.""" def __init__(self, context): self.__context = context def __getitem__(self, name): blocks = self.__context.blocks[name] wrap = self.__context.environment.autoescape and \ Markup or (lambda x: x) return BlockReference(name, self.__context, blocks, 0) def __repr__(self): return '<%s %r>' % ( self.__class__.__name__, self.__context.name ) class Context(object): """The template context holds the variables of a template. It stores the values passed to the template and also the names the template exports. Creating instances is neither supported nor useful as it's created automatically at various stages of the template evaluation and should not be created by hand. The context is immutable. Modifications on :attr:`parent` **must not** happen and modifications on :attr:`vars` are allowed from generated template code only. Template filters and global functions marked as :func:`contextfunction`\s get the active context passed as first argument and are allowed to access the context read-only. The template context supports read only dict operations (`get`, `keys`, `values`, `items`, `iterkeys`, `itervalues`, `iteritems`, `__getitem__`, `__contains__`). Additionally there is a :meth:`resolve` method that doesn't fail with a `KeyError` but returns an :class:`Undefined` object for missing variables. """ __slots__ = ('parent', 'vars', 'environment', 'exported_vars', 'name', 'blocks', '__weakref__') def __init__(self, environment, parent, name, blocks): self.parent = parent self.vars = {} self.environment = environment self.exported_vars = set() self.name = name # create the initial mapping of blocks. Whenever template inheritance # takes place the runtime will update this mapping with the new blocks # from the template. self.blocks = dict((k, [v]) for k, v in blocks.iteritems()) def super(self, name, current): """Render a parent block.""" try: blocks = self.blocks[name] index = blocks.index(current) + 1 blocks[index] except LookupError: return self.environment.undefined('there is no parent block ' 'called %r.' % name, name='super') return BlockReference(name, self, blocks, index) def get(self, key, default=None): """Returns an item from the template context, if it doesn't exist `default` is returned. """ try: return self[key] except KeyError: return default def resolve(self, key): """Looks up a variable like `__getitem__` or `get` but returns an :class:`Undefined` object with the name of the name looked up. """ if key in self.vars: return self.vars[key] if key in self.parent: return self.parent[key] return self.environment.undefined(name=key) def get_exported(self): """Get a new dict with the exported variables.""" return dict((k, self.vars[k]) for k in self.exported_vars) def get_all(self): """Return a copy of the complete context as dict including the exported variables. """ return dict(self.parent, **self.vars) @internalcode def call(__self, __obj, *args, **kwargs): """Call the callable with the arguments and keyword arguments provided but inject the active context or environment as first argument if the callable is a :func:`contextfunction` or :func:`environmentfunction`. """ if __debug__: __traceback_hide__ = True if isinstance(__obj, _context_function_types): if getattr(__obj, 'contextfunction', 0): args = (__self,) + args elif getattr(__obj, 'environmentfunction', 0): args = (__self.environment,) + args return __obj(*args, **kwargs) def derived(self, locals=None): """Internal helper function to create a derived context.""" context = new_context(self.environment, self.name, {}, self.parent, True, None, locals) context.blocks.update((k, list(v)) for k, v in self.blocks.iteritems()) return context def _all(meth): proxy = lambda self: getattr(self.get_all(), meth)() proxy.__doc__ = getattr(dict, meth).__doc__ proxy.__name__ = meth return proxy keys = _all('keys') values = _all('values') items = _all('items') # not available on python 3 if hasattr(dict, 'iterkeys'): iterkeys = _all('iterkeys') itervalues = _all('itervalues') iteritems = _all('iteritems') del _all def __contains__(self, name): return name in self.vars or name in self.parent def __getitem__(self, key): """Lookup a variable or raise `KeyError` if the variable is undefined. """ item = self.resolve(key) if isinstance(item, Undefined): raise KeyError(key) return item def __repr__(self): return '<%s %s of %r>' % ( self.__class__.__name__, repr(self.get_all()), self.name ) # register the context as mapping if possible try: from collections import Mapping Mapping.register(Context) except ImportError: pass class BlockReference(object): """One block on a template reference.""" def __init__(self, name, context, stack, depth): self.name = name self._context = context self._stack = stack self._depth = depth @property def super(self): """Super the block.""" if self._depth + 1 >= len(self._stack): return self._context.environment. \ undefined('there is no parent block called %r.' % self.name, name='super') return BlockReference(self.name, self._context, self._stack, self._depth + 1) @internalcode def __call__(self): rv = concat(self._stack[self._depth](self._context)) if self._context.environment.autoescape: rv = Markup(rv) return rv class LoopContext(object): """A loop context for dynamic iteration.""" def __init__(self, iterable, recurse=None): self._iterator = iter(iterable) self._recurse = recurse self.index0 = -1 # try to get the length of the iterable early. This must be done # here because there are some broken iterators around where there # __len__ is the number of iterations left (i'm looking at your # listreverseiterator!). try: self._length = len(iterable) except (TypeError, AttributeError): self._length = None def cycle(self, *args): """Cycles among the arguments with the current loop index.""" if not args: raise TypeError('no items for cycling given') return args[self.index0 % len(args)] first = property(lambda x: x.index0 == 0) last = property(lambda x: x.index0 + 1 == x.length) index = property(lambda x: x.index0 + 1) revindex = property(lambda x: x.length - x.index0) revindex0 = property(lambda x: x.length - x.index) def __len__(self): return self.length def __iter__(self): return LoopContextIterator(self) @internalcode def loop(self, iterable): if self._recurse is None: raise TypeError('Tried to call non recursive loop. Maybe you ' "forgot the 'recursive' modifier.") return self._recurse(iterable, self._recurse) # a nifty trick to enhance the error message if someone tried to call # the the loop without or with too many arguments. __call__ = loop; del loop @property def length(self): if self._length is None: # if was not possible to get the length of the iterator when # the loop context was created (ie: iterating over a generator) # we have to convert the iterable into a sequence and use the # length of that. iterable = tuple(self._iterator) self._iterator = iter(iterable) self._length = len(iterable) + self.index0 + 1 return self._length def __repr__(self): return '<%s %r/%r>' % ( self.__class__.__name__, self.index, self.length ) class LoopContextIterator(object): """The iterator for a loop context.""" __slots__ = ('context',) def __init__(self, context): self.context = context def __iter__(self): return self def next(self): ctx = self.context ctx.index0 += 1 return next(ctx._iterator), ctx class Macro(object): """Wraps a macro.""" def __init__(self, environment, func, name, arguments, defaults, catch_kwargs, catch_varargs, caller): self._environment = environment self._func = func self._argument_count = len(arguments) self.name = name self.arguments = arguments self.defaults = defaults self.catch_kwargs = catch_kwargs self.catch_varargs = catch_varargs self.caller = caller @internalcode def __call__(self, *args, **kwargs): arguments = [] for idx, name in enumerate(self.arguments): try: value = args[idx] except: try: value = kwargs.pop(name) except: try: value = self.defaults[idx - self._argument_count] except: value = self._environment.undefined( 'parameter %r was not provided' % name, name=name) arguments.append(value) # it's important that the order of these arguments does not change # if not also changed in the compiler's `function_scoping` method. # the order is caller, keyword arguments, positional arguments! if self.caller: caller = kwargs.pop('caller', None) if caller is None: caller = self._environment.undefined('No caller defined', name='caller') arguments.append(caller) if self.catch_kwargs: arguments.append(kwargs) elif kwargs: raise TypeError('macro %r takes no keyword argument %r' % (self.name, next(iter(kwargs)))) if self.catch_varargs: arguments.append(args[self._argument_count:]) elif len(args) > self._argument_count: raise TypeError('macro %r takes not more than %d argument(s)' % (self.name, len(self.arguments))) return self._func(*arguments) def __repr__(self): return '<%s %s>' % ( self.__class__.__name__, self.name is None and 'anonymous' or repr(self.name) ) class Undefined(object): """The default undefined type. This undefined type can be printed and iterated over, but every other access will raise an :exc:`UndefinedError`: >>> foo = Undefined(name='foo') >>> str(foo) '' >>> not foo True >>> foo + 42 Traceback (most recent call last): ... UndefinedError: 'foo' is undefined """ __slots__ = ('_undefined_hint', '_undefined_obj', '_undefined_name', '_undefined_exception') def __init__(self, hint=None, obj=None, name=None, exc=UndefinedError): self._undefined_hint = hint self._undefined_obj = obj self._undefined_name = name self._undefined_exception = exc @internalcode def _fail_with_undefined_error(self, *args, **kwargs): """Regular callback function for undefined objects that raises an `UndefinedError` on call. """ if self._undefined_hint is None: if self._undefined_obj is None: hint = '%r is undefined' % self._undefined_name elif not isinstance(self._undefined_name, basestring): hint = '%r object has no element %r' % ( self._undefined_obj.__class__.__name__, self._undefined_name ) else: hint = '%r object has no attribute %r' % ( self._undefined_obj.__class__.__name__, self._undefined_name ) else: hint = self._undefined_hint raise self._undefined_exception(hint) __add__ = __radd__ = __mul__ = __rmul__ = __div__ = __rdiv__ = \ __truediv__ = __rtruediv__ = __floordiv__ = __rfloordiv__ = \ __mod__ = __rmod__ = __pos__ = __neg__ = __call__ = \ __getattr__ = __getitem__ = __lt__ = __le__ = __gt__ = __ge__ = \ __int__ = __float__ = __complex__ = __pow__ = __rpow__ = \ _fail_with_undefined_error def __str__(self): return unicode(self).encode('utf-8') # unicode goes after __str__ because we configured 2to3 to rename # __unicode__ to __str__. because the 2to3 tree is not designed to # remove nodes from it, we leave the above __str__ around and let # it override at runtime. def __unicode__(self): return u'' def __len__(self): return 0 def __iter__(self): if 0: yield None def __nonzero__(self): return False def __repr__(self): return 'Undefined' class DebugUndefined(Undefined): """An undefined that returns the debug info when printed. >>> foo = DebugUndefined(name='foo') >>> str(foo) '{{ foo }}' >>> not foo True >>> foo + 42 Traceback (most recent call last): ... UndefinedError: 'foo' is undefined """ __slots__ = () def __unicode__(self): if self._undefined_hint is None: if self._undefined_obj is None: return u'{{ %s }}' % self._undefined_name return '{{ no such element: %s[%r] }}' % ( self._undefined_obj.__class__.__name__, self._undefined_name ) return u'{{ undefined value printed: %s }}' % self._undefined_hint class StrictUndefined(Undefined): """An undefined that barks on print and iteration as well as boolean tests and all kinds of comparisons. In other words: you can do nothing with it except checking if it's defined using the `defined` test. >>> foo = StrictUndefined(name='foo') >>> str(foo) Traceback (most recent call last): ... UndefinedError: 'foo' is undefined >>> not foo Traceback (most recent call last): ... UndefinedError: 'foo' is undefined >>> foo + 42 Traceback (most recent call last): ... UndefinedError: 'foo' is undefined """ __slots__ = () __iter__ = __unicode__ = __str__ = __len__ = __nonzero__ = __eq__ = \ __ne__ = Undefined._fail_with_undefined_error # remove remaining slots attributes, after the metaclass did the magic they # are unneeded and irritating as they contain wrong data for the subclasses. del Undefined.__slots__, DebugUndefined.__slots__, StrictUndefined.__slots__ weblog+jinja2+markdown2-2.5/jinja2/parser.py010064400017500000000000001034351133707607400200100ustar00henrywheel# -*- coding: utf-8 -*- """ jinja2.parser ~~~~~~~~~~~~~ Implements the template parser. :copyright: (c) 2010 by the Jinja Team. :license: BSD, see LICENSE for more details. """ from jinja2 import nodes from jinja2.exceptions import TemplateSyntaxError, TemplateAssertionError from jinja2.utils import next from jinja2.lexer import describe_token, describe_token_expr #: statements that callinto _statement_keywords = frozenset(['for', 'if', 'block', 'extends', 'print', 'macro', 'include', 'from', 'import', 'set']) _compare_operators = frozenset(['eq', 'ne', 'lt', 'lteq', 'gt', 'gteq']) class Parser(object): """This is the central parsing class Jinja2 uses. It's passed to extensions and can be used to parse expressions or statements. """ def __init__(self, environment, source, name=None, filename=None, state=None): self.environment = environment self.stream = environment._tokenize(source, name, filename, state) self.name = name self.filename = filename self.closed = False self.extensions = {} for extension in environment.extensions.itervalues(): for tag in extension.tags: self.extensions[tag] = extension.parse self._last_identifier = 0 self._tag_stack = [] self._end_token_stack = [] def fail(self, msg, lineno=None, exc=TemplateSyntaxError): """Convenience method that raises `exc` with the message, passed line number or last line number as well as the current name and filename. """ if lineno is None: lineno = self.stream.current.lineno raise exc(msg, lineno, self.name, self.filename) def _fail_ut_eof(self, name, end_token_stack, lineno): expected = [] for exprs in end_token_stack: expected.extend(map(describe_token_expr, exprs)) currently_looking = ' or '.join("'%s'" % describe_token_expr(expr) for expr in end_token_stack[-1]) if name is None: message = ['Unexpected end of template.'] else: message = ['Encountered unknown tag \'%s\'.' % name] if name is not None and name in expected: message.append('You probably made a nesting mistake. Jinja ' 'is expecting this tag, but currently looking ' 'for %s.' % currently_looking) else: message.append('Jinja was looking for the following tags: ' '%s.' % currently_looking) if self._tag_stack: message.append('The innermost block that needs to be ' 'closed is \'%s\'.' % self._tag_stack[-1]) self.fail(' '.join(message), lineno) def fail_unknown_tag(self, name, lineno=None): """Called if the parser encounters an unknown tag. Tries to fail with a human readable error message that could help to identify the problem. """ return self._fail_ut_eof(name, self._end_token_stack, lineno) def fail_eof(self, end_tokens=None, lineno=None): """Like fail_unknown_tag but for end of template situations.""" stack = list(self._end_token_stack) if end_tokens is not None: stack.append(end_tokens) return self._fail_ut_eof(None, stack, lineno) def is_tuple_end(self, extra_end_rules=None): """Are we at the end of a tuple?""" if self.stream.current.type in ('variable_end', 'block_end', 'rparen'): return True elif extra_end_rules is not None: return self.stream.current.test_any(extra_end_rules) return False def free_identifier(self, lineno=None): """Return a new free identifier as :class:`~jinja2.nodes.InternalName`.""" self._last_identifier += 1 rv = object.__new__(nodes.InternalName) nodes.Node.__init__(rv, 'fi%d' % self._last_identifier, lineno=lineno) return rv def parse_statement(self): """Parse a single statement.""" token = self.stream.current if token.type != 'name': self.fail('tag name expected', token.lineno) self._tag_stack.append(token.value) pop_tag = True try: if token.value in _statement_keywords: return getattr(self, 'parse_' + self.stream.current.value)() if token.value == 'call': return self.parse_call_block() if token.value == 'filter': return self.parse_filter_block() ext = self.extensions.get(token.value) if ext is not None: return ext(self) # did not work out, remove the token we pushed by accident # from the stack so that the unknown tag fail function can # produce a proper error message. self._tag_stack.pop() pop_tag = False self.fail_unknown_tag(token.value, token.lineno) finally: if pop_tag: self._tag_stack.pop() def parse_statements(self, end_tokens, drop_needle=False): """Parse multiple statements into a list until one of the end tokens is reached. This is used to parse the body of statements as it also parses template data if appropriate. The parser checks first if the current token is a colon and skips it if there is one. Then it checks for the block end and parses until if one of the `end_tokens` is reached. Per default the active token in the stream at the end of the call is the matched end token. If this is not wanted `drop_needle` can be set to `True` and the end token is removed. """ # the first token may be a colon for python compatibility self.stream.skip_if('colon') # in the future it would be possible to add whole code sections # by adding some sort of end of statement token and parsing those here. self.stream.expect('block_end') result = self.subparse(end_tokens) # we reached the end of the template too early, the subparser # does not check for this, so we do that now if self.stream.current.type == 'eof': self.fail_eof(end_tokens) if drop_needle: next(self.stream) return result def parse_set(self): """Parse an assign statement.""" lineno = next(self.stream).lineno target = self.parse_assign_target() self.stream.expect('assign') expr = self.parse_tuple() return nodes.Assign(target, expr, lineno=lineno) def parse_for(self): """Parse a for loop.""" lineno = self.stream.expect('name:for').lineno target = self.parse_assign_target(extra_end_rules=('name:in',)) self.stream.expect('name:in') iter = self.parse_tuple(with_condexpr=False, extra_end_rules=('name:recursive',)) test = None if self.stream.skip_if('name:if'): test = self.parse_expression() recursive = self.stream.skip_if('name:recursive') body = self.parse_statements(('name:endfor', 'name:else')) if next(self.stream).value == 'endfor': else_ = [] else: else_ = self.parse_statements(('name:endfor',), drop_needle=True) return nodes.For(target, iter, body, else_, test, recursive, lineno=lineno) def parse_if(self): """Parse an if construct.""" node = result = nodes.If(lineno=self.stream.expect('name:if').lineno) while 1: node.test = self.parse_tuple(with_condexpr=False) node.body = self.parse_statements(('name:elif', 'name:else', 'name:endif')) token = next(self.stream) if token.test('name:elif'): new_node = nodes.If(lineno=self.stream.current.lineno) node.else_ = [new_node] node = new_node continue elif token.test('name:else'): node.else_ = self.parse_statements(('name:endif',), drop_needle=True) else: node.else_ = [] break return result def parse_block(self): node = nodes.Block(lineno=next(self.stream).lineno) node.name = self.stream.expect('name').value node.scoped = self.stream.skip_if('name:scoped') # common problem people encounter when switching from django # to jinja. we do not support hyphens in block names, so let's # raise a nicer error message in that case. if self.stream.current.type == 'sub': self.fail('Block names in Jinja have to be valid Python ' 'identifiers and may not contain hypens, use an ' 'underscore instead.') node.body = self.parse_statements(('name:endblock',), drop_needle=True) self.stream.skip_if('name:' + node.name) return node def parse_extends(self): node = nodes.Extends(lineno=next(self.stream).lineno) node.template = self.parse_expression() return node def parse_import_context(self, node, default): if self.stream.current.test_any('name:with', 'name:without') and \ self.stream.look().test('name:context'): node.with_context = next(self.stream).value == 'with' self.stream.skip() else: node.with_context = default return node def parse_include(self): node = nodes.Include(lineno=next(self.stream).lineno) node.template = self.parse_expression() if self.stream.current.test('name:ignore') and \ self.stream.look().test('name:missing'): node.ignore_missing = True self.stream.skip(2) else: node.ignore_missing = False return self.parse_import_context(node, True) def parse_import(self): node = nodes.Import(lineno=next(self.stream).lineno) node.template = self.parse_expression() self.stream.expect('name:as') node.target = self.parse_assign_target(name_only=True).name return self.parse_import_context(node, False) def parse_from(self): node = nodes.FromImport(lineno=next(self.stream).lineno) node.template = self.parse_expression() self.stream.expect('name:import') node.names = [] def parse_context(): if self.stream.current.value in ('with', 'without') and \ self.stream.look().test('name:context'): node.with_context = next(self.stream).value == 'with' self.stream.skip() return True return False while 1: if node.names: self.stream.expect('comma') if self.stream.current.type == 'name': if parse_context(): break target = self.parse_assign_target(name_only=True) if target.name.startswith('_'): self.fail('names starting with an underline can not ' 'be imported', target.lineno, exc=TemplateAssertionError) if self.stream.skip_if('name:as'): alias = self.parse_assign_target(name_only=True) node.names.append((target.name, alias.name)) else: node.names.append(target.name) if parse_context() or self.stream.current.type != 'comma': break else: break if not hasattr(node, 'with_context'): node.with_context = False self.stream.skip_if('comma') return node def parse_signature(self, node): node.args = args = [] node.defaults = defaults = [] self.stream.expect('lparen') while self.stream.current.type != 'rparen': if args: self.stream.expect('comma') arg = self.parse_assign_target(name_only=True) arg.set_ctx('param') if self.stream.skip_if('assign'): defaults.append(self.parse_expression()) args.append(arg) self.stream.expect('rparen') def parse_call_block(self): node = nodes.CallBlock(lineno=next(self.stream).lineno) if self.stream.current.type == 'lparen': self.parse_signature(node) else: node.args = [] node.defaults = [] node.call = self.parse_expression() if not isinstance(node.call, nodes.Call): self.fail('expected call', node.lineno) node.body = self.parse_statements(('name:endcall',), drop_needle=True) return node def parse_filter_block(self): node = nodes.FilterBlock(lineno=next(self.stream).lineno) node.filter = self.parse_filter(None, start_inline=True) node.body = self.parse_statements(('name:endfilter',), drop_needle=True) return node def parse_macro(self): node = nodes.Macro(lineno=next(self.stream).lineno) node.name = self.parse_assign_target(name_only=True).name self.parse_signature(node) node.body = self.parse_statements(('name:endmacro',), drop_needle=True) return node def parse_print(self): node = nodes.Output(lineno=next(self.stream).lineno) node.nodes = [] while self.stream.current.type != 'block_end': if node.nodes: self.stream.expect('comma') node.nodes.append(self.parse_expression()) return node def parse_assign_target(self, with_tuple=True, name_only=False, extra_end_rules=None): """Parse an assignment target. As Jinja2 allows assignments to tuples, this function can parse all allowed assignment targets. Per default assignments to tuples are parsed, that can be disable however by setting `with_tuple` to `False`. If only assignments to names are wanted `name_only` can be set to `True`. The `extra_end_rules` parameter is forwarded to the tuple parsing function. """ if name_only: token = self.stream.expect('name') target = nodes.Name(token.value, 'store', lineno=token.lineno) else: if with_tuple: target = self.parse_tuple(simplified=True, extra_end_rules=extra_end_rules) else: target = self.parse_primary(with_postfix=False) target.set_ctx('store') if not target.can_assign(): self.fail('can\'t assign to %r' % target.__class__. __name__.lower(), target.lineno) return target def parse_expression(self, with_condexpr=True): """Parse an expression. Per default all expressions are parsed, if the optional `with_condexpr` parameter is set to `False` conditional expressions are not parsed. """ if with_condexpr: return self.parse_condexpr() return self.parse_or() def parse_condexpr(self): lineno = self.stream.current.lineno expr1 = self.parse_or() while self.stream.skip_if('name:if'): expr2 = self.parse_or() if self.stream.skip_if('name:else'): expr3 = self.parse_condexpr() else: expr3 = None expr1 = nodes.CondExpr(expr2, expr1, expr3, lineno=lineno) lineno = self.stream.current.lineno return expr1 def parse_or(self): lineno = self.stream.current.lineno left = self.parse_and() while self.stream.skip_if('name:or'): right = self.parse_and() left = nodes.Or(left, right, lineno=lineno) lineno = self.stream.current.lineno return left def parse_and(self): lineno = self.stream.current.lineno left = self.parse_not() while self.stream.skip_if('name:and'): right = self.parse_not() left = nodes.And(left, right, lineno=lineno) lineno = self.stream.current.lineno return left def parse_not(self): if self.stream.current.test('name:not'): lineno = next(self.stream).lineno return nodes.Not(self.parse_not(), lineno=lineno) return self.parse_compare() def parse_compare(self): lineno = self.stream.current.lineno expr = self.parse_add() ops = [] while 1: token_type = self.stream.current.type if token_type in _compare_operators: next(self.stream) ops.append(nodes.Operand(token_type, self.parse_add())) elif self.stream.skip_if('name:in'): ops.append(nodes.Operand('in', self.parse_add())) elif self.stream.current.test('name:not') and \ self.stream.look().test('name:in'): self.stream.skip(2) ops.append(nodes.Operand('notin', self.parse_add())) else: break lineno = self.stream.current.lineno if not ops: return expr return nodes.Compare(expr, ops, lineno=lineno) def parse_add(self): lineno = self.stream.current.lineno left = self.parse_sub() while self.stream.current.type == 'add': next(self.stream) right = self.parse_sub() left = nodes.Add(left, right, lineno=lineno) lineno = self.stream.current.lineno return left def parse_sub(self): lineno = self.stream.current.lineno left = self.parse_concat() while self.stream.current.type == 'sub': next(self.stream) right = self.parse_concat() left = nodes.Sub(left, right, lineno=lineno) lineno = self.stream.current.lineno return left def parse_concat(self): lineno = self.stream.current.lineno args = [self.parse_mul()] while self.stream.current.type == 'tilde': next(self.stream) args.append(self.parse_mul()) if len(args) == 1: return args[0] return nodes.Concat(args, lineno=lineno) def parse_mul(self): lineno = self.stream.current.lineno left = self.parse_div() while self.stream.current.type == 'mul': next(self.stream) right = self.parse_div() left = nodes.Mul(left, right, lineno=lineno) lineno = self.stream.current.lineno return left def parse_div(self): lineno = self.stream.current.lineno left = self.parse_floordiv() while self.stream.current.type == 'div': next(self.stream) right = self.parse_floordiv() left = nodes.Div(left, right, lineno=lineno) lineno = self.stream.current.lineno return left def parse_floordiv(self): lineno = self.stream.current.lineno left = self.parse_mod() while self.stream.current.type == 'floordiv': next(self.stream) right = self.parse_mod() left = nodes.FloorDiv(left, right, lineno=lineno) lineno = self.stream.current.lineno return left def parse_mod(self): lineno = self.stream.current.lineno left = self.parse_pow() while self.stream.current.type == 'mod': next(self.stream) right = self.parse_pow() left = nodes.Mod(left, right, lineno=lineno) lineno = self.stream.current.lineno return left def parse_pow(self): lineno = self.stream.current.lineno left = self.parse_unary() while self.stream.current.type == 'pow': next(self.stream) right = self.parse_unary() left = nodes.Pow(left, right, lineno=lineno) lineno = self.stream.current.lineno return left def parse_unary(self): token_type = self.stream.current.type lineno = self.stream.current.lineno if token_type == 'sub': next(self.stream) node = self.parse_unary() return nodes.Neg(node, lineno=lineno) if token_type == 'add': next(self.stream) node = self.parse_unary() return nodes.Pos(node, lineno=lineno) return self.parse_primary() def parse_primary(self, with_postfix=True): token = self.stream.current if token.type == 'name': if token.value in ('true', 'false', 'True', 'False'): node = nodes.Const(token.value in ('true', 'True'), lineno=token.lineno) elif token.value in ('none', 'None'): node = nodes.Const(None, lineno=token.lineno) else: node = nodes.Name(token.value, 'load', lineno=token.lineno) next(self.stream) elif token.type == 'string': next(self.stream) buf = [token.value] lineno = token.lineno while self.stream.current.type == 'string': buf.append(self.stream.current.value) next(self.stream) node = nodes.Const(''.join(buf), lineno=lineno) elif token.type in ('integer', 'float'): next(self.stream) node = nodes.Const(token.value, lineno=token.lineno) elif token.type == 'lparen': next(self.stream) node = self.parse_tuple(explicit_parentheses=True) self.stream.expect('rparen') elif token.type == 'lbracket': node = self.parse_list() elif token.type == 'lbrace': node = self.parse_dict() else: self.fail("unexpected '%s'" % describe_token(token), token.lineno) if with_postfix: node = self.parse_postfix(node) return node def parse_tuple(self, simplified=False, with_condexpr=True, extra_end_rules=None, explicit_parentheses=False): """Works like `parse_expression` but if multiple expressions are delimited by a comma a :class:`~jinja2.nodes.Tuple` node is created. This method could also return a regular expression instead of a tuple if no commas where found. The default parsing mode is a full tuple. If `simplified` is `True` only names and literals are parsed. The `no_condexpr` parameter is forwarded to :meth:`parse_expression`. Because tuples do not require delimiters and may end in a bogus comma an extra hint is needed that marks the end of a tuple. For example for loops support tuples between `for` and `in`. In that case the `extra_end_rules` is set to ``['name:in']``. `explicit_parentheses` is true if the parsing was triggered by an expression in parentheses. This is used to figure out if an empty tuple is a valid expression or not. """ lineno = self.stream.current.lineno if simplified: parse = lambda: self.parse_primary(with_postfix=False) elif with_condexpr: parse = self.parse_expression else: parse = lambda: self.parse_expression(with_condexpr=False) args = [] is_tuple = False while 1: if args: self.stream.expect('comma') if self.is_tuple_end(extra_end_rules): break args.append(parse()) if self.stream.current.type == 'comma': is_tuple = True else: break lineno = self.stream.current.lineno if not is_tuple: if args: return args[0] # if we don't have explicit parentheses, an empty tuple is # not a valid expression. This would mean nothing (literally # nothing) in the spot of an expression would be an empty # tuple. if not explicit_parentheses: self.fail('Expected an expression, got \'%s\'' % describe_token(self.stream.current)) return nodes.Tuple(args, 'load', lineno=lineno) def parse_list(self): token = self.stream.expect('lbracket') items = [] while self.stream.current.type != 'rbracket': if items: self.stream.expect('comma') if self.stream.current.type == 'rbracket': break items.append(self.parse_expression()) self.stream.expect('rbracket') return nodes.List(items, lineno=token.lineno) def parse_dict(self): token = self.stream.expect('lbrace') items = [] while self.stream.current.type != 'rbrace': if items: self.stream.expect('comma') if self.stream.current.type == 'rbrace': break key = self.parse_expression() self.stream.expect('colon') value = self.parse_expression() items.append(nodes.Pair(key, value, lineno=key.lineno)) self.stream.expect('rbrace') return nodes.Dict(items, lineno=token.lineno) def parse_postfix(self, node): while 1: token_type = self.stream.current.type if token_type == 'dot' or token_type == 'lbracket': node = self.parse_subscript(node) elif token_type == 'lparen': node = self.parse_call(node) elif token_type == 'pipe': node = self.parse_filter(node) elif token_type == 'name' and self.stream.current.value == 'is': node = self.parse_test(node) else: break return node def parse_subscript(self, node): token = next(self.stream) if token.type == 'dot': attr_token = self.stream.current next(self.stream) if attr_token.type == 'name': return nodes.Getattr(node, attr_token.value, 'load', lineno=token.lineno) elif attr_token.type != 'integer': self.fail('expected name or number', attr_token.lineno) arg = nodes.Const(attr_token.value, lineno=attr_token.lineno) return nodes.Getitem(node, arg, 'load', lineno=token.lineno) if token.type == 'lbracket': priority_on_attribute = False args = [] while self.stream.current.type != 'rbracket': if args: self.stream.expect('comma') args.append(self.parse_subscribed()) self.stream.expect('rbracket') if len(args) == 1: arg = args[0] else: arg = nodes.Tuple(args, 'load', lineno=token.lineno) return nodes.Getitem(node, arg, 'load', lineno=token.lineno) self.fail('expected subscript expression', self.lineno) def parse_subscribed(self): lineno = self.stream.current.lineno if self.stream.current.type == 'colon': next(self.stream) args = [None] else: node = self.parse_expression() if self.stream.current.type != 'colon': return node next(self.stream) args = [node] if self.stream.current.type == 'colon': args.append(None) elif self.stream.current.type not in ('rbracket', 'comma'): args.append(self.parse_expression()) else: args.append(None) if self.stream.current.type == 'colon': next(self.stream) if self.stream.current.type not in ('rbracket', 'comma'): args.append(self.parse_expression()) else: args.append(None) else: args.append(None) return nodes.Slice(lineno=lineno, *args) def parse_call(self, node): token = self.stream.expect('lparen') args = [] kwargs = [] dyn_args = dyn_kwargs = None require_comma = False def ensure(expr): if not expr: self.fail('invalid syntax for function call expression', token.lineno) while self.stream.current.type != 'rparen': if require_comma: self.stream.expect('comma') # support for trailing comma if self.stream.current.type == 'rparen': break if self.stream.current.type == 'mul': ensure(dyn_args is None and dyn_kwargs is None) next(self.stream) dyn_args = self.parse_expression() elif self.stream.current.type == 'pow': ensure(dyn_kwargs is None) next(self.stream) dyn_kwargs = self.parse_expression() else: ensure(dyn_args is None and dyn_kwargs is None) if self.stream.current.type == 'name' and \ self.stream.look().type == 'assign': key = self.stream.current.value self.stream.skip(2) value = self.parse_expression() kwargs.append(nodes.Keyword(key, value, lineno=value.lineno)) else: ensure(not kwargs) args.append(self.parse_expression()) require_comma = True self.stream.expect('rparen') if node is None: return args, kwargs, dyn_args, dyn_kwargs return nodes.Call(node, args, kwargs, dyn_args, dyn_kwargs, lineno=token.lineno) def parse_filter(self, node, start_inline=False): while self.stream.current.type == 'pipe' or start_inline: if not start_inline: next(self.stream) token = self.stream.expect('name') name = token.value while self.stream.current.type == 'dot': next(self.stream) name += '.' + self.stream.expect('name').value if self.stream.current.type == 'lparen': args, kwargs, dyn_args, dyn_kwargs = self.parse_call(None) else: args = [] kwargs = [] dyn_args = dyn_kwargs = None node = nodes.Filter(node, name, args, kwargs, dyn_args, dyn_kwargs, lineno=token.lineno) start_inline = False return node def parse_test(self, node): token = next(self.stream) if self.stream.current.test('name:not'): next(self.stream) negated = True else: negated = False name = self.stream.expect('name').value while self.stream.current.type == 'dot': next(self.stream) name += '.' + self.stream.expect('name').value dyn_args = dyn_kwargs = None kwargs = [] if self.stream.current.type == 'lparen': args, kwargs, dyn_args, dyn_kwargs = self.parse_call(None) elif self.stream.current.type in ('name', 'string', 'integer', 'float', 'lparen', 'lbracket', 'lbrace') and not \ self.stream.current.test_any('name:else', 'name:or', 'name:and'): if self.stream.current.test('name:is'): self.fail('You cannot chain multiple tests with is') args = [self.parse_expression()] else: args = [] node = nodes.Test(node, name, args, kwargs, dyn_args, dyn_kwargs, lineno=token.lineno) if negated: node = nodes.Not(node, lineno=token.lineno) return node def subparse(self, end_tokens=None): body = [] data_buffer = [] add_data = data_buffer.append if end_tokens is not None: self._end_token_stack.append(end_tokens) def flush_data(): if data_buffer: lineno = data_buffer[0].lineno body.append(nodes.Output(data_buffer[:], lineno=lineno)) del data_buffer[:] try: while self.stream: token = self.stream.current if token.type == 'data': if token.value: add_data(nodes.TemplateData(token.value, lineno=token.lineno)) next(self.stream) elif token.type == 'variable_begin': next(self.stream) add_data(self.parse_tuple(with_condexpr=True)) self.stream.expect('variable_end') elif token.type == 'block_begin': flush_data() next(self.stream) if end_tokens is not None and \ self.stream.current.test_any(*end_tokens): return body rv = self.parse_statement() if isinstance(rv, list): body.extend(rv) else: body.append(rv) self.stream.expect('block_end') else: raise AssertionError('internal parsing error') flush_data() finally: if end_tokens is not None: self._end_token_stack.pop() return body def parse(self): """Parse the whole template into a `Template` node.""" result = nodes.Template(self.subparse(), lineno=1) result.set_environment(self.environment) return result weblog+jinja2+markdown2-2.5/jinja2/optimizer.py010064400017500000000000000043761133707607400205420ustar00henrywheel# -*- coding: utf-8 -*- """ jinja2.optimizer ~~~~~~~~~~~~~~~~ The jinja optimizer is currently trying to constant fold a few expressions and modify the AST in place so that it should be easier to evaluate it. Because the AST does not contain all the scoping information and the compiler has to find that out, we cannot do all the optimizations we want. For example loop unrolling doesn't work because unrolled loops would have a different scoping. The solution would be a second syntax tree that has the scoping rules stored. :copyright: (c) 2010 by the Jinja Team. :license: BSD. """ from jinja2 import nodes from jinja2.visitor import NodeTransformer def optimize(node, environment): """The context hint can be used to perform an static optimization based on the context given.""" optimizer = Optimizer(environment) return optimizer.visit(node) class Optimizer(NodeTransformer): def __init__(self, environment): self.environment = environment def visit_If(self, node): """Eliminate dead code.""" # do not optimize ifs that have a block inside so that it doesn't # break super(). if node.find(nodes.Block) is not None: return self.generic_visit(node) try: val = self.visit(node.test).as_const() except nodes.Impossible: return self.generic_visit(node) if val: body = node.body else: body = node.else_ result = [] for node in body: result.extend(self.visit_list(node)) return result def fold(self, node): """Do constant folding.""" node = self.generic_visit(node) try: return nodes.Const.from_untrusted(node.as_const(), lineno=node.lineno, environment=self.environment) except nodes.Impossible: return node visit_Add = visit_Sub = visit_Mul = visit_Div = visit_FloorDiv = \ visit_Pow = visit_Mod = visit_And = visit_Or = visit_Pos = visit_Neg = \ visit_Not = visit_Compare = visit_Getitem = visit_Getattr = visit_Call = \ visit_Filter = visit_Test = visit_CondExpr = fold del fold weblog+jinja2+markdown2-2.5/jinja2/nodes.py010064400017500000000000000564201133707607400176250ustar00henrywheel# -*- coding: utf-8 -*- """ jinja2.nodes ~~~~~~~~~~~~ This module implements additional nodes derived from the ast base node. It also provides some node tree helper functions like `in_lineno` and `get_nodes` used by the parser and translator in order to normalize python and jinja nodes. :copyright: (c) 2010 by the Jinja Team. :license: BSD, see LICENSE for more details. """ import operator from itertools import chain, izip from collections import deque from jinja2.utils import Markup _binop_to_func = { '*': operator.mul, '/': operator.truediv, '//': operator.floordiv, '**': operator.pow, '%': operator.mod, '+': operator.add, '-': operator.sub } _uaop_to_func = { 'not': operator.not_, '+': operator.pos, '-': operator.neg } _cmpop_to_func = { 'eq': operator.eq, 'ne': operator.ne, 'gt': operator.gt, 'gteq': operator.ge, 'lt': operator.lt, 'lteq': operator.le, 'in': lambda a, b: a in b, 'notin': lambda a, b: a not in b } class Impossible(Exception): """Raised if the node could not perform a requested action.""" class NodeType(type): """A metaclass for nodes that handles the field and attribute inheritance. fields and attributes from the parent class are automatically forwarded to the child.""" def __new__(cls, name, bases, d): for attr in 'fields', 'attributes': storage = [] storage.extend(getattr(bases[0], attr, ())) storage.extend(d.get(attr, ())) assert len(bases) == 1, 'multiple inheritance not allowed' assert len(storage) == len(set(storage)), 'layout conflict' d[attr] = tuple(storage) d.setdefault('abstract', False) return type.__new__(cls, name, bases, d) class Node(object): """Baseclass for all Jinja2 nodes. There are a number of nodes available of different types. There are three major types: - :class:`Stmt`: statements - :class:`Expr`: expressions - :class:`Helper`: helper nodes - :class:`Template`: the outermost wrapper node All nodes have fields and attributes. Fields may be other nodes, lists, or arbitrary values. Fields are passed to the constructor as regular positional arguments, attributes as keyword arguments. Each node has two attributes: `lineno` (the line number of the node) and `environment`. The `environment` attribute is set at the end of the parsing process for all nodes automatically. """ __metaclass__ = NodeType fields = () attributes = ('lineno', 'environment') abstract = True def __init__(self, *fields, **attributes): if self.abstract: raise TypeError('abstract nodes are not instanciable') if fields: if len(fields) != len(self.fields): if not self.fields: raise TypeError('%r takes 0 arguments' % self.__class__.__name__) raise TypeError('%r takes 0 or %d argument%s' % ( self.__class__.__name__, len(self.fields), len(self.fields) != 1 and 's' or '' )) for name, arg in izip(self.fields, fields): setattr(self, name, arg) for attr in self.attributes: setattr(self, attr, attributes.pop(attr, None)) if attributes: raise TypeError('unknown attribute %r' % iter(attributes).next()) def iter_fields(self, exclude=None, only=None): """This method iterates over all fields that are defined and yields ``(key, value)`` tuples. Per default all fields are returned, but it's possible to limit that to some fields by providing the `only` parameter or to exclude some using the `exclude` parameter. Both should be sets or tuples of field names. """ for name in self.fields: if (exclude is only is None) or \ (exclude is not None and name not in exclude) or \ (only is not None and name in only): try: yield name, getattr(self, name) except AttributeError: pass def iter_child_nodes(self, exclude=None, only=None): """Iterates over all direct child nodes of the node. This iterates over all fields and yields the values of they are nodes. If the value of a field is a list all the nodes in that list are returned. """ for field, item in self.iter_fields(exclude, only): if isinstance(item, list): for n in item: if isinstance(n, Node): yield n elif isinstance(item, Node): yield item def find(self, node_type): """Find the first node of a given type. If no such node exists the return value is `None`. """ for result in self.find_all(node_type): return result def find_all(self, node_type): """Find all the nodes of a given type. If the type is a tuple, the check is performed for any of the tuple items. """ for child in self.iter_child_nodes(): if isinstance(child, node_type): yield child for result in child.find_all(node_type): yield result def set_ctx(self, ctx): """Reset the context of a node and all child nodes. Per default the parser will all generate nodes that have a 'load' context as it's the most common one. This method is used in the parser to set assignment targets and other nodes to a store context. """ todo = deque([self]) while todo: node = todo.popleft() if 'ctx' in node.fields: node.ctx = ctx todo.extend(node.iter_child_nodes()) return self def set_lineno(self, lineno, override=False): """Set the line numbers of the node and children.""" todo = deque([self]) while todo: node = todo.popleft() if 'lineno' in node.attributes: if node.lineno is None or override: node.lineno = lineno todo.extend(node.iter_child_nodes()) return self def set_environment(self, environment): """Set the environment for all nodes.""" todo = deque([self]) while todo: node = todo.popleft() node.environment = environment todo.extend(node.iter_child_nodes()) return self def __eq__(self, other): return type(self) is type(other) and \ tuple(self.iter_fields()) == tuple(other.iter_fields()) def __ne__(self, other): return not self.__eq__(other) def __repr__(self): return '%s(%s)' % ( self.__class__.__name__, ', '.join('%s=%r' % (arg, getattr(self, arg, None)) for arg in self.fields) ) class Stmt(Node): """Base node for all statements.""" abstract = True class Helper(Node): """Nodes that exist in a specific context only.""" abstract = True class Template(Node): """Node that represents a template. This must be the outermost node that is passed to the compiler. """ fields = ('body',) class Output(Stmt): """A node that holds multiple expressions which are then printed out. This is used both for the `print` statement and the regular template data. """ fields = ('nodes',) class Extends(Stmt): """Represents an extends statement.""" fields = ('template',) class For(Stmt): """The for loop. `target` is the target for the iteration (usually a :class:`Name` or :class:`Tuple`), `iter` the iterable. `body` is a list of nodes that are used as loop-body, and `else_` a list of nodes for the `else` block. If no else node exists it has to be an empty list. For filtered nodes an expression can be stored as `test`, otherwise `None`. """ fields = ('target', 'iter', 'body', 'else_', 'test', 'recursive') class If(Stmt): """If `test` is true, `body` is rendered, else `else_`.""" fields = ('test', 'body', 'else_') class Macro(Stmt): """A macro definition. `name` is the name of the macro, `args` a list of arguments and `defaults` a list of defaults if there are any. `body` is a list of nodes for the macro body. """ fields = ('name', 'args', 'defaults', 'body') class CallBlock(Stmt): """Like a macro without a name but a call instead. `call` is called with the unnamed macro as `caller` argument this node holds. """ fields = ('call', 'args', 'defaults', 'body') class FilterBlock(Stmt): """Node for filter sections.""" fields = ('body', 'filter') class Block(Stmt): """A node that represents a block.""" fields = ('name', 'body', 'scoped') class Include(Stmt): """A node that represents the include tag.""" fields = ('template', 'with_context', 'ignore_missing') class Import(Stmt): """A node that represents the import tag.""" fields = ('template', 'target', 'with_context') class FromImport(Stmt): """A node that represents the from import tag. It's important to not pass unsafe names to the name attribute. The compiler translates the attribute lookups directly into getattr calls and does *not* use the subscript callback of the interface. As exported variables may not start with double underscores (which the parser asserts) this is not a problem for regular Jinja code, but if this node is used in an extension extra care must be taken. The list of names may contain tuples if aliases are wanted. """ fields = ('template', 'names', 'with_context') class ExprStmt(Stmt): """A statement that evaluates an expression and discards the result.""" fields = ('node',) class Assign(Stmt): """Assigns an expression to a target.""" fields = ('target', 'node') class Expr(Node): """Baseclass for all expressions.""" abstract = True def as_const(self): """Return the value of the expression as constant or raise :exc:`Impossible` if this was not possible: >>> Add(Const(23), Const(42)).as_const() 65 >>> Add(Const(23), Name('var', 'load')).as_const() Traceback (most recent call last): ... Impossible This requires the `environment` attribute of all nodes to be set to the environment that created the nodes. """ raise Impossible() def can_assign(self): """Check if it's possible to assign something to this node.""" return False class BinExpr(Expr): """Baseclass for all binary expressions.""" fields = ('left', 'right') operator = None abstract = True def as_const(self): f = _binop_to_func[self.operator] try: return f(self.left.as_const(), self.right.as_const()) except: raise Impossible() class UnaryExpr(Expr): """Baseclass for all unary expressions.""" fields = ('node',) operator = None abstract = True def as_const(self): f = _uaop_to_func[self.operator] try: return f(self.node.as_const()) except: raise Impossible() class Name(Expr): """Looks up a name or stores a value in a name. The `ctx` of the node can be one of the following values: - `store`: store a value in the name - `load`: load that name - `param`: like `store` but if the name was defined as function parameter. """ fields = ('name', 'ctx') def can_assign(self): return self.name not in ('true', 'false', 'none', 'True', 'False', 'None') class Literal(Expr): """Baseclass for literals.""" abstract = True class Const(Literal): """All constant values. The parser will return this node for simple constants such as ``42`` or ``"foo"`` but it can be used to store more complex values such as lists too. Only constants with a safe representation (objects where ``eval(repr(x)) == x`` is true). """ fields = ('value',) def as_const(self): return self.value @classmethod def from_untrusted(cls, value, lineno=None, environment=None): """Return a const object if the value is representable as constant value in the generated code, otherwise it will raise an `Impossible` exception. """ from compiler import has_safe_repr if not has_safe_repr(value): raise Impossible() return cls(value, lineno=lineno, environment=environment) class TemplateData(Literal): """A constant template string.""" fields = ('data',) def as_const(self): if self.environment.autoescape: return Markup(self.data) return self.data class Tuple(Literal): """For loop unpacking and some other things like multiple arguments for subscripts. Like for :class:`Name` `ctx` specifies if the tuple is used for loading the names or storing. """ fields = ('items', 'ctx') def as_const(self): return tuple(x.as_const() for x in self.items) def can_assign(self): for item in self.items: if not item.can_assign(): return False return True class List(Literal): """Any list literal such as ``[1, 2, 3]``""" fields = ('items',) def as_const(self): return [x.as_const() for x in self.items] class Dict(Literal): """Any dict literal such as ``{1: 2, 3: 4}``. The items must be a list of :class:`Pair` nodes. """ fields = ('items',) def as_const(self): return dict(x.as_const() for x in self.items) class Pair(Helper): """A key, value pair for dicts.""" fields = ('key', 'value') def as_const(self): return self.key.as_const(), self.value.as_const() class Keyword(Helper): """A key, value pair for keyword arguments where key is a string.""" fields = ('key', 'value') def as_const(self): return self.key, self.value.as_const() class CondExpr(Expr): """A conditional expression (inline if expression). (``{{ foo if bar else baz }}``) """ fields = ('test', 'expr1', 'expr2') def as_const(self): if self.test.as_const(): return self.expr1.as_const() # if we evaluate to an undefined object, we better do that at runtime if self.expr2 is None: raise Impossible() return self.expr2.as_const() class Filter(Expr): """This node applies a filter on an expression. `name` is the name of the filter, the rest of the fields are the same as for :class:`Call`. If the `node` of a filter is `None` the contents of the last buffer are filtered. Buffers are created by macros and filter blocks. """ fields = ('node', 'name', 'args', 'kwargs', 'dyn_args', 'dyn_kwargs') def as_const(self, obj=None): if self.node is obj is None: raise Impossible() # we have to be careful here because we call filter_ below. # if this variable would be called filter, 2to3 would wrap the # call in a list beause it is assuming we are talking about the # builtin filter function here which no longer returns a list in # python 3. because of that, do not rename filter_ to filter! filter_ = self.environment.filters.get(self.name) if filter_ is None or getattr(filter_, 'contextfilter', False): raise Impossible() if obj is None: obj = self.node.as_const() args = [x.as_const() for x in self.args] if getattr(filter_, 'environmentfilter', False): args.insert(0, self.environment) kwargs = dict(x.as_const() for x in self.kwargs) if self.dyn_args is not None: try: args.extend(self.dyn_args.as_const()) except: raise Impossible() if self.dyn_kwargs is not None: try: kwargs.update(self.dyn_kwargs.as_const()) except: raise Impossible() try: return filter_(obj, *args, **kwargs) except: raise Impossible() class Test(Expr): """Applies a test on an expression. `name` is the name of the test, the rest of the fields are the same as for :class:`Call`. """ fields = ('node', 'name', 'args', 'kwargs', 'dyn_args', 'dyn_kwargs') class Call(Expr): """Calls an expression. `args` is a list of arguments, `kwargs` a list of keyword arguments (list of :class:`Keyword` nodes), and `dyn_args` and `dyn_kwargs` has to be either `None` or a node that is used as node for dynamic positional (``*args``) or keyword (``**kwargs``) arguments. """ fields = ('node', 'args', 'kwargs', 'dyn_args', 'dyn_kwargs') def as_const(self): obj = self.node.as_const() # don't evaluate context functions args = [x.as_const() for x in self.args] if getattr(obj, 'contextfunction', False): raise Impossible() elif getattr(obj, 'environmentfunction', False): args.insert(0, self.environment) kwargs = dict(x.as_const() for x in self.kwargs) if self.dyn_args is not None: try: args.extend(self.dyn_args.as_const()) except: raise Impossible() if self.dyn_kwargs is not None: try: kwargs.update(self.dyn_kwargs.as_const()) except: raise Impossible() try: return obj(*args, **kwargs) except: raise Impossible() class Getitem(Expr): """Get an attribute or item from an expression and prefer the item.""" fields = ('node', 'arg', 'ctx') def as_const(self): if self.ctx != 'load': raise Impossible() try: return self.environment.getitem(self.node.as_const(), self.arg.as_const()) except: raise Impossible() def can_assign(self): return False class Getattr(Expr): """Get an attribute or item from an expression that is a ascii-only bytestring and prefer the attribute. """ fields = ('node', 'attr', 'ctx') def as_const(self): if self.ctx != 'load': raise Impossible() try: return self.environment.getattr(self.node.as_const(), arg) except: raise Impossible() def can_assign(self): return False class Slice(Expr): """Represents a slice object. This must only be used as argument for :class:`Subscript`. """ fields = ('start', 'stop', 'step') def as_const(self): def const(obj): if obj is None: return obj return obj.as_const() return slice(const(self.start), const(self.stop), const(self.step)) class Concat(Expr): """Concatenates the list of expressions provided after converting them to unicode. """ fields = ('nodes',) def as_const(self): return ''.join(unicode(x.as_const()) for x in self.nodes) class Compare(Expr): """Compares an expression with some other expressions. `ops` must be a list of :class:`Operand`\s. """ fields = ('expr', 'ops') def as_const(self): result = value = self.expr.as_const() try: for op in self.ops: new_value = op.expr.as_const() result = _cmpop_to_func[op.op](value, new_value) value = new_value except: raise Impossible() return result class Operand(Helper): """Holds an operator and an expression.""" fields = ('op', 'expr') if __debug__: Operand.__doc__ += '\nThe following operators are available: ' + \ ', '.join(sorted('``%s``' % x for x in set(_binop_to_func) | set(_uaop_to_func) | set(_cmpop_to_func))) class Mul(BinExpr): """Multiplies the left with the right node.""" operator = '*' class Div(BinExpr): """Divides the left by the right node.""" operator = '/' class FloorDiv(BinExpr): """Divides the left by the right node and truncates conver the result into an integer by truncating. """ operator = '//' class Add(BinExpr): """Add the left to the right node.""" operator = '+' class Sub(BinExpr): """Substract the right from the left node.""" operator = '-' class Mod(BinExpr): """Left modulo right.""" operator = '%' class Pow(BinExpr): """Left to the power of right.""" operator = '**' class And(BinExpr): """Short circuited AND.""" operator = 'and' def as_const(self): return self.left.as_const() and self.right.as_const() class Or(BinExpr): """Short circuited OR.""" operator = 'or' def as_const(self): return self.left.as_const() or self.right.as_const() class Not(UnaryExpr): """Negate the expression.""" operator = 'not' class Neg(UnaryExpr): """Make the expression negative.""" operator = '-' class Pos(UnaryExpr): """Make the expression positive (noop for most expressions)""" operator = '+' # Helpers for extensions class EnvironmentAttribute(Expr): """Loads an attribute from the environment object. This is useful for extensions that want to call a callback stored on the environment. """ fields = ('name',) class ExtensionAttribute(Expr): """Returns the attribute of an extension bound to the environment. The identifier is the identifier of the :class:`Extension`. This node is usually constructed by calling the :meth:`~jinja2.ext.Extension.attr` method on an extension. """ fields = ('identifier', 'name') class ImportedName(Expr): """If created with an import name the import name is returned on node access. For example ``ImportedName('cgi.escape')`` returns the `escape` function from the cgi module on evaluation. Imports are optimized by the compiler so there is no need to assign them to local variables. """ fields = ('importname',) class InternalName(Expr): """An internal name in the compiler. You cannot create these nodes yourself but the parser provides a :meth:`~jinja2.parser.Parser.free_identifier` method that creates a new identifier for you. This identifier is not available from the template and is not threated specially by the compiler. """ fields = ('name',) def __init__(self): raise TypeError('Can\'t create internal names. Use the ' '`free_identifier` method on a parser.') class MarkSafe(Expr): """Mark the wrapped expression as safe (wrap it as `Markup`).""" fields = ('expr',) def as_const(self): return Markup(self.expr.as_const()) class ContextReference(Expr): """Returns the current template context.""" class Continue(Stmt): """Continue a loop.""" class Break(Stmt): """Break a loop.""" class Scope(Stmt): """An artificial scope.""" fields = ('body',) # make sure nobody creates custom nodes def _failing_new(*args, **kwargs): raise TypeError('can\'t create custom node types') NodeType.__new__ = staticmethod(_failing_new); del _failing_new weblog+jinja2+markdown2-2.5/jinja2/meta.py010064400017500000000000000100601133707607400174310ustar00henrywheel# -*- coding: utf-8 -*- """ jinja2.meta ~~~~~~~~~~~ This module implements various functions that exposes information about templates that might be interesting for various kinds of applications. :copyright: (c) 2010 by the Jinja Team, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ from jinja2 import nodes from jinja2.compiler import CodeGenerator class TrackingCodeGenerator(CodeGenerator): """We abuse the code generator for introspection.""" def __init__(self, environment): CodeGenerator.__init__(self, environment, '', '') self.undeclared_identifiers = set() def write(self, x): """Don't write.""" def pull_locals(self, frame): """Remember all undeclared identifiers.""" self.undeclared_identifiers.update(frame.identifiers.undeclared) def find_undeclared_variables(ast): """Returns a set of all variables in the AST that will be looked up from the context at runtime. Because at compile time it's not known which variables will be used depending on the path the execution takes at runtime, all variables are returned. >>> from jinja2 import Environment, meta >>> env = Environment() >>> ast = env.parse('{% set foo = 42 %}{{ bar + foo }}') >>> meta.find_undeclared_variables(ast) set(['bar']) .. admonition:: Implementation Internally the code generator is used for finding undeclared variables. This is good to know because the code generator might raise a :exc:`TemplateAssertionError` during compilation and as a matter of fact this function can currently raise that exception as well. """ codegen = TrackingCodeGenerator(ast.environment) codegen.visit(ast) return codegen.undeclared_identifiers def find_referenced_templates(ast): """Finds all the referenced templates from the AST. This will return an iterator over all the hardcoded template extensions, inclusions and imports. If dynamic inheritance or inclusion is used, `None` will be yielded. >>> from jinja2 import Environment, meta >>> env = Environment() >>> ast = env.parse('{% extends "layout.html" %}{% include helper %}') >>> list(meta.find_referenced_templates(ast)) ['layout.html', None] This function is useful for dependency tracking. For example if you want to rebuild parts of the website after a layout template has changed. """ for node in ast.find_all((nodes.Extends, nodes.FromImport, nodes.Import, nodes.Include)): if not isinstance(node.template, nodes.Const): # a tuple with some non consts in there if isinstance(node.template, (nodes.Tuple, nodes.List)): for template_name in node.template.items: # something const, only yield the strings and ignore # non-string consts that really just make no sense if isinstance(template_name, nodes.Const): if isinstance(template_name.value, basestring): yield template_name.value # something dynamic in there else: yield None # something dynamic we don't know about here else: yield None continue # constant is a basestring, direct template name if isinstance(node.template.value, basestring): yield node.template.value # a tuple or list (latter *should* not happen) made of consts, # yield the consts that are strings. We could warn here for # non string values elif isinstance(node, nodes.Include) and \ isinstance(node.template.value, (tuple, list)): for template_name in node.template.value: if isinstance(template_name, basestring): yield template_name # something else we don't care about, we could warn here else: yield None weblog+jinja2+markdown2-2.5/jinja2/loaders.py010064400017500000000000000267251133707607400201530ustar00henrywheel# -*- coding: utf-8 -*- """ jinja2.loaders ~~~~~~~~~~~~~~ Jinja loader classes. :copyright: (c) 2010 by the Jinja Team. :license: BSD, see LICENSE for more details. """ from os import path try: from hashlib import sha1 except ImportError: from sha import new as sha1 from jinja2.exceptions import TemplateNotFound from jinja2.utils import LRUCache, open_if_exists, internalcode def split_template_path(template): """Split a path into segments and perform a sanity check. If it detects '..' in the path it will raise a `TemplateNotFound` error. """ pieces = [] for piece in template.split('/'): if path.sep in piece \ or (path.altsep and path.altsep in piece) or \ piece == path.pardir: raise TemplateNotFound(template) elif piece and piece != '.': pieces.append(piece) return pieces class BaseLoader(object): """Baseclass for all loaders. Subclass this and override `get_source` to implement a custom loading mechanism. The environment provides a `get_template` method that calls the loader's `load` method to get the :class:`Template` object. A very basic example for a loader that looks up templates on the file system could look like this:: from jinja2 import BaseLoader, TemplateNotFound from os.path import join, exists, getmtime class MyLoader(BaseLoader): def __init__(self, path): self.path = path def get_source(self, environment, template): path = join(self.path, template) if not exists(path): raise TemplateNotFound(template) mtime = getmtime(path) with file(path) as f: source = f.read().decode('utf-8') return source, path, lambda: mtime == getmtime(path) """ def get_source(self, environment, template): """Get the template source, filename and reload helper for a template. It's passed the environment and template name and has to return a tuple in the form ``(source, filename, uptodate)`` or raise a `TemplateNotFound` error if it can't locate the template. The source part of the returned tuple must be the source of the template as unicode string or a ASCII bytestring. The filename should be the name of the file on the filesystem if it was loaded from there, otherwise `None`. The filename is used by python for the tracebacks if no loader extension is used. The last item in the tuple is the `uptodate` function. If auto reloading is enabled it's always called to check if the template changed. No arguments are passed so the function must store the old state somewhere (for example in a closure). If it returns `False` the template will be reloaded. """ raise TemplateNotFound(template) @internalcode def load(self, environment, name, globals=None): """Loads a template. This method looks up the template in the cache or loads one by calling :meth:`get_source`. Subclasses should not override this method as loaders working on collections of other loaders (such as :class:`PrefixLoader` or :class:`ChoiceLoader`) will not call this method but `get_source` directly. """ code = None if globals is None: globals = {} # first we try to get the source for this template together # with the filename and the uptodate function. source, filename, uptodate = self.get_source(environment, name) # try to load the code from the bytecode cache if there is a # bytecode cache configured. bcc = environment.bytecode_cache if bcc is not None: bucket = bcc.get_bucket(environment, name, filename, source) code = bucket.code # if we don't have code so far (not cached, no longer up to # date) etc. we compile the template if code is None: code = environment.compile(source, name, filename) # if the bytecode cache is available and the bucket doesn't # have a code so far, we give the bucket the new code and put # it back to the bytecode cache. if bcc is not None and bucket.code is None: bucket.code = code bcc.set_bucket(bucket) return environment.template_class.from_code(environment, code, globals, uptodate) class FileSystemLoader(BaseLoader): """Loads templates from the file system. This loader can find templates in folders on the file system and is the preferred way to load them. The loader takes the path to the templates as string, or if multiple locations are wanted a list of them which is then looked up in the given order: >>> loader = FileSystemLoader('/path/to/templates') >>> loader = FileSystemLoader(['/path/to/templates', '/other/path']) Per default the template encoding is ``'utf-8'`` which can be changed by setting the `encoding` parameter to something else. """ def __init__(self, searchpath, encoding='utf-8'): if isinstance(searchpath, basestring): searchpath = [searchpath] self.searchpath = list(searchpath) self.encoding = encoding def get_source(self, environment, template): pieces = split_template_path(template) for searchpath in self.searchpath: filename = path.join(searchpath, *pieces) f = open_if_exists(filename) if f is None: continue try: contents = f.read().decode(self.encoding) finally: f.close() mtime = path.getmtime(filename) def uptodate(): try: return path.getmtime(filename) == mtime except OSError: return False return contents, filename, uptodate raise TemplateNotFound(template) class PackageLoader(BaseLoader): """Load templates from python eggs or packages. It is constructed with the name of the python package and the path to the templates in that package:: loader = PackageLoader('mypackage', 'views') If the package path is not given, ``'templates'`` is assumed. Per default the template encoding is ``'utf-8'`` which can be changed by setting the `encoding` parameter to something else. Due to the nature of eggs it's only possible to reload templates if the package was loaded from the file system and not a zip file. """ def __init__(self, package_name, package_path='templates', encoding='utf-8'): from pkg_resources import DefaultProvider, ResourceManager, \ get_provider provider = get_provider(package_name) self.encoding = encoding self.manager = ResourceManager() self.filesystem_bound = isinstance(provider, DefaultProvider) self.provider = provider self.package_path = package_path def get_source(self, environment, template): pieces = split_template_path(template) p = '/'.join((self.package_path,) + tuple(pieces)) if not self.provider.has_resource(p): raise TemplateNotFound(template) filename = uptodate = None if self.filesystem_bound: filename = self.provider.get_resource_filename(self.manager, p) mtime = path.getmtime(filename) def uptodate(): try: return path.getmtime(filename) == mtime except OSError: return False source = self.provider.get_resource_string(self.manager, p) return source.decode(self.encoding), filename, uptodate class DictLoader(BaseLoader): """Loads a template from a python dict. It's passed a dict of unicode strings bound to template names. This loader is useful for unittesting: >>> loader = DictLoader({'index.html': 'source here'}) Because auto reloading is rarely useful this is disabled per default. """ def __init__(self, mapping): self.mapping = mapping def get_source(self, environment, template): if template in self.mapping: source = self.mapping[template] return source, None, lambda: source != self.mapping.get(template) raise TemplateNotFound(template) class FunctionLoader(BaseLoader): """A loader that is passed a function which does the loading. The function becomes the name of the template passed and has to return either an unicode string with the template source, a tuple in the form ``(source, filename, uptodatefunc)`` or `None` if the template does not exist. >>> def load_template(name): ... if name == 'index.html': ... return '...' ... >>> loader = FunctionLoader(load_template) The `uptodatefunc` is a function that is called if autoreload is enabled and has to return `True` if the template is still up to date. For more details have a look at :meth:`BaseLoader.get_source` which has the same return value. """ def __init__(self, load_func): self.load_func = load_func def get_source(self, environment, template): rv = self.load_func(template) if rv is None: raise TemplateNotFound(template) elif isinstance(rv, basestring): return rv, None, None return rv class PrefixLoader(BaseLoader): """A loader that is passed a dict of loaders where each loader is bound to a prefix. The prefix is delimited from the template by a slash per default, which can be changed by setting the `delimiter` argument to something else:: loader = PrefixLoader({ 'app1': PackageLoader('mypackage.app1'), 'app2': PackageLoader('mypackage.app2') }) By loading ``'app1/index.html'`` the file from the app1 package is loaded, by loading ``'app2/index.html'`` the file from the second. """ def __init__(self, mapping, delimiter='/'): self.mapping = mapping self.delimiter = delimiter def get_source(self, environment, template): try: prefix, name = template.split(self.delimiter, 1) loader = self.mapping[prefix] except (ValueError, KeyError): raise TemplateNotFound(template) try: return loader.get_source(environment, name) except TemplateNotFound: # re-raise the exception with the correct fileame here. # (the one that includes the prefix) raise TemplateNotFound(template) class ChoiceLoader(BaseLoader): """This loader works like the `PrefixLoader` just that no prefix is specified. If a template could not be found by one loader the next one is tried. >>> loader = ChoiceLoader([ ... FileSystemLoader('/path/to/user/templates'), ... FileSystemLoader('/path/to/system/templates') ... ]) This is useful if you want to allow users to override builtin templates from a different location. """ def __init__(self, loaders): self.loaders = loaders def get_source(self, environment, template): for loader in self.loaders: try: return loader.get_source(environment, template) except TemplateNotFound: pass raise TemplateNotFound(template) weblog+jinja2+markdown2-2.5/jinja2/lexer.py010064400017500000000000000626721133707607400176420ustar00henrywheel# -*- coding: utf-8 -*- """ jinja2.lexer ~~~~~~~~~~~~ This module implements a Jinja / Python combination lexer. The `Lexer` class provided by this module is used to do some preprocessing for Jinja. On the one hand it filters out invalid operators like the bitshift operators we don't allow in templates. On the other hand it separates template code and python code in expressions. :copyright: (c) 2010 by the Jinja Team. :license: BSD, see LICENSE for more details. """ import re from operator import itemgetter from collections import deque from jinja2.exceptions import TemplateSyntaxError from jinja2.utils import LRUCache, next # cache for the lexers. Exists in order to be able to have multiple # environments with the same lexer _lexer_cache = LRUCache(50) # static regular expressions whitespace_re = re.compile(r'\s+', re.U) string_re = re.compile(r"('([^'\\]*(?:\\.[^'\\]*)*)'" r'|"([^"\\]*(?:\\.[^"\\]*)*)")', re.S) integer_re = re.compile(r'\d+') # we use the unicode identifier rule if this python version is able # to handle unicode identifiers, otherwise the standard ASCII one. try: compile('föö', '', 'eval') except SyntaxError: name_re = re.compile(r'\b[a-zA-Z_][a-zA-Z0-9_]*\b') else: from jinja2 import _stringdefs name_re = re.compile(r'[%s][%s]*' % (_stringdefs.xid_start, _stringdefs.xid_continue)) float_re = re.compile(r'(?': TOKEN_GT, '>=': TOKEN_GTEQ, '<': TOKEN_LT, '<=': TOKEN_LTEQ, '=': TOKEN_ASSIGN, '.': TOKEN_DOT, ':': TOKEN_COLON, '|': TOKEN_PIPE, ',': TOKEN_COMMA, ';': TOKEN_SEMICOLON } reverse_operators = dict([(v, k) for k, v in operators.iteritems()]) assert len(operators) == len(reverse_operators), 'operators dropped' operator_re = re.compile('(%s)' % '|'.join(re.escape(x) for x in sorted(operators, key=lambda x: -len(x)))) ignored_tokens = frozenset([TOKEN_COMMENT_BEGIN, TOKEN_COMMENT, TOKEN_COMMENT_END, TOKEN_WHITESPACE, TOKEN_WHITESPACE, TOKEN_LINECOMMENT_BEGIN, TOKEN_LINECOMMENT_END, TOKEN_LINECOMMENT]) ignore_if_empty = frozenset([TOKEN_WHITESPACE, TOKEN_DATA, TOKEN_COMMENT, TOKEN_LINECOMMENT]) def _describe_token_type(token_type): if token_type in reverse_operators: return reverse_operators[token_type] return { TOKEN_COMMENT_BEGIN: 'begin of comment', TOKEN_COMMENT_END: 'end of comment', TOKEN_COMMENT: 'comment', TOKEN_LINECOMMENT: 'comment', TOKEN_BLOCK_BEGIN: 'begin of statement block', TOKEN_BLOCK_END: 'end of statement block', TOKEN_VARIABLE_BEGIN: 'begin of print statement', TOKEN_VARIABLE_END: 'end of print statement', TOKEN_LINESTATEMENT_BEGIN: 'begin of line statement', TOKEN_LINESTATEMENT_END: 'end of line statement', TOKEN_DATA: 'template data / text', TOKEN_EOF: 'end of template' }.get(token_type, token_type) def describe_token(token): """Returns a description of the token.""" if token.type == 'name': return token.value return _describe_token_type(token.type) def describe_token_expr(expr): """Like `describe_token` but for token expressions.""" if ':' in expr: type, value = expr.split(':', 1) if type == 'name': return value else: type = expr return _describe_token_type(type) def count_newlines(value): """Count the number of newline characters in the string. This is useful for extensions that filter a stream. """ return len(newline_re.findall(value)) def compile_rules(environment): """Compiles all the rules from the environment into a list of rules.""" e = re.escape rules = [ (len(environment.comment_start_string), 'comment', e(environment.comment_start_string)), (len(environment.block_start_string), 'block', e(environment.block_start_string)), (len(environment.variable_start_string), 'variable', e(environment.variable_start_string)) ] if environment.line_statement_prefix is not None: rules.append((len(environment.line_statement_prefix), 'linestatement', r'^\s*' + e(environment.line_statement_prefix))) if environment.line_comment_prefix is not None: rules.append((len(environment.line_comment_prefix), 'linecomment', r'(?:^|(?<=\S))[^\S\r\n]*' + e(environment.line_comment_prefix))) return [x[1:] for x in sorted(rules, reverse=True)] class Failure(object): """Class that raises a `TemplateSyntaxError` if called. Used by the `Lexer` to specify known errors. """ def __init__(self, message, cls=TemplateSyntaxError): self.message = message self.error_class = cls def __call__(self, lineno, filename): raise self.error_class(self.message, lineno, filename) class Token(tuple): """Token class.""" __slots__ = () lineno, type, value = (property(itemgetter(x)) for x in range(3)) def __new__(cls, lineno, type, value): return tuple.__new__(cls, (lineno, intern(str(type)), value)) def __str__(self): if self.type in reverse_operators: return reverse_operators[self.type] elif self.type == 'name': return self.value return self.type def test(self, expr): """Test a token against a token expression. This can either be a token type or ``'token_type:token_value'``. This can only test against string values and types. """ # here we do a regular string equality check as test_any is usually # passed an iterable of not interned strings. if self.type == expr: return True elif ':' in expr: return expr.split(':', 1) == [self.type, self.value] return False def test_any(self, *iterable): """Test against multiple token expressions.""" for expr in iterable: if self.test(expr): return True return False def __repr__(self): return 'Token(%r, %r, %r)' % ( self.lineno, self.type, self.value ) class TokenStreamIterator(object): """The iterator for tokenstreams. Iterate over the stream until the eof token is reached. """ def __init__(self, stream): self.stream = stream def __iter__(self): return self def next(self): token = self.stream.current if token.type is TOKEN_EOF: self.stream.close() raise StopIteration() next(self.stream) return token class TokenStream(object): """A token stream is an iterable that yields :class:`Token`\s. The parser however does not iterate over it but calls :meth:`next` to go one token ahead. The current active token is stored as :attr:`current`. """ def __init__(self, generator, name, filename): self._next = iter(generator).next self._pushed = deque() self.name = name self.filename = filename self.closed = False self.current = Token(1, TOKEN_INITIAL, '') next(self) def __iter__(self): return TokenStreamIterator(self) def __nonzero__(self): return bool(self._pushed) or self.current.type is not TOKEN_EOF eos = property(lambda x: not x, doc="Are we at the end of the stream?") def push(self, token): """Push a token back to the stream.""" self._pushed.append(token) def look(self): """Look at the next token.""" old_token = next(self) result = self.current self.push(result) self.current = old_token return result def skip(self, n=1): """Got n tokens ahead.""" for x in xrange(n): next(self) def next_if(self, expr): """Perform the token test and return the token if it matched. Otherwise the return value is `None`. """ if self.current.test(expr): return next(self) def skip_if(self, expr): """Like :meth:`next_if` but only returns `True` or `False`.""" return self.next_if(expr) is not None def next(self): """Go one token ahead and return the old one""" rv = self.current if self._pushed: self.current = self._pushed.popleft() elif self.current.type is not TOKEN_EOF: try: self.current = self._next() except StopIteration: self.close() return rv def close(self): """Close the stream.""" self.current = Token(self.current.lineno, TOKEN_EOF, '') self._next = None self.closed = True def expect(self, expr): """Expect a given token type and return it. This accepts the same argument as :meth:`jinja2.lexer.Token.test`. """ if not self.current.test(expr): expr = describe_token_expr(expr) if self.current.type is TOKEN_EOF: raise TemplateSyntaxError('unexpected end of template, ' 'expected %r.' % expr, self.current.lineno, self.name, self.filename) raise TemplateSyntaxError("expected token %r, got %r" % (expr, describe_token(self.current)), self.current.lineno, self.name, self.filename) try: return self.current finally: next(self) def get_lexer(environment): """Return a lexer which is probably cached.""" key = (environment.block_start_string, environment.block_end_string, environment.variable_start_string, environment.variable_end_string, environment.comment_start_string, environment.comment_end_string, environment.line_statement_prefix, environment.line_comment_prefix, environment.trim_blocks, environment.newline_sequence) lexer = _lexer_cache.get(key) if lexer is None: lexer = Lexer(environment) _lexer_cache[key] = lexer return lexer class Lexer(object): """Class that implements a lexer for a given environment. Automatically created by the environment class, usually you don't have to do that. Note that the lexer is not automatically bound to an environment. Multiple environments can share the same lexer. """ def __init__(self, environment): # shortcuts c = lambda x: re.compile(x, re.M | re.S) e = re.escape # lexing rules for tags tag_rules = [ (whitespace_re, TOKEN_WHITESPACE, None), (float_re, TOKEN_FLOAT, None), (integer_re, TOKEN_INTEGER, None), (name_re, TOKEN_NAME, None), (string_re, TOKEN_STRING, None), (operator_re, TOKEN_OPERATOR, None) ] # assamble the root lexing rule. because "|" is ungreedy # we have to sort by length so that the lexer continues working # as expected when we have parsing rules like <% for block and # <%= for variables. (if someone wants asp like syntax) # variables are just part of the rules if variable processing # is required. root_tag_rules = compile_rules(environment) # block suffix if trimming is enabled block_suffix_re = environment.trim_blocks and '\\n?' or '' self.newline_sequence = environment.newline_sequence # global lexing rules self.rules = { 'root': [ # directives (c('(.*?)(?:%s)' % '|'.join( [r'(?P(?:\s*%s\-|%s)\s*raw\s*%s)' % ( e(environment.block_start_string), e(environment.block_start_string), e(environment.block_end_string) )] + [ r'(?P<%s_begin>\s*%s\-|%s)' % (n, r, r) for n, r in root_tag_rules ])), (TOKEN_DATA, '#bygroup'), '#bygroup'), # data (c('.+'), TOKEN_DATA, None) ], # comments TOKEN_COMMENT_BEGIN: [ (c(r'(.*?)((?:\-%s\s*|%s)%s)' % ( e(environment.comment_end_string), e(environment.comment_end_string), block_suffix_re )), (TOKEN_COMMENT, TOKEN_COMMENT_END), '#pop'), (c('(.)'), (Failure('Missing end of comment tag'),), None) ], # blocks TOKEN_BLOCK_BEGIN: [ (c('(?:\-%s\s*|%s)%s' % ( e(environment.block_end_string), e(environment.block_end_string), block_suffix_re )), TOKEN_BLOCK_END, '#pop'), ] + tag_rules, # variables TOKEN_VARIABLE_BEGIN: [ (c('\-%s\s*|%s' % ( e(environment.variable_end_string), e(environment.variable_end_string) )), TOKEN_VARIABLE_END, '#pop') ] + tag_rules, # raw block TOKEN_RAW_BEGIN: [ (c('(.*?)((?:\s*%s\-|%s)\s*endraw\s*(?:\-%s\s*|%s%s))' % ( e(environment.block_start_string), e(environment.block_start_string), e(environment.block_end_string), e(environment.block_end_string), block_suffix_re )), (TOKEN_DATA, TOKEN_RAW_END), '#pop'), (c('(.)'), (Failure('Missing end of raw directive'),), None) ], # line statements TOKEN_LINESTATEMENT_BEGIN: [ (c(r'\s*(\n|$)'), TOKEN_LINESTATEMENT_END, '#pop') ] + tag_rules, # line comments TOKEN_LINECOMMENT_BEGIN: [ (c(r'(.*?)()(?=\n|$)'), (TOKEN_LINECOMMENT, TOKEN_LINECOMMENT_END), '#pop') ] } def _normalize_newlines(self, value): """Called for strings and template data to normlize it to unicode.""" return newline_re.sub(self.newline_sequence, value) def tokenize(self, source, name=None, filename=None, state=None): """Calls tokeniter + tokenize and wraps it in a token stream. """ stream = self.tokeniter(source, name, filename, state) return TokenStream(self.wrap(stream, name, filename), name, filename) def wrap(self, stream, name=None, filename=None): """This is called with the stream as returned by `tokenize` and wraps every token in a :class:`Token` and converts the value. """ for lineno, token, value in stream: if token in ignored_tokens: continue elif token == 'linestatement_begin': token = 'block_begin' elif token == 'linestatement_end': token = 'block_end' # we are not interested in those tokens in the parser elif token in ('raw_begin', 'raw_end'): continue elif token == 'data': value = self._normalize_newlines(value) elif token == 'keyword': token = value elif token == 'name': value = str(value) elif token == 'string': # try to unescape string try: value = self._normalize_newlines(value[1:-1]) \ .encode('ascii', 'backslashreplace') \ .decode('unicode-escape') except Exception, e: msg = str(e).split(':')[-1].strip() raise TemplateSyntaxError(msg, lineno, name, filename) # if we can express it as bytestring (ascii only) # we do that for support of semi broken APIs # as datetime.datetime.strftime. On python 3 this # call becomes a noop thanks to 2to3 try: value = str(value) except UnicodeError: pass elif token == 'integer': value = int(value) elif token == 'float': value = float(value) elif token == 'operator': token = operators[value] yield Token(lineno, token, value) def tokeniter(self, source, name, filename=None, state=None): """This method tokenizes the text and returns the tokens in a generator. Use this method if you just want to tokenize a template. """ source = '\n'.join(unicode(source).splitlines()) pos = 0 lineno = 1 stack = ['root'] if state is not None and state != 'root': assert state in ('variable', 'block'), 'invalid state' stack.append(state + '_begin') else: state = 'root' statetokens = self.rules[stack[-1]] source_length = len(source) balancing_stack = [] while 1: # tokenizer loop for regex, tokens, new_state in statetokens: m = regex.match(source, pos) # if no match we try again with the next rule if m is None: continue # we only match blocks and variables if brances / parentheses # are balanced. continue parsing with the lower rule which # is the operator rule. do this only if the end tags look # like operators if balancing_stack and \ tokens in ('variable_end', 'block_end', 'linestatement_end'): continue # tuples support more options if isinstance(tokens, tuple): for idx, token in enumerate(tokens): # failure group if token.__class__ is Failure: raise token(lineno, filename) # bygroup is a bit more complex, in that case we # yield for the current token the first named # group that matched elif token == '#bygroup': for key, value in m.groupdict().iteritems(): if value is not None: yield lineno, key, value lineno += value.count('\n') break else: raise RuntimeError('%r wanted to resolve ' 'the token dynamically' ' but no group matched' % regex) # normal group else: data = m.group(idx + 1) if data or token not in ignore_if_empty: yield lineno, token, data lineno += data.count('\n') # strings as token just are yielded as it. else: data = m.group() # update brace/parentheses balance if tokens == 'operator': if data == '{': balancing_stack.append('}') elif data == '(': balancing_stack.append(')') elif data == '[': balancing_stack.append(']') elif data in ('}', ')', ']'): if not balancing_stack: raise TemplateSyntaxError('unexpected \'%s\'' % data, lineno, name, filename) expected_op = balancing_stack.pop() if expected_op != data: raise TemplateSyntaxError('unexpected \'%s\', ' 'expected \'%s\'' % (data, expected_op), lineno, name, filename) # yield items if data or tokens not in ignore_if_empty: yield lineno, tokens, data lineno += data.count('\n') # fetch new position into new variable so that we can check # if there is a internal parsing error which would result # in an infinite loop pos2 = m.end() # handle state changes if new_state is not None: # remove the uppermost state if new_state == '#pop': stack.pop() # resolve the new state by group checking elif new_state == '#bygroup': for key, value in m.groupdict().iteritems(): if value is not None: stack.append(key) break else: raise RuntimeError('%r wanted to resolve the ' 'new state dynamically but' ' no group matched' % regex) # direct state name given else: stack.append(new_state) statetokens = self.rules[stack[-1]] # we are still at the same position and no stack change. # this means a loop without break condition, avoid that and # raise error elif pos2 == pos: raise RuntimeError('%r yielded empty string without ' 'stack change' % regex) # publish new function and start again pos = pos2 break # if loop terminated without break we havn't found a single match # either we are at the end of the file or we have a problem else: # end of text if pos >= source_length: return # something went wrong raise TemplateSyntaxError('unexpected char %r at %d' % (source[pos], pos), lineno, name, filename) weblog+jinja2+markdown2-2.5/jinja2/filters.py010064400017500000000000000527701133707607400201710ustar00henrywheel# -*- coding: utf-8 -*- """ jinja2.filters ~~~~~~~~~~~~~~ Bundled jinja filters. :copyright: (c) 2010 by the Jinja Team. :license: BSD, see LICENSE for more details. """ import re import math from random import choice from operator import itemgetter from itertools import imap, groupby from jinja2.utils import Markup, escape, pformat, urlize, soft_unicode from jinja2.runtime import Undefined from jinja2.exceptions import FilterArgumentError, SecurityError _word_re = re.compile(r'\w+(?u)') def contextfilter(f): """Decorator for marking context dependent filters. The current :class:`Context` will be passed as first argument. """ if getattr(f, 'environmentfilter', False): raise TypeError('filter already marked as environment filter') f.contextfilter = True return f def environmentfilter(f): """Decorator for marking evironment dependent filters. The current :class:`Environment` is passed to the filter as first argument. """ if getattr(f, 'contextfilter', False): raise TypeError('filter already marked as context filter') f.environmentfilter = True return f def do_forceescape(value): """Enforce HTML escaping. This will probably double escape variables.""" if hasattr(value, '__html__'): value = value.__html__() return escape(unicode(value)) @environmentfilter def do_replace(environment, s, old, new, count=None): """Return a copy of the value with all occurrences of a substring replaced with a new one. The first argument is the substring that should be replaced, the second is the replacement string. If the optional third argument ``count`` is given, only the first ``count`` occurrences are replaced: .. sourcecode:: jinja {{ "Hello World"|replace("Hello", "Goodbye") }} -> Goodbye World {{ "aaaaargh"|replace("a", "d'oh, ", 2) }} -> d'oh, d'oh, aaargh """ if count is None: count = -1 if not environment.autoescape: return unicode(s).replace(unicode(old), unicode(new), count) if hasattr(old, '__html__') or hasattr(new, '__html__') and \ not hasattr(s, '__html__'): s = escape(s) else: s = soft_unicode(s) return s.replace(soft_unicode(old), soft_unicode(new), count) def do_upper(s): """Convert a value to uppercase.""" return soft_unicode(s).upper() def do_lower(s): """Convert a value to lowercase.""" return soft_unicode(s).lower() @environmentfilter def do_xmlattr(_environment, d, autospace=True): """Create an SGML/XML attribute string based on the items in a dict. All values that are neither `none` nor `undefined` are automatically escaped: .. sourcecode:: html+jinja ... Results in something like this: .. sourcecode:: html
    ...
As you can see it automatically prepends a space in front of the item if the filter returned something unless the second parameter is false. """ rv = u' '.join( u'%s="%s"' % (escape(key), escape(value)) for key, value in d.iteritems() if value is not None and not isinstance(value, Undefined) ) if autospace and rv: rv = u' ' + rv if _environment.autoescape: rv = Markup(rv) return rv def do_capitalize(s): """Capitalize a value. The first character will be uppercase, all others lowercase. """ return soft_unicode(s).capitalize() def do_title(s): """Return a titlecased version of the value. I.e. words will start with uppercase letters, all remaining characters are lowercase. """ return soft_unicode(s).title() def do_dictsort(value, case_sensitive=False, by='key'): """Sort a dict and yield (key, value) pairs. Because python dicts are unsorted you may want to use this function to order them by either key or value: .. sourcecode:: jinja {% for item in mydict|dictsort %} sort the dict by key, case insensitive {% for item in mydict|dicsort(true) %} sort the dict by key, case sensitive {% for item in mydict|dictsort(false, 'value') %} sort the dict by key, case insensitive, sorted normally and ordered by value. """ if by == 'key': pos = 0 elif by == 'value': pos = 1 else: raise FilterArgumentError('You can only sort by either ' '"key" or "value"') def sort_func(item): value = item[pos] if isinstance(value, basestring) and not case_sensitive: value = value.lower() return value return sorted(value.items(), key=sort_func) def do_sort(value, case_sensitive=False): """Sort an iterable. If the iterable is made of strings the second parameter can be used to control the case sensitiveness of the comparison which is disabled by default. .. sourcecode:: jinja {% for item in iterable|sort %} ... {% endfor %} """ if not case_sensitive: def sort_func(item): if isinstance(item, basestring): item = item.lower() return item else: sort_func = None return sorted(seq, key=sort_func) def do_default(value, default_value=u'', boolean=False): """If the value is undefined it will return the passed default value, otherwise the value of the variable: .. sourcecode:: jinja {{ my_variable|default('my_variable is not defined') }} This will output the value of ``my_variable`` if the variable was defined, otherwise ``'my_variable is not defined'``. If you want to use default with variables that evaluate to false you have to set the second parameter to `true`: .. sourcecode:: jinja {{ ''|default('the string was empty', true) }} """ if (boolean and not value) or isinstance(value, Undefined): return default_value return value @environmentfilter def do_join(environment, value, d=u''): """Return a string which is the concatenation of the strings in the sequence. The separator between elements is an empty string per default, you can define it with the optional parameter: .. sourcecode:: jinja {{ [1, 2, 3]|join('|') }} -> 1|2|3 {{ [1, 2, 3]|join }} -> 123 """ # no automatic escaping? joining is a lot eaiser then if not environment.autoescape: return unicode(d).join(imap(unicode, value)) # if the delimiter doesn't have an html representation we check # if any of the items has. If yes we do a coercion to Markup if not hasattr(d, '__html__'): value = list(value) do_escape = False for idx, item in enumerate(value): if hasattr(item, '__html__'): do_escape = True else: value[idx] = unicode(item) if do_escape: d = escape(d) else: d = unicode(d) return d.join(value) # no html involved, to normal joining return soft_unicode(d).join(imap(soft_unicode, value)) def do_center(value, width=80): """Centers the value in a field of a given width.""" return unicode(value).center(width) @environmentfilter def do_first(environment, seq): """Return the first item of a sequence.""" try: return iter(seq).next() except StopIteration: return environment.undefined('No first item, sequence was empty.') @environmentfilter def do_last(environment, seq): """Return the last item of a sequence.""" try: return iter(reversed(seq)).next() except StopIteration: return environment.undefined('No last item, sequence was empty.') @environmentfilter def do_random(environment, seq): """Return a random item from the sequence.""" try: return choice(seq) except IndexError: return environment.undefined('No random item, sequence was empty.') def do_filesizeformat(value, binary=False): """Format the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB, 102 bytes, etc). Per default decimal prefixes are used (mega, giga, etc.), if the second parameter is set to `True` the binary prefixes are used (mebi, gibi). """ bytes = float(value) base = binary and 1024 or 1000 middle = binary and 'i' or '' if bytes < base: return "%d Byte%s" % (bytes, bytes != 1 and 's' or '') elif bytes < base * base: return "%.1f K%sB" % (bytes / base, middle) elif bytes < base * base * base: return "%.1f M%sB" % (bytes / (base * base), middle) return "%.1f G%sB" % (bytes / (base * base * base), middle) def do_pprint(value, verbose=False): """Pretty print a variable. Useful for debugging. With Jinja 1.2 onwards you can pass it a parameter. If this parameter is truthy the output will be more verbose (this requires `pretty`) """ return pformat(value, verbose=verbose) @environmentfilter def do_urlize(environment, value, trim_url_limit=None, nofollow=False): """Converts URLs in plain text into clickable links. If you pass the filter an additional integer it will shorten the urls to that number. Also a third argument exists that makes the urls "nofollow": .. sourcecode:: jinja {{ mytext|urlize(40, true) }} links are shortened to 40 chars and defined with rel="nofollow" """ rv = urlize(value, trim_url_limit, nofollow) if environment.autoescape: rv = Markup(rv) return rv def do_indent(s, width=4, indentfirst=False): """Return a copy of the passed string, each line indented by 4 spaces. The first line is not indented. If you want to change the number of spaces or indent the first line too you can pass additional parameters to the filter: .. sourcecode:: jinja {{ mytext|indent(2, true) }} indent by two spaces and indent the first line too. """ indention = u' ' * width rv = (u'\n' + indention).join(s.splitlines()) if indentfirst: rv = indention + rv return rv def do_truncate(s, length=255, killwords=False, end='...'): """Return a truncated copy of the string. The length is specified with the first parameter which defaults to ``255``. If the second parameter is ``true`` the filter will cut the text at length. Otherwise it will try to save the last word. If the text was in fact truncated it will append an ellipsis sign (``"..."``). If you want a different ellipsis sign than ``"..."`` you can specify it using the third parameter. .. sourcecode jinja:: {{ mytext|truncate(300, false, '»') }} truncate mytext to 300 chars, don't split up words, use a right pointing double arrow as ellipsis sign. """ if len(s) <= length: return s elif killwords: return s[:length] + end words = s.split(' ') result = [] m = 0 for word in words: m += len(word) + 1 if m > length: break result.append(word) result.append(end) return u' '.join(result) def do_wordwrap(s, width=79, break_long_words=True): """ Return a copy of the string passed to the filter wrapped after ``79`` characters. You can override this default using the first parameter. If you set the second parameter to `false` Jinja will not split words apart if they are longer than `width`. """ import textwrap return u'\n'.join(textwrap.wrap(s, width=width, expand_tabs=False, replace_whitespace=False, break_long_words=break_long_words)) def do_wordcount(s): """Count the words in that string.""" return len(_word_re.findall(s)) def do_int(value, default=0): """Convert the value into an integer. If the conversion doesn't work it will return ``0``. You can override this default using the first parameter. """ try: return int(value) except (TypeError, ValueError): # this quirk is necessary so that "42.23"|int gives 42. try: return int(float(value)) except (TypeError, ValueError): return default def do_float(value, default=0.0): """Convert the value into a floating point number. If the conversion doesn't work it will return ``0.0``. You can override this default using the first parameter. """ try: return float(value) except (TypeError, ValueError): return default def do_format(value, *args, **kwargs): """ Apply python string formatting on an object: .. sourcecode:: jinja {{ "%s - %s"|format("Hello?", "Foo!") }} -> Hello? - Foo! """ if args and kwargs: raise FilterArgumentError('can\'t handle positional and keyword ' 'arguments at the same time') return soft_unicode(value) % (kwargs or args) def do_trim(value): """Strip leading and trailing whitespace.""" return soft_unicode(value).strip() def do_striptags(value): """Strip SGML/XML tags and replace adjacent whitespace by one space. """ if hasattr(value, '__html__'): value = value.__html__() return Markup(unicode(value)).striptags() def do_slice(value, slices, fill_with=None): """Slice an iterator and return a list of lists containing those items. Useful if you want to create a div containing three ul tags that represent columns: .. sourcecode:: html+jinja
{%- for column in items|slice(3) %}
    {%- for item in column %}
  • {{ item }}
  • {%- endfor %}
{%- endfor %}
If you pass it a second argument it's used to fill missing values on the last iteration. """ seq = list(value) length = len(seq) items_per_slice = length // slices slices_with_extra = length % slices offset = 0 for slice_number in xrange(slices): start = offset + slice_number * items_per_slice if slice_number < slices_with_extra: offset += 1 end = offset + (slice_number + 1) * items_per_slice tmp = seq[start:end] if fill_with is not None and slice_number >= slices_with_extra: tmp.append(fill_with) yield tmp def do_batch(value, linecount, fill_with=None): """ A filter that batches items. It works pretty much like `slice` just the other way round. It returns a list of lists with the given number of items. If you provide a second parameter this is used to fill missing items. See this example: .. sourcecode:: html+jinja {%- for row in items|batch(3, ' ') %} {%- for column in row %} {%- endfor %} {%- endfor %}
{{ column }}
""" result = [] tmp = [] for item in value: if len(tmp) == linecount: yield tmp tmp = [] tmp.append(item) if tmp: if fill_with is not None and len(tmp) < linecount: tmp += [fill_with] * (linecount - len(tmp)) yield tmp def do_round(value, precision=0, method='common'): """Round the number to a given precision. The first parameter specifies the precision (default is ``0``), the second the rounding method: - ``'common'`` rounds either up or down - ``'ceil'`` always rounds up - ``'floor'`` always rounds down If you don't specify a method ``'common'`` is used. .. sourcecode:: jinja {{ 42.55|round }} -> 43.0 {{ 42.55|round(1, 'floor') }} -> 42.5 Note that even if rounded to 0 precision, a float is returned. If you need a real integer, pipe it through `int`: .. sourcecode:: jinja {{ 42.55|round|int }} -> 43 """ if not method in ('common', 'ceil', 'floor'): raise FilterArgumentError('method must be common, ceil or floor') if precision < 0: raise FilterArgumentError('precision must be a postive integer ' 'or zero.') if method == 'common': return round(value, precision) func = getattr(math, method) if precision: return func(value * 10 * precision) / (10 * precision) else: return func(value) def do_sort(value, reverse=False): """Sort a sequence. Per default it sorts ascending, if you pass it true as first argument it will reverse the sorting. """ return sorted(value, reverse=reverse) @environmentfilter def do_groupby(environment, value, attribute): """Group a sequence of objects by a common attribute. If you for example have a list of dicts or objects that represent persons with `gender`, `first_name` and `last_name` attributes and you want to group all users by genders you can do something like the following snippet: .. sourcecode:: html+jinja
    {% for group in persons|groupby('gender') %}
  • {{ group.grouper }}
      {% for person in group.list %}
    • {{ person.first_name }} {{ person.last_name }}
    • {% endfor %}
  • {% endfor %}
Additionally it's possible to use tuple unpacking for the grouper and list: .. sourcecode:: html+jinja
    {% for grouper, list in persons|groupby('gender') %} ... {% endfor %}
As you can see the item we're grouping by is stored in the `grouper` attribute and the `list` contains all the objects that have this grouper in common. """ expr = lambda x: environment.getitem(x, attribute) return sorted(map(_GroupTuple, groupby(sorted(value, key=expr), expr))) class _GroupTuple(tuple): __slots__ = () grouper = property(itemgetter(0)) list = property(itemgetter(1)) def __new__(cls, (key, value)): return tuple.__new__(cls, (key, list(value))) def do_list(value): """Convert the value into a list. If it was a string the returned list will be a list of characters. """ return list(value) def do_mark_safe(value): """Mark the value as safe which means that in an environment with automatic escaping enabled this variable will not be escaped. """ return Markup(value) def do_mark_unsafe(value): """Mark a value as unsafe. This is the reverse operation for :func:`safe`.""" return unicode(value) def do_reverse(value): """Reverse the object or return an iterator the iterates over it the other way round. """ if isinstance(value, basestring): return value[::-1] try: return reversed(value) except TypeError: try: rv = list(value) rv.reverse() return rv except TypeError: raise FilterArgumentError('argument must be iterable') @environmentfilter def do_attr(environment, obj, name): """Get an attribute of an object. ``foo|attr("bar")`` works like ``foo["bar"]`` just that always an attribute is returned and items are not looked up. See :ref:`Notes on subscriptions ` for more details. """ try: name = str(name) except UnicodeError: pass else: try: value = getattr(obj, name) except AttributeError: pass else: if environment.sandboxed and not \ environment.is_safe_attribute(obj, name, value): return environment.unsafe_undefined(obj, name) return value return environment.undefined(obj=obj, name=name) FILTERS = { 'attr': do_attr, 'replace': do_replace, 'upper': do_upper, 'lower': do_lower, 'escape': escape, 'e': escape, 'forceescape': do_forceescape, 'capitalize': do_capitalize, 'title': do_title, 'default': do_default, 'd': do_default, 'join': do_join, 'count': len, 'dictsort': do_dictsort, 'sort': do_sort, 'length': len, 'reverse': do_reverse, 'center': do_center, 'indent': do_indent, 'title': do_title, 'capitalize': do_capitalize, 'first': do_first, 'last': do_last, 'random': do_random, 'filesizeformat': do_filesizeformat, 'pprint': do_pprint, 'truncate': do_truncate, 'wordwrap': do_wordwrap, 'wordcount': do_wordcount, 'int': do_int, 'float': do_float, 'string': soft_unicode, 'list': do_list, 'urlize': do_urlize, 'format': do_format, 'trim': do_trim, 'striptags': do_striptags, 'slice': do_slice, 'batch': do_batch, 'sum': sum, 'abs': abs, 'round': do_round, 'sort': do_sort, 'groupby': do_groupby, 'safe': do_mark_safe, 'xmlattr': do_xmlattr } weblog+jinja2+markdown2-2.5/jinja2/ext.py010064400017500000000000000510761133707607400173170ustar00henrywheel# -*- coding: utf-8 -*- """ jinja2.ext ~~~~~~~~~~ Jinja extensions allow to add custom tags similar to the way django custom tags work. By default two example extensions exist: an i18n and a cache extension. :copyright: (c) 2010 by the Jinja Team. :license: BSD. """ from collections import deque from jinja2 import nodes from jinja2.defaults import * from jinja2.environment import get_spontaneous_environment from jinja2.runtime import Undefined, concat from jinja2.exceptions import TemplateAssertionError, TemplateSyntaxError from jinja2.utils import contextfunction, import_string, Markup, next # the only real useful gettext functions for a Jinja template. Note # that ugettext must be assigned to gettext as Jinja doesn't support # non unicode strings. GETTEXT_FUNCTIONS = ('_', 'gettext', 'ngettext') class ExtensionRegistry(type): """Gives the extension an unique identifier.""" def __new__(cls, name, bases, d): rv = type.__new__(cls, name, bases, d) rv.identifier = rv.__module__ + '.' + rv.__name__ return rv class Extension(object): """Extensions can be used to add extra functionality to the Jinja template system at the parser level. Custom extensions are bound to an environment but may not store environment specific data on `self`. The reason for this is that an extension can be bound to another environment (for overlays) by creating a copy and reassigning the `environment` attribute. As extensions are created by the environment they cannot accept any arguments for configuration. One may want to work around that by using a factory function, but that is not possible as extensions are identified by their import name. The correct way to configure the extension is storing the configuration values on the environment. Because this way the environment ends up acting as central configuration storage the attributes may clash which is why extensions have to ensure that the names they choose for configuration are not too generic. ``prefix`` for example is a terrible name, ``fragment_cache_prefix`` on the other hand is a good name as includes the name of the extension (fragment cache). """ __metaclass__ = ExtensionRegistry #: if this extension parses this is the list of tags it's listening to. tags = set() def __init__(self, environment): self.environment = environment def bind(self, environment): """Create a copy of this extension bound to another environment.""" rv = object.__new__(self.__class__) rv.__dict__.update(self.__dict__) rv.environment = environment return rv def preprocess(self, source, name, filename=None): """This method is called before the actual lexing and can be used to preprocess the source. The `filename` is optional. The return value must be the preprocessed source. """ return source def filter_stream(self, stream): """It's passed a :class:`~jinja2.lexer.TokenStream` that can be used to filter tokens returned. This method has to return an iterable of :class:`~jinja2.lexer.Token`\s, but it doesn't have to return a :class:`~jinja2.lexer.TokenStream`. In the `ext` folder of the Jinja2 source distribution there is a file called `inlinegettext.py` which implements a filter that utilizes this method. """ return stream def parse(self, parser): """If any of the :attr:`tags` matched this method is called with the parser as first argument. The token the parser stream is pointing at is the name token that matched. This method has to return one or a list of multiple nodes. """ raise NotImplementedError() def attr(self, name, lineno=None): """Return an attribute node for the current extension. This is useful to pass constants on extensions to generated template code:: self.attr('_my_attribute', lineno=lineno) """ return nodes.ExtensionAttribute(self.identifier, name, lineno=lineno) def call_method(self, name, args=None, kwargs=None, dyn_args=None, dyn_kwargs=None, lineno=None): """Call a method of the extension. This is a shortcut for :meth:`attr` + :class:`jinja2.nodes.Call`. """ if args is None: args = [] if kwargs is None: kwargs = [] return nodes.Call(self.attr(name, lineno=lineno), args, kwargs, dyn_args, dyn_kwargs, lineno=lineno) @contextfunction def _gettext_alias(context, string): return context.resolve('gettext')(string) class InternationalizationExtension(Extension): """This extension adds gettext support to Jinja2.""" tags = set(['trans']) # TODO: the i18n extension is currently reevaluating values in a few # situations. Take this example: # {% trans count=something() %}{{ count }} foo{% pluralize # %}{{ count }} fooss{% endtrans %} # something is called twice here. One time for the gettext value and # the other time for the n-parameter of the ngettext function. def __init__(self, environment): Extension.__init__(self, environment) environment.globals['_'] = _gettext_alias environment.extend( install_gettext_translations=self._install, install_null_translations=self._install_null, uninstall_gettext_translations=self._uninstall, extract_translations=self._extract ) def _install(self, translations): gettext = getattr(translations, 'ugettext', None) if gettext is None: gettext = translations.gettext ngettext = getattr(translations, 'ungettext', None) if ngettext is None: ngettext = translations.ngettext self.environment.globals.update(gettext=gettext, ngettext=ngettext) def _install_null(self): self.environment.globals.update( gettext=lambda x: x, ngettext=lambda s, p, n: (n != 1 and (p,) or (s,))[0] ) def _uninstall(self, translations): for key in 'gettext', 'ngettext': self.environment.globals.pop(key, None) def _extract(self, source, gettext_functions=GETTEXT_FUNCTIONS): if isinstance(source, basestring): source = self.environment.parse(source) return extract_from_ast(source, gettext_functions) def parse(self, parser): """Parse a translatable tag.""" lineno = next(parser.stream).lineno # find all the variables referenced. Additionally a variable can be # defined in the body of the trans block too, but this is checked at # a later state. plural_expr = None variables = {} while parser.stream.current.type != 'block_end': if variables: parser.stream.expect('comma') # skip colon for python compatibility if parser.stream.skip_if('colon'): break name = parser.stream.expect('name') if name.value in variables: parser.fail('translatable variable %r defined twice.' % name.value, name.lineno, exc=TemplateAssertionError) # expressions if parser.stream.current.type == 'assign': next(parser.stream) variables[name.value] = var = parser.parse_expression() else: variables[name.value] = var = nodes.Name(name.value, 'load') if plural_expr is None: plural_expr = var parser.stream.expect('block_end') plural = plural_names = None have_plural = False referenced = set() # now parse until endtrans or pluralize singular_names, singular = self._parse_block(parser, True) if singular_names: referenced.update(singular_names) if plural_expr is None: plural_expr = nodes.Name(singular_names[0], 'load') # if we have a pluralize block, we parse that too if parser.stream.current.test('name:pluralize'): have_plural = True next(parser.stream) if parser.stream.current.type != 'block_end': name = parser.stream.expect('name') if name.value not in variables: parser.fail('unknown variable %r for pluralization' % name.value, name.lineno, exc=TemplateAssertionError) plural_expr = variables[name.value] parser.stream.expect('block_end') plural_names, plural = self._parse_block(parser, False) next(parser.stream) referenced.update(plural_names) else: next(parser.stream) # register free names as simple name expressions for var in referenced: if var not in variables: variables[var] = nodes.Name(var, 'load') # no variables referenced? no need to escape if not referenced: singular = singular.replace('%%', '%') if plural: plural = plural.replace('%%', '%') if not have_plural: plural_expr = None elif plural_expr is None: parser.fail('pluralize without variables', lineno) if variables: variables = nodes.Dict([nodes.Pair(nodes.Const(x, lineno=lineno), y) for x, y in variables.items()]) else: variables = None node = self._make_node(singular, plural, variables, plural_expr) node.set_lineno(lineno) return node def _parse_block(self, parser, allow_pluralize): """Parse until the next block tag with a given name.""" referenced = [] buf = [] while 1: if parser.stream.current.type == 'data': buf.append(parser.stream.current.value.replace('%', '%%')) next(parser.stream) elif parser.stream.current.type == 'variable_begin': next(parser.stream) name = parser.stream.expect('name').value referenced.append(name) buf.append('%%(%s)s' % name) parser.stream.expect('variable_end') elif parser.stream.current.type == 'block_begin': next(parser.stream) if parser.stream.current.test('name:endtrans'): break elif parser.stream.current.test('name:pluralize'): if allow_pluralize: break parser.fail('a translatable section can have only one ' 'pluralize section') parser.fail('control structures in translatable sections are ' 'not allowed') elif parser.stream.eos: parser.fail('unclosed translation block') else: assert False, 'internal parser error' return referenced, concat(buf) def _make_node(self, singular, plural, variables, plural_expr): """Generates a useful node from the data provided.""" # singular only: if plural_expr is None: gettext = nodes.Name('gettext', 'load') node = nodes.Call(gettext, [nodes.Const(singular)], [], None, None) # singular and plural else: ngettext = nodes.Name('ngettext', 'load') node = nodes.Call(ngettext, [ nodes.Const(singular), nodes.Const(plural), plural_expr ], [], None, None) # mark the return value as safe if we are in an # environment with autoescaping turned on if self.environment.autoescape: node = nodes.MarkSafe(node) if variables: node = nodes.Mod(node, variables) return nodes.Output([node]) class ExprStmtExtension(Extension): """Adds a `do` tag to Jinja2 that works like the print statement just that it doesn't print the return value. """ tags = set(['do']) def parse(self, parser): node = nodes.ExprStmt(lineno=next(parser.stream).lineno) node.node = parser.parse_tuple() return node class LoopControlExtension(Extension): """Adds break and continue to the template engine.""" tags = set(['break', 'continue']) def parse(self, parser): token = next(parser.stream) if token.value == 'break': return nodes.Break(lineno=token.lineno) return nodes.Continue(lineno=token.lineno) class WithExtension(Extension): """Adds support for a django-like with block.""" tags = set(['with']) def parse(self, parser): node = nodes.Scope(lineno=next(parser.stream).lineno) assignments = [] while parser.stream.current.type != 'block_end': lineno = parser.stream.current.lineno if assignments: parser.stream.expect('comma') target = parser.parse_assign_target() parser.stream.expect('assign') expr = parser.parse_expression() assignments.append(nodes.Assign(target, expr, lineno=lineno)) node.body = assignments + \ list(parser.parse_statements(('name:endwith',), drop_needle=True)) return node def extract_from_ast(node, gettext_functions=GETTEXT_FUNCTIONS, babel_style=True): """Extract localizable strings from the given template node. Per default this function returns matches in babel style that means non string parameters as well as keyword arguments are returned as `None`. This allows Babel to figure out what you really meant if you are using gettext functions that allow keyword arguments for placeholder expansion. If you don't want that behavior set the `babel_style` parameter to `False` which causes only strings to be returned and parameters are always stored in tuples. As a consequence invalid gettext calls (calls without a single string parameter or string parameters after non-string parameters) are skipped. This example explains the behavior: >>> from jinja2 import Environment >>> env = Environment() >>> node = env.parse('{{ (_("foo"), _(), ngettext("foo", "bar", 42)) }}') >>> list(extract_from_ast(node)) [(1, '_', 'foo'), (1, '_', ()), (1, 'ngettext', ('foo', 'bar', None))] >>> list(extract_from_ast(node, babel_style=False)) [(1, '_', ('foo',)), (1, 'ngettext', ('foo', 'bar'))] For every string found this function yields a ``(lineno, function, message)`` tuple, where: * ``lineno`` is the number of the line on which the string was found, * ``function`` is the name of the ``gettext`` function used (if the string was extracted from embedded Python code), and * ``message`` is the string itself (a ``unicode`` object, or a tuple of ``unicode`` objects for functions with multiple string arguments). This extraction function operates on the AST and is because of that unable to extract any comments. For comment support you have to use the babel extraction interface or extract comments yourself. """ for node in node.find_all(nodes.Call): if not isinstance(node.node, nodes.Name) or \ node.node.name not in gettext_functions: continue strings = [] for arg in node.args: if isinstance(arg, nodes.Const) and \ isinstance(arg.value, basestring): strings.append(arg.value) else: strings.append(None) for arg in node.kwargs: strings.append(None) if node.dyn_args is not None: strings.append(None) if node.dyn_kwargs is not None: strings.append(None) if not babel_style: strings = tuple(x for x in strings if x is not None) if not strings: continue else: if len(strings) == 1: strings = strings[0] else: strings = tuple(strings) yield node.lineno, node.node.name, strings class _CommentFinder(object): """Helper class to find comments in a token stream. Can only find comments for gettext calls forwards. Once the comment from line 4 is found, a comment for line 1 will not return a usable value. """ def __init__(self, tokens, comment_tags): self.tokens = tokens self.comment_tags = comment_tags self.offset = 0 self.last_lineno = 0 def find_backwards(self, offset): try: for _, token_type, token_value in \ reversed(self.tokens[self.offset:offset]): if token_type in ('comment', 'linecomment'): try: prefix, comment = token_value.split(None, 1) except ValueError: continue if prefix in self.comment_tags: return [comment.rstrip()] return [] finally: self.offset = offset def find_comments(self, lineno): if not self.comment_tags or self.last_lineno > lineno: return [] for idx, (token_lineno, _, _) in enumerate(self.tokens[self.offset:]): if token_lineno > lineno: return self.find_backwards(self.offset + idx) return self.find_backwards(len(self.tokens)) def babel_extract(fileobj, keywords, comment_tags, options): """Babel extraction method for Jinja templates. .. versionchanged:: 2.3 Basic support for translation comments was added. If `comment_tags` is now set to a list of keywords for extraction, the extractor will try to find the best preceeding comment that begins with one of the keywords. For best results, make sure to not have more than one gettext call in one line of code and the matching comment in the same line or the line before. :param fileobj: the file-like object the messages should be extracted from :param keywords: a list of keywords (i.e. function names) that should be recognized as translation functions :param comment_tags: a list of translator tags to search for and include in the results. :param options: a dictionary of additional options (optional) :return: an iterator over ``(lineno, funcname, message, comments)`` tuples. (comments will be empty currently) """ extensions = set() for extension in options.get('extensions', '').split(','): extension = extension.strip() if not extension: continue extensions.add(import_string(extension)) if InternationalizationExtension not in extensions: extensions.add(InternationalizationExtension) environment = get_spontaneous_environment( options.get('block_start_string', BLOCK_START_STRING), options.get('block_end_string', BLOCK_END_STRING), options.get('variable_start_string', VARIABLE_START_STRING), options.get('variable_end_string', VARIABLE_END_STRING), options.get('comment_start_string', COMMENT_START_STRING), options.get('comment_end_string', COMMENT_END_STRING), options.get('line_statement_prefix') or LINE_STATEMENT_PREFIX, options.get('line_comment_prefix') or LINE_COMMENT_PREFIX, str(options.get('trim_blocks', TRIM_BLOCKS)).lower() in \ ('1', 'on', 'yes', 'true'), NEWLINE_SEQUENCE, frozenset(extensions), # fill with defaults so that environments are shared # with other spontaneus environments. The rest of the # arguments are optimizer, undefined, finalize, autoescape, # loader, cache size, auto reloading setting and the # bytecode cache True, Undefined, None, False, None, 0, False, None ) source = fileobj.read().decode(options.get('encoding', 'utf-8')) try: node = environment.parse(source) tokens = list(environment.lex(environment.preprocess(source))) except TemplateSyntaxError, e: # skip templates with syntax errors return finder = _CommentFinder(tokens, comment_tags) for lineno, func, message in extract_from_ast(node, keywords): yield lineno, func, message, finder.find_comments(lineno) #: nicer import names i18n = InternationalizationExtension do = ExprStmtExtension loopcontrols = LoopControlExtension with_ = WithExtension weblog+jinja2+markdown2-2.5/jinja2/exceptions.py010064400017500000000000000106621133707607400206740ustar00henrywheel# -*- coding: utf-8 -*- """ jinja2.exceptions ~~~~~~~~~~~~~~~~~ Jinja exceptions. :copyright: (c) 2010 by the Jinja Team. :license: BSD, see LICENSE for more details. """ class TemplateError(Exception): """Baseclass for all template errors.""" def __init__(self, message=None): if message is not None: message = unicode(message).encode('utf-8') Exception.__init__(self, message) @property def message(self): if self.args: message = self.args[0] if message is not None: return message.decode('utf-8', 'replace') class TemplateNotFound(IOError, LookupError, TemplateError): """Raised if a template does not exist.""" # looks weird, but removes the warning descriptor that just # bogusly warns us about message being deprecated message = None def __init__(self, name, message=None): IOError.__init__(self) if message is None: message = name self.message = message self.name = name self.templates = [name] def __str__(self): return self.message.encode('utf-8') # unicode goes after __str__ because we configured 2to3 to rename # __unicode__ to __str__. because the 2to3 tree is not designed to # remove nodes from it, we leave the above __str__ around and let # it override at runtime. def __unicode__(self): return self.message class TemplatesNotFound(TemplateNotFound): """Like :class:`TemplateNotFound` but raised if multiple templates are selected. This is a subclass of :class:`TemplateNotFound` exception, so just catching the base exception will catch both. .. versionadded:: 2.2 """ def __init__(self, names=(), message=None): if message is None: message = u'non of the templates given were found: ' + \ u', '.join(map(unicode, names)) TemplateNotFound.__init__(self, names and names[-1] or None, message) self.templates = list(names) class TemplateSyntaxError(TemplateError): """Raised to tell the user that there is a problem with the template.""" def __init__(self, message, lineno, name=None, filename=None): TemplateError.__init__(self, message) self.lineno = lineno self.name = name self.filename = filename self.source = None # this is set to True if the debug.translate_syntax_error # function translated the syntax error into a new traceback self.translated = False def __str__(self): return unicode(self).encode('utf-8') # unicode goes after __str__ because we configured 2to3 to rename # __unicode__ to __str__. because the 2to3 tree is not designed to # remove nodes from it, we leave the above __str__ around and let # it override at runtime. def __unicode__(self): # for translated errors we only return the message if self.translated: return self.message # otherwise attach some stuff location = 'line %d' % self.lineno name = self.filename or self.name if name: location = 'File "%s", %s' % (name, location) lines = [self.message, ' ' + location] # if the source is set, add the line to the output if self.source is not None: try: line = self.source.splitlines()[self.lineno - 1] except IndexError: line = None if line: lines.append(' ' + line.strip()) return u'\n'.join(lines) class TemplateAssertionError(TemplateSyntaxError): """Like a template syntax error, but covers cases where something in the template caused an error at compile time that wasn't necessarily caused by a syntax error. However it's a direct subclass of :exc:`TemplateSyntaxError` and has the same attributes. """ class TemplateRuntimeError(TemplateError): """A generic runtime error in the template engine. Under some situations Jinja may raise this exception. """ class UndefinedError(TemplateRuntimeError): """Raised if a template tries to operate on :class:`Undefined`.""" class SecurityError(TemplateRuntimeError): """Raised if a template tries to do something insecure if the sandbox is enabled. """ class FilterArgumentError(TemplateRuntimeError): """This error is raised if a filter was called with inappropriate arguments """ weblog+jinja2+markdown2-2.5/jinja2/environment.py010064400017500000000000001072141133707607400210570ustar00henrywheel# -*- coding: utf-8 -*- """ jinja2.environment ~~~~~~~~~~~~~~~~~~ Provides a class that holds runtime and parsing time options. :copyright: (c) 2010 by the Jinja Team. :license: BSD, see LICENSE for more details. """ import sys from jinja2 import nodes from jinja2.defaults import * from jinja2.lexer import get_lexer, TokenStream from jinja2.parser import Parser from jinja2.optimizer import optimize from jinja2.compiler import generate from jinja2.runtime import Undefined, new_context from jinja2.exceptions import TemplateSyntaxError, TemplateNotFound, \ TemplatesNotFound from jinja2.utils import import_string, LRUCache, Markup, missing, \ concat, consume, internalcode, _encode_filename # for direct template usage we have up to ten living environments _spontaneous_environments = LRUCache(10) # the function to create jinja traceback objects. This is dynamically # imported on the first exception in the exception handler. _make_traceback = None def get_spontaneous_environment(*args): """Return a new spontaneous environment. A spontaneous environment is an unnamed and unaccessible (in theory) environment that is used for templates generated from a string and not from the file system. """ try: env = _spontaneous_environments.get(args) except TypeError: return Environment(*args) if env is not None: return env _spontaneous_environments[args] = env = Environment(*args) env.shared = True return env def create_cache(size): """Return the cache class for the given size.""" if size == 0: return None if size < 0: return {} return LRUCache(size) def copy_cache(cache): """Create an empty copy of the given cache.""" if cache is None: return None elif type(cache) is dict: return {} return LRUCache(cache.capacity) def load_extensions(environment, extensions): """Load the extensions from the list and bind it to the environment. Returns a dict of instanciated environments. """ result = {} for extension in extensions: if isinstance(extension, basestring): extension = import_string(extension) result[extension.identifier] = extension(environment) return result def _environment_sanity_check(environment): """Perform a sanity check on the environment.""" assert issubclass(environment.undefined, Undefined), 'undefined must ' \ 'be a subclass of undefined because filters depend on it.' assert environment.block_start_string != \ environment.variable_start_string != \ environment.comment_start_string, 'block, variable and comment ' \ 'start strings must be different' assert environment.newline_sequence in ('\r', '\r\n', '\n'), \ 'newline_sequence set to unknown line ending string.' return environment class Environment(object): r"""The core component of Jinja is the `Environment`. It contains important shared variables like configuration, filters, tests, globals and others. Instances of this class may be modified if they are not shared and if no template was loaded so far. Modifications on environments after the first template was loaded will lead to surprising effects and undefined behavior. Here the possible initialization parameters: `block_start_string` The string marking the begin of a block. Defaults to ``'{%'``. `block_end_string` The string marking the end of a block. Defaults to ``'%}'``. `variable_start_string` The string marking the begin of a print statement. Defaults to ``'{{'``. `variable_end_string` The string marking the end of a print statement. Defaults to ``'}}'``. `comment_start_string` The string marking the begin of a comment. Defaults to ``'{#'``. `comment_end_string` The string marking the end of a comment. Defaults to ``'#}'``. `line_statement_prefix` If given and a string, this will be used as prefix for line based statements. See also :ref:`line-statements`. `line_comment_prefix` If given and a string, this will be used as prefix for line based based comments. See also :ref:`line-statements`. .. versionadded:: 2.2 `trim_blocks` If this is set to ``True`` the first newline after a block is removed (block, not variable tag!). Defaults to `False`. `newline_sequence` The sequence that starts a newline. Must be one of ``'\r'``, ``'\n'`` or ``'\r\n'``. The default is ``'\n'`` which is a useful default for Linux and OS X systems as well as web applications. `extensions` List of Jinja extensions to use. This can either be import paths as strings or extension classes. For more information have a look at :ref:`the extensions documentation `. `optimized` should the optimizer be enabled? Default is `True`. `undefined` :class:`Undefined` or a subclass of it that is used to represent undefined values in the template. `finalize` A callable that can be used to process the result of a variable expression before it is output. For example one can convert `None` implicitly into an empty string here. `autoescape` If set to true the XML/HTML autoescaping feature is enabled. For more details about auto escaping see :class:`~jinja2.utils.Markup`. `loader` The template loader for this environment. `cache_size` The size of the cache. Per default this is ``50`` which means that if more than 50 templates are loaded the loader will clean out the least recently used template. If the cache size is set to ``0`` templates are recompiled all the time, if the cache size is ``-1`` the cache will not be cleaned. `auto_reload` Some loaders load templates from locations where the template sources may change (ie: file system or database). If `auto_reload` is set to `True` (default) every time a template is requested the loader checks if the source changed and if yes, it will reload the template. For higher performance it's possible to disable that. `bytecode_cache` If set to a bytecode cache object, this object will provide a cache for the internal Jinja bytecode so that templates don't have to be parsed if they were not changed. See :ref:`bytecode-cache` for more information. """ #: if this environment is sandboxed. Modifying this variable won't make #: the environment sandboxed though. For a real sandboxed environment #: have a look at jinja2.sandbox sandboxed = False #: True if the environment is just an overlay overlayed = False #: the environment this environment is linked to if it is an overlay linked_to = None #: shared environments have this set to `True`. A shared environment #: must not be modified shared = False #: these are currently EXPERIMENTAL undocumented features. exception_handler = None exception_formatter = None def __init__(self, block_start_string=BLOCK_START_STRING, block_end_string=BLOCK_END_STRING, variable_start_string=VARIABLE_START_STRING, variable_end_string=VARIABLE_END_STRING, comment_start_string=COMMENT_START_STRING, comment_end_string=COMMENT_END_STRING, line_statement_prefix=LINE_STATEMENT_PREFIX, line_comment_prefix=LINE_COMMENT_PREFIX, trim_blocks=TRIM_BLOCKS, newline_sequence=NEWLINE_SEQUENCE, extensions=(), optimized=True, undefined=Undefined, finalize=None, autoescape=False, loader=None, cache_size=50, auto_reload=True, bytecode_cache=None): # !!Important notice!! # The constructor accepts quite a few arguments that should be # passed by keyword rather than position. However it's important to # not change the order of arguments because it's used at least # internally in those cases: # - spontaneus environments (i18n extension and Template) # - unittests # If parameter changes are required only add parameters at the end # and don't change the arguments (or the defaults!) of the arguments # existing already. # lexer / parser information self.block_start_string = block_start_string self.block_end_string = block_end_string self.variable_start_string = variable_start_string self.variable_end_string = variable_end_string self.comment_start_string = comment_start_string self.comment_end_string = comment_end_string self.line_statement_prefix = line_statement_prefix self.line_comment_prefix = line_comment_prefix self.trim_blocks = trim_blocks self.newline_sequence = newline_sequence # runtime information self.undefined = undefined self.optimized = optimized self.finalize = finalize self.autoescape = autoescape # defaults self.filters = DEFAULT_FILTERS.copy() self.tests = DEFAULT_TESTS.copy() self.globals = DEFAULT_NAMESPACE.copy() # set the loader provided self.loader = loader self.bytecode_cache = None self.cache = create_cache(cache_size) self.bytecode_cache = bytecode_cache self.auto_reload = auto_reload # load extensions self.extensions = load_extensions(self, extensions) _environment_sanity_check(self) def extend(self, **attributes): """Add the items to the instance of the environment if they do not exist yet. This is used by :ref:`extensions ` to register callbacks and configuration values without breaking inheritance. """ for key, value in attributes.iteritems(): if not hasattr(self, key): setattr(self, key, value) def overlay(self, block_start_string=missing, block_end_string=missing, variable_start_string=missing, variable_end_string=missing, comment_start_string=missing, comment_end_string=missing, line_statement_prefix=missing, line_comment_prefix=missing, trim_blocks=missing, extensions=missing, optimized=missing, undefined=missing, finalize=missing, autoescape=missing, loader=missing, cache_size=missing, auto_reload=missing, bytecode_cache=missing): """Create a new overlay environment that shares all the data with the current environment except of cache and the overridden attributes. Extensions cannot be removed for an overlayed environment. An overlayed environment automatically gets all the extensions of the environment it is linked to plus optional extra extensions. Creating overlays should happen after the initial environment was set up completely. Not all attributes are truly linked, some are just copied over so modifications on the original environment may not shine through. """ args = dict(locals()) del args['self'], args['cache_size'], args['extensions'] rv = object.__new__(self.__class__) rv.__dict__.update(self.__dict__) rv.overlayed = True rv.linked_to = self for key, value in args.iteritems(): if value is not missing: setattr(rv, key, value) if cache_size is not missing: rv.cache = create_cache(cache_size) else: rv.cache = copy_cache(self.cache) rv.extensions = {} for key, value in self.extensions.iteritems(): rv.extensions[key] = value.bind(rv) if extensions is not missing: rv.extensions.update(load_extensions(extensions)) return _environment_sanity_check(rv) lexer = property(get_lexer, doc="The lexer for this environment.") def getitem(self, obj, argument): """Get an item or attribute of an object but prefer the item.""" try: return obj[argument] except (TypeError, LookupError): if isinstance(argument, basestring): try: attr = str(argument) except: pass else: try: return getattr(obj, attr) except AttributeError: pass return self.undefined(obj=obj, name=argument) def getattr(self, obj, attribute): """Get an item or attribute of an object but prefer the attribute. Unlike :meth:`getitem` the attribute *must* be a bytestring. """ try: return getattr(obj, attribute) except AttributeError: pass try: return obj[attribute] except (TypeError, LookupError, AttributeError): return self.undefined(obj=obj, name=attribute) @internalcode def parse(self, source, name=None, filename=None): """Parse the sourcecode and return the abstract syntax tree. This tree of nodes is used by the compiler to convert the template into executable source- or bytecode. This is useful for debugging or to extract information from templates. If you are :ref:`developing Jinja2 extensions ` this gives you a good overview of the node tree generated. """ try: return self._parse(source, name, filename) except TemplateSyntaxError: exc_info = sys.exc_info() self.handle_exception(exc_info, source_hint=source) def _parse(self, source, name, filename): """Internal parsing function used by `parse` and `compile`.""" return Parser(self, source, name, _encode_filename(filename)).parse() def lex(self, source, name=None, filename=None): """Lex the given sourcecode and return a generator that yields tokens as tuples in the form ``(lineno, token_type, value)``. This can be useful for :ref:`extension development ` and debugging templates. This does not perform preprocessing. If you want the preprocessing of the extensions to be applied you have to filter source through the :meth:`preprocess` method. """ source = unicode(source) try: return self.lexer.tokeniter(source, name, filename) except TemplateSyntaxError: exc_info = sys.exc_info() self.handle_exception(exc_info, source_hint=source) def preprocess(self, source, name=None, filename=None): """Preprocesses the source with all extensions. This is automatically called for all parsing and compiling methods but *not* for :meth:`lex` because there you usually only want the actual source tokenized. """ return reduce(lambda s, e: e.preprocess(s, name, filename), self.extensions.itervalues(), unicode(source)) def _tokenize(self, source, name, filename=None, state=None): """Called by the parser to do the preprocessing and filtering for all the extensions. Returns a :class:`~jinja2.lexer.TokenStream`. """ source = self.preprocess(source, name, filename) stream = self.lexer.tokenize(source, name, filename, state) for ext in self.extensions.itervalues(): stream = ext.filter_stream(stream) if not isinstance(stream, TokenStream): stream = TokenStream(stream, name, filename) return stream @internalcode def compile(self, source, name=None, filename=None, raw=False): """Compile a node or template source code. The `name` parameter is the load name of the template after it was joined using :meth:`join_path` if necessary, not the filename on the file system. the `filename` parameter is the estimated filename of the template on the file system. If the template came from a database or memory this can be omitted. The return value of this method is a python code object. If the `raw` parameter is `True` the return value will be a string with python code equivalent to the bytecode returned otherwise. This method is mainly used internally. """ source_hint = None try: if isinstance(source, basestring): source_hint = source source = self._parse(source, name, filename) if self.optimized: source = optimize(source, self) source = generate(source, self, name, filename) if raw: return source if filename is None: filename = '