From d17d509adc5b08b6ca8c32e8cef01f9626b14ab2 Mon Sep 17 00:00:00 2001 From: Sam Ruby Date: Tue, 29 Aug 2006 01:56:39 -0400 Subject: [PATCH] Rough in HTMLTMPL support --- .bzrignore | 1 + planet/htmltmpl.py | 1480 +++++++++++++++++ planet/shell/__init__.py | 23 + planet/shell/tmpl.py | 190 +++ planet/shell/xslt.py | 34 + planet/splice.py | 48 +- tests/data/filter/tmpl/author_email.xml | 13 + tests/data/filter/tmpl/author_name.xml | 13 + tests/data/filter/tmpl/author_uri.xml | 13 + tests/data/filter/tmpl/content_html.xml | 10 + tests/data/filter/tmpl/content_lang.xml | 10 + tests/data/filter/tmpl/content_text.xml | 10 + tests/data/filter/tmpl/content_xhtml.xml | 13 + tests/data/filter/tmpl/id.xml | 11 + tests/data/filter/tmpl/id_only_content.xml | 13 + .../data/filter/tmpl/id_only_description.xml | 13 + tests/data/filter/tmpl/id_only_link.xml | 13 + tests/data/filter/tmpl/id_only_title.xml | 13 + tests/data/filter/tmpl/link_href.xml | 11 + tests/data/filter/tmpl/link_rel.xml | 11 + tests/data/filter/tmpl/link_type.xml | 11 + tests/data/filter/tmpl/planet_name.xml | 14 + tests/data/filter/tmpl/published.xml | 11 + tests/data/filter/tmpl/rights.xml | 11 + tests/data/filter/tmpl/source_author.xml | 18 + tests/data/filter/tmpl/source_icon.xml | 14 + tests/data/filter/tmpl/source_id.xml | 14 + tests/data/filter/tmpl/source_logo.xml | 14 + tests/data/filter/tmpl/source_planet_name.xml | 14 + tests/data/filter/tmpl/source_rights.xml | 14 + tests/data/filter/tmpl/source_subtitle.xml | 14 + tests/data/filter/tmpl/source_title.xml | 14 + tests/data/filter/tmpl/source_updated.xml | 14 + tests/data/filter/tmpl/summary_html.xml | 10 + tests/data/filter/tmpl/summary_lang.xml | 10 + tests/data/filter/tmpl/summary_text.xml | 10 + tests/data/filter/tmpl/summary_xhtml.xml | 13 + tests/data/filter/tmpl/title_html.xml | 10 + tests/data/filter/tmpl/title_lang.xml | 10 + tests/data/filter/tmpl/title_text.xml | 10 + tests/data/filter/tmpl/title_xhtml.xml | 13 + tests/data/filter/tmpl/updated.xml | 11 + tests/test_filter_tmpl.py | 37 + themes/asf/config.ini | 1 + themes/common/rss20.xml.tmpl | 30 + 45 files changed, 2239 insertions(+), 46 deletions(-) create mode 100644 .bzrignore create mode 100644 planet/htmltmpl.py create mode 100644 planet/shell/__init__.py create mode 100644 planet/shell/tmpl.py create mode 100644 planet/shell/xslt.py create mode 100644 tests/data/filter/tmpl/author_email.xml create mode 100644 tests/data/filter/tmpl/author_name.xml create mode 100644 tests/data/filter/tmpl/author_uri.xml create mode 100644 tests/data/filter/tmpl/content_html.xml create mode 100644 tests/data/filter/tmpl/content_lang.xml create mode 100644 tests/data/filter/tmpl/content_text.xml create mode 100644 tests/data/filter/tmpl/content_xhtml.xml create mode 100644 tests/data/filter/tmpl/id.xml create mode 100644 tests/data/filter/tmpl/id_only_content.xml create mode 100644 tests/data/filter/tmpl/id_only_description.xml create mode 100644 tests/data/filter/tmpl/id_only_link.xml create mode 100644 tests/data/filter/tmpl/id_only_title.xml create mode 100644 tests/data/filter/tmpl/link_href.xml create mode 100644 tests/data/filter/tmpl/link_rel.xml create mode 100644 tests/data/filter/tmpl/link_type.xml create mode 100644 tests/data/filter/tmpl/planet_name.xml create mode 100644 tests/data/filter/tmpl/published.xml create mode 100644 tests/data/filter/tmpl/rights.xml create mode 100644 tests/data/filter/tmpl/source_author.xml create mode 100644 tests/data/filter/tmpl/source_icon.xml create mode 100644 tests/data/filter/tmpl/source_id.xml create mode 100644 tests/data/filter/tmpl/source_logo.xml create mode 100644 tests/data/filter/tmpl/source_planet_name.xml create mode 100644 tests/data/filter/tmpl/source_rights.xml create mode 100644 tests/data/filter/tmpl/source_subtitle.xml create mode 100644 tests/data/filter/tmpl/source_title.xml create mode 100644 tests/data/filter/tmpl/source_updated.xml create mode 100644 tests/data/filter/tmpl/summary_html.xml create mode 100644 tests/data/filter/tmpl/summary_lang.xml create mode 100644 tests/data/filter/tmpl/summary_text.xml create mode 100644 tests/data/filter/tmpl/summary_xhtml.xml create mode 100644 tests/data/filter/tmpl/title_html.xml create mode 100644 tests/data/filter/tmpl/title_lang.xml create mode 100644 tests/data/filter/tmpl/title_text.xml create mode 100644 tests/data/filter/tmpl/title_xhtml.xml create mode 100644 tests/data/filter/tmpl/updated.xml create mode 100644 tests/test_filter_tmpl.py create mode 100644 themes/common/rss20.xml.tmpl diff --git a/.bzrignore b/.bzrignore new file mode 100644 index 0000000..f8b5310 --- /dev/null +++ b/.bzrignore @@ -0,0 +1 @@ +*.tmplc diff --git a/planet/htmltmpl.py b/planet/htmltmpl.py new file mode 100644 index 0000000..be6e41b --- /dev/null +++ b/planet/htmltmpl.py @@ -0,0 +1,1480 @@ + +""" A templating engine for separation of code and HTML. + + The documentation of this templating engine is separated to two parts: + + 1. Description of the templating language. + + 2. Documentation of classes and API of this module that provides + a Python implementation of the templating language. + + All the documentation can be found in 'doc' directory of the + distribution tarball or at the homepage of the engine. + Latest versions of this module are also available at that website. + + You can use and redistribute this module under conditions of the + GNU General Public License that can be found either at + [ http://www.gnu.org/ ] or in file "LICENSE" contained in the + distribution tarball of this module. + + Copyright (c) 2001 Tomas Styblo, tripie@cpan.org + + @name htmltmpl + @version 1.22 + @author-name Tomas Styblo + @author-email tripie@cpan.org + @website http://htmltmpl.sourceforge.net/ + @license-name GNU GPL + @license-url http://www.gnu.org/licenses/gpl.html +""" + +__version__ = 1.22 +__author__ = "Tomas Styblo (tripie@cpan.org)" + +# All imported modules are part of the standard Python library. + +from types import * +import re +import os +import os.path +import pprint # only for debugging +import sys +import copy +import cgi # for HTML escaping of variables +import urllib # for URL escaping of variables +import cPickle # for template compilation +import gettext + +INCLUDE_DIR = "inc" + +# Total number of possible parameters. +# Increment if adding a parameter to any statement. +PARAMS_NUMBER = 3 + +# Relative positions of parameters in TemplateCompiler.tokenize(). +PARAM_NAME = 1 +PARAM_ESCAPE = 2 +PARAM_GLOBAL = 3 +PARAM_GETTEXT_STRING = 1 + +# Find a way to lock files. Currently implemented only for UNIX and windows. +LOCKTYPE_FCNTL = 1 +LOCKTYPE_MSVCRT = 2 +LOCKTYPE = None +try: + import fcntl +except: + try: + import msvcrt + except: + LOCKTYPE = None + else: + LOCKTYPE = LOCKTYPE_MSVCRT +else: + LOCKTYPE = LOCKTYPE_FCNTL +LOCK_EX = 1 +LOCK_SH = 2 +LOCK_UN = 3 + +############################################## +# CLASS: TemplateManager # +############################################## + +class TemplateManager: + """ Class that manages compilation and precompilation of templates. + + You should use this class whenever you work with templates + that are stored in a file. The class can create a compiled + template and transparently manage its precompilation. It also + keeps the precompiled templates up-to-date by modification times + comparisons. + """ + + def __init__(self, include=1, max_include=5, precompile=1, comments=1, + gettext=0, debug=0): + """ Constructor. + + @header + __init__(include=1, max_include=5, precompile=1, comments=1, + gettext=0, debug=0) + + @param include Enable or disable included templates. + This optional parameter can be used to enable or disable + TMPL_INCLUDE inclusion of templates. Disabling of + inclusion can improve performance a bit. The inclusion is + enabled by default. + + @param max_include Maximum depth of nested inclusions. + This optional parameter can be used to specify maximum depth of + nested TMPL_INCLUDE inclusions. It defaults to 5. + This setting prevents infinite recursive inclusions. + + @param precompile Enable or disable precompilation of templates. + This optional parameter can be used to enable or disable + creation and usage of precompiled templates. + + A precompiled template is saved to the same directory in + which the main template file is located. You need write + permissions to that directory. + + Precompilation provides a significant performance boost because + it's not necessary to parse the templates over and over again. + The boost is especially noticeable when templates that include + other templates are used. + + Comparison of modification times of the main template and all + included templates is used to ensure that the precompiled + templates are up-to-date. Templates are also recompiled if the + htmltmpl module is updated. + + The TemplateErrorexception is raised when the precompiled + template cannot be saved. Precompilation is enabled by default. + + Precompilation is available only on UNIX and Windows platforms, + because proper file locking which is necessary to ensure + multitask safe behaviour is platform specific and is not + implemented for other platforms. Attempts to enable precompilation + on the other platforms result in raise of the + TemplateError exception. + + @param comments Enable or disable template comments. + This optional parameter can be used to enable or disable + template comments. + Disabling of the comments can improve performance a bit. + Comments are enabled by default. + + @param gettext Enable or disable gettext support. + + @param debug Enable or disable debugging messages. + This optional parameter is a flag that can be used to enable + or disable debugging messages which are printed to the standard + error output. The debugging messages are disabled by default. + """ + # Save the optional parameters. + # These values are not modified by any method. + self._include = include + self._max_include = max_include + self._precompile = precompile + self._comments = comments + self._gettext = gettext + self._debug = debug + + # Find what module to use to lock files. + # File locking is necessary for the 'precompile' feature to be + # multitask/thread safe. Currently it works only on UNIX + # and Windows. Anyone willing to implement it on Mac ? + if precompile and not LOCKTYPE: + raise TemplateError, "Template precompilation is not "\ + "available on this platform." + self.DEB("INIT DONE") + + def prepare(self, file): + """ Preprocess, parse, tokenize and compile the template. + + If precompilation is enabled then this method tries to load + a precompiled form of the template from the same directory + in which the template source file is located. If it succeeds, + then it compares modification times stored in the precompiled + form to modification times of source files of the template, + including source files of all templates included via the + TMPL_INCLUDE statements. If any of the modification times + differs, then the template is recompiled and the precompiled + form updated. + + If precompilation is disabled, then this method parses and + compiles the template. + + @header prepare(file) + + @return Compiled template. + The methods returns an instance of the Template class + which is a compiled form of the template. This instance can be + used as input for the TemplateProcessor. + + @param file Path to the template file to prepare. + The method looks for the template file in current directory + if the parameter is a relative path. All included templates must + be placed in subdirectory 'inc' of the + directory in which the main template file is located. + """ + compiled = None + if self._precompile: + if self.is_precompiled(file): + try: + precompiled = self.load_precompiled(file) + except PrecompiledError, template: + print >> sys.stderr, "Htmltmpl: bad precompiled "\ + "template '%s' removed" % template + compiled = self.compile(file) + self.save_precompiled(compiled) + else: + precompiled.debug(self._debug) + compile_params = (self._include, self._max_include, + self._comments, self._gettext) + if precompiled.is_uptodate(compile_params): + self.DEB("PRECOMPILED: UPTODATE") + compiled = precompiled + else: + self.DEB("PRECOMPILED: NOT UPTODATE") + compiled = self.update(precompiled) + else: + self.DEB("PRECOMPILED: NOT PRECOMPILED") + compiled = self.compile(file) + self.save_precompiled(compiled) + else: + self.DEB("PRECOMPILATION DISABLED") + compiled = self.compile(file) + return compiled + + def update(self, template): + """ Update (recompile) a compiled template. + + This method recompiles a template compiled from a file. + If precompilation is enabled then the precompiled form saved on + disk is also updated. + + @header update(template) + + @return Recompiled template. + It's ensured that the returned template is up-to-date. + + @param template A compiled template. + This parameter should be an instance of the Template + class, created either by the TemplateManager or by the + TemplateCompiler. The instance must represent a template + compiled from a file on disk. + """ + self.DEB("UPDATE") + updated = self.compile(template.file()) + if self._precompile: + self.save_precompiled(updated) + return updated + + ############################################## + # PRIVATE METHODS # + ############################################## + + def DEB(self, str): + """ Print debugging message to stderr if debugging is enabled. + @hidden + """ + if self._debug: print >> sys.stderr, str + + def lock_file(self, file, lock): + """ Provide platform independent file locking. + @hidden + """ + fd = file.fileno() + if LOCKTYPE == LOCKTYPE_FCNTL: + if lock == LOCK_SH: + fcntl.flock(fd, fcntl.LOCK_SH) + elif lock == LOCK_EX: + fcntl.flock(fd, fcntl.LOCK_EX) + elif lock == LOCK_UN: + fcntl.flock(fd, fcntl.LOCK_UN) + else: + raise TemplateError, "BUG: bad lock in lock_file" + elif LOCKTYPE == LOCKTYPE_MSVCRT: + if lock == LOCK_SH: + # msvcrt does not support shared locks :-( + msvcrt.locking(fd, msvcrt.LK_LOCK, 1) + elif lock == LOCK_EX: + msvcrt.locking(fd, msvcrt.LK_LOCK, 1) + elif lock == LOCK_UN: + msvcrt.locking(fd, msvcrt.LK_UNLCK, 1) + else: + raise TemplateError, "BUG: bad lock in lock_file" + else: + raise TemplateError, "BUG: bad locktype in lock_file" + + def compile(self, file): + """ Compile the template. + @hidden + """ + return TemplateCompiler(self._include, self._max_include, + self._comments, self._gettext, + self._debug).compile(file) + + def is_precompiled(self, file): + """ Return true if the template is already precompiled on the disk. + This method doesn't check whether the compiled template is + uptodate. + @hidden + """ + filename = file + "c" # "template.tmplc" + if os.path.isfile(filename): + return 1 + else: + return 0 + + def load_precompiled(self, file): + """ Load precompiled template from disk. + + Remove the precompiled template file and recompile it + if the file contains corrupted or unpicklable data. + + @hidden + """ + filename = file + "c" # "template.tmplc" + self.DEB("LOADING PRECOMPILED") + try: + remove_bad = 0 + file = None + try: + file = open(filename, "rb") + self.lock_file(file, LOCK_SH) + precompiled = cPickle.load(file) + except IOError, (errno, errstr): + raise TemplateError, "IO error in load precompiled "\ + "template '%s': (%d) %s"\ + % (filename, errno, errstr) + except cPickle.UnpicklingError: + remove_bad = 1 + raise PrecompiledError, filename + except: + remove_bad = 1 + raise + else: + return precompiled + finally: + if file: + self.lock_file(file, LOCK_UN) + file.close() + if remove_bad and os.path.isfile(filename): + # X: We may lose the original exception here, raising OSError. + os.remove(filename) + + def save_precompiled(self, template): + """ Save compiled template to disk in precompiled form. + + Associated metadata is also saved. It includes: filename of the + main template file, modification time of the main template file, + modification times of all included templates and version of the + htmltmpl module which compiled the template. + + The method removes a file which is saved only partially because + of some error. + + @hidden + """ + filename = template.file() + "c" # creates "template.tmplc" + # Check if we have write permission to the template's directory. + template_dir = os.path.dirname(os.path.abspath(filename)) + if not os.access(template_dir, os.W_OK): + raise TemplateError, "Cannot save precompiled templates "\ + "to '%s': write permission denied."\ + % template_dir + try: + remove_bad = 0 + file = None + try: + file = open(filename, "wb") # may truncate existing file + self.lock_file(file, LOCK_EX) + BINARY = 1 + READABLE = 0 + if self._debug: + cPickle.dump(template, file, READABLE) + else: + cPickle.dump(template, file, BINARY) + except IOError, (errno, errstr): + remove_bad = 1 + raise TemplateError, "IO error while saving precompiled "\ + "template '%s': (%d) %s"\ + % (filename, errno, errstr) + except cPickle.PicklingError, error: + remove_bad = 1 + raise TemplateError, "Pickling error while saving "\ + "precompiled template '%s': %s"\ + % (filename, error) + except: + remove_bad = 1 + raise + else: + self.DEB("SAVING PRECOMPILED") + finally: + if file: + self.lock_file(file, LOCK_UN) + file.close() + if remove_bad and os.path.isfile(filename): + # X: We may lose the original exception here, raising OSError. + os.remove(filename) + + +############################################## +# CLASS: TemplateProcessor # +############################################## + +class TemplateProcessor: + """ Fill the template with data and process it. + + This class provides actual processing of a compiled template. + Use it to set template variables and loops and then obtain + result of the processing. + """ + + def __init__(self, html_escape=1, magic_vars=1, global_vars=0, debug=0): + """ Constructor. + + @header __init__(html_escape=1, magic_vars=1, global_vars=0, + debug=0) + + @param html_escape Enable or disable HTML escaping of variables. + This optional parameter is a flag that can be used to enable or + disable automatic HTML escaping of variables. + All variables are by default automatically HTML escaped. + The escaping process substitutes HTML brackets, ampersands and + double quotes with appropriate HTML entities. + + @param magic_vars Enable or disable loop magic variables. + This parameter can be used to enable or disable + "magic" context variables, that are automatically defined inside + loops. Magic variables are enabled by default. + + Refer to the language specification for description of these + magic variables. + + @param global_vars Globally activate global lookup of variables. + This optional parameter is a flag that can be used to specify + whether variables which cannot be found in the current scope + should be automatically looked up in enclosing scopes. + + Automatic global lookup is disabled by default. Global lookup + can be overriden on a per-variable basis by the + GLOBAL parameter of a TMPL_VAR + statement. + + @param debug Enable or disable debugging messages. + """ + self._html_escape = html_escape + self._magic_vars = magic_vars + self._global_vars = global_vars + self._debug = debug + + # Data structure containing variables and loops set by the + # application. Use debug=1, process some template and + # then check stderr to see how the structure looks. + # It's modified only by set() and reset() methods. + self._vars = {} + + # Following variables are for multipart templates. + self._current_part = 1 + self._current_pos = 0 + + def set(self, var, value): + """ Associate a value with top-level template variable or loop. + + A template identifier can represent either an ordinary variable + (string) or a loop. + + To assign a value to a string identifier pass a scalar + as the 'value' parameter. This scalar will be automatically + converted to string. + + To assign a value to a loop identifier pass a list of mappings as + the 'value' parameter. The engine iterates over this list and + assigns values from the mappings to variables in a template loop + block if a key in the mapping corresponds to a name of a variable + in the loop block. The number of mappings contained in this list + is equal to number of times the loop block is repeated in the + output. + + @header set(var, value) + @return No return value. + + @param var Name of template variable or loop. + @param value The value to associate. + + """ + # The correctness of character case is verified only for top-level + # variables. + if self.is_ordinary_var(value): + # template top-level ordinary variable + if not var.islower(): + raise TemplateError, "Invalid variable name '%s'." % var + elif type(value) == ListType: + # template top-level loop + if var != var.capitalize(): + raise TemplateError, "Invalid loop name '%s'." % var + else: + raise TemplateError, "Value of toplevel variable '%s' must "\ + "be either a scalar or a list." % var + self._vars[var] = value + self.DEB("VALUE SET: " + str(var)) + + def reset(self, keep_data=0): + """ Reset the template data. + + This method resets the data contained in the template processor + instance. The template processor instance can be used to process + any number of templates, but this method must be called after + a template is processed to reuse the instance, + + @header reset(keep_data=0) + @return No return value. + + @param keep_data Do not reset the template data. + Use this flag if you do not want the template data to be erased. + This way you can reuse the data contained in the instance of + the TemplateProcessor. + """ + self._current_part = 1 + self._current_pos = 0 + if not keep_data: + self._vars.clear() + self.DEB("RESET") + + def process(self, template, part=None): + """ Process a compiled template. Return the result as string. + + This method actually processes a template and returns + the result. + + @header process(template, part=None) + @return Result of the processing as string. + + @param template A compiled template. + Value of this parameter must be an instance of the + Template class created either by the + TemplateManager or by the TemplateCompiler. + + @param part The part of a multipart template to process. + This parameter can be used only together with a multipart + template. It specifies the number of the part to process. + It must be greater than zero, because the parts are numbered + from one. + + The parts must be processed in the right order. You + cannot process a part which precedes an already processed part. + + If this parameter is not specified, then the whole template + is processed, or all remaining parts are processed. + """ + self.DEB("APP INPUT:") + if self._debug: pprint.pprint(self._vars, sys.stderr) + if part != None and (part == 0 or part < self._current_part): + raise TemplateError, "process() - invalid part number" + + # This flag means "jump behind the end of current statement" or + # "skip the parameters of current statement". + # Even parameters that actually are not present in the template + # do appear in the list of tokens as empty items ! + skip_params = 0 + + # Stack for enabling or disabling output in response to TMPL_IF, + # TMPL_UNLESS, TMPL_ELSE and TMPL_LOOPs with no passes. + output_control = [] + ENABLE_OUTPUT = 1 + DISABLE_OUTPUT = 0 + + # Stacks for data related to loops. + loop_name = [] # name of a loop + loop_pass = [] # current pass of a loop (counted from zero) + loop_start = [] # index of loop start in token list + loop_total = [] # total number of passes in a loop + + tokens = template.tokens() + len_tokens = len(tokens) + out = "" # buffer for processed output + + # Recover position at which we ended after processing of last part. + i = self._current_pos + + # Process the list of tokens. + while 1: + if i == len_tokens: break + if skip_params: + # Skip the parameters following a statement. + skip_params = 0 + i += PARAMS_NUMBER + continue + + token = tokens[i] + if token.startswith("." + escape = tokens[i + PARAM_ESCAPE] + globalp = tokens[i + PARAM_GLOBAL] + skip_params = 1 + + # If output of current block is not disabled then append + # the substitued and escaped variable to the output. + if DISABLE_OUTPUT not in output_control: + value = str(self.find_value(var, loop_name, loop_pass, + loop_total, globalp)) + out += self.escape(value, escape) + self.DEB("VAR: " + str(var)) + + elif token == "." + skip_params = 1 + + # Find total number of passes in this loop. + passtotal = self.find_value(var, loop_name, loop_pass, + loop_total) + if not passtotal: passtotal = 0 + # Push data for this loop on the stack. + loop_total.append(passtotal) + loop_start.append(i) + loop_pass.append(0) + loop_name.append(var) + + # Disable output of loop block if the number of passes + # in this loop is zero. + if passtotal == 0: + # This loop is empty. + output_control.append(DISABLE_OUTPUT) + self.DEB("LOOP: DISABLE: " + str(var)) + else: + output_control.append(ENABLE_OUTPUT) + self.DEB("LOOP: FIRST PASS: %s TOTAL: %d"\ + % (var, passtotal)) + + elif token == "." + globalp = tokens[i + PARAM_GLOBAL] + skip_params = 1 + if self.find_value(var, loop_name, loop_pass, + loop_total, globalp): + output_control.append(ENABLE_OUTPUT) + self.DEB("IF: ENABLE: " + str(var)) + else: + output_control.append(DISABLE_OUTPUT) + self.DEB("IF: DISABLE: " + str(var)) + + elif token == "." + globalp = tokens[i + PARAM_GLOBAL] + skip_params = 1 + if self.find_value(var, loop_name, loop_pass, + loop_total, globalp): + output_control.append(DISABLE_OUTPUT) + self.DEB("UNLESS: DISABLE: " + str(var)) + else: + output_control.append(ENABLE_OUTPUT) + self.DEB("UNLESS: ENABLE: " + str(var)) + + elif token == "." + + # If this loop was not disabled, then record the pass. + if loop_total[-1] > 0: loop_pass[-1] += 1 + + if loop_pass[-1] == loop_total[-1]: + # There are no more passes in this loop. Pop + # the loop from stack. + loop_pass.pop() + loop_name.pop() + loop_start.pop() + loop_total.pop() + output_control.pop() + self.DEB("LOOP: END") + else: + # Jump to the beggining of this loop block + # to process next pass of the loop. + i = loop_start[-1] + self.DEB("LOOP: NEXT PASS") + + elif token == "." + output_control.pop() + self.DEB("IF: END") + + elif token == "." + output_control.pop() + self.DEB("UNLESS: END") + + elif token == "." + if output_control[-1] == DISABLE_OUTPUT: + # Condition was false, activate the ELSE block. + output_control[-1] = ENABLE_OUTPUT + self.DEB("ELSE: ENABLE") + elif output_control[-1] == ENABLE_OUTPUT: + # Condition was true, deactivate the ELSE block. + output_control[-1] = DISABLE_OUTPUT + self.DEB("ELSE: DISABLE") + else: + raise TemplateError, "BUG: ELSE: INVALID FLAG" + + elif token == " +

+ HTMLTMPL WARNING:
+ Cannot include template: %s +

+
+ """ % filename + self.DEB("CANNOT INCLUDE WARNING") + + elif token == "." % token + + elif DISABLE_OUTPUT not in output_control: + # Raw textual template data. + # If output of current block is not disabled, then + # append template data to the output buffer. + out += token + + i += 1 + # end of the big while loop + + # Check whether all opening statements were closed. + if loop_name: raise TemplateError, "Missing ." + if output_control: raise TemplateError, "Missing or " + return out + + ############################################## + # PRIVATE METHODS # + ############################################## + + def DEB(self, str): + """ Print debugging message to stderr if debugging is enabled. + @hidden + """ + if self._debug: print >> sys.stderr, str + + def find_value(self, var, loop_name, loop_pass, loop_total, + global_override=None): + """ Search the self._vars data structure to find variable var + located in currently processed pass of a loop which + is currently being processed. If the variable is an ordinary + variable, then return it. + + If the variable is an identificator of a loop, then + return the total number of times this loop will + be executed. + + Return an empty string, if the variable is not + found at all. + + @hidden + """ + # Search for the requested variable in magic vars if the name + # of the variable starts with "__" and if we are inside a loop. + if self._magic_vars and var.startswith("__") and loop_name: + return self.magic_var(var, loop_pass[-1], loop_total[-1]) + + # Search for an ordinary variable or for a loop. + # Recursively search in self._vars for the requested variable. + scope = self._vars + globals = [] + for i in range(len(loop_name)): + # If global lookup is on then push the value on the stack. + if ((self._global_vars and global_override != "0") or \ + global_override == "1") and scope.has_key(var) and \ + self.is_ordinary_var(scope[var]): + globals.append(scope[var]) + + # Descent deeper into the hierarchy. + if scope.has_key(loop_name[i]) and scope[loop_name[i]]: + scope = scope[loop_name[i]][loop_pass[i]] + else: + return "" + + if scope.has_key(var): + # Value exists in current loop. + if type(scope[var]) == ListType: + # The requested value is a loop. + # Return total number of its passes. + return len(scope[var]) + else: + return scope[var] + elif globals and \ + ((self._global_vars and global_override != "0") or \ + global_override == "1"): + # Return globally looked up value. + return globals.pop() + else: + # No value found. + if var[0].isupper(): + # This is a loop name. + # Return zero, because the user wants to know number + # of its passes. + return 0 + else: + return "" + + def magic_var(self, var, loop_pass, loop_total): + """ Resolve and return value of a magic variable. + Raise an exception if the magic variable is not recognized. + + @hidden + """ + self.DEB("MAGIC: '%s', PASS: %d, TOTAL: %d"\ + % (var, loop_pass, loop_total)) + if var == "__FIRST__": + if loop_pass == 0: + return 1 + else: + return 0 + elif var == "__LAST__": + if loop_pass == loop_total - 1: + return 1 + else: + return 0 + elif var == "__INNER__": + # If this is neither the first nor the last pass. + if loop_pass != 0 and loop_pass != loop_total - 1: + return 1 + else: + return 0 + elif var == "__PASS__": + # Magic variable __PASS__ counts passes from one. + return loop_pass + 1 + elif var == "__PASSTOTAL__": + return loop_total + elif var == "__ODD__": + # Internally pass numbers stored in loop_pass are counted from + # zero. But the template language presents them counted from one. + # Therefore we must add one to the actual loop_pass value to get + # the value we present to the user. + if (loop_pass + 1) % 2 != 0: + return 1 + else: + return 0 + elif var.startswith("__EVERY__"): + # Magic variable __EVERY__x is never true in first or last pass. + if loop_pass != 0 and loop_pass != loop_total - 1: + # Check if an integer follows the variable name. + try: + every = int(var[9:]) # nine is length of "__EVERY__" + except ValueError: + raise TemplateError, "Magic variable __EVERY__x: "\ + "Invalid pass number." + else: + if not every: + raise TemplateError, "Magic variable __EVERY__x: "\ + "Pass number cannot be zero." + elif (loop_pass + 1) % every == 0: + self.DEB("MAGIC: EVERY: " + str(every)) + return 1 + else: + return 0 + else: + return 0 + else: + raise TemplateError, "Invalid magic variable '%s'." % var + + def escape(self, str, override=""): + """ Escape a string either by HTML escaping or by URL escaping. + @hidden + """ + ESCAPE_QUOTES = 1 + if (self._html_escape and override != "NONE" and override != "0" and \ + override != "URL") or override == "HTML" or override == "1": + return cgi.escape(str, ESCAPE_QUOTES) + elif override == "URL": + return urllib.quote_plus(str) + else: + return str + + def is_ordinary_var(self, var): + """ Return true if var is a scalar. (not a reference to loop) + @hidden + """ + if type(var) == StringType or type(var) == IntType or \ + type(var) == LongType or type(var) == FloatType: + return 1 + else: + return 0 + + +############################################## +# CLASS: TemplateCompiler # +############################################## + +class TemplateCompiler: + """ Preprocess, parse, tokenize and compile the template. + + This class parses the template and produces a 'compiled' form + of it. This compiled form is an instance of the Template + class. The compiled form is used as input for the TemplateProcessor + which uses it to actually process the template. + + This class should be used direcly only when you need to compile + a template from a string. If your template is in a file, then you + should use the TemplateManager class which provides + a higher level interface to this class and also can save the + compiled template to disk in a precompiled form. + """ + + def __init__(self, include=1, max_include=5, comments=1, gettext=0, + debug=0): + """ Constructor. + + @header __init__(include=1, max_include=5, comments=1, gettext=0, + debug=0) + + @param include Enable or disable included templates. + @param max_include Maximum depth of nested inclusions. + @param comments Enable or disable template comments. + @param gettext Enable or disable gettext support. + @param debug Enable or disable debugging messages. + """ + + self._include = include + self._max_include = max_include + self._comments = comments + self._gettext = gettext + self._debug = debug + + # This is a list of filenames of all included templates. + # It's modified by the include_templates() method. + self._include_files = [] + + # This is a counter of current inclusion depth. It's used to prevent + # infinite recursive includes. + self._include_level = 0 + + def compile(self, file): + """ Compile template from a file. + + @header compile(file) + @return Compiled template. + The return value is an instance of the Template + class. + + @param file Filename of the template. + See the prepare() method of the TemplateManager + class for exaplanation of this parameter. + """ + + self.DEB("COMPILING FROM FILE: " + file) + self._include_path = os.path.join(os.path.dirname(file), INCLUDE_DIR) + tokens = self.parse(self.read(file)) + compile_params = (self._include, self._max_include, self._comments, + self._gettext) + return Template(__version__, file, self._include_files, + tokens, compile_params, self._debug) + + def compile_string(self, data): + """ Compile template from a string. + + This method compiles a template from a string. The + template cannot include any templates. + TMPL_INCLUDE statements are turned into warnings. + + @header compile_string(data) + @return Compiled template. + The return value is an instance of the Template + class. + + @param data String containing the template data. + """ + self.DEB("COMPILING FROM STRING") + self._include = 0 + tokens = self.parse(data) + compile_params = (self._include, self._max_include, self._comments, + self._gettext) + return Template(__version__, None, None, tokens, compile_params, + self._debug) + + ############################################## + # PRIVATE METHODS # + ############################################## + + def DEB(self, str): + """ Print debugging message to stderr if debugging is enabled. + @hidden + """ + if self._debug: print >> sys.stderr, str + + def read(self, filename): + """ Read content of file and return it. Raise an error if a problem + occurs. + @hidden + """ + self.DEB("READING: " + filename) + try: + f = None + try: + f = open(filename, "r") + data = f.read() + except IOError, (errno, errstr): + raise TemplateError, "IO error while reading template '%s': "\ + "(%d) %s" % (filename, errno, errstr) + else: + return data + finally: + if f: f.close() + + def parse(self, template_data): + """ Parse the template. This method is recursively called from + within the include_templates() method. + + @return List of processing tokens. + @hidden + """ + if self._comments: + self.DEB("PREPROCESS: COMMENTS") + template_data = self.remove_comments(template_data) + tokens = self.tokenize(template_data) + if self._include: + self.DEB("PREPROCESS: INCLUDES") + self.include_templates(tokens) + return tokens + + def remove_comments(self, template_data): + """ Remove comments from the template data. + @hidden + """ + pattern = r"### .*" + return re.sub(pattern, "", template_data) + + def include_templates(self, tokens): + """ Process TMPL_INCLUDE statements. Use the include_level counter + to prevent infinite recursion. Record paths to all included + templates to self._include_files. + @hidden + """ + i = 0 + out = "" # buffer for output + skip_params = 0 + + # Process the list of tokens. + while 1: + if i == len(tokens): break + if skip_params: + skip_params = 0 + i += PARAMS_NUMBER + continue + + token = tokens[i] + if token == "." + self._include_level += 1 + if self._include_level > self._max_include: + # Do not include the template. + # Protection against infinite recursive includes. + skip_params = 1 + self.DEB("INCLUDE: LIMIT REACHED: " + filename) + else: + # Include the template. + skip_params = 0 + include_file = os.path.join(self._include_path, filename) + self._include_files.append(include_file) + include_data = self.read(include_file) + include_tokens = self.parse(include_data) + + # Append the tokens from the included template to actual + # position in the tokens list, replacing the TMPL_INCLUDE + # token and its parameters. + tokens[i:i+PARAMS_NUMBER+1] = include_tokens + i = i + len(include_tokens) + self.DEB("INCLUDED: " + filename) + continue # Do not increment 'i' below. + i += 1 + # end of the main while loop + + if self._include_level > 0: self._include_level -= 1 + return out + + def tokenize(self, template_data): + """ Split the template into tokens separated by template statements. + The statements itself and associated parameters are also + separately included in the resulting list of tokens. + Return list of the tokens. + + @hidden + """ + self.DEB("TOKENIZING TEMPLATE") + # NOTE: The TWO double quotes in character class in the regexp below + # are there only to prevent confusion of syntax highlighter in Emacs. + pattern = r""" + (?:^[ \t]+)? # eat spaces, tabs (opt.) + (< + (?:!--[ ])? # comment start + space (opt.) + /?TMPL_[A-Z]+ # closing slash / (opt.) + statement + [ a-zA-Z0-9""/.=:_\\-]* # this spans also comments ending (--) + >) + [%s]? # eat trailing newline (opt.) + """ % os.linesep + rc = re.compile(pattern, re.VERBOSE | re.MULTILINE) + split = rc.split(template_data) + tokens = [] + for statement in split: + if statement.startswith(" 0 and '=' not in params[0]: + # implicit identifier + name = params[0] + del params[0] + else: + # explicit identifier as a 'NAME' parameter + name = self.find_param("NAME", params) + self.DEB("TOKENIZER: NAME: " + str(name)) + return name + + def find_param(self, param, params): + """ Extract value of parameter from a statement. + @hidden + """ + for pair in params: + name, value = pair.split("=") + if not name or not value: + raise TemplateError, "Syntax error in template." + if name == param: + if value[0] == '"': + # The value is in double quotes. + ret_value = value[1:-1] + else: + # The value is without double quotes. + ret_value = value + self.DEB("TOKENIZER: PARAM: '%s' => '%s'" % (param, ret_value)) + return ret_value + else: + self.DEB("TOKENIZER: PARAM: '%s' => NOT DEFINED" % param) + return None + + +############################################## +# CLASS: Template # +############################################## + +class Template: + """ This class represents a compiled template. + + This class provides storage and methods for the compiled template + and associated metadata. It's serialized by pickle if we need to + save the compiled template to disk in a precompiled form. + + You should never instantiate this class directly. Always use the + TemplateManager or TemplateCompiler classes to + create the instances of this class. + + The only method which you can directly use is the is_uptodate + method. + """ + + def __init__(self, version, file, include_files, tokens, compile_params, + debug=0): + """ Constructor. + @hidden + """ + self._version = version + self._file = file + self._tokens = tokens + self._compile_params = compile_params + self._debug = debug + self._mtime = None + self._include_mtimes = {} + + if not file: + self.DEB("TEMPLATE WAS COMPILED FROM A STRING") + return + + # Save modifitcation time of the main template file. + if os.path.isfile(file): + self._mtime = os.path.getmtime(file) + else: + raise TemplateError, "Template: file does not exist: '%s'" % file + + # Save modificaton times of all included template files. + for inc_file in include_files: + if os.path.isfile(inc_file): + self._include_mtimes[inc_file] = os.path.getmtime(inc_file) + else: + raise TemplateError, "Template: file does not exist: '%s'"\ + % inc_file + + self.DEB("NEW TEMPLATE CREATED") + + def is_uptodate(self, compile_params=None): + """ Check whether the compiled template is uptodate. + + Return true if this compiled template is uptodate. + Return false, if the template source file was changed on the + disk since it was compiled. + Works by comparison of modification times. + Also takes modification times of all included templates + into account. + + @header is_uptodate(compile_params=None) + @return True if the template is uptodate, false otherwise. + + @param compile_params Only for internal use. + Do not use this optional parameter. It's intended only for + internal use by the TemplateManager. + """ + if not self._file: + self.DEB("TEMPLATE COMPILED FROM A STRING") + return 0 + + if self._version != __version__: + self.DEB("TEMPLATE: VERSION NOT UPTODATE") + return 0 + + if compile_params != None and compile_params != self._compile_params: + self.DEB("TEMPLATE: DIFFERENT COMPILATION PARAMS") + return 0 + + # Check modification times of the main template and all included + # templates. If the included template no longer exists, then + # the problem will be resolved when the template is recompiled. + + # Main template file. + if not (os.path.isfile(self._file) and \ + self._mtime == os.path.getmtime(self._file)): + self.DEB("TEMPLATE: NOT UPTODATE: " + self._file) + return 0 + + # Included templates. + for inc_file in self._include_mtimes.keys(): + if not (os.path.isfile(inc_file) and \ + self._include_mtimes[inc_file] == \ + os.path.getmtime(inc_file)): + self.DEB("TEMPLATE: NOT UPTODATE: " + inc_file) + return 0 + else: + self.DEB("TEMPLATE: UPTODATE") + return 1 + + def tokens(self): + """ Get tokens of this template. + @hidden + """ + return self._tokens + + def file(self): + """ Get filename of the main file of this template. + @hidden + """ + return self._file + + def debug(self, debug): + """ Get debugging state. + @hidden + """ + self._debug = debug + + ############################################## + # PRIVATE METHODS # + ############################################## + + def __getstate__(self): + """ Used by pickle when the class is serialized. + Remove the 'debug' attribute before serialization. + @hidden + """ + dict = copy.copy(self.__dict__) + del dict["_debug"] + return dict + + def __setstate__(self, dict): + """ Used by pickle when the class is unserialized. + Add the 'debug' attribute. + @hidden + """ + dict["_debug"] = 0 + self.__dict__ = dict + + + def DEB(self, str): + """ Print debugging message to stderr. + @hidden + """ + if self._debug: print >> sys.stderr, str + + +############################################## +# EXCEPTIONS # +############################################## + +class TemplateError(Exception): + """ Fatal exception. Raised on runtime or template syntax errors. + + This exception is raised when a runtime error occurs or when a syntax + error in the template is found. It has one parameter which always + is a string containing a description of the error. + + All potential IOError exceptions are handled by the module and are + converted to TemplateError exceptions. That means you should catch the + TemplateError exception if there is a possibility that for example + the template file will not be accesssible. + + The exception can be raised by constructors or by any method of any + class. + + The instance is no longer usable when this exception is raised. + """ + + def __init__(self, error): + """ Constructor. + @hidden + """ + Exception.__init__(self, "Htmltmpl error: " + error) + + +class PrecompiledError(Exception): + """ This exception is _PRIVATE_ and non fatal. + @hidden + """ + + def __init__(self, template): + """ Constructor. + @hidden + """ + Exception.__init__(self, template) + diff --git a/planet/shell/__init__.py b/planet/shell/__init__.py new file mode 100644 index 0000000..ba8ea78 --- /dev/null +++ b/planet/shell/__init__.py @@ -0,0 +1,23 @@ +import planet +import os + +def run(template_file, doc): + """ select a template module based on file extension and execute it """ + log = planet.getLogger(planet.config.log_level()) + + for template_dir in planet.config.template_directories(): + template_resolved = os.path.join(template_dir, template_file) + if os.path.exists(template_resolved): break + else: + return log.error("Unable to locate template %s", template_file) + + base,ext = os.path.splitext(os.path.basename(template_resolved)) + try: + module = __import__('planet/shell/' + ext[1:]) + except: + return log.error("Skipping template %s", template_resolved) + + log.info("Processing template %s", template_resolved) + output_dir = planet.config.output_dir() + output_file = os.path.join(output_dir, base) + module.run(template_resolved, doc, output_file) diff --git a/planet/shell/tmpl.py b/planet/shell/tmpl.py new file mode 100644 index 0000000..d36a113 --- /dev/null +++ b/planet/shell/tmpl.py @@ -0,0 +1,190 @@ +from xml.sax.saxutils import escape +import sgmllib, time, os, sys +from planet import config, feedparser, htmltmpl + +class stripHtml(sgmllib.SGMLParser): + "remove all tags from the data" + def __init__(self, data): + sgmllib.SGMLParser.__init__(self) + self.result='' + if isinstance(data, str): + try: + self.feed(data.decode('utf-8')) + except: + self.feed(data) + else: + self.feed(data) + self.close() + def __str__(self): + if isinstance(self.result, unicode): + return self.result.encode('utf-8') + return self.result + def handle_entityref(self, ref): + import htmlentitydefs + if ref in htmlentitydefs.entitydefs: + ref=htmlentitydefs.entitydefs[ref] + if len(ref)==1: + self.result+=unichr(ord(ref)) + elif ref.startswith('&#') and ref.endswith(';'): + self.handle_charref(ref[2:-1]) + else: + self.result+='&%s;' % ref + else: + self.result+='&%s;' % ref + def handle_charref(self, ref): + try: + if ref.startswith('x'): + self.result+=unichr(int(ref[1:],16)) + else: + self.result+=unichr(int(ref)) + except: + self.result+='&#%s;' % ref + def handle_data(self, data): + if data: self.result+=data + +# Data format mappers + +def String(value): + if isinstance(value, unicode): return value.encode('utf-8') + return value + +def Plain(value): + return str(stripHtml(value)) + +def PlanetDate(value): + return time.strftime(config.date_format(), value) + +def Rfc822(value): + return time.strftime("%a, %d %b %Y %H:%M:%S +0000", value) + +def Rfc3399(value): + return time.strftime("%Y-%m-%dT%H:%M:%S+00:00", value) + +# Map from FeedParser path to Planet tmpl names +Base = [ + ['author', String, 'author'], + ['author_name', String, 'author_detail', 'name'], + ['feed', String, 'links', {'rel':'self'}, 'href'], + ['generator', String, 'generator'], + ['id', String, 'id'], + ['icon', String, 'icon'], + ['last_updated_822', Rfc822, 'updated_parsed'], + ['last_updated_iso', Rfc3399, 'updated_parsed'], + ['last_updated', PlanetDate, 'updated_parsed'], + ['logo', String, 'logo'], + ['rights', String, 'rights_detail', 'value'], + ['subtitle', String, 'subtitle_detail', 'value'], + ['title', String, 'title_detail', 'value'], + ['title_plain', Plain, 'title_detail', 'value'], +] + +# ? new_date, new_channel +Items = [ + ['author', String, 'author'], + ['author_email', String, 'author_detail', 'email'], + ['author_name', String, 'author_detail', 'name'], + ['author_uri', String, 'author_detail', 'href'], + ['content_language', String, 'content', 0, 'language'], + ['content', String, 'summary_detail', 'value'], + ['content', String, 'content', 0, 'value'], + ['date', PlanetDate, 'published_parsed'], + ['date', PlanetDate, 'updated_parsed'], + ['date_822', Rfc822, 'published_parsed'], + ['date_822', Rfc822, 'updated_parsed'], + ['date_iso', Rfc3399, 'published_parsed'], + ['date_iso', Rfc3399, 'updated_parsed'], + ['id', String, 'id'], + ['link', String, 'links', {'rel': 'alternate'}, 'href'], + ['rights', String, 'rights_detail', 'value'], + ['title_language', String, 'title_detail', 'language'], + ['title_plain', Plain, 'title_detail', 'value'], + ['title', String, 'title_detail', 'value'], + ['summary_language', String, 'summary_detail', 'language'], + ['updated', PlanetDate, 'updated_parsed'], + ['updated_822', Rfc822, 'updated_parsed'], + ['updated_iso', Rfc3399, 'updated_parsed'], + ['published', PlanetDate, 'published_parsed'], + ['published_822', Rfc822, 'published_parsed'], + ['published_iso', Rfc3399, 'published_parsed'], +] + +Channels = [ + ['url', None], + ['link', None], + ['message', None], + ['title_plain', None], + ['name', None], +] + +# Add additional rules for source information +for rule in Base: + Items.append(['channel_'+rule[0], rule[1], 'source'] + rule[2:]) + +def tmpl_mapper(source, rules): + "Apply specified rules to the source, and return a template dictionary" + output = {} + + for rule in rules: + node = source + for path in rule[2:]: + if isinstance(path, str) and path in node: + if path == 'value' and node.get('type','')=='text/plain': + node['value'] = escape(node['value']) + node['type'] = 'text/html' + node = node[path] + elif isinstance(path, int): + node = node[path] + elif isinstance(path, dict): + for test in node: + for key, value in path.items(): + if test.get(key,None) != value: break + else: + node = test + break + else: + break + else: + break + else: + if node: output[rule[0]] = rule[1](node) + + # copy over all planet namespaced elements from parent source + for name,value in source.items(): + if name.startswith('planet_'): + output[name[7:]] = String(value) + + # copy over all planet namespaced elements from child source element + if 'source' in source: + for name,value in source.source.items(): + if name.startswith('planet_'): + output['channel_' + name[7:]] = String(value) + + return output + +def template_info(source): + """ get template information from a feedparser output """ + data=feedparser.parse(source) + output = {'Channels': [], 'Items': []} + output['Channels'].append(tmpl_mapper(data.feed, Base)) + for entry in data.entries: + output['Items'].append(tmpl_mapper(entry, Items)) + return output + +def run(script, doc, output_file=None): + """ process an HTMLTMPL file """ + manager = htmltmpl.TemplateManager() + template = manager.prepare(script) + tp = htmltmpl.TemplateProcessor(html_escape=0) + for key,value in template_info(doc).items(): + tp.set(key, value) + output = open(output_file, "w") + output.write(tp.process(template)) + output.close() + +if __name__ == '__main__': + sys.path.insert(0, os.path.split(sys.path[0])[0]) + + for test in sys.argv[1:]: + from pprint import pprint + pprint(template_info('/home/rubys/bzr/venus/tests/data/filter/tmpl/'+test)) + diff --git a/planet/shell/xslt.py b/planet/shell/xslt.py new file mode 100644 index 0000000..6757d8f --- /dev/null +++ b/planet/shell/xslt.py @@ -0,0 +1,34 @@ +import os + +def run(script, doc, output_file=None): + """ process an XSLT stylesheet """ + + try: + # if available, use the python interface to libxslt + import libxml2 + import libxslt + dom = libxml2.parseDoc(doc) + docfile = None + except: + # otherwise, use the command line interface + dom = None + import warnings + warnings.simplefilter('ignore', RuntimeWarning) + docfile = os.tmpnam() + file = open(docfile,'w') + file.write(doc) + file.close() + + # do it + if dom: + styledoc = libxml2.parseFile(script) + style = libxslt.parseStylesheetDoc(styledoc) + result = style.applyStylesheet(dom, None) + style.saveResultToFilename(output_file, result, 0) + style.freeStylesheet() + result.freeDoc() + else: + os.system('xsltproc %s %s > %s' % (script, docfile, output_file)) + + if dom: dom.freeDoc() + if docfile: os.unlink(docfile) diff --git a/planet/splice.py b/planet/splice.py index 815326a..f2ec5e8 100644 --- a/planet/splice.py +++ b/planet/splice.py @@ -1,7 +1,7 @@ """ Splice together a planet from a cache of feed entries """ import glob, os, time, shutil from xml.dom import minidom -import planet, config, feedparser, reconstitute +import planet, config, feedparser, reconstitute, shell from reconstitute import createTextElement, date from spider import filename @@ -57,53 +57,9 @@ def apply(doc): if not os.path.exists(output_dir): os.makedirs(output_dir) log = planet.getLogger(config.log_level()) - try: - # if available, use the python interface to libxslt - import libxml2 - import libxslt - dom = libxml2.parseDoc(doc) - docfile = None - except: - # otherwise, use the command line interface - dom = None - import warnings - warnings.simplefilter('ignore', RuntimeWarning) - docfile = os.tmpnam() - file = open(docfile,'w') - file.write(doc) - file.close() - # Go-go-gadget-template for template_file in config.template_files(): - for template_dir in config.template_directories(): - template_resolved = os.path.join(template_dir, template_file) - if os.path.exists(template_resolved): break - else: - log.error("Unable to locate template %s", template_file) - continue - - base,ext = os.path.splitext(os.path.basename(template_resolved)) - if ext != '.xslt': - log.warning("Skipping template %s", template_resolved) - continue - - log.info("Processing template %s", template_resolved) - output_file = os.path.join(output_dir, base) - if dom: - styledoc = libxml2.parseFile(template_resolved) - style = libxslt.parseStylesheetDoc(styledoc) - result = style.applyStylesheet(dom, None) - log.info("Writing %s", output_file) - style.saveResultToFilename(output_file, result, 0) - style.freeStylesheet() - result.freeDoc() - else: - log.info("Writing %s", output_file) - os.system('xsltproc %s %s > %s' % - (template_resolved, docfile, output_file)) - - if dom: dom.freeDoc() - if docfile: os.unlink(docfile) + shell.run(template_file, doc) # Process bill of materials for copy_file in config.bill_of_materials(): diff --git a/tests/data/filter/tmpl/author_email.xml b/tests/data/filter/tmpl/author_email.xml new file mode 100644 index 0000000..bdbda8e --- /dev/null +++ b/tests/data/filter/tmpl/author_email.xml @@ -0,0 +1,13 @@ + + + + + + john@example.com + + + + diff --git a/tests/data/filter/tmpl/author_name.xml b/tests/data/filter/tmpl/author_name.xml new file mode 100644 index 0000000..cb1ddb4 --- /dev/null +++ b/tests/data/filter/tmpl/author_name.xml @@ -0,0 +1,13 @@ + + + + + + John Doe + + + + diff --git a/tests/data/filter/tmpl/author_uri.xml b/tests/data/filter/tmpl/author_uri.xml new file mode 100644 index 0000000..d88d30d --- /dev/null +++ b/tests/data/filter/tmpl/author_uri.xml @@ -0,0 +1,13 @@ + + + + + + http://example.com/~john/ + + + + diff --git a/tests/data/filter/tmpl/content_html.xml b/tests/data/filter/tmpl/content_html.xml new file mode 100644 index 0000000..0ba4ac2 --- /dev/null +++ b/tests/data/filter/tmpl/content_html.xml @@ -0,0 +1,10 @@ + + + + + D&eacute;tente + + diff --git a/tests/data/filter/tmpl/content_lang.xml b/tests/data/filter/tmpl/content_lang.xml new file mode 100644 index 0000000..4b41832 --- /dev/null +++ b/tests/data/filter/tmpl/content_lang.xml @@ -0,0 +1,10 @@ + + + + + foo + + diff --git a/tests/data/filter/tmpl/content_text.xml b/tests/data/filter/tmpl/content_text.xml new file mode 100644 index 0000000..ad2cbc9 --- /dev/null +++ b/tests/data/filter/tmpl/content_text.xml @@ -0,0 +1,10 @@ + + + + + AT&T + + diff --git a/tests/data/filter/tmpl/content_xhtml.xml b/tests/data/filter/tmpl/content_xhtml.xml new file mode 100644 index 0000000..b20521b --- /dev/null +++ b/tests/data/filter/tmpl/content_xhtml.xml @@ -0,0 +1,13 @@ + + + + + +
A very bad day
+
+
+
+ diff --git a/tests/data/filter/tmpl/id.xml b/tests/data/filter/tmpl/id.xml new file mode 100644 index 0000000..55da7af --- /dev/null +++ b/tests/data/filter/tmpl/id.xml @@ -0,0 +1,11 @@ + + + + + http://example.com/1 + + + diff --git a/tests/data/filter/tmpl/id_only_content.xml b/tests/data/filter/tmpl/id_only_content.xml new file mode 100644 index 0000000..41f78af --- /dev/null +++ b/tests/data/filter/tmpl/id_only_content.xml @@ -0,0 +1,13 @@ + + + + + + content + + + + diff --git a/tests/data/filter/tmpl/id_only_description.xml b/tests/data/filter/tmpl/id_only_description.xml new file mode 100644 index 0000000..204f992 --- /dev/null +++ b/tests/data/filter/tmpl/id_only_description.xml @@ -0,0 +1,13 @@ + + + + + + description + + + + diff --git a/tests/data/filter/tmpl/id_only_link.xml b/tests/data/filter/tmpl/id_only_link.xml new file mode 100644 index 0000000..ac5bf23 --- /dev/null +++ b/tests/data/filter/tmpl/id_only_link.xml @@ -0,0 +1,13 @@ + + + + + + http://example.com/1 + + + + diff --git a/tests/data/filter/tmpl/id_only_title.xml b/tests/data/filter/tmpl/id_only_title.xml new file mode 100644 index 0000000..4dab3e4 --- /dev/null +++ b/tests/data/filter/tmpl/id_only_title.xml @@ -0,0 +1,13 @@ + + + + + + title + + + + diff --git a/tests/data/filter/tmpl/link_href.xml b/tests/data/filter/tmpl/link_href.xml new file mode 100644 index 0000000..77a8964 --- /dev/null +++ b/tests/data/filter/tmpl/link_href.xml @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/tests/data/filter/tmpl/link_rel.xml b/tests/data/filter/tmpl/link_rel.xml new file mode 100644 index 0000000..77a8964 --- /dev/null +++ b/tests/data/filter/tmpl/link_rel.xml @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/tests/data/filter/tmpl/link_type.xml b/tests/data/filter/tmpl/link_type.xml new file mode 100644 index 0000000..77a8964 --- /dev/null +++ b/tests/data/filter/tmpl/link_type.xml @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/tests/data/filter/tmpl/planet_name.xml b/tests/data/filter/tmpl/planet_name.xml new file mode 100644 index 0000000..564f12b --- /dev/null +++ b/tests/data/filter/tmpl/planet_name.xml @@ -0,0 +1,14 @@ + + + + foo + + + foo + + + + diff --git a/tests/data/filter/tmpl/published.xml b/tests/data/filter/tmpl/published.xml new file mode 100644 index 0000000..b7efb67 --- /dev/null +++ b/tests/data/filter/tmpl/published.xml @@ -0,0 +1,11 @@ + + + + + 2004-02-28T18:14:55-08:00 + + + diff --git a/tests/data/filter/tmpl/rights.xml b/tests/data/filter/tmpl/rights.xml new file mode 100644 index 0000000..8dd99bc --- /dev/null +++ b/tests/data/filter/tmpl/rights.xml @@ -0,0 +1,11 @@ + + + + + &copy; 2006 + + + diff --git a/tests/data/filter/tmpl/source_author.xml b/tests/data/filter/tmpl/source_author.xml new file mode 100644 index 0000000..1cb6a62 --- /dev/null +++ b/tests/data/filter/tmpl/source_author.xml @@ -0,0 +1,18 @@ + + + + + John Doe + + + + + John Doe + + + + + diff --git a/tests/data/filter/tmpl/source_icon.xml b/tests/data/filter/tmpl/source_icon.xml new file mode 100644 index 0000000..ad49609 --- /dev/null +++ b/tests/data/filter/tmpl/source_icon.xml @@ -0,0 +1,14 @@ + + + + http://www.example.com/favicon.ico + + + http://www.example.com/favicon.ico + + + + diff --git a/tests/data/filter/tmpl/source_id.xml b/tests/data/filter/tmpl/source_id.xml new file mode 100644 index 0000000..751f8de --- /dev/null +++ b/tests/data/filter/tmpl/source_id.xml @@ -0,0 +1,14 @@ + + + + http://example.com/ + + + http://example.com/ + + + + diff --git a/tests/data/filter/tmpl/source_logo.xml b/tests/data/filter/tmpl/source_logo.xml new file mode 100644 index 0000000..d56f44b --- /dev/null +++ b/tests/data/filter/tmpl/source_logo.xml @@ -0,0 +1,14 @@ + + + + http://www.example.com/logo.jpg + + + http://www.example.com/logo.jpg + + + + diff --git a/tests/data/filter/tmpl/source_planet_name.xml b/tests/data/filter/tmpl/source_planet_name.xml new file mode 100644 index 0000000..e21b24c --- /dev/null +++ b/tests/data/filter/tmpl/source_planet_name.xml @@ -0,0 +1,14 @@ + + + + foo + + + foo + + + + diff --git a/tests/data/filter/tmpl/source_rights.xml b/tests/data/filter/tmpl/source_rights.xml new file mode 100644 index 0000000..0da67d9 --- /dev/null +++ b/tests/data/filter/tmpl/source_rights.xml @@ -0,0 +1,14 @@ + + + + &copy; 2006 + + + &copy; 2006 + + + + diff --git a/tests/data/filter/tmpl/source_subtitle.xml b/tests/data/filter/tmpl/source_subtitle.xml new file mode 100644 index 0000000..ace4902 --- /dev/null +++ b/tests/data/filter/tmpl/source_subtitle.xml @@ -0,0 +1,14 @@ + + + + snarky phrase + + + snarky phrase + + + + diff --git a/tests/data/filter/tmpl/source_title.xml b/tests/data/filter/tmpl/source_title.xml new file mode 100644 index 0000000..2e5ebd3 --- /dev/null +++ b/tests/data/filter/tmpl/source_title.xml @@ -0,0 +1,14 @@ + + + + visible name + + + visible name + + + + diff --git a/tests/data/filter/tmpl/source_updated.xml b/tests/data/filter/tmpl/source_updated.xml new file mode 100644 index 0000000..225b003 --- /dev/null +++ b/tests/data/filter/tmpl/source_updated.xml @@ -0,0 +1,14 @@ + + + + 2004-02-28T18:14:55-08:00 + + + 2004-02-28T18:14:55-08:00 + + + + diff --git a/tests/data/filter/tmpl/summary_html.xml b/tests/data/filter/tmpl/summary_html.xml new file mode 100644 index 0000000..f335c17 --- /dev/null +++ b/tests/data/filter/tmpl/summary_html.xml @@ -0,0 +1,10 @@ + + + + + D&eacute;tente + + diff --git a/tests/data/filter/tmpl/summary_lang.xml b/tests/data/filter/tmpl/summary_lang.xml new file mode 100644 index 0000000..eb30b5c --- /dev/null +++ b/tests/data/filter/tmpl/summary_lang.xml @@ -0,0 +1,10 @@ + + + + + foo + + diff --git a/tests/data/filter/tmpl/summary_text.xml b/tests/data/filter/tmpl/summary_text.xml new file mode 100644 index 0000000..0984402 --- /dev/null +++ b/tests/data/filter/tmpl/summary_text.xml @@ -0,0 +1,10 @@ + + + + + AT&T + + diff --git a/tests/data/filter/tmpl/summary_xhtml.xml b/tests/data/filter/tmpl/summary_xhtml.xml new file mode 100644 index 0000000..02c9db3 --- /dev/null +++ b/tests/data/filter/tmpl/summary_xhtml.xml @@ -0,0 +1,13 @@ + + + + + +
A very bad day
+
+
+
+ diff --git a/tests/data/filter/tmpl/title_html.xml b/tests/data/filter/tmpl/title_html.xml new file mode 100644 index 0000000..544ed38 --- /dev/null +++ b/tests/data/filter/tmpl/title_html.xml @@ -0,0 +1,10 @@ + + + + + D&eacute;tente + + diff --git a/tests/data/filter/tmpl/title_lang.xml b/tests/data/filter/tmpl/title_lang.xml new file mode 100644 index 0000000..325b913 --- /dev/null +++ b/tests/data/filter/tmpl/title_lang.xml @@ -0,0 +1,10 @@ + + + + + foo + + diff --git a/tests/data/filter/tmpl/title_text.xml b/tests/data/filter/tmpl/title_text.xml new file mode 100644 index 0000000..cec48b2 --- /dev/null +++ b/tests/data/filter/tmpl/title_text.xml @@ -0,0 +1,10 @@ + + + + + AT&T + + diff --git a/tests/data/filter/tmpl/title_xhtml.xml b/tests/data/filter/tmpl/title_xhtml.xml new file mode 100644 index 0000000..dae2191 --- /dev/null +++ b/tests/data/filter/tmpl/title_xhtml.xml @@ -0,0 +1,13 @@ + + + + + + <div xmlns="http://www.w3.org/1999/xhtml">A <b>very</b> bad day</div> + + + + diff --git a/tests/data/filter/tmpl/updated.xml b/tests/data/filter/tmpl/updated.xml new file mode 100644 index 0000000..1230e7c --- /dev/null +++ b/tests/data/filter/tmpl/updated.xml @@ -0,0 +1,11 @@ + + + + + 2004-02-28T18:14:55-08:00 + + + diff --git a/tests/test_filter_tmpl.py b/tests/test_filter_tmpl.py new file mode 100644 index 0000000..26e090c --- /dev/null +++ b/tests/test_filter_tmpl.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python + +import unittest, os, sys, glob, new, re, StringIO, time +from planet.shell import tmpl + +testfiles = 'tests/data/filter/tmpl/%s.xml' + +class FilterTmplTest(unittest.TestCase): + desc_re = re.compile("Description:\s*(.*?)\s*Expect:\s*(.*)\s*-->") + simple_re = re.compile("^(\S+) == (u?'[^']*'|\([0-9, ]+\))$") + + def eval(self, name): + # read the test case + try: + testcase = open(testfiles % name) + data = testcase.read() + description, expect = self.desc_re.search(data).groups() + testcase.close() + except: + raise RuntimeError, "can't parse %s" % name + + # map to template info + results = tmpl.template_info(data) + + # verify the results + if not self.simple_re.match(expect): + self.assertTrue(eval(expect, results), expect) + else: + lhs, rhs = self.simple_re.match(expect).groups() + self.assertEqual(eval(rhs), eval(lhs, results)) + +# build a test method for each test file +for testcase in glob.glob(testfiles % '*'): + root = os.path.splitext(os.path.basename(testcase))[0] + func = lambda self, name=root: self.eval(name) + method = new.instancemethod(func, None, FilterTmplTest) + setattr(FilterTmplTest, "test_" + root, method) diff --git a/themes/asf/config.ini b/themes/asf/config.ini index 10f6a22..4a883e1 100644 --- a/themes/asf/config.ini +++ b/themes/asf/config.ini @@ -4,6 +4,7 @@ [Planet] template_files: atom.xml.xslt + rss20.xml.tmpl foafroll.xml.xslt index.html.xslt opml.xml.xslt diff --git a/themes/common/rss20.xml.tmpl b/themes/common/rss20.xml.tmpl new file mode 100644 index 0000000..81cbffb --- /dev/null +++ b/themes/common/rss20.xml.tmpl @@ -0,0 +1,30 @@ + + + + + <TMPL_VAR name> + + en + - + + + + <TMPL_VAR channel_name ESCAPE="HTML"><TMPL_IF title>: <TMPL_VAR title_plain ESCAPE="HTML"></TMPL_IF> + + + + + + + + + () + + + + + + + + +