"""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