#!/usr/local/bin/python
"""pystyler.py: A Web style generator program.
Author: John W. Shipman (john@nmt.edu), Applications Specialist,
New Mexico Tech Computer Center, Socorro, NM 87801
Written: July 1999
$Revision: 1.25 $ $Date: 2002/08/01 07:17:35 $
Revision history:
2.03 2002-08-01: Previously, PyStyler did not allow hyphens
to be used in attribute names. However, when XSLT
generates a file, it always includes a tag like
", so clearly
hyphens *should* be allowed.
2.02 2001-01-16: Fixed a bug found by Hedges. If a page
links to an anchor elsewhere in itself, such as
in page foo.g, the result link had
href=".html#anch" instead of href="foo.html#anch".
2.01 2000-06-06:
- Added "-u" option to check that all page titles are unique.
- Force page rewrite if Plan file is newer than .html file.
- Report number of pages rewritten.
2.00 Complete rewrite in Python, May 2000
1.00 Original version in Icon, Oct.--Nov. 1995
"""
EXTERNAL_VERSION = "2.03"
PROGRAM_NAME = "PyStyler"
#----------------------------------------------------------------
# Imports
#----------------------------------------------------------------
import sys # Standard system functions
import getopt # Command line argument processing functions
import string # Standard string functions
import os # For pathname operations
import stat # For filename status tuple interpretation
#--
# From author's generic Python library
#--
sys.path.insert(0, "/u/john/tcc/python/lib")
import log # Author's error logging object
import pathinfo # Takes snapshots of file status
#--
# Other modules in primary source directory
#--
sys.path.insert(0, "/u/john/projects/pystyler")
from pathmap import * # Represents the PathMap file
from plan import * # Represents the Plan file
from template import * # Template and TmplPool objects
from body import * # Body (input) files
#----------------------------------------------------------------
# Manifest constants
#----------------------------------------------------------------
#--
# Command line options
#--
FORCE_ARG = "f" # -f: Force writing of all output files
UNIQUE_ARG = "u" # -u: Report non-unique page titles
WRITE_ARG = "w" # -w: Write to specified directory tree
NAV_ARG = "n" # -n: Generate navigational shock report
#--
# File names and extensions
#--
PATH_MAP_NAME = "PathMap" # Source for PathMap object
PLAN_NAME = "Plan" # Source for Plan object
LOG_NAME = "webstyler.log" # Error log file
#--
# The next two pertain to the `navigational shock report'
#--
INDENT_PER_DEPTH = 3 # Amount to indent each outline level
VAR_INDENT = 4 # Amount to indent link variants
# - - - VERIFICATION FUNCTIONS - - -
#----------------------------------------------------------------
# These defines are used in Cleanroom verification.
#----------------------------------------------------------------
# all-output-pages(args, pathMap, plan) ==
# the files corresponding to the set of pages P named in plan,
# where each file's name is output-path(P, args, pathMap)
#--
# This is the set of output pages to be written if the force (-f)
# option is given.
#----------------------------------------------------------------
# all-subtree-pages(topic, force, outRoot, pathMap, plan) ==
# for all topics T at or under topic in the topic tree, the
# set of output files named output-path(T, outRoot, pathMap, plan)
#--
# The set of pages rewritten if the FORCE_ARG is not given on the
# command line.
#----------------------------------------------------------------
# effective-template(topic, contents) ==
# if contents includes a tag ->
# the file named by that tag
# else ->
# topic's template
#--
# There are three different sources for templates:
# 1. There is a default template file whose name is given by
# template.DEFAULT_TEMPLATE.
# 2. A given topic may be affected by a "$template" control
# line in the plan file, overriding the default template.
# 3. The input file may contain a tag. This tag is
# deprecated; see the specification for a discussion of why.
#----------------------------------------------------------------
# expand-file(topic, args, pathMap, plan) ==
# file named by input-file-name(topic, pathMap), expanded
# according to effective-template(P, contents of that file)
#----------------------------------------------------------------
# input-dir(topic, pathMap) ==
# topic's path part expanded by pathMap
#--
# The directory in which a given topic's input file is located.
#----------------------------------------------------------------
# input-file-name(P, pathMap) ==
# input-dir(P, pathMap) + input-name(P)
#----------------------------------------------------------------
# input-files(pathMap, plan) ==
# all files named in plan, with short names expanded as per
# pathMap, with .html appended
#----------------------------------------------------------------
# input-name(topic) ==
# topic's file name part
#----------------------------------------------------------------
# nav-report(args, plan) ==
# report showing all topics P in plan in postorder, showing the
# tree location of each by section numbers (e.g., 2.9.3),
# followed by a report showing all variant link texts from
# elements that refer to P
#--
# This report is specified by NAV_ARG on the command line.
#----------------------------------------------------------------
# old-output-pages(args, pathMap, plan) ==
# members of all-output-pages(args, pathMap, plan) for which
# no output file exists, or for which the output file's
# modification time > max (mod-time of corresponding input file,
# mod-time of file's template from plan
#--
# This is the set of output pages to be written if the force
# (-f) option is given. Note: see the specification, under the
# discussion of why the tag is deprecated, for ugly
# functional problems related to this tag.
#----------------------------------------------------------------
# old-subtree-pages(topic, force, outRoot, pathMap, plan) ==
# members of all-subtree-pages(topic, force, outRoot, pathMap, plan)
# for which the output file is nonexistent, or (existing
# file's modification time > max (mod-time of the corresponding
# input file, mod-time of topic's effective template according
# to plan)
#--
# Like old-output-pages, but used in the method that recursively
# expands a subtree of the topic tree.
#----------------------------------------------------------------
# output-contents(args, pathMap, plan) ==
# for all entries P in plan, expand-file(P, args, pathMap, plan)
#--
# This describes the content written to an .html file.
#----------------------------------------------------------------
# output-dir(topic, outRoot, pathMap) ==
# output-root(outRoot) + rel-path(topic, pathMap)
#--
# The directory in which a given output file lives.
#----------------------------------------------------------------
# output-name(topic) ==
# the filename part of topic's short name
#----------------------------------------------------------------
# output-pages(force, args, pathMap, plan) ==
# if force ->
# all-output-pages(args.force, args.outRoot, pathMap, plan)
# else ->
# old-output-pages(args.force, args.outRoot, pathMap, plan)
#--
# This defines the set of .html files that will be affected by
# the program's operation.
#----------------------------------------------------------------
# output-path(topic, outRoot, pathMap) ==
# output-dir(topic, outRoot, pathMap) + "/" + output-name(topic)
#----------------------------------------------------------------
# output-root(outRoot) ==
# if outRoot is nonempty -> outRoot
# else -> "."
#--
# The root directory for all output pages.
#----------------------------------------------------------------
# rel-path(topic, pathMap) ==
# "/" + (short name from topic as expanded by pathMap)
#--
# This is the part of an output page's path name between the
# effective output root directory and the page's file.
#----------------------------------------------------------------
# subtree-output-pages(topic, force, outRoot, pathMap, plan) ==
# if force ->
# all-subtree-pages(topic, force, outRoot, pathMap, plan)
# else ->
# old-subtree-pages(topic, force, outRoot, pathMap, plan)
#--
# This definition is like output-pages but is used in the method
# that recursively expands a subtree of the topic tree.
#----------------------------------------------------------------
# - - - - - c l a s s A r g s - - - - -
class Args:
"""Represents the command line arguments.
Exports:
Args ( ):
[ if sys.argv describes a valid set of command line arguments
for this program ->
return a new Args object representing those arguments
else ->
Log() +:= error message
stop execution ]
.force:
[ if self includes the force option -> 1
else -> 0 ]
.unique:
[ if self includes the unique-title option -> 1
else -> 0 ]
.outRoot:
[ if self includes the output root directory option ->
that directory as a string
else -> None ]
.nav:
[ if self includes the navigation shock report flag -> 1
else -> 0 ]
"""
def __init__ ( self ):
"""Constructor for the Args object
"""
#-- 1 --
# [ switchPairs := a sequence of 2-sequences (opt, val) where
# opt is each option and val is the value for
# options that take values
# argList := list of non-switch options ]
switches = string.join ( [ FORCE_ARG,
UNIQUE_ARG,
WRITE_ARG, ":",
NAV_ARG ], "" )
switchPairs, argList = getopt.getopt ( sys.argv[1:], switches )
#-- 2 --
# [ self.force := Boolean value of -f switch from switchPairs
# self.nav := Boolean value of -n switch from switchPairs
# self.outRoot := string value of -w switch if given, else None ]
self.force = 0
self.unique = 0
self.outRoot = None
self.nav = 0
for switch, value in switchPairs:
if switch == "-w": self.outRoot = value
elif switch == "-f": self.force = 1
elif switch == "-u": self.unique = 1
elif switch == "-n": self.nav = 1
# - - - r e a d P l a n - - -
def readPlan():
"""Read primary input files: PathMap, Plan, Template
[ if (PATH_MAP_NAME names a readable, valid PathMap file)
and (PLAN_NAME names a readable, valid Plan file)
and (plan's default template file is valid) ->
pathMap := a PathMap object representing PATH_MAP_NAME
plan := a Plan object representing PLAN_NAME, with
the default template as the sole entry in
its template stack
else ->
Log() +:= error message(s)
stop execution ]
"""
global pathMap, plan
#-- 1 --
# [ if PATH_MAP_NAME names a readable, valid path map file ->
# pathMap := a PathMap object representing that file
# else ->
# +:= error message(s)
# stop execution ]
try:
pathMap = PathMap ( PATH_MAP_NAME )
except IOError, detail:
Log().fatal ( "Terminated due to errors in the %s file." %
PATH_MAP_NAME )
#-- 2 --
# [ if PLAN_NAME names a readable, valid plan file ->
# plan := a Plan object representing that file
# else ->
# +:= error message(s)
# stop execution ]
try:
plan = Plan ( pathMap, PLAN_NAME, unique=args.unique )
except IOError, detail:
Log().fatal ( "Terminated due to errors in the %s file." %
PLAN_NAME )
#-- 3 --
# [ if plan's default template file is valid ->
# template := a Template object representing that file
# else ->
# +:= error message(s)
# stop execution ]
template = plan.lookupTemplate ( None )
if template is None:
Log().fatal ( "Terminated due to errors in the template file." )
# - - - n a v R e p o r t - - -
def navReport():
"""Produce the `navigational shock report'
[ +:= navigational shock report from plan ]
Output format:
----------------------------------------------------------------
root-title (root-shortname)
[root-variants]
1. first-child-title (first-child-shortname)
[first-child-variants]
1.1 ...
----------------------------------------------------------------
where variants are shown as:
`link-text' (ref-short-name lineno)
"""
navSubtree ( plan.root )
# - - - n a v S u b t r e e - - -
def navSubtree ( topic ):
"""Generate navigational shock report for subtree rooted at topic
[ if topic is a Topic object ->
+:= navigational shock report for the subtree
rooted at (topic) ]
"""
#-- 1 --
# [ +:= report for node (topic) ]
navTopic ( topic )
#-- 2 --
# [ +:= report for children of (topic), if any ]
for childx in range(topic.nChildren()):
childTopic = topic.nthChild ( childx )
navSubtree ( childTopic )
# - - - n a v T o p i c - - -
def navTopic ( topic ):
"""Generate navigational shock report for one topic
[ if topic is a Topic object ->
+:= navigational shock report for (topic) ]
"""
#-- 1 --
# [ indent := spaces needed to indent to topic's level ]
indent = " " * (INDENT_PER_DEPTH * topic.depth)
#-- 2 --
# [ sectNos := the string "Ci.Cj.Ck. ..." where the sequence of
# C-values is the sequence of child numbers on the
# path between the root topic and (topic), with 1
# added to each element ]
pathNos = topic.fullPath()
for i in range(len(pathNos)):
pathNos[i] = pathNos[i] + 1
sectNos = string.join ( map ( str, pathNos ), "." )
#-- 3 --
# [ if topic.depth is 0 ->
# +:= topic's title and short name
# else ->
# +:= indent + sectNos + (topic's title and short name) ]
if topic.depth == 0:
print "%s (%s)" % ( topic.title, topic.shortName )
else:
print ( "%s%s %s (%s)" %
( indent, sectNos, topic.title, topic.shortName ) )
#-- 4 --
# [ +:= link variants for topic, if any ]
navVariants ( topic )
# - - - n a v V a r i a n t s - - -
def navVariants ( topic ):
"""Display report of link text variants for topic, if any
[ +:= list of all text variants for topic, if any ]
"""
#-- 1 --
# [ indent := indentation string for topic's level, plus extra
# indentation for text variants ]
indent = " " * (INDENT_PER_DEPTH * topic.depth + VAR_INDENT)
#-- 2 --
# [ +:= link variants of topic, one per line, each
# line prefixed by (indent) ]
#--
# NB: The variant list is currently in Plan order. It might be
# more useful to sort it alphabetically, either in place or
# make a copy and sort it here.
#--
for var in topic.linkVarList:
print ( "%s`%s' (%s %d)" %
( indent, var.text, var.shortName, var.lineNo ) )
# - - - - - p y s t y l e r . p y - - m a i n - - - - -
# [ if (sys.args is syntactically valid for this program)
# and (PATH_MAP_NAME names a readable, valid PathMap file)
# and (PLAN_NAME) names a readable, valid Plan file)
# and (the default Template file is a readable, valid Template file) ->
# output-pages(sys.args, pathmap-file, plan-file, template-file)
# := output-contents(sys.args, pathmap-file, plan-file,
# input-files(pathmap-file, plan-file)
# Log(LOG_NAME) +:= (opening message) +
# (error messages, if any) +
# (final message)
# +:= nav-report(sys.args, pathmap-file, plan-file)
# else ->
# Log(LOG_NAME) +:= error message(s) ]
#-- 1 --
# [ Log() +:= opening message ]
Log().addLogFile ( LOG_NAME )
Log().write ( "==== %s %s ====" % ( PROGRAM_NAME, EXTERNAL_VERSION ) )
#-- 2 --
# [ if command line arguments are valid ->
# args := an Args object representing the command line arguments
# else ->
# +:= error message(s)
# stop execution ]
args = Args ( )
#-- 3 --
# [ if (PATH_MAP_NAME names a readable, valid PathMap file)
# and (PLAN_NAME names a readable, valid Plan file)
# and (plan's default template file is valid) ->
# pathMap := a PathMap object representing PATH_MAP_NAME
# plan := a Plan object representing PLAN_NAME, with
# the default template as the sole entry in
# its template stack
# else ->
# Log() +:= error message(s)
# stop execution ]
readPlan()
#-- 4 --
# [ output-pages(args, pathMap, plan) :=
# output-contents(args, pathmap, plan, input-files(pathMap, plan))
# Log() +:= error messages, if any, from expanding input-files
# plan := plan with text variants added from elements in
# input-files
# nChanged := number of pages actually rewritten ]
nChanged = plan.root.expandTree ( args.force, args.outRoot )
#-- 6 --
# [ if Log() contains no errors ->
# errCount := 0
# Log() +:= count of errors in Log()
# else ->
# Log() +:= (count of errors in Log()) + (termination message)
# stop execution ]
errCount = Log().count()
Log().write ( "==== Error count: %d" % errCount )
if errCount > 0:
Log().fatal ( "Terminated due to error in input files." )
#-- 7 --
# [ if args.nav is true ->
# +:= navigational shock report from plan
# else -> I ]
if args.nav:
navReport ( )
#-- 8 --
# [ Log() +:= final message ]
Log().write ( "==== %s complete, wrote %d of %d pages." %
( PROGRAM_NAME, nChanged, plan.nTopics ) )