# plan.icn: Object to represent the Plan file for webstyler.icn #-- $ifndef __PLAN_ICN__ $define __PLAN_ICN__ $define PLAN_REVISION "$Revision: 1.15 $" $define PLAN_DATE "$Date: 2000/05/18 22:42:03 $" #================================================================ # Class Plan: Each instance encapsulates the contents of a plan # file. #---------------------------------------------------------------- # plan := Plan_New ( fileName, pathMap, log ) # [ if fileName (or DEFAULT_FILE_NAME, if fileName is &null) # names a valid plan file -> # returns a new plan object with fields: # .fileName := fileName, or "Plan" if argument missing # .root := tree of topics from fileName # .log := log # .shortNameMap := table mapping each topic.shortName |-> topic # .pathMap := pathMap # .tmplPool := a new TmplPool object # | else -> # log ||:= error message(s) # fails # ] #-- # topic := Plan_Root ( plan ) # [ returns the root topic of plan; see topic.icn ] #-- # topic := Plan_Lookup_Short_Name ( plan, shortName ) # [ if shortName is found in plan -> # returns the corresponding Target object # | else -> fails # ] #-- # pathMap := Plan_Path_Map [ returns plan.pathMap ] #-- # template := Plan_Lookup_Template ( plan, fileName ) # [ if fileName is &null and the default template file is valid -> # return a Template object representing that file # | else if fileName names a valid template file relative to # the startup directory -> # return a Template object representing that file # | else -> # plan.log ||:= error message(s) # fail # ] #-- # integer := Plan_N_Topics ( plan ) [ returns number of topics ] #-- # string := Plan_Version ( plan ) [ returns RCS revision keywords ] #-- record planTag ( # State for the plan object fileName, # Source file name log, # Relate log object; see log.icn scan, # Scan object; see scan.icn; local to this file pathMap, # Related path map object; see pathmap.icn tmplPool, # Template pool object; see tmplpool.icn root, # Root of the tree of topics; see topic.icn nTopics, # Invariant: number of topics in .root shortNameMap ) # Table: maps topic.shortName |-> topic #================================================================ # Internal methods and structures #---------------------------------------------------------------- # Plan_Read ( plan ) # [ if plan.fileName names a valid plan file -> # plan.root := tree of topics from plan.fileName, or # &null if there are no topics # plan.shortNameMap := table, maps topic.shortName |-> topic # for all topics in plan.fileName # | else -> # plan.log ||:= error message(s) # fail # | In any case -> # plan.root := # plan.shortNameMap := # plan.scan := # ] #---------------------------------------------------------------- record rawTopicTag ( # Interface between Plan_Build_Topic/Plan_Parse_Topic depth, # Tree depth, integer, 1 for root, 2 for child of root, etc. title, # Title field shortName ) # Short name field $define OUTLINE_CHAR "*" # Outline character for emacs outline-mode $define PLAN_WHITE_SET ' \t' # Whitespace in plan files $define BAR_CHAR '|' # Separates fields in the source file $define CONTROL_CHAR '$' # Denotes control lines in source $define DEFAULT_PLAN_NAME "Plan" # - - - P l a n _ N e w - - - procedure Plan_New ( fileName, pathMap, log ) #-- 1 -- #-[ plan := a planTag with fields: # .fileName := &null # .log := log # .scan := &null # .pathMap := pathMap # .tmplPool := a new TmplPool object # .root := &null # .shortNameMap := an empty table #-] local plan plan := planTag ( ); plan.log := log; plan.pathMap := pathMap; plan.tmplPool := Tmpl_Pool_New ( log ); plan.nTopics := 0; plan.shortNameMap := table (); #-- 2 -- #-[ if fileName was given -> # plan.fileName := fileName # | else -> # plan.fileName := DEFAULT_PLAN_NAME #-] plan.fileName := ( \ fileName ) | DEFAULT_PLAN_NAME; #-- 3 -- # [ if plan.fileName names a valid plan file -> # plan.root := tree of topics from plan.fileName # plan.shortNameMap +:= entries that map topic.shortName |-> topic # for every topic in plan.fileName # | else -> # plan.log ||:= error message(s) # fail # ] if not Plan_Read ( plan ) then fail; #-- 4 -- return plan; end # --- Plan_New --- # - - - P l a n _ R o o t - - - procedure Plan_Root ( plan ) return plan.root; end # - - - P l a n _ L o o k u p _ S h o r t _ N a m e - - - procedure Plan_Lookup_Short_Name ( plan, shortName ) #-- 1 -- #-[ anchor := &null #-] local anchorPos local anchor local pathPart #-- 2 -- #-[ if there is a "#" anywhere in shortName -> # pathPart := characters of shortName up to the first "#" # anchor := characters from the first "#" in shortName on # | else -> # pathPart := shortName #-] if anchorPos := find ( "#", shortName ) then { pathPart := shortName[1:anchorPos]; anchor := shortName[anchorPos:0]; } else pathPart := shortName; #-- 3 -- #-[ if pathPart is a key defined in plan.shortName -> # return a Target whose topic is the related value and # whose anchor is (anchor) # | else -> fail #-] return Target_New ( \ plan.shortNameMap[pathPart], anchor ); end # - - - P l a n _ P a t h _ M a p - - - procedure Plan_Path_Map ( plan ) return plan.pathMap; end # - - - P l a n _ L o o k u p _ T e m p l a t e - - - procedure Plan_Lookup_Template ( plan, fileName ) return Tmpl_Pool_Lookup ( plan.tmplPool, fileName ); end # - - - P l a n _ N _ T o p i c s - - - procedure Plan_N_Topics ( plan ) return plan.nTopics; end # - - - P l a n _ V e r s i o n - - - procedure Plan_Version ( plan ) return "Plan " || RCS_Extract ( PLAN_REVISION ) || " " || RCS_Extract ( PLAN_DATE ); end # - - - P l a n _ R e a d - - - procedure Plan_Read ( plan ) #-- 1 -- #-[ topicStack := an empty list # plan.scan := &null # plan.root := &null #-] local topicStack topicStack := []; plan.scan := &null; plan.root := &null; #-- 2 -- #-[ if plan.fileName can be opened for reading -> # &subject := first line from file plan.fileName # &pos := 1 # plan.scan := a scan object positioned on the first line of # plan.fileName # | else -> I #-] plan.scan := Scan_Open ( plan.fileName, plan.log ); #-- 3 -- #-[ if plan.scan is &null -> # plan.log ||:= error message, can't open (plan.fileName) # fails # | else -> I #-] if / plan.scan then return Log_Error ( plan.log, "Can't open plan file `", plan.fileName, "' for reading." ); #-- 4 -- #-[ 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) -> # plan.root := tree of topics from plan.scan # plan.shortNameMap := plan.shortNameMap with new entries mapping # topic.shortName |-> topic for all valid # lines in plan.scan # plan.log ||:= error messages for bad lines in plan.scan # topicStack := list of the path of nodes in the topic tree # from the root to the topic from plan.scan # plan.scan := plan.scan advanced to EOF #-] while not ( Scan_End_File ( plan.scan ) ) do { #-- 4.1 -- #-[ 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), and the # current subject is a valid topic line -> # plan.root := plan.root with new topic in place # plan.shortNameMap := plan.shortNameMap plus a new entry # mapping new short name to new topic # topicStack := list of topics traversed from root # to new topic # | else -> # plan.log ||:= plan.scan error message about bad topic line #-] Plan_Build_Topic ( plan, topicStack ); #-- 4.2 -- #-[ if there remains at least one line in plan.scan -> # &subject := next line from plan.scan # &pos := 1 # plan.scan := plan.scan advanced to next line # | else -> # plan.scan := plan.scan advanced to EOF #-] Scan_Next_Line ( plan.scan ); } #-- 5 -- #-[ if plan.log has any errors -> fail # | else -> return plan #-] Scan_Close ( plan.scan ); if Log_Error_Count ( plan.log ) = 0 then return plan else fail; end # --- Plan_Read --- # - - - P l a n _ B u i l d _ T o p i c - - - #-Invariants for arguments: # (1) plan.root is the root of a tree of topics # (2) topicStack is a list of topics such that the first is the root # topic and the list as a whole traces the path from the root to # the rightmost leaf #-[ if current subject is a valid topic line -> # plan.root := plan.root with new topic added as the next # child of the rightmost node at the next # higher level above the new topic, or # added as the root if the tree is empty # topicStack := topicStack adjusted to trace a path from # the root to the newly added leaf # plan.shortNameMap ||:= an entry mapping the new topic.shortName # to the new topic # return &null # | else if current subject is blank or empty -> # fail # | else -> # topicStack := # plan.log ||:= error message(s) # fail #-] procedure Plan_Build_Topic ( plan, topicStack ) #-- 1 -- #-[ topic := &null # rawTopic := &null #-] local topic # The topic being built and added local rawTopic # A rawTopicTag to hold the parsed fields #-- 2 -- #-[ if current subject is a syntactically valid topic line -> # rawTopic := a rawTopicTag with fields: # .depth := the depth of the topic from the subject # .title := the title field from the subject # .shortName := the short name field from the subject # | else if subject is empty or all blank -> fail # | else -> # plan.log ||:= plan.scan error message # fail #-] if not ( rawTopic := Plan_Parse_Topic ( plan ) ) then fail; #-- 3 -- #-[ if ( rawTopic.depth - 1 ) > ( size of topicStack ) -> # plan.log ||:= plan.scan error message, skipped levels # fails # | else -> # topicStack := topicStack minus all entries with depth >= # rawTopic.depth #-] if not Plan_Adjust_Topic_Stack ( plan, topicStack, rawTopic.depth ) then fail; #-- 4 -- #-[ if rawTopic is invalid -> # plan.log ||:= plan.scan error # fail # | if rawTopic is valid and topicStack is an empty list -> # plan.root := new topic made from rawTopic, topicStack, # and plan, with no parent # topicStack := topicStack || that new topic # plan.shortNameMap +:= an entry mapping that new topic's shortName # field to that new topic # | if rawTopic is valid and topicStack is nonempty -> # last topic in topicStack := same, with a new topic made from # rawTopic, topicStack, and plan # added as its new last child, # with the new child's parent link # pointing back up # topicStack := topicStack || new topic as above # plan.shortNameMap +:= an entry mapping the new topic's shortName # field to the topic #-] if not Plan_Add_Topic ( plan, topicStack, rawTopic ) then fail; #-- 6 -- return; end # --- Plan_Build_Topic --- # - - - P l a n _ P a r s e _ T o p i c - - - #-[ if current subject is a syntactically valid topic line -> # returns a rawTopic with fields: # .depth := depth of topic as an integer (root is 1) # .title := title field from subject # .shortName := short name field from subject # | else if subject is a blank or empty line -> fail # | else -> # plan.log ||:= plan.scan error message(s) # fail #-] procedure Plan_Parse_Topic ( plan ) local rawTopic #-- 1 -- #-[ if there are any nonblank characters in the subject -> # plan.scan := plan.scan advanced past initial whitespace # | else -> # plan.scan := plan.scan advanced to end of line # fail #-] tab ( many ( PLAN_WHITE_SET ) ); # Skip leading blanks if Scan_End_Line ( plan.scan ) then # Empty/blank case fail; #-- # This causes lines starting with CONTROL_CHAr to be ignored. # It allows `stylindex' to work with WebStyler 2.0 control lines. #-- if match ( CONTROL_CHAR ) then fail; #-- 2 -- #-[ if subject starts with OUTLINE_CHAR -> # plan.scan := plan.scan advanced past leading OUTLINE_CHAR # characters in subject and any following whitespace # rawtopic := a new rawTopicTag with fields: # .depth := number of leading OUTLINE_CHAR characters in subject # .title, .shortName := &null # | else -> # plan.log ||:= error message, must start with OUTLINE_CHAR # fail #-] if not = OUTLINE_CHAR then return Scan_Error ( plan.scan, "Each topic line must start with one or more `", OUTLINE_CHAR, "' characters." ); rawTopic := rawTopicTag ( ); rawTopic.depth := 1; # For the first OUTLINE_CHAR, already seen while = OUTLINE_CHAR do rawTopic.depth +:= 1; # Count additional OUTLINE_CHARs tab ( many ( PLAN_WHITE_SET ) ); # Skip blanks before the title #-- 3 -- #-[ if subject contains a BAR_CHAR -> # rawTopic.title := characters up to BAR_CHAR, with trailing # spaces trimmed # rawTopic.shortName := characters from BAR_CHAR to end of line, # with leading and trailing spaces trimmed # | else -> # plan.log ||:= error message, must contain BAR_CHAR # fail #-] if not ( rawTopic.title := trim ( tab ( upto ( BAR_CHAR ) ) ) ) then return Scan_Error ( plan.scan, "There must be a `", BAR_CHAR, "' separating the title and short name." ); move ( 1 ); # Move past the BAR_CHAR tab ( many ( PLAN_WHITE_SET ) ); # Skip blanks before the short name rawTopic.shortName := trim ( tab ( 0 ) ); #-- 4 -- #-[ if either rawTopic.title or rawTopic.shortName is empty -> # plan.scan.log ||:= scan error, empty field # fail # | else -> # return rawTopic #-] if ( ( * rawTopic.title ) = 0 ) then return Scan_Error ( plan.scan, "The title field is empty." ); if ( ( * rawTopic.shortName ) = 0 ) then return Scan_Error ( plan.scan, "The short name field is empty." ); return rawTopic; end # --- Plan_Parse_Topic --- # - - - P l a n _ A d j u s t _ T o p i c _ S t a c k - - - #-[ if ( rawTopic.depth - 1 ) > ( size of topicStack ) -> # plan.log ||:= plan.scan error, skipped level(s) # fails # | else -> # topicStack := topicStack minus all depths >= newDepth # returns &null #-] procedure Plan_Adjust_Topic_Stack ( plan, topicStack, newDepth ) if ( newDepth - * topicStack ) > 1 then return Scan_Error ( plan.scan, "This topic is at depth ", newDepth, " while the previous topic was at depth ", * topicStack ); while ( * topicStack ) >= newDepth do pull ( topicStack ); # Trim entries from the right return; end # --- Plan_Adjust_Topic_Stack --- # - - - P l a n _ A d d _ T o p i c - - - #-[ Let # newTopic(parent) == a new topicTag with fields: # .plan := plan # .title := rawTopic.title # .shortName := rawTopic.shortName # .path := expanded path name from rawTopic.shortName # .depth := rawTopic.depth # .parent := parent # .birthOrder := 1 if parent is &null, else 1 plus the number # of existing children of parent # .childList := &null # | if rawTopic is invalid -> # plan.log ||:= plan.scan error message(s) # fails # | else if rawTopic is valid and topicStack is empty -> # plan.root := newTopic(&null) # plan.shortNameMap +:= entry mapping that new topic's short name # to that new topic # topicStack ||:= that new topic # plan.ntopics +:= 1 # return &null # | else if rawTopic is valid and topicStack is nonempty -> # topicStack := topicStack || newTopic(topicStack[-1]), # with that new topic added as the new # child of newTopic(topicStack[-1]) # plan.shortNameMap +:= entry mapping rawTopic.shortName to # the new topic # plan.ntopics +:= 1 # return &null #-] procedure Plan_Add_Topic ( plan, topicStack, rawTopic ) #-- 1 -- #-[ topic := &null # path := &null # birthOrder := &null # parent := &null #-] local topic # The new topic being built local path # The expanded path name from rawTopic.shortName local birthOrder # The new topic's child number of its parent local parent # The parent topic #-- 2 -- #-[ if rawTopic.shortName is a valid short name -> # path := the path from the web root to the topic's directory, # expanded according to plan.pathMap || "/" || # file name from .shortName # | else -> # plan.scan.log ||:= scan error message, bad short name # fail #-] if not ( path := Path_Map_Expand_Short_Name ( plan.pathMap, rawTopic.shortName ) ) then return Scan_Error ( plan.scan, "The short name `", rawTopic.shortName, "' was not defined in the path map." ); #-- 3 -- #-[ if plan.shortNameMap already contains rawTopic.shortName -> # plan.scan.log ||:= scan error message, duplicate short name # fail # | else -> I #-] if \ plan.shortNameMap[rawTopic.shortName] then return Scan_Error ( plan.scan, "The short name `", rawTopic.shortName, "' is a duplicate." ); #-- 4 -- #-[ if topicStack is nonempty -> # parent := last element of the topic stack # birthOrder := 1 + (no. of children of last element of topic stack) # | else -> # birthOrder := 1 #-] if ( * topicStack ) > 0 then { #-- 4.1 -- parent := topicStack[-1]; birthOrder := Topic_N_Children(parent) + 1; } #-- 4.1 -- else { #-- 4.2 -- birthOrder := 1; } #-- 4.2 -- #-- 5 -- #-[ topic := a new topicTag with fields: # .plan := plan # .title := rawTopic.title # .shortName := rawTopic.shortName # .path := path # .depth := rawTopic.depth # .parent := parent # .birthOrder := birthOrder # plan.shortNameMap +:= an entry mapping rawTopic.shortName to # the new topicTag as above #-] topic := Topic_New ( plan, # Plan object rawTopic.title, # Topic title rawTopic.shortName, # Short name path, # Expanded pathname rawTopic.depth, # Topic depth parent, # Parent topic, or &null birthOrder ); # Birth order plan.shortNameMap[rawTopic.shortName] := topic; #-- 6 -- #-[ if parent is &null -> # plan.root := topic # | else -> # parent := parent with topic added as its new last child #-] if / parent then plan.root := topic else Topic_Add_Child ( parent, topic ); #-- 7 -- #-[ topicStack ||:= topic # plan.nTopics +:= 1 #-] put ( topicStack, topic ); plan.nTopics +:= 1; #-- 8 -- return; end # --- Plan_Add_Topic --- $endif