"""template.py: Template-related objects for PyStyler
$Revision: 1.40 $ $Date: 2010/09/17 22:35:52 $
Exports:
class TmplPool: Pool of previously parsed templates (singleton)
class TToken: Abstract class for tokens within a template
Classes derived from TToken include:
LiteralToken NextURLToken
AuthorToken PrevTitleToken
BodyToken PrevURLToken
ElseToken SeeToken
EndifToken TitleToken
IfNextToken UpdateToken
IfPrevToken UpTitleToken
IfSeeToken UpURLToken
IfUpToken URLToken
NextTitleToken
Note: this class and all those derived from it must appear
before the Template class because Template.formatScanners
refers to these class constructors.
class Template: Represents one template file
class OpenCond: Tracks conditionals during file parsing
class IfStack: Tracks conditionals during expansion
class Condition: Tracks one conditional during expansion
"""
import string, sys
from log import * # Standard file parsing/error logging stuff
from scan import *
from cset import *
import pathinfo # Takes a snapshot of file status info
from body import * # Needed only for the Target() constructor
#================================================================
# Manifest constants
#----------------------------------------------------------------
DEFAULT_TEMPLATE = "Template" # Default template file
START_FORMAT = "%" # The characters that start...
END_FORMAT = ";" # ...and end a format code
FORMAT_NAME_CSET = letters # Allowable chars. in format code names
#================================================================
# Verification functions
#----------------------------------------------------------------
# eff-name(name) ==
# if name is None -> DEFAULT_TEMPLATE
# else -> name
#----------------------------------------------------------------
# - - - - - c l a s s T m p l P o o l - - - - -
def TmplPool ( defTemplate=None ):
"""Singleton wrapper for _TmplPool
"""
if not _TmplPool.instance:
_TmplPool.instance = _TmplPool ( defTemplate )
return _TmplPool.instance
class _TmplPool:
"""Represents a pool of previously parsed template files.
Exports:
_TmplPool(defTemplate):
[ if (defTemplate is a string or None) ->
return a new, empty TmplPool object with defTemplate as its
default template name, defaulting to DEFAULT_TEMPLATE ]
.__getitem__(self, name):
[ if (name is a string or None) ->
if self contains a Template for file eff-name(name) ->
return that Template
else if eff_name(name) names a readable, valid template
file ->
self := self + a Template object representing that file
return that Template object
else ->
Log() +:= error message(s)
raise IOError ]
State/Invariants:
.__templateMap:
[ a dictionary mapping K_i |-> V_i such that for every
template V_i contained in self, K_i is that template's
file name ]
"""
instance = None # Singleton instance, when instantiated
# - - - T m p l P o o l . _ _ i n i t _ _ - - -
def __init__ ( self, defTemplate=None ):
"""Constructor for a TmplPool object
"""
self.__templateMap = {}
if defTemplate is None:
self.defTemplate = DEFAULT_TEMPLATE
else:
self.defTemplate = defTemplate
# - - - T m p l P o o l . _ _ g e t i t e m _ _ - - -
def __getitem__ ( self, key ):
"""Given a file name, retrieves it as a Template, parsing if necessary
"""
#-- 1 --
if key is None: fileName = self.defTemplate
else: fileName = key
#-- 2 --
# [ if self.templateMap has a key (fileName) ->
# return the corresponding value
# else -> I ]
try:
return self.__templateMap[fileName]
except KeyError:
pass
#-- 3 --
# [ if fileName names a readable, valid template file ->
# result := a Template object representing that file
# else -> raise IOError ]
result = Template ( fileName )
#-- 4 --
self.__templateMap[fileName] = result
#-- 5 --
return result
# - - - - - c l a s s T T o k e n - - - - -
class TToken:
"""Abstract class for all template atoms (tokens)
There are two kinds of classes that inherit from TToken:
- Each format code (e.g., "%ifnext;") has its own class.
- Text other than format codes is represented by a LiteralToken
In each class, the constructor is called during scanning of the
template file, and checks validity of the construct (e.g., that
an "%else;" matches an open "%if...;"), and the .expand()
method is called during expansion of a body file to generate
the actual HTML.
Exports:
TToken ( openCondStack, scan ):
[ if (openCondStack is a stack of OpenCond objects
representing %if-constructs not yet matched by "%endif;")
and (scan is a Scan object) ->
if this token is valid in the context of openCondStack ->
openCondStack := openCondStack adjusted for the token,
with a new OpenCond pushed containing scan.lineNo
if the token is an %if-construct
else ->
raise SyntaxError ]
.writeLink ( outFile, fromTopic, toTarget ):
[ if (outFile is a writeable file handle)
and (fromTopic is a Topic object)
and (toTarget is a Target object) ->
outFile +:= an HTML link that will point from
fromTopic's .html file to the target
represented by toTarget ]
.writeStdLink ( outFile, fromTopic, toTarget ):
[ if (outFile is a writeable file handle)
and (fromTopic is a Topic object)
and (toTarget is a Target object) ->
outFile +:= an HTML ... construct whose tag
will point from fromTopic's .html file to the target
represented by toTarget and whose link text is the
title of toTarget's topic ]
Virtual methods:
.expand ( ifStack, body, outFile ):
[ if (ifStack is an IfStack object representing the current
nesting of %if-constructs)
and (body is a Body object representing the context in which
the token is to be expanded)
and (outFile is a writeable file handle) ->
if self is an invalid conditional expansion token in
the context of ifStack ->
raise SyntaxError
else if self is a valid conditional expansion token
in the context of ifStack ->
ifStack := ifStack adjusted to account for processing self
else if ifStack is skipping -> I
else ->
outFile +:= the expansion of self in the context of body ]
"""
# - - - T T o k e n . _ _ i n i t _ _ - - -
def __init__ ( self, openCondStack, scan ):
pass
# - - - T T o k e n . w r i t e L i n k - - -
def writeLink ( self, outFile, fromTopic, toTarget ):
"""Create an link from `fromTopic' to `toTarget'
"""
outFile.write ( '' %
fromTopic.relPath ( toTarget ) )
# - - - T T o k e n . w r i t e S t d L i n k - - -
def writeStdLink ( self, outFile, fromTopic, toTarget ):
"""Write an ... construct linking to `toTarget'
"""
#-- 1 --
# [ outFile +:= an construct that will point from
# fromTopic's .html file to the target represented by toTarget ]
self.writeLink ( outFile, fromTopic, toTarget )
#-- 2 --
# [ outFile +:= (title of toTarget's topic) + "" ]
outFile.write ( "%s" % toTarget.topic.title )
# - - - - - c l a s s L i t e r a l T o k e n - - - - -
class LiteralToken(TToken):
"""Represents text from a template containing no format codes.
Exports:
LiteralToken ( openCondStack, scan, text ):
[ if (openCondStack and scan are as in the base class)
and (text is a string) ->
return a new LiteralToken representing text ]
State/Invariants:
.text: [ as passed to the constructor ]
"""
# - - - L i t e r a l T o k e n . _ _ i n i t _ _ - - -
def __init__ ( self, openCondStack, scan, text ):
"""Constructor for a LiteralToken
"""
TToken.__init__ ( self, openCondStack, scan )
self.text = text
# - - - L i t e r a l T o k e n . e x p a n d - - -
def expand ( self, ifStack, body, outFile ):
"""Expand a literal token by adding self.text to outFile
"""
if not ifStack.isSkipping():
outFile.write ( self.text )
# - - - - - c l a s s A u t h o r T o k e n - - - - -
class AuthorToken(TToken):
"""Represents an "%author;" format code
"""
def expand ( self, ifStack, body, outFile ):
"""Expand the "%author;" format code
"""
if ( ( not ifStack.isSkipping() ) and
( body.author ) ):
outFile.write ( body.author )
# - - - - - c l a s s B o d y T o k e n - - - - -
class BodyToken(TToken):
"""Represents a "%body;" format code, which expands to the body content
"""
def expand ( self, ifStack, body, outFile ):
"""Expand the "%body;" format code
"""
if not ifStack.isSkipping():
outFile.writelines ( body.text )
# - - - - - c l a s s E l s e T o k e n - - - - -
class ElseToken(TToken):
"""Represents an "%else;" format code
"""
def __init__ ( self, openCondStack, scan ):
"""Constructor for the "%else;" format code
"""
#-- 1 --
# [ call parent constructor ]
TToken.__init__ ( self, openCondStack, scan )
#-- 2 --
# [ if openCondStack is empty ->
# raise SyntaxError
# else -> I ]
if len ( openCondStack ) == 0:
raise SyntaxError, "This `%else;' has no matching if"
#-- 3 --
# [ if the top element of openCondStack has already
# seen an "%else;" ->
# raise SyntaxError
# else ->
# openCondStack := openCondStack with its top element's
# skipping state inverted and the
# passage of an "%else;" recorded ]
topCond = openCondStack[-1]
topCond.elseCheck()
# - - - E l s e T o k e n . e x p a n d - - -
def expand ( self, ifStack, body, outFile ):
"""Expand the `%endif;' token
"""
#-- 1 --
# [ if ifStack is empty ->
# raise SyntaxError
# else if ifStack's top element has seen an else ->
# raise SyntaxError
# else ->
# ifStack := ifStack with the top Condition object's
# skipping state inverted ]
ifStack.elseCheck()
# - - - - - c l a s s E n d i f T o k e n - - - - -
class EndifToken(TToken):
"""Represents an "%endif;" token
"""
# - - - E n d i f T o k e n . _ _ i n i t _ _ - - -
def __init__ ( self, openCondStack, scan ):
"""Constructor for the "%endif;" format code
"""
#-- 1 --
# [ call parent constructor ]
TToken.__init__ ( self, openCondStack, scan )
#-- 2 --
# [ if openCondStack is empty ->
# raise SyntaxError
# else ->
# openCondStack := openCondStack with its top element popped ]
if len(openCondStack) == 0:
raise SyntaxError, "This `%endif;' has no matching `%if...;'"
else:
del openCondStack[-1]
# - - - E n d i f T o k e n . e x p a n d - - -
def expand ( self, ifStack, body, outFile ):
"""Expand the `%endif;' token
"""
ifStack.end()
# - - - - - c l a s s I f N e x t T o k e n - - - - -
class IfNextToken(TToken):
"""Represents the `%ifnext;' token
"""
# - - - I f N e x t T o k e n . _ _ i n i t _ _ - - -
def __init__ ( self, openCondStack, scan ):
"""Constructor for the `%ifnext;' token
"""
#-- 1 --
# [ call parent constructor ]
TToken.__init__ ( self, openCondStack, scan )
#-- 2 --
# [ openCondStack := openCondStack with a new OpenCond pushed
# that records scan.lineNo ]
newCond = OpenCond ( scan.lineNo, "ifnext" )
openCondStack.append ( newCond )
# - - - I f N e x t T o k e n . e x p a n d - - -
def expand ( self, ifStack, body, outFile ):
"""Expand the `%ifnext;' token
"""
#-- 1 --
# [ if body has an effective next topic ->
# skipping := 0
# else ->
# skipping := 1 ]
if body.enext is None: skipping = 1
else: skipping = 0
#-- 2 --
# [ ifStack := ifStack with a new Condition object pushed,
# whose skipping attribute is (skipping) ]
ifStack.start ( Condition ( skipping ) )
# - - - - - c l a s s I f P r e v T o k e n - - - - -
class IfPrevToken(TToken):
"""Represents the `%ifprev;' token
"""
# - - - I f P r e v T o k e n . _ _ i n i t _ _ - - -
def __init__ ( self, openCondStack, scan ):
"""Constructor for the `%ifprev;' token
"""
#-- 1 --
# [ call parent constructor ]
TToken.__init__ ( self, openCondStack, scan )
#-- 2 --
# [ openCondStack := openCondStack with a new OpenCond pushed
# that records scan.lineNo ]
newCond = OpenCond ( scan.lineNo, "ifprev" )
openCondStack.append ( newCond )
# - - - I f P r e v T o k e n . e x p a n d - - -
def expand ( self, ifStack, body, outFile ):
"""Expand the `%ifprev;' token
"""
#-- 1 --
# [ if body has an effective previous topic ->
# skipping := 0
# else ->
# skipping := 1 ]
if body.eprev is None: skipping = 1
else: skipping = 0
#-- 2 --
# [ ifStack := ifStack with a new Condition object pushed,
# whose skipping attribute is (skipping) ]
ifStack.start ( Condition ( skipping ) )
# - - - - - c l a s s I f S e e T o k e n - - - - -
class IfSeeToken(TToken):
"""Represents the `%ifsee;' token
"""
# - - - I f S e e T o k e n . _ _ i n i t _ _ - - -
def __init__ ( self, openCondStack, scan ):
"""Constructor for the `%ifsee;' token
"""
#-- 1 --
# [ call parent constructor ]
TToken.__init__ ( self, openCondStack, scan )
#-- 2 --
# [ openCondStack := openCondStack with a new OpenCond pushed
# that records scan.lineNo ]
newCond = OpenCond ( scan.lineNo, "ifsee" )
openCondStack.append ( newCond )
# - - - I f S e e T o k e n . e x p a n d - - -
def expand ( self, ifStack, body, outFile ):
"""Expand the `%ifsee;' token
"""
#-- 1 --
# [ if body has an effective see-also topic ->
# skipping := 0
# else ->
# skipping := 1 ]
if len(body.seeList) == 0: skipping = 1
else: skipping = 0
#-- 2 --
# [ ifStack := ifStack with a new Condition object pushed,
# whose skipping attribute is (skipping) ]
ifStack.start ( Condition ( skipping ) )
# - - - - - c l a s s I f U p T o k e n - - - - -
class IfUpToken(TToken):
"""Represents the `%ifup;' token
"""
# - - - I f U p T o k e n . _ _ i n i t _ _ - - -
def __init__ ( self, openCondStack, scan ):
"""Constructor for the `%ifup;' token
"""
#-- 1 --
# [ call parent constructor ]
TToken.__init__ ( self, openCondStack, scan )
#-- 2 --
# [ openCondStack := openCondStack with a new OpenCond pushed
# that records scan.lineNo ]
newCond = OpenCond ( scan.lineNo, "ifup" )
openCondStack.append ( newCond )
# - - - I f U p T o k e n . e x p a n d - - -
def expand ( self, ifStack, body, outFile ):
"""Expand the `%ifup;' token
"""
#-- 1 --
# [ if body's topic has a parent topic ->
# skipping := 0
# else ->
# skipping := 1 ]
if body.topic.parent: skipping = 0
else: skipping = 1
#-- 2 --
# [ ifStack := ifStack with a new Condition object pushed,
# whose skipping attribute is (skipping) ]
ifStack.start ( Condition ( skipping ) )
# - - - - - c l a s s N e x t T i t l e T o k e n - - - - -
class NextTitleToken(TToken):
"""Represents the `%nexttitle;' token
"""
# - - - N e x t T i t l e T o k e n . e x p a n d - - -
def expand ( self, ifStack, body, outFile ):
"""Expands the `%nexttitle;' token
"""
# [ if (ifStack is skipping) or (body has no next topic) -> I
# else ->
# outFile +:= title of body's effective next topic ]
if ( ( not ifStack.isSkipping() ) and
( body.enext ) ):
outFile.write ( body.enext.topic.title )
# - - - - - c l a s s N e x t U R L T o k e n - - - - -
class NextURLToken(TToken):
"""Represents the `%nexturl;' token
"""
# - - - N e x t U R L T o k e n . e x p a n d - - -
def expand ( self, ifStack, body, outFile ):
"""Expands the `%nexturl;' token
"""
# [ if (ifStack is skipping) or (body has no next topic) -> I
# else ->
# outFile +:= an tag linking from body to body's
# next topic ]
if ( ( not ifStack.isSkipping() ) and
( body.enext ) ):
self.writeLink ( outFile, body.topic, body.enext )
# - - - - - c l a s s P r e v T i t l e T o k e n - - - - -
class PrevTitleToken(TToken):
"""Represents the `%prevtitle;' token
"""
# - - - P r e v T i t l e T o k e n . e x p a n d - - -
def expand ( self, ifStack, body, outFile ):
"""Expands the `%prevtitle;' token
"""
# [ if (ifStack is skipping) or (body has no previous topic) -> I
# else ->
# outFile +:= title of body's effective previous topic ]
if ( ( not ifStack.isSkipping() ) and
( body.eprev ) ):
outFile.write ( body.eprev.topic.title )
# - - - - - c l a s s P r e v U R L T o k e n - - - - -
class PrevURLToken(TToken):
"""Represents the `%prevurl;' token
"""
# - - - P r e v U R L T o k e n . e x p a n d - - -
def expand ( self, ifStack, body, outFile ):
"""Expands the `%prevurl;' token
"""
# [ if (ifStack is skipping) or (body has no previous topic) -> I
# else ->
# outFile +:= an tag linking from body to body's
# previous topic ]
if ( ( not ifStack.isSkipping() ) and
( body.eprev ) ):
self.writeLink ( outFile, body.topic, body.eprev )
# - - - - - c l a s s S e e T o k e n - - - - -
class SeeToken(TToken):
"""Represents the `%see;' token
"""
# - - - S e e T o k e n . e x p a n d - - -
def expand ( self, ifStack, body, outFile ):
"""Expand the `%see;' token
"""
#-- 1 --
# [ if ifStack is skipping ->
# return
# else -> I ]
if ifStack.isSkipping():
return
#-- 2 --
prefix = ""
#-- 3 --
# [ outFile := outFile + prefix + (a list of ...
# constructs, one per element of body.seeList, where each
# tag links from body to that element, and each link
# text is the title of that element, and the list elements
# are separated by "; " ]
for target in body.seeList:
#-- 3.1 --
# [ outFile +:= prefix
# prefix := "; " ]
outFile.write ( prefix )
prefix = "; "
#-- 3.2 --
# [ outFile +:= an ... construct where
# the tag links from body to target and the
# link text is the title of target ]
self.writeStdLink ( outFile, body.topic, target )
# - - - - - c l a s s T i t l e T o k e n - - - - -
class TitleToken(TToken):
"""Represents a `%title;' token
"""
# - - - T i t l e T o k e n . e x p a n d - - -
def expand ( self, ifStack, body, outFile ):
"""Expands the `%title;' token
"""
# [ if ifStack is skipping -> I
# else ->
# outFile +:= body's title ]
if not ifStack.isSkipping():
outFile.write ( body.topic.title )
# - - - - - c l a s s U p d a t e T o k e n - - - - -
class UpdateToken(TToken):
"""Represents the `%update;' token
"""
# - - - U p d a t e T o k e n . e x p a n d - - -
def expand ( self, ifStack, body, outFile ):
"""Expands the `%update;' token
"""
# [ if (ifStack is skipping) or (body has no update string) -> I
# else ->
# outFile +:= body's update string ]
if ( ( not ifStack.isSkipping() ) and
( body.updated ) ):
outFile.write ( body.updated )
# - - - - - c l a s s U p T i t l e T o k e n - - - - -
class UpTitleToken(TToken):
"""Represents the `%uptitle;' token
"""
# - - - U p T i t l e T o k e n . e x p a n d - - -
def expand ( self, ifStack, body, outFile ):
"""Expands the `%uptitle;' token
"""
# [ if (ifStack is skipping) or (body has no parent topic) -> I
# else ->
# outFile +:= title of body's parent topic ]
if ( ( not ifStack.isSkipping() ) and
( body.topic.parent ) ):
outFile.write ( body.topic.parent.title )
# - - - - - c l a s s U p U R L T o k e n - - - - -
class UpURLToken(TToken):
"""Represents the `%upurl;' token
"""
# - - - U p U R L T o k e n . e x p a n d - - -
def expand ( self, ifStack, body, outFile ):
"""Expands the `%upurl;' token
"""
# [ if (ifStack is skipping) or (body has no parent) -> I
# else ->
# outFile +:= an tag linking from body to body's
# parent topic ]
if ( ( not ifStack.isSkipping() ) and
( body.topic.parent ) ):
self.writeLink ( outFile, body.topic,
Target ( body.topic.parent, None ) )
# - - - - - c l a s s U R L T o k e n - - - - -
class URLToken(TToken):
"""Represents the `%url;' token
"""
# - - - U R L T o k e n . e x p a n d - - -
def expand ( self, ifStack, body, outFile ):
"""Expands the `%url;' token
"""
# [ if ifStack is skipping -> I
# else ->
# outFile +:= body's URL ]
if not ifStack.isSkipping():
outFile.write ( "%s%s.html" %
( body.topic.plan.pathMap.rootURL,
body.topic.path ) )
# - - - - - c l a s s T e m p l a t e - - - - -
class Template:
"""Represents a template file used to expand body files to HTML
Exports:
Template ( fileName, defTemplate=None )
[ if (fileName is a string or file-like object) and
(defTemplate is a string defaulting to None) ->
if fileName names a readable, valid template file,
defaulting to DEFAULT_TEMPLATE ->
return a new Template object representing that file
else ->
Log() +:= error message(s)
raise IOError ]
NB: On 2010-09-17 I discovered that the 'defTemplate' argument and
attribute are never used except that the constructor stores them.
.fileName [ as passed to constructor ]
.modEpoch [ modification timestamp of fileName as epoch time ]
.expand ( body, outFile ):
[ if (body is a Body object)
and (outFile is a writeable file handle) ->
outFile +:= expansion of body using self as a template
Log() +:= errors from expansion, if any ]
State/Invariants:
.__tokenList:
[ a list of TToken objects representing the atoms of self ]
.__openCondStack:
[ a stack of OpenCond objects representing open conditional
constructs during scanning ]
.__ifStack:
[ an IfStack object representing open conditional constructs
during expansion ]
"""
# - - - T e m p l a t e . _ _ i n i t _ _ - - -
def __init__ ( self, fileName, defTemplate=None ):
"""Constructor for a Template
"""
#-- 1 --
self.fileName = fileName
self.defTemplate = defTemplate
self.__tokenList = []
self.__openCondStack = []
self.__ifStack = IfStack()
errCount = Log().count()
#-- 2 --
# [ if self.fileName names a readable, valid template file ->
# self.__tokenList +:= a sequence of TToken objects
# representing the contents of that file
# self.__openCondStack := self.__openCondStack adjusted
# according to conditionals in that file
# else ->
# self.__tokenList +:= valid TTokens, if any
# self.__openCondStack := self.__openCondStack adjusted
# according to valid conditionals in that file
# Log() +:= error message(s) ]
self.__readFile()
#-- 3 --
if errCount < Log().count():
raise IOError, "The template file was not error-free."
# - - - T e m p l a t e . _ _ r e a d F i l e - - -
def __readFile ( self ):
"""Read a template file and store it in self.
[ if self.fileName names a readable, valid template file ->
self.__tokenList +:= a sequence of TToken objects
representing the contents of that file
self.__openCondStack := self.__openCondStack adjusted
according to conditionals in that file
else ->
self.__tokenList +:= valid TTokens, if any
self.__openCondStack := self.__openCondStack adjusted
according to valid conditionals in that file
Log() +:= error message(s) ]
"""
#-- 1 --
# [ if self.fileName names a readable file ->
# scan := a new Scan object pointing to the
# start of that file
# self.modEpoch := modification timestamp (epoch time)
# of the file
# else ->
# Log() +:= error message
# return ]
if isinstance(self.fileName, str):
try:
scan = Scan ( self.fileName )
info = pathinfo.PathInfo(self.fileName)
self.modEpoch = info.modEpoch
except IOError:
Log().error ( "Can't open template file `%s' for "
"reading." % self.fileName )
return
else:
#--
# Patch of 2010-09-17: If self.fileName is a file-like
# object, just use it directly. See sitemap.cgi for
# the application of this option.
#--
scan = Scan ( self.fileName )
self.modeEpoch = 0L
#-- 2 --
# [ if scan contains a valid template file ->
# scan := scan advanced to end of file
# self.__tokenList +:= a sequence of TToken objects
# representing the contents of scan
# self.__openCondStack := self.__openCondStack adjusted
# according to conditionals in scan
# else ->
# scan := scan advanced to end of file
# self.__tokenList +:= valid TTokens from scan, if any
# self.__openCondStack := self.__openCondStack adjusted
# according to valid conditionals in scan
# Log() +:= error message(s) ]
while not scan.atEndFile:
# [ if scan starts with a valid format code ->
# scan := scan advanced past that format code
# self.__tokenList +:= a TToken object representing
# that code
# self.__openCondStack := self.__openCondStack
# adjusted according to that format code
# else if scan starts with a bad format code ->
# scan := scan advanced past the valid part, at least one
# Log() +:= error message
# else ->
# scan := scan advanced up to the next format code or EOF
# self.__tokenList +:= a TToken representing all text
# up to the next format code or EOF ]
self.__readToken ( scan )
#-- 3 --
# [ if self.__openCondStack is empty -> I
# else ->
# Log() +:= error message(s) ]
if len ( self.__openCondStack ) > 0:
for openCond in self.__openCondStack:
scan.error ( "A conditional `%%%s;' construct that "
"starts on line %d is unclosed." %
( openCond.name, openCond.lineNo ) )
#-- 4 --
scan.close()
# - - - T e m p l a t e . _ _ r e a d T o k e n - - -
def __readToken ( self, scan ):
"""Read and store the next token from scan.
[ if scan starts with a valid format code ->
scan := scan advanced past that format code
self.__tokenList +:= a TToken object representing
that code
self.__openCondStack := self.__openCondStack adjusted
for that code, if necessary
else if scan starts with a bad format code ->
scan := scan advanced past the valid part, at least one
Log() +:= error message
else ->
scan := scan advanced up to the next format code
self.__tokenList +:= a TToken representing all text
up to the next format code or EOF ]
"""
#-- 1 --
# [ if scan starts with a START_FORMAT not followed by a valid
# format code ->
# scan := scan advanced past the START_FORMAT and any
# following valid part
# Log() +:= error message
# else if scan starts with a valid format code ->
# scan := scan advanced past the format code
# self.__tokenList +:= a TToken representing the format code
# self.__openCondStack := self.__openCondStack adjusted
# for that code, if necessary
# else ->
# scan := scan advanced up to the next format code or EOF
# self.__tokenList +:= a TToken representing all text
# up to the next format code or EOF ]
if scan.tabMatch ( START_FORMAT ):
#-- 1.1 --
# [ if scan starts with a valid format code name followed by
# END_FORMAT ->
# scan := scan advanced past all that
# self.__tokenList +:= a TToken representing that
# format code
# self.__openCondStack := self.__openCondStack adjusted
# for that code, if necessary
# else ->
# scan := scan advanced past any leading characters
# in FORMAT_NAME_CSET, then END_FORMAT if present
# Log() +:= error message ]
self.__readFormat ( scan )
else:
#-- 1.2 --
# [ scan := scan advanced up to the next
# START_FORMAT or EOF
# self.__tokenList +:= a literal TToken containing all
# characters from scan up to the
# next START_FORMAT or EOF ]
self.__readLiteral ( scan )
# - - - T e m p l a t e . _ _ r e a d L i t e r a l - - -
def __readLiteral ( self, scan ):
"""Copy literal text up to the next format code or EOF.
[ scan := scan advanced up to the next
START_FORMAT or EOF
self.__tokenList +:= characters from scan up to the
next START_FORMAT or EOF ]
"""
#-- 1 --
L = []
#-- 2 --
# [ if at least one START_FORMAT remains in scan ->
# scan := scan advanced up to the first START_FORMAT
# L +:= text up to the first START_FORMAT in scan,
# as a sequence of strings
# done := 1
# else ->
# scan := scan advanced to EOF
# L +:= text remaining in scan as a sequence of strings ]
# done := 1 ]
done = 0
while not done:
#-- 2 body --
# [ if line in scan contains a START_FORMAT ->
# scan := scan advanced up to the first START_FORMAT
# L +:= text from scan up to the first START_FORMAT
# done := 1
# else if line in scan is the last in the file ->
# scan := scan advanced to EOF
# L +:= remaining text in scan
# done := 1
# else ->
# scan := scan advanced to the next line
# L +:= (remaining text on line in scan) + "\n"
# done := 0 ]
done = self.__readLiteralLine ( scan, L )
#-- 3 --
# [ self.__tokenList := a LiteralToken representing the
# concatenation of the strings in L ]
token = LiteralToken ( self.__openCondStack, scan,
string.join ( L, "" ) )
self.__tokenList.append ( token )
# - - - T e m p l a t e . _ _ r e a d L i t e r a l L i n e - - -
def __readLiteralLine ( self, scan, L ):
"""Add to L text up to the next START_FORMAT, EOL, or EOF.
[ if (scan is a Scan object) and (L is a list) ->
if line in scan contains a START_FORMAT ->
scan := scan advanced up to the first START_FORMAT
L +:= text from scan up to the first START_FORMAT
return 1
else if line in scan is the last in the file ->
scan := scan advanced to EOF
L +:= remaining text in scan
return 1
else ->
scan := scan advanced to the next line
L +:= (remaining text on line in scan) + "\n"
return 0 ]
"""
#-- 1 --
# [ if line in scan contains a START_FORMAT ->
# endp := position of the first such
# else ->
# endp := None ]
endp = scan.find ( "%" )
#-- 2 --
# [ if endp is None ->
# scan := scan advanced to the next line
# L +:= (remainder of line in scan) + "\n"
# else ->
# scan := endp
# L +:= text from scan up to endp
# return 1 ]
if endp is None:
L.append ( scan.tab ( -1 ) )
L.append ( "\n" )
scan.nextLine()
else:
L.append ( scan.tab ( endp ) )
return 1
#-- 3 --
if scan.atEndFile:
return 1
else:
return 0
# - - - T e m p l a t e . _ _ r e a d F o r m a t - - -
# [ self.formatScanners := a dictionary mapping all the lowercased
# format code names |-> corresponding class constructors inheriting
# from TToken ]
formatScanners = {
"author": AuthorToken,
"body": BodyToken,
"else": ElseToken,
"endif": EndifToken,
"ifnext": IfNextToken,
"ifprev": IfPrevToken,
"ifsee": IfSeeToken,
"ifup": IfUpToken,
"nexttitle": NextTitleToken,
"nexturl": NextURLToken,
"prevtitle": PrevTitleToken,
"prevurl": PrevURLToken,
"seealso": SeeToken,
"title": TitleToken,
"updated": UpdateToken,
"uptitle": UpTitleToken,
"upurl": UpURLToken,
"url": URLToken }
def __readFormat ( self, scan ):
"""Process a template format code and append its token to self.
[ if (scan is a Scan object) ->
if scan starts with START_FORMAT ->
scan := scan advanced one
self.__tokenList +:= a literal TToken containing
START_FORMAT
else if scan starts with a valid format code name followed by
END_FORMAT ->
scan := scan advanced past all that
self.__tokenList +:= a TToken representing that
format code
self.__openCondStack := self.__openCondStack adjusted
for that format code
else ->
scan := scan advanced past any leading characters
in FORMAT_NAME_CSET, then END_FORMAT if present
Log() +:= error message ]
"""
#-- 1 --
# [ if scan starts with START_FORMAT ->
# scan := scan advanced one
# self.__tokenList +:= a LiteralToken containing START_FORMAT
# return
# else -> I ]
if scan.tabMatch ( START_FORMAT ):
self.__tokenList.append (
LiteralToken ( self.__openCondStack, scan, START_FORMAT ) )
return
#-- 2 --
# [ if scan starts with a character in FORMAT_NAME_CSET ->
# scan := scan advanced past all leading characters in
# FORMAT_NAME_CSET
# gi := those characters, lowercased (NB: `gi' stands
# for `generic identifier,' which is the standard term for
# the name of an SGML tag)
# else ->
# Log() +:= error message
# return ]
if scan.any ( FORMAT_NAME_CSET ):
gi = string.lower ( scan.tabMany ( FORMAT_NAME_CSET ) )
else:
scan.error ( "The `%' must be followed by a format code name." )
return
#-- 3 --
# [ if scan starts with END_FORMAT ->
# scan := scan advanced one
# else ->
# Log() +:= error message
# return ]
if not scan.tabMatch ( ";" ):
scan.error ( "The format code name must be followed by "
"`%s'." % END_FORMAT )
return
#-- 4 --
# [ if gi is a key in self.formatScanners)
# and (the corresponding value can be used as a constructor
# in the context of self.__openCondStack and scan) ->
# self.__tokenList +:= an object so constructed
# self.__openCondStack := self.__openCondStack adjusted
# by that constructor
# else ->
# Log() +:= error message ]
try:
constructor = self.formatScanners[gi]
token = constructor ( self.__openCondStack, scan )
self.__tokenList.append ( token )
except KeyError:
scan.error ( "There is no such format code as `%s%s%s'." %
( START_FORMAT, gi, END_FORMAT ) )
except SyntaxError, detail:
scan.error ( detail )
# - - - T e m p l a t e . e x p a n d - - -
def expand ( self, body, outFile ):
"""Expand all tokens in the context of a given body file.
"""
#-- 1 --
for token in self.__tokenList:
#-- 1 body --
# [ if self is an invalid conditional expansion token in
# the context of ifStack ->
# Log() +:= error message(s)
# else if self is a valid conditional expansion token
# in the context of ifStack ->
# ifStack := ifStack adjusted to account for processing self
# else if ifStack is skipping -> I
# else ->
# outFile +:= the expansion of self in the context of body ]
try:
token.expand ( self.__ifStack, body, outFile )
except SyntaxError, detail:
Log().error ( "Error in expanding conditional template "
"item (shouldn't happen): %s" % detail )
# - - - - - c l a s s I f S t a c k - - - - -
class IfStack:
"""Represents the state of conditional template expansion.
This class is basically a list of Condition objects,
treated as a stack with the base at 0.
Exports:
IfStack() [ return a new, empty IfStack object ]
.start(cond):
[ if (cond is a Condition object) ->
self := self with cond pushed on the stack ]
.end()
[ if self's stack contains at least one element ->
self := self with the top condition popped
return that condition
else -> raise SyntaxError ]
.elseCheck()
[ if self's stack is empty ->
raise SyntaxError
else if self's top element has seen an else ->
raise SyntaxError
else ->
self := self with the top Condition object's skipping
state inverted and marked as having seen an `%else;' ]
.isSkipping():
[ if self's state implies that we are currently skipping tokens ->
return 1
else -> return 0 ]
State/Invariants:
.__stack:
[ a list of Condition objects, used as a stack with the base
at element 0, representing the nested %if-condition constructs
at the current point of template expansion ]
"""
# - - - I f S t a c k . _ _ i n i t _ _ - - -
def __init__ ( self ):
"""IfStack class constructor
"""
self.__stack = []
# - - - I f S t a c k . s t a r t - - -
def start ( self, cond ):
"""Push a new condition on the stack
"""
self.__stack.append ( cond )
# - - - I f S t a c k . e n d - - -
def end ( self ):
"""Pop a condition off the stack
"""
if len ( self.__stack ) == 0:
raise SyntaxError, "Mismatched `%end;' conditional"
else:
result = self.__stack[-1]
del self.__stack[-1]
return result
# - - - I f S t a c k . e l s e C h e c k - - -
def elseCheck ( self ):
"""Reverse the sense of the top condition on the stack
"""
if len ( self.__stack ) == 0:
#-- 1.1 --
raise SyntaxError, "Mismatched `%else;' conditional"
else:
#-- 1.2 --
# [ if self.__stack has at least one element ->
# if top of self.__stack has seen an `%else;' ->
# raise SyntaxError
# else ->
# self.__stack := self.__stack with the sense of
# its top condition reversed ]
topCond = self.__stack[-1]
topCond.elseCheck()
# - - - I f S t a c k . i s S k i p p i n g - - -
def isSkipping ( self ):
"""Checks to see if we're expanding tokens or skipping them.
"""
#-- 1 --
# [ if self.__stack is empty ->
# return 0
# else -> I ]
if len ( self.__stack ) == 0:
return 0 # No `%if...;' constructs, we're expanding
#-- 2 --
# [ if any of the conditions in self.__stack is skipping ->
# return 1
# else -> I ]
for cond in self.__stack:
if cond.skipping:
return 1
#-- 3 --
return 0
# - - - - - c l a s s O p e n C o n d - - - - -
class OpenCond:
"""An instance tracks the scanning of one "%if...;" construct.
Exports:
OpenCond ( lineNo, name ):
[ if (lineNo is the line number where the construct starts)
and (name is the name of the construct, e.g., "ifnext"
for the "%ifnext;" construct) ->
return a new OpenCond with those values ]
.lineNo [ as passed to constructor ]
.name [ as passed to constroctor ]
.hasElse
[ if an "%else;" construct has been scanned that matches
the opening "%if...;" ->
1
else -> 0 ]
.elseCheck():
[ if self.hasElse ->
raise SyntaxError
else ->
self.hasElse := 1 ]
"""
# - - - O p e n C o n d . _ _ i n i t _ _ - - -
def __init__ ( self, lineNo, name ):
"""Constructor for an OpenCond
"""
self.lineNo = lineNo
self.name = name
self.hasElse = 0
# - - - O p e n C o n d . e l s e - - -
def elseCheck ( self ):
"""Check to see if an "%else;" is legal in the current context
"""
if self.hasElse:
raise SyntaxError, ( "Only one `%else;' clause is allowed "
"to match the `%%%s;' on line %d." %
( self.name, self.lineNo ) )
else:
self.hasElse = 1
# - - - - - c l a s s C o n d i t i o n - - - - -
class Condition:
"""An instance tracks the state of one conditional expansion.
Exports:
Condition ( skipping ):
[ if skipping is 0 ->
return a new Condition that says it is not skipping
and has not seen an `%else;'
if skipping is 1 ->
return a new condition that says it *is* skipping
and has not seen an `%else;' ]
.skipping:
[ if self is skipping -> 1
else -> 0 ]
.hasElse:
[ if self has seen an `%else;' -> 1
else -> 0 ]
.elseCheck():
[ if self has seen an `%else' ->
raise SyntaxError
else if self.skipping is 1 ->
self.skipping := 0
self.hasElse := 1
else ->
self.skipping := 1
self.hasElse := 1 ]
"""
# - - - C o n d i t i o n . _ _ i n i t _ _ - - -
def __init__ ( self, skipping ):
"""Constructor for a Condition object
"""
self.skipping = skipping
self.hasElse = 0
# - - - C o n d i t i o n . e l s e C h e c k - - -
def elseCheck ( self ):
"""Process an `%else;' by inverting the sense of `skipping'
"""
if self.hasElse:
raise SyntaxError, ( "Only one `%else;' can be used per "
"`%if...;' construct." )
else:
self.hasElse = 1
if self.skipping: self.skipping = 0
else: self.skipping = 1