weblog-2.5/.hg_archival.txt0000644000000000000000000000022311337074725016075 0ustar00usergroup00000000000000repo: 00ecfb3367fecf6d8ba7a94c3f995bc789c18d1e node: 35376e9bf8d1e47e534a8065262615d1c784d9e9 branch: default latesttag: 2.4 latesttagdistance: 16 weblog-2.5/.hgignore0000644000000000000000000000010111337074725014605 0ustar00usergroup00000000000000syntax: glob *.pyc *~ .*.swp build weblog.egg-info doc/.build/* weblog-2.5/.hgtags0000644000000000000000000000207011337074725014267 0ustar00usergroup000000000000009bafbb9dfb928172d988390ea61932b610278ea3 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-2.5/COPYING0000644000000000000000000000137011337074725014046 0ustar00usergroup00000000000000Copyright (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. weblog-2.5/INSTALL0000644000000000000000000000416311337074725014047 0ustar00usergroup00000000000000Installation ============ 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-2.5/MANIFEST.in0000644000000000000000000000036111337074725014550 0ustar00usergroup00000000000000include 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-2.5/README0000644000000000000000000000062011337074725013670 0ustar00usergroup00000000000000Weblog is a web log or blog publisher. It takes structured text files as input and outputs static HTML / RSS files. Weblog aims to be simple and robust. to learn how to install and use weblog please read the text file: doc/weblog.rst If you have docutils installed you can turn it into a HTML file: $ rst2html.py weblog.rst > weblog.html Jinja is needed to use Weblog (http://jinja.pocoo.org) weblog-2.5/contrib/dist.sh0000644000000000000000000000372311337074725015756 0ustar00usergroup00000000000000#!/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-2.5/contrib/migrate.py0000644000000000000000000000702211337074725016455 0ustar00usergroup00000000000000import 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-2.5/doc/conf.py0000644000000000000000000001315211337074725015060 0ustar00usergroup00000000000000# -*- 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-2.5/doc/index.rst0000644000000000000000000000066311337074725015425 0ustar00usergroup00000000000000Weblog'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-2.5/doc/install.rst0000644000000000000000000000003011337074725015750 0ustar00usergroup00000000000000.. include:: ../INSTALL weblog-2.5/doc/reference.rst0000644000000000000000000002731711337074725016261 0ustar00usergroup00000000000000.. _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-2.5/doc/short_description.txt0000644000000000000000000000017611337074725020066 0ustar00usergroup00000000000000Simple blog publisher. Read structured text files and generate static HTML / Feed files. Weblog aims to be simple and robust. weblog-2.5/doc/tutorial.rst0000644000000000000000000000765311337074725016167 0ustar00usergroup00000000000000Tutorial ======== 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-2.5/doc/update.sh0000755000000000000000000000031511337074725015377 0ustar00usergroup00000000000000#!/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-2.5/setup.cfg0000644000000000000000000000004711337074725014634 0ustar00usergroup00000000000000[nosetests] verbosity=3 with-doctest=1 weblog-2.5/setup.py0000644000000000000000000000260611337074725014530 0ustar00usergroup00000000000000try: 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-2.5/test.py0000644000000000000000000003065411337074725014353 0ustar00usergroup00000000000000from __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-2.5/test/date/no_date.txt0000644000000000000000000000005411337074725017057 0ustar00usergroup00000000000000title: Post with no date Post with no date weblog-2.5/test/duplicate/config.py0000644000000000000000000000015311337074725017561 0ustar00usergroup00000000000000title = 'Test blog' url = 'http://blog.test.org' description = 'Test blog' author = 'test ' weblog-2.5/test/duplicate/post1.html0000644000000000000000000000005711337074725017701 0ustar00usergroup00000000000000title: duplicate date: 2007-01-01 duplicate 1 weblog-2.5/test/duplicate/post2.html0000644000000000000000000000005711337074725017702 0ustar00usergroup00000000000000title: duplicate date: 2007-01-01 duplicate 1 weblog-2.5/test/empty/config.py0000644000000000000000000000000011337074725016734 0ustar00usergroup00000000000000weblog-2.5/test/encoding/config.py0000644000000000000000000000017611337074725017402 0ustar00usergroup00000000000000title = 'Test blog' url = 'http://blog.test.org' description = 'Test blog' author = 'test ' encoding = 'utf-8' weblog-2.5/test/encoding/latin-1.html0000644000000000000000000000010411337074725017705 0ustar00usergroup00000000000000title: latin post ÖÉÈÄ ... date: 2008-02-04 encoding: latin-1 Öéèä weblog-2.5/test/encoding/utf-8.html0000644000000000000000000000007211337074725017407 0ustar00usergroup00000000000000title: UTF-8 post ÖÉÈÄ ... date: 2008-02-03 Öéèä weblog-2.5/test/full_url/config.py0000644000000000000000000000017611337074725017440 0ustar00usergroup00000000000000title = 'Test blog' url = 'http://blog.test.org' description = 'Test blog' author = 'test ' encoding = 'utf-8' weblog-2.5/test/full_url/utf-8.html0000644000000000000000000000025011337074725017443 0ustar00usergroup00000000000000title: UTF-8 post ÖÉÈÄ ... date: 2008-02-03 Öéèä
äyÔÀ Weblog weblog-2.5/test/simple/config.py0000644000000000000000000000015311337074725017100 0ustar00usergroup00000000000000title = 'Test blog' url = 'http://blog.test.org' description = 'Test blog' author = 'test ' weblog-2.5/test/simple/post1.html0000644000000000000000000000004511337074725017215 0ustar00usergroup00000000000000title: post1 date: 2007-01-01 post1 weblog-2.5/test/simple/post2.html0000644000000000000000000000004411337074725017215 0ustar00usergroup00000000000000title: post2 date: 2007-6-15 post2 weblog-2.5/test/simple/post3.html0000644000000000000000000000004511337074725017217 0ustar00usergroup00000000000000title: post3 date: 2007-12-31 post3 weblog-2.5/test/template_encoding/config.py0000644000000000000000000000005411337074725021270 0ustar00usergroup00000000000000url = 'http://test.org/' encoding = 'UTF-8' weblog-2.5/test/template_encoding/templates/base.html.tmpl0000644000000000000000000000140011337074725024216 0ustar00usergroup00000000000000 {% 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-2.5/weblog/__init__.py0000644000000000000000000000472111337074725016406 0ustar00usergroup00000000000000__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-2.5/weblog/configuration.py0000644000000000000000000000253611337074725017520 0ustar00usergroup00000000000000import 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-2.5/weblog/date.py0000644000000000000000000000526711337074725015572 0ustar00usergroup00000000000000import 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-2.5/weblog/html_full_url.py0000644000000000000000000001053511337074725017517 0ustar00usergroup00000000000000from 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-2.5/weblog/html_to_xhtml.py0000644000000000000000000000333711337074725017533 0ustar00usergroup00000000000000import 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-2.5/weblog/markup.py0000644000000000000000000000376511337074725016155 0ustar00usergroup00000000000000from 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-2.5/weblog/page.py0000644000000000000000000002175511337074725015571 0ustar00usergroup00000000000000import 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-2.5/weblog/publish.py0000644000000000000000000001251011337074725016310 0ustar00usergroup00000000000000import 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-2.5/weblog/rfc3339.py0000644000000000000000000002203411337074725015740 0ustar00usergroup00000000000000#!/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-2.5/weblog/template.py0000644000000000000000000000372611337074725016466 0ustar00usergroup00000000000000import 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-2.5/weblog/templates/archives.html.tmpl0000644000000000000000000000323111337074725021733 0ustar00usergroup00000000000000{% 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-2.5/weblog/templates/base.html.tmpl0000644000000000000000000000377311337074725021054 0ustar00usergroup00000000000000 {% 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-2.5/weblog/templates/feed.atom.tmpl0000644000000000000000000000232411337074725021030 0ustar00usergroup00000000000000 {{ 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-2.5/weblog/templates/index.html.tmpl0000644000000000000000000000136711337074725021246 0ustar00usergroup00000000000000{% 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-2.5/weblog/templates/post.html.tmpl0000644000000000000000000000075411337074725021123 0ustar00usergroup00000000000000{% 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-2.5/weblog/utf8_html_parser.py0000644000000000000000000000456211337074725020140 0ustar00usergroup00000000000000from 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-2.5/weblog_run.py0000755000000000000000000000006611337074725015534 0ustar00usergroup00000000000000#!/usr/bin/env python from weblog import main main()