"""plan.py: Objects representing the Plan file for pystyler.py $Revision: 1.54 $ $Date: 2010/09/17 22:40:08 $ Exports: class RawTopic: One parsed line from the Plan file class Plan: Represents the entire Plan file class Topic: Represents one line of the Plan file """ import string # Standard string functions import re # Standard regular expression package import copy # Standard structure copying package import time # Standard time functions import os # Standard operating system functions from log import * # Error logging object from cset import * # Character set type from scan import * # Scanning object from tree import * # Generic N-ary tree node object from template import * # Template and template pool from body import * # Body file package #================================================================ # Verification functions: See pystyler.py for more functions #---------------------------------------------------------------- # path-to-topic(topic) == # list containing the path of nodes in the Topic tree starting # at the root and extending through (topic) #---------------------------------------------------------------- #================================================================ # Manifest constants #---------------------------------------------------------------- OUTLINE_CHAR = "*" # Outline char for emacs outline-mode PLAN_WHITE_SET = Cset ( " \t" ) # Characters considered whitespace BAR_CHAR = "|" # Separates fields in plan file DEFAULT_PLAN_NAME = "Plan" # Default plan file name CONTROL_LINE_CHAR = "$" # Character for Plan control lines CONTROL_WORD_CSET = letters # Control words cset (from cset.py) BODY_EXT = ".g" # Input file extension HTML_EXT = ".html" # Output file extension WARNING_KIND = "Warning" # Message class for warnings # - - - - - c l a s s R a w T o p i c - - - class RawTopic: """Represents one line from a topic file. This object is passed from the strictly syntactic part of the topic line processing to the logic that fits it into the tree. Exports: RawTopic ( depth, title, shortName ): [ if (depth is the topic's depth as a nonegative integer, with 0 meaning root, 1 child of root, etc.) and (title is the topic's title as a nonempty string) and (shortName is the topic's short name as a nonempty string) -> return a new RawTopic object representing those values ] .depth: [ as passed to the constructor ] .title: [ as passed to the constructor ] .shortName: [ as passed to the constructor ] """ def __init__ ( self, depth, title, shortName ): self.depth = depth self.title = title self.shortName = shortName # - - - - - c l a s s P l a n - - - - - class Plan: """Represents the entire Plan file, a catalog of all pages. Exports: Plan ( pathMap, fileName=None, defTemplate=None, unique=0 ): [ if (fileName is a string, defaulting to DEFAULT_PLAN_NAME) and (pathMap is a PathMap object) and (defTemplate is a string or a file-like object containing a template) -> if (fileName names a readable, valid plan file in the context of pathMap) -> return a new Plan object representing that file else -> Log() +:= error message(s) raise IOError ] .pathMap: [ as passed to constructor ] .root: [ the root of the tree of Topic objects ] .defTemplate [ as passed to constructor ] .lookupShortName ( shortName ): [ if shortName is a string -> if shortName (up to but not including any "#" anchor name) matches the short name of a topic in self -> return a Target object representing that topic, and that anchor name if given else -> raise KeyError ] .lookupTemplate ( fileName ): [ if fileName is a string or None -> if (fileName is None) and (the default template file is valid) -> return a Template object representing the default template file else if fileName names a valid template file relative to the starting directory -> return a Template object representing that file else -> Log() +:= error message(s) raise KeyError ] .lookupTitle ( s ): [ if s is a string -> if self contains a topic whose title is s -> return the Topic object for that topic else -> raise KeyError ] .nTopics: [ INV: number of topics in self ] .modEpoch: [ INV: modification epoch timestamp of Plan file ] State/invariants: .tmplPool: [ a TmplPool object containing all Templates mentioned in the plan file ] .shortNameMap: [ a dictionary mapping N |-> T, where N is each short name in self and the corresponding T is the Topic object for that short name ] .titleMap: [ a dictionary mapping K |-> T, where K is each title text in self and the corresponding T is the Topic object for that short name ] .tmplStack: [ a list of one or more Template objects such that the first is the default template and the rest represent the stack of templates pushed by $template commands in the plan file and not yet popped by empty $template commands ] .__unique: [ as passed to constructor ] """ # - - - P l a n . l o o k u p S h o r t N a m e - - - reAnchor = re.compile ( r'^(.*?)(#.*)$' ) def lookupShortName ( self, shortName ): """Find the topic for a given short name. Preserves anchor names. """ #-- 1 -- # [ if shortName contains a '#' -> # pathPart := shortName up to the first '#' # anchor := shortName from the first '#' on # else -> # pathPart := shortName # anchor := None ] m = self.reAnchor.match ( shortName ) if m is None: pathPart, anchor = shortName, None else: pathPart, anchor = m.groups() #-- 2 -- # [ if pathPart is a key in self.shortNameMap -> # return a Target whose topic is the corresponding value # and whose anchor is (anchor) # else -> raise KeyError ] topic = self.shortNameMap[pathPart] return Target ( topic, anchor ) # - - - P l a n . l o o k u p T e m p l a t e - - - def lookupTemplate ( self, fileName=None ): """Find the template that lives in a given file, or the default. """ try: return self.tmplPool [ fileName ] except IOError, detail: raise KeyError, detail # - - - P l a n . l o o k u p T i t l e - - - def lookupTitle ( self, s ): """Find the topic with title `s' """ return self.titleMap [ s ] # - - - P l a n . _ _ i n i t _ _ - - - def __init__ ( self, pathMap, fileName=None, defTemplate=None, unique=0 ): """Constructor for a Plan object """ #-- 1 -- self.pathMap = pathMap self.nTopics = 0 self.tmplStack = [] self.defTemplate = defTemplate self.__unique = unique #-- 2 -- # [ self.tmplPool := a new, empty TmplPool object using # defTemplate as the default template name ] self.tmplPool = TmplPool ( defTemplate ) #-- 3 -- # [ if defTemplate is readable, valid template file -> # self.tmplStack +:= a Template object representing that file # else -> # Log() +:= error message(s) # raise IOError ] self.tmplStack.append(self.tmplPool[None]) #-- 4 -- if fileName is None: fileName = DEFAULT_PLAN_NAME #-- 5 -- # [ if fileName names a readable, valid plan file -> # self.root := a tree of topics from the file # self.shortNameMap := as invariant # self.nTopics +:= number of topics from the file # else -> # Log() +:= error message(s) # raise IOError ] self.__read ( fileName ) # - - - P l a n . _ _ r e a d - - - def __read ( self, fileName ): """Read the plan file and store it in self as a tree of Topics [ if fileName names a readable, valid plan file -> self.root := a tree of topics from the file self.shortNameMap := as invariant self.titleMap := as invariant self.nTopics +:= number of topics from the file else -> Log() +:= error message(s) raise IOError ] """ #-- 1 -- topicStack = [] self.root = None self.shortNameMap = {} self.titleMap = {} errCount = Log().count() #-- 2 -- # [ if fileName can be opened for reading -> # scan := a new Scan object pointing to the start # of that file # self.modEpoch := modification timestamp of that file # else -> # raise IOError ] if isinstance(fileName, str): scan = Scan ( fileName ) info = pathinfo.PathInfo ( fileName ) self.modEpoch = info.modEpoch else: scan = Scan ( fileName ) self.modEpoch = 0L #-- 3 -- # [ if topicStack is a list of the path of nodes in the topic tree # from the root (if any) to the previous topic (if any) -> # scan := scan advanced to end of file # self.root +:= valid topics from remainder of scan # self.shortNameMap +:= new entries added mapping shortName # |-> topic for valid topics from the # remainder of scan # self.titleMap +:= new entries added mapping title # |-> topic for valid topics from the # remainder of scan # topicStack := path-to-topic(last topic from scan) # self.tmplStack := self.tmplStack adjusted for # control lines in scan # self.nTopics +:= number of topic lines in scan # Log() +:= error message(s), if any, about # invalid lines from scan ] while not scan.atEndFile: # -- 3 body -- # [ if the line in scan is empty -> I # else if line in scan is a valid control line -> # self := self modified to reflect that control line # else if line in scan is a valid topic in the context of # topicStack -> # self.root := self.root with a Topic object # representing that topic # self.shortNameMap +:= an entry mapping that topic's # short name |-> the Topic object # self.titleMap +:= an entry mapping that topic's # title |-> the Topic object # topicStack := path-to-topic(that new Topic) # self.nTopics +:= 1 # else -> # Log() +:= error message(s) # In all cases -> # scan := scan advanced to the next line or EOF ] #-- 3.1 -- # NB: The call to scan.tabMatchArb has the side effect of # advanced scan past the CONTROL_LINE_CHAR if present if not scan.atEndLine(): if scan.tabMatchArb ( CONTROL_LINE_CHAR ): self.__controlLine ( scan ) else: self.__buildTopic ( scan, topicStack ) #-- 3.2 -- scan.nextLine() #-- 4 -- # [ if self.tmplStack has exactly one element -> I # else -> # Log() +:= error message(s) ] if len(self.tmplStack) > 1: Log().error ( "There were %d unmatched `$template' lines " "in the `%s' file." % ( len(self.tmplStack), fileName ) ) #-- 5 -- if self.root is None: Log().error ( "The `%s' file contains no topics." ) #-- 6 -- scan.close() if errCount < Log().count(): raise IOError, "The plan file contained one or more errors." # - - - P l a n . _ _ c o n t r o l L i n e - - - def __controlLine ( self, scan ): """Process a control line in the plan file. [ if scan is a Scan object -> if (CONTROL_LINE_CHAR+(the line in scan)) is a valid control line in the context of self -> scan := scan advanced to end of line self := self modified to reflect that control line else -> scan := scan advanced past the valid part, if any Log() +:= error message(s) ] """ #-- 1 -- # [ if the line in scan starts with a syntactically valid # control word -> # scan := scan advanced past that word and any trailing # blanks # word := the control word, lowercased # else -> # scan := scan advanced past the valid part, if any # Log() +:= error message(s) # return ] scan.deblankLine() # Forgive blanks before keyword word = string.lower ( scan.tabMany ( CONTROL_WORD_CSET ) ) if word is None: scan.error ( "Expecting a control line word, e.g., " "`template'." ) return #-- 2 -- # NB: If someday there are multiple control words, use a # dictionary to dispatch them. # [ if (word is not a valid control word) # or (word is valid but remainder of line in scan is not) -> # scan := scan advanced past the valid part, if any # Log() +:= error message(s) # else -> # scan := scan advanced to end of line # self := self modified as per the command from line # in scan ] if word == "template": self.__controlTemplate(scan) else: scan.error ( "`$%s' is not a valid control line." % word ) # - - - P l a n . _ _ c o n t r o l T e m p l a t e - - - def __controlTemplate ( self, scan ): """Process the $template control line [ if "$template"+(line in scan) is a valid $template line -> scan := scan advanced to end of line self := self modified to reflect that line else -> scan := scan advanced past valid parts, if any Log() +:= error message(s) ] """ #-- 1 -- # [ scan := scan advanced past all whitespace ] scan.deblankLine() #-- 2 -- if scan.atEndLine(): #-- 2.1 -- # [ if len(self.tmplStack) < 2 -> # Log() +:= error message # else -> # self.tmplStack := self.tmplStack with last element # removed ] if len(self.tmplStack) < 2: scan.error ( "This end-$template line does not match " "any open $template line." ) else: del self.tmplStack[-1] else: #-- 2.2 -- # [ if line in scan is the name of a readable, valid # template file -> # scan := scan advanced to end of line # self.tmplStack := self.tmplStack + (a new Template # object representing that file) # else -> # scan := scan advanced past valid part, if any # Log() +:= error message ] self.__pushTemplate ( scan ) # - - - P l a n . _ _ p u s h T e m p l a t e - - - def __pushTemplate ( self, scan ): """Process a $template-open line [ if line in scan is the name of a readable, valid template file -> scan := scan advanced to end of line self.tmplStack := self.tmplStack + (a new Template object representing that file) else -> scan := scan advanced past valid part, if any Log() +:= error message ] """ #-- 1 -- # [ scan := scan advanced to end of line # tmplName := remainder of line in scan, trailing blanks # trimmed ] tmplName = string.rstrip ( scan.tab ( -1 ) ) #-- 2 -- # [ if self.tmplPool contains a Template object for tmplName -> # template := that Template object # else if tmplName names a readable, valid template file -> # self.tmplPool := self.tmplPool + (a new Template object # representing that file) # else -> # Log() +:= error message(s) # return ] try: template = self.tmplPool[tmplName] except IOError: return #-- 3 -- self.tmplStack.append ( template ) # - - - P l a n . _ _ b u i l d T o p i c - - - def __buildTopic ( self, scan, topicStack ): """Processes one line of the plan file. [ if (self.root is the tree of existing topics) and (scan is a Scan object) and (topicStack is path-to-topic(previous topic, if any) -> if the line in scan is a valid topic line -> scan := scan advanced to end of line self.root := self.root with that topic added self.shortNameMap +:= a new entry mapping the short name from that line |-> the new Topic made from that line self.titleMap +:= a new entry mapping the title from that line |-> the new Topic made from that line topicStack := path-to-topic(the new Topic made from that line) else -> topicStack := topicStack, possibly trimmed of elements not shallower than new topic line Log() +:= error message(s) ] """ #-- 1 -- # [ if the line in scan is a syntactically valid topic line -> # scan := scan advanced to end of line # rawTopic := a RawTopic object containing the fields # from that line # else -> # Log() +:= error message(s) # return rawTopic = self.__parseTopic ( scan ) if rawTopic is None: return #-- 2 -- # [ if rawTopic.depth > (size of topicStack) -> # Log() +:= error message about missing levels # return # else -> # topicStack := first rawTopic.depth elements of # topicStack ] flag = self.__adjustTopicStack ( scan, topicStack, rawTopic.depth ) if not flag: return #-- 3 -- # [ if rawTopic is invalid -> # Log() +:= error message(s) # else if rawTopic is valid and topicStack is empty -> # self.root := a new Topic made from rawTopic, and # no parent # self.shortNameMap +:= an entry mapping rawTopic.shortName # |-> that new Topic # self.titleMap +:= an entry mapping rawTopic.title # |-> that new Topic # else -> # self.root := self.root with a new Topic, made # from rawTopic, added as the next # child of topicStack[-1] # topicStack := topicStack + (that new Topic) # self.shortNameMap +:= an entry mapping rawTopic.shortName # |-> that new Topic ] self.__addTopic ( topicStack, rawTopic, scan ) # - - - P l a n . _ _ p a r s e T o p i c - - - topicPat = re.compile ( r'^' # Beginning-of-line anchor r'\s*' # Ignore leading whitespace r'(?P\%s+)' # One or more OUTLINE_CHAR r'\s*' # Any whitespace r'(?P.*)' # Title and possible whitespace r'\%s' # BAR_CHAR, escaped r'\s*' # Any whitespace r'(?P<short>.*)' # Short name and possible whitespace r'$' % # End-of-line anchor (OUTLINE_CHAR, BAR_CHAR) ) def __parseTopic ( self, scan ): """Check a topic line for syntactic correctness [ if the line in scan is a syntactically valid topic line -> scan := scan advanced to end of line return a RawTopic object containing the fields from that line else -> Log() +:= error message(s) return None ] """ #-- 1 -- # [ if the line in scan matches self.topicPat -> # scan := scan advanced to end of line # m := a Match object containing the matched fields # else -> # Log() +:= error message # return None ] m = scan.tabReMatch ( self.topicPat ) if m is None: scan.error ( "Invalid syntax for a Plan line" ) return None #-- 2 -- # [ depth := (number of characters in m.match("stars"))-1 # title := m.match("title") with trailing whitespace trimmed # short := m.match("short") with trailing whitespace trimmed ] depth = len ( m.group ( "stars" ) ) - 1 title = string.rstrip ( m.group ( "title" ) ) short = string.rstrip ( m.group ( "short" ) ) #-- 3 -- return RawTopic ( depth, title, short ) # - - - P l a n . _ _ a d j u s t T o p i c S t a c k - - - def __adjustTopicStack ( self, scan, topicStack, newDepth ): """Check & adjust topic stack for a new topic at a given depth [ if (topicStack is a stack of Topic objects) and (newDepth is a positive integer) -> if newDepth > (size of topicStack) -> Log() +:= error message about missing levels return 0 else -> topicStack := first newDepth elements of topicStack return 1 ] """ #-- 1 -- # [ if newDepth > len ( topicStack ) -> # Log() +:= error message # return 0 # else -> I ] if newDepth > len ( topicStack ): scan.error ( "This topic is at depth %d while the previous " "topic was at depth %d." % ( newDepth+1, len(topicStack) ) ) return 0 #-- 2 -- # [ topicStack := topicStack shortened to newDepth entries ] del topicStack[newDepth:] #-- 3 -- return 1 # - - - P l a n . _ _ a d d T o p i c - - - def __addTopic ( self, topicStack, rawTopic, scan ): """Add a new topic to self and the topicStack [ if (topicStack is a stack of Topic objects of size rawTopic.depth) and (rawTopic is a RawTopic object) -> if rawTopic is invalid -> Log() +:= error message(s) else if rawTopic is valid and topicStack is empty -> self.root := a new Topic made from rawTopic, and no parent self.shortNameMap +:= an entry mapping rawTopic.shortName |-> that new Topic self.titleMap +:= an entry mapping rawTopic.title |-> that new Topic else -> topicStack := topicStack with a new Topic pushed, made from rawTopic, as a new child of the last element of topicStack self.shortNameMap +:= an entry mapping rawTopic.shortName |-> that new Topic ] self.titleMap +:= an entry mapping rawTopic.title |-> that new Topic ] """ #-- 1 -- # [ if rawTopic.shortName is a valid short name in self.pathMap -> # path := the path from the web root to the topic's # directory, expanded according to plan.pathMap # else -> # Log() +:= error message # return ] path = self.pathMap.expandShortName ( rawTopic.shortName ) if path is None: scan.error ( "The short name `%s' is not defined in the " "path map." % rawTopic.shortName ) return #-- 2 -- # [ if self.shortNameMap already has a key like # rawTopic.shortName -> # Log() +:= error message # return # else -> I ] if self.shortNameMap.has_key ( rawTopic.shortName ): scan.error ( "The short name `%s' is already defined." % rawTopic.shortName ) return #-- 3 -- # [ if (self.__unique) # and (self.titleMap already has a key like rawTopic.title) -> # Log() +:= error message # return # else -> I ] if ( ( self.__unique ) and ( self.titleMap.has_key ( rawTopic.title ) ) ): otherTopic = self.titleMap[rawTopic.title] scan.msgKind ( WARNING_KIND, "Title `%s' is already defined, for " "short name %s." % ( rawTopic.title, otherTopic.shortName ) ) #-- 4 -- # [ if topicStack is empty -> # parent := None # else -> # parent := last element of topicStack ] if len ( topicStack ) > 0: parent = topicStack[-1] else: parent = None #-- 5 -- # [ topic := a new Topic with plan=self, parent=parent, # title=rawTopic.title, shortName=rawTopic.shortName, # and path=path ] topic = Topic ( self, parent, rawTopic.depth, rawTopic.title, rawTopic.shortName, path ) #-- 6 -- if parent is None: self.root = topic #-- 7 -- # [ self.shortNameMap +:= an entry mapping topic.shortName |-> # topic # self.titleMap +:= an entry mapping topic.title |-> # topic ] self.shortNameMap[topic.shortName] = topic self.titleMap[topic.title] = topic #-- 8 -- # [ topicStack := topicStack with topic appended # self.nTopics +:= 1 ] topicStack.append ( topic ) self.nTopics = self.nTopics + 1 # - - - - - c l a s s T o p i c - - - - - class Topic: """Represents one topic in the plan. Exports: Topic ( plan, parent, depth, title, shortName, path ): [ if (plan is a Plan object) and (parent is None for the root topic, else a Topic whose depth is one less than (depth)) and (depth is a nonnegative integer, with 0 meaning root, 1 child of root, etc.) and (title is the topic's title as a nonempty string) and (shortName is the topic's short name as a nonempty string) and (path is a relative path as a string) -> return a new Topic object with those values and an empty list of LinkVar objects ] .plan: [ as passed to constructor ] .parent: [ as passed to constructor ] .depth: [ as passed to constructor ] .title: [ as passed to constructor ] .shortName: [ as passed to constructor ] .path: [ as passed to constructor ] .template: [ default Template object for this topic ] .linkVarList: [ a sequence of LinkVar objects representing variant link texts for self ] .birthOrder(): [ if self is the root topic -> return 0 else -> return self's birth order relative to its parent, >=0 ] .nChildren(): [ returns the number of self's children ] .childList(): [ returns a new list of self's children in birth order ] .nthChild(n): [ if n is an integer -> if self has at least n-1 children -> return self's nth children, counting from 0 else -> raise IndexError ] .relPath(toTarget): [ if toTarget is a Target object -> return the pathname (and optional anchor) which, when used in self's directory, points to the output file of toTarget ] .addLinkVar(linkVar): [ if linkVar is a LinkVar object whose .shortName matches self.shortName -> self := self with linkVar added to its .linkVarList ] .url(): [ returns the URL of topic's HTML file ] .fullPath(): [ returns a list of integers [c0, c1, ..., ci] such that self is root.nthChild(c0).nthChild(c1). ... .nthChild(ci) ] .isOnPath(nodeId): [ if nodeId is None or a NodeId object -> if (nodeId is None) and (self is the root) -> 1 else if (nodeId is a NodeId) and (self.fullPath is a prefix of, or equal to, nodeId's path) -> return 1 else -> return 0 ] .find(nodeId): [ if (self is the root Topic) and (nodeId is None or a NodeId object) -> if nodeId is None -> return self else if nodeId describes the path to a node that exists in self's topic tree -> return that Topic object else -> return None ] .expandTree(force, outRoot): [ if (force is a boolean) and (outRoot is None or a string) -> subtree-output-pages(self, force, outRoot, pathMap, plan) := subtree-contents(self, force, outRoot, pathMap, plan, subtree-files(self, pathMap, plan)) Log() +:= errors, if any, from that expansion plan := plan with text variants added from <RR> elements in subtree-files return number of pages actually written ] State/Invariants: .__tree: [ a Tree object connecting self to its parent, and children if any ] """ # - - - T o p i c . _ _ i n i t _ _ - - - def __init__ ( self, plan, parent, depth, title, shortName, path ): """Constructor for the Topic object. """ #-- 1 -- self.plan = plan self.parent = parent self.depth = depth self.title = title self.shortName = shortName self.path = path self.template = plan.tmplStack[-1] self.linkVarList = [] #-- 2 -- # [ if parent is None -> # self.__tree := a new Tree object with no parent # else -> # self.__tree := a new Tree object with parent.__tree as its # parent ] if parent: self.__tree = Tree ( parent.__tree, self ) else: self.__tree = Tree ( None, self ) # - - - T o p i c . _ _ s t r _ _ - - - def __str__ ( self ): """For debug display of topics """ return ( "topic(%s %s (%s))" % ("*" * (self.depth+1), self.title, self.shortName) ) # - - - T o p i c . b i r t h O r d e r - - - def birthOrder ( self ): """Returns self's birth order, counting from 0 """ return self.__tree.birthOrder # - - - T o p i c . n C h i l d r e n - - - def nChildren ( self ): """Returns the number of self's children """ return self.__tree.nChildren() # - - - T o p i c . c h i l d L i s t - - - def childList ( self ): """Returns a new list of self's children """ #-- 1 -- result = [] #-- 2 -- # [ result := result with values of self's tree's children # appended in the same order ] for child in self.__tree.childList: result.append ( child.value ) #-- 3 -- return result # - - - T o p i c . n t h C h i l d - - - def nthChild ( self, n ): """Return self's (n)th child. """ #-- 1 -- # [ if self.__tree has an nth child -> # childTree := the Tree object for that child # else -> # raise IndexError ] childTree = self.__tree.nthChild(n) #-- 2 -- return childTree.value # - - - T o p i c . r e l P a t h - - - def relPath ( self, toTarget ): """Find the pathname from self's topic to the given target. Method: Self's and toTarget's paths should, in general, have this form: d1 / d2 / ... / dn / f where the di are directories and f is the unqualified filename. 1. First find the largest sequence of directories d1, d2, ... that is a prefix of both self's and toTarget's paths, e.g.: self's path: d1/d2/.../dc/a1/a2/.../aj/fa other's path: d1/d2/.../dc/b1/b2/.../bk/fb 2. Form the two lists that remain when these prefixes are removed, e.g.: self: a1/a2/.../aj/fa other: b1/b2/.../bk/fb 3. The result consists of: a. One "../" for each a1, a2, ..., aj; this gets us up to the lowest directory that is a parent of self and the other. b. The sequence "b1/b2/.../bk/fb". c. The suffix ".html". d. The anchor from toTarget, if any. Examples: self's path toTarget's path result ----------- --------------- ------ here/new here/new new.html here/old here/new new.html here/old here/there/new there/new.html foo/x/y/z/a foo/ear/danke/b ../../../ear/danke/b.html """ #-- 1 -- # [ fromList := list of slash-separated strings # a1, a2, ..., am, fa from self's path # toList := list of slash-separated strings # b1, b2, ..., bn, fb from toTarget's path ] fromList = string.split ( self.path, "/" ) toList = string.split ( toTarget.topic.path, "/" ) #-- 2 -- # [ fromList := elements of fromList not in common prefix # with toList, except for the last element # toList := elements of toList not in common prefix # with fromList, except for the last element ] while ( ( len(fromList) > 1 ) and ( len(toList) > 1 ) and ( fromList[0] == toList[0] ) ): del fromList[0] del toList[0] #-- 3 -- # [ return (one "../" for each element of fromList but the last) + # (elements of toList, separated by "/") + # ".html" + (anchor from toTarget, if any) ] result = [] result.append ( "../" * ( len(fromList) - 1 ) ) result.append ( string.join ( toList, "/" ) ) result.append ( HTML_EXT ) if toTarget.anchor: result.append ( toTarget.anchor ) return string.join ( result, "") # - - - T o p i c . a d d L i n k V a r - - - def addLinkVar ( self, linkVar ): """Add a LinkVar to self.linkVarList """ self.linkVarList.append ( linkVar ) # - - - T o p i c . u r l - - - def url ( self): """Return the URL of topic's HTML page. """ return ( self.plan.pathMap.rootURL + self.path + ".html" ) # - - - T o p i c . f u l l P a t h - - - def fullPath ( self ): """Return route from root->self as a sequence of birthOrder values """ return self.__tree.fullPath() # - - - T o p i c . i s O n P a t h - - - def isOnPath ( self, nodeId ): """Is self somewhere on the path described by nodeId? """ if nodeId is None: if self.parent is None: return 1 else: return 0 else: return nodeId.isOnPath(self.__tree) # - - - T o p i c . f i n d - - - def find ( self, nodeId ): """Find the topic that corresponds to a given NodeId; self is root """ if nodeId is None: return self # None means the root else: return nodeId.find ( self.__tree ) # - - - T o p i c . e x p a n d T r e e - - - def expandTree ( self, force, outRoot ): """Read all body files at or under self and expand them. """ #-- 1 -- nChanged = 0 #-- 2 -- # [ if (not force) # and (self's output page exists and is up to date) -> # I # else (if self's body page exists and is valid and its # template exists and is valid) -> # self's output page := expansion of self's body page # in context of plan and the body page's template # self.plan := self.plan with self's effective # template added if not already present # nChanged := nChanged + (1 if page was rewritten, # else 0) # else -> # Log() +:= error message(s) ] nChanged = nChanged + self.expand ( force, outRoot ) #-- 3 -- # [ same intended function as above recursively applied to # all self's descendants ] for childx in range(self.nChildren()): child = self.nthChild ( childx ) nChanged = nChanged + child.expandTree ( force, outRoot ) #-- 4 -- return nChanged # - - - T o p i c . e x p a n d - - - def expand ( self, force, outRoot ): """Expand self's body file (unless up to date and not force) [ if (force is a Boolean) and (outRoot is None or a string) -> if (not force) and (self's body page exists) and (self's output page exists in outRoot and is up to date) -> return 0 else (if self's body page exists and is valid and its template exists and is valid) -> self's output page := expansion of self's body page in context of plan and the body page's template, in path starting at outRoot plan := plan with self's effective template added if not already present return 1 else -> Log() +:= error message(s) ] """ #-- 1 -- # [ bodyName := name of self's body file # htmlName := name of self's output file ] bodyName = self.path + BODY_EXT htmlName = self.__outPath ( outRoot ) #-- 2 -- # [ if (self's output page exists and is up to date) # and (self's body page exists) # and (not force) -> # return # else -> I ] if self.__pageUpToDate ( bodyName, htmlName, force ): return 0 #-- 3 -- # [ if bodyName is readable and valid and its # effective template is readable and valid -> # body := a new Body object representing that file # self.plan := self.plan with that effective template # added if not already present # else -> # Log() +:= error(s) # return ] try: body = Body ( self, bodyName ) except IOError: return 0 #-- 4 -- # [ if self's output file can be created anew, with any # directories between outRoot and that file # created if necessary -> # outFile := a writeable handle for that new, empty file # else -> # possibly create some of the directories in htmlName's path # Log() +:= error message(s) # return ] outFile = self.__openOut(htmlName) if not outFile: return #-- 5 -- # [ outFile +:= expansion of body using body.etemplate as # its template # Log() +:= error messages from expansion, if any # return 1 ] body.etemplate.expand ( body, outFile ) outFile.close() return 1 # - - - T o p i c . _ _ p a g e U p T o D a t e - - - def __pageUpToDate ( self, bodyName, htmlName, force ): """Check to see if the output page is okay as it stands. [ if (bodyName is the name of self's body page as a string) and (htmlName is the name of self's output page as a string) and (force is a Boolean) -> if (self's output page exists and is up to date) and (self's body page exists) and (not force) -> return 1 else -> return 0 ] NOTE: There is one thing not considered in our definition of "up to date". If the actual body file contains a <TEMPLATE> tag, we don't check to see if the template named in that tag has a newer modification time than the HTML file. The <TEMPLATE> tag is deprecated because we don't want to have read every body file to see if it has such a tag; that would defeat half the purpose of this rewrite, which was to speed up rebuilds. """ #-- 1 -- # [ if htmlName names an existing file -> # oldModTime := its modification timestamp as epoch time # else -> # oldModTime := None ] try: oldModTime = pathinfo.PathInfo(htmlName).modEpoch except OSError: return 0 #-- 2 -- # [ if bodyName names an existing file -> # bodyModTime := its modification epoch time # else -> # return 0 ] try: bodyModTime = pathinfo.PathInfo(bodyName).modEpoch except OSError, detail: return 0 #-- 3 -- # [ templateTime := modification epoch time of self's template ] templateTime = self.template.modEpoch #-- 4 -- if ( ( oldModTime > templateTime ) and ( oldModTime > bodyModTime ) and ( oldModTime > self.plan.modEpoch ) and ( not force ) ): return 1 else: return 0 # - - - T o p i c . _ _ o u t P a t h - - - def __outPath ( self, outRoot ): """Find the pathname of self's output file [ if outRoot is None or a string -> if outRoot is None -> return self.path + HTML_EXT else -> return outRoot + ("/" if needed) + self.path + HTML_EXT ] """ #-- 1 -- relPath = self.path + HTML_EXT #-- 2 -- # [ if outRoot is None -> I # else -> # relPath := outRoot + ( "/" if necessary ) + relPath ] if outRoot: relPath = os.path.join ( outRoot, relPath ) #-- 3 -- return relPath # - - - T o p i c . _ _ o p e n O u t - - - def __openOut ( self, outName ): """Try to open outName anew, possible creating directories [ if outName is a string -> if outName can be created anew, with any directories in outName's path created if necessary -> return a writeable handle for that new, empty file else -> possibly create some of the directories in outName's path Log() +:= error message(s) return None ] """ #-- 1 -- # [ if outName can opened anew -> # return a writeable file handle to that file, empty # else -> I ] try: return open ( outName, "w" ) except IOError: pass #-- 2 -- # [ if all directories in outName's path exist -> I # else if all nonexistent directories in outName's path can be # created top to bottom -> # create those directories # else -> # possibly create some directories # Log() +:= error message(s) # return None ] if not self.__makeDirs ( outName ): return None #-- 3 -- # [ if outName can opened anew -> # return a writeable file handle to that file, empty # else -> # Log() +:= error message(s) # return None ] try: return open ( outName, "w" ) except IOError, detail: Log().error ( "Can't create a new, empty output file " "`%s': %s" % ( outName, str(detail) ) ) return None # - - - T o p i c . _ _ m a k e D i r s - - - def __makeDirs ( self, path ): """Try to make all nonexistent directories along the given path [ if path is a path name as a string -> if all directories in path exist -> return 1 else if all nonexistent directories in path can be created top to bottom -> create those directories return 1 else -> possibly create some of those directories Log() +:= error message(s) return 0 ] """ #-- 1 -- # [ dirList := a list of the names of each directory in path, # left to right ] dirList = self.__buildDirList ( path ) #-- 2 -- # [ if all elements of dirList are existing directories -> # I # else if all nonexistent elements of dirList can be created # as directories -> # create those directories # else -> # create directories up to the first one that can't be created # Log() +:= error message(s) # return 0 ] for dir in dirList: #-- 2 body -- # [ if dir is an existing directory -> # I # else if dir can be created as a directory -> # create it # else -> # Log() +:= error message(s) # return 0 ] if not self.__creDir ( dir ): return 0 #-- 3 -- return 1 # - - - T o p i c . _ _ b u i l d D i r L i s t - - - def __buildDirList ( self, path ): """Find the list of all directory names in a given path [ if path is a string containing a path name -> if path has the form "/d0/d1/.../dn/f" -> return a list ["/d0", "/d0/d1", ..., "/d0/d1/.../dn"] else -> return a list ["d0", "d0/d1", ..., "d0/d1/.../dn" ] """ #-- 1 -- # [ if path contains no "/" -> # dirPart := "" # else -> # dirPart := text from path up to the last "/" ] dirPart, filePart = os.path.split ( path ) #-- 2 -- # [ L := list of strings in dirPart separated by "/" # result := a new, empty list # curPath := "" # sep := "" ] #-- # NB: If dirPart starts with "/", the first element of the # result of string.split will be an empty string #-- L = string.split ( dirPart, "/" ) result = [] curPath = "" sep = "" #-- 3 -- # [ result := result + nonempty members of the sequence # [ curPath+sep+L[0], curPath+sep+L[0]+"/"+L[1], ..., # curPath+sep+L[0]+"/"+L[1]+"/"+...+"/"+L[-1]] # curPath := curPath+sep+L[0]+"/"+L[1]+"/"+...+"/"+L[-1] # sep := "/" if len(L)>0, else unchanged ] for part in L: #-- 3 body -- # [ if part is "" -> # curPath := curPath+sep+part # sep := "/" # else -> # result := result + [curPath+sep+part] # curPath := curPath+sep+part # sep := "/" ] #-- 3.1 -- curPath = curPath + sep + part sep = "/" #-- 3.2 -- if part: result.append ( curPath ) #-- 4 -- return result # - - - T o p i c . _ _ c r e D i r - - - def __creDir ( self, path ): """If path does not exist, try to create it as a directory [ if path is a string -> if path names an existing directory -> return 1 else if path names a directory that can be created -> create that directory return 1 else -> Log() +:= error message(s) return 0 ] """ #-- 1 -- # [ if path can be statted -> # info := a PathInfo object representing that path # else -> # info := None ] try: info = pathinfo.PathInfo ( path ) except OSError, detail: info = None #-- 2 -- # [ if info is None -> I # else if info is a directory or link -> return 1 # else -> # Log() +:= error message(s) # return 0 ] if info: if ( info.isDir() or info.isLink() ): return 1 else: Log().error ( "Trying to create directory `%s' but " "there is an existing file or link by " "that name." % path ) return 0 #-- 3 -- # [ if a directory named path can be created -> # create it # return 1 # else -> # Log() +:= error message(s) # return 0 ] try: os.mkdir ( path ) return 1 except OSError, detail: Log().error ( "Can't create directory `%s': %s" % ( path, str(detail) ) ) return 0