#! /usr/bin/python
# Copyright (c) 1999-2000 by Alex Schroeder
"""HTML-TOC
USAGE
html-toc [-dv] toc-file
OPTIONS
-d debuggin
-v verbose
EXAMPLES
creater master toc and link tags in all HTML files listed in the TOC file:
html-toc
FILES
The TOC file lists the HTML files that make up a web site in the
correct order. Every file can have one of the following attributes:
chapter, section, subsection, contents, index. All files with the
section attribute will be indented one level in the TOC (table of
contents); all files with the subsection attribute will be indented
two levels in the TOC.
Example:
contents index.html
index site-index.html
chapter awww.html
chapter emacs.html
chapter atlantis/index.html
section atlantis/juenger.html
The file with the contents attribute will have an HTML TOC block
updated. The HTML TOC block has an entry for each HTML file in the
TOC file. Each entry consists of a link to the HTML file, the name of
the HTML file and a description of the HTML file. The name is taken
from the first H1 header found in the HTML file (stripping all HTML
tags). The description is taken from the relevant META tag.
Here is an example HTML fragment:
Alex: Emacs
Emacs
The name of the HTML will be "Emacs" and not "Alex: Emacs". The
content of the TITLE tag is intended to be used in bookmark files of
visitors; it is not supposed to appear in the TOC.
The HTML TOC block replaced in the HTML file with the contents
attribute is recognized by the following starting fragment (could be
any heading level from 1 to 6):
...
...
RESULT
Every HTML file will have an HTML LINK block updated. An HTML LINK
block consists of several consecutive lines of LINK tags. It contains
a link rev="chapter" and a link rel="contents" tag to the file
with the contents attribute, a link rel="index" tag to the file with
the index attribute, a link rel="next" and a link rel="prev" tag
to the files before and after itself in the TOC, and a link rel="up"
tag to the last file with a smaller indentation level (or to the file
with the contents attribute if there is no such file).
Example:
"""
import getopt
import sys
import string
import re
import os
verbose = 0
def main():
# must be declared global in order to change it
global verbose
try:
opts, args = getopt.getopt(sys.argv[1:], 'vd')
except getopt.error, msg:
sys.stdout = sys.stderr
print msg
print __doc__%globals()
sys.exit(2)
for o, a in opts:
if o == '-v':
verbose = verbose + 1
if o == '-d':
verbose = verbose + 2
if len(args) != 1:
sys.exit("A TOC file is required")
Toc(args[0]).update()
class Toc:
def __init__(self, filename):
try:
f = open(filename)
except IOError, msg:
sys.exit(msg)
self.toc = []
for line in f.readlines():
line = string.strip(line)
(attr, filename) = string.split(line,None,1)
entry = Entry(filename,attr)
self.toc.append(entry)
if verbose >= 2:
print entry
f.close()
if len(self.toc) == 0:
sys.exit("%s: No pages listed" % filename)
def update(self):
self.update_toc()
self.update_files()
self.update_contents(self.master_toc())
def update_toc(self):
"Complete the datastructure."
# determine the contents and index pages
self.find_special_pages()
# set all the links
up_links = []
level = self.starting_level()
pagecount = len(self.toc)
for i in range(pagecount):
# set previous link if not on the first page
if i > 0:
self.toc[i].prev = self.toc[i-1]
# set next link if not on the last page
if i+1 < pagecount:
self.toc[i].next = self.toc[i+1]
# if on the same level, use the same up link as the previous page.
if self.toc[i].level == level and self.toc[i].prev:
self.toc[i].up = self.toc[i].prev.up
# if on a higher level, use the previous page as an up
# link and add the current link to the list of up links.
while self.toc[i].level > level:
self.toc[i].up = self.toc[i].prev
up_links.append(self.toc[i])
level = level + 1
# if on a lower level, pop an up link from the list of up links.
while self.toc[i].level < level:
self.toc[i].up = up_links.pop().prev.up
level = level - 1
# set contents and index link
self.toc[i].contents = self.contents
self.toc[i].index = self.index
if verbose > 1:
print "%d %s prev: %s, next: %s, up: %s, contents: %s, index: %s" % (
self.toc[i].level, self.toc[i],
self.toc[i].prev_link(),
self.toc[i].next_link(),
self.toc[i].up_link(),
self.toc[i].contents_link(),
self.toc[i].index_link())
def find_special_pages(self):
self.contents = None
self.index = None
for page in self.toc:
if page.attr == 'contents':
self.contents = page
if page.attr == 'index':
self.index = page
def starting_level(self):
level = self.toc[0].level
if level != 1:
sys.exit("First page ('%s') has type '%s' which is not on level 1"
% (self.toc[0].filename, self.toc[0].attr))
return level
def update_files(self):
"Replace LINK tags in the HTML files."
for page in self.toc:
links = ''
# This used to contain at least one LINK REV links but doesn't anymore... :)
if page.attr != 'contents' and page.contents:
links = links + '\n' % (
page.contents_link(), page.contents.name )
if page.attr != 'index' and page.index:
links = links + '\n' % (
page.index_link(), page.index.name )
if page.next:
links = links + '\n' % (
page.next_link(), page.next.name )
if page.prev:
links = links + '\n' % (
page.prev_link(), page.prev.name )
if page.up:
links = links + '\n' % (
page.up_link(), page.up.name )
self.update_links(page.filename, links)
links_regexp = re.compile( r"(\s*)+", re.IGNORECASE | re.DOTALL )
def update_links(self, filename, links):
"Open FILENAME and replace the link block with the LINKS string."
try:
f = open(filename)
except IOError, msg:
sys.exit(msg)
text = f.read()
replacement = self.links_regexp.sub(links, text, 1)
f.close()
if verbose > 1:
print filename
print links
print "old:"
print text
print "new:"
print replacement
if text != replacement:
os.rename(filename, filename + "~")
self.write_file(filename, replacement)
else:
if verbose > 0:
print "No changes in %s" % filename
def write_file(self, filename, text):
"Write TEXT into FILENAME."
try:
f = open(filename, "w")
except IOError, msg:
sys.exit(msg)
f.write(text)
f.close()
def master_toc(self):
"Return an HTML TOC block."
level = 0
toc_block = ''
for page in self.toc:
while page.level > level:
toc_block = toc_block + "
%s: %s\n" % (
self.contents.link_to(page), page.name, page.desc )
while level > 0:
toc_block = toc_block + "\n"
level = level - 1
return toc_block
toc_regexp = re.compile( r"(.*?).*?\n\n",
re.IGNORECASE | re.DOTALL | re.MULTILINE )
def update_contents(self, toc_block):
"Replace HTML TOC block in the contents file."
try:
f = open(self.contents.filename)
except IOError, msg:
sys.exit(msg)
text = f.read()
match = self.toc_regexp.search(text)
title = match.group(1) + "\n"
replacement = self.toc_regexp.sub(title + toc_block + "\n", text, 1)
f.close()
if text != replacement:
os.rename(self.contents.filename, self.contents.filename + "~")
self.write_file(self.contents.filename, replacement)
else:
if verbose > 0:
print "No TOC changes in %s" % self.contents.filename
class Entry:
name_regexp = re.compile( r"(.*?)", re.IGNORECASE | re.DOTALL )
desc_regexp = re.compile( r"",
re.IGNORECASE | re.DOTALL )
tag_regexp = re.compile( r"<.*?>", re.DOTALL )
level_equiv = { 'chapter': 1,
'section': 2,
'subsection': 3,
'index' : 1,
'contents': 1 }
def __init__(self, filename, attr):
self.filename = filename
self.attr = attr
try:
self.level = self.level_equiv[attr]
except KeyError:
sys.exit("TOC lists %s with unknown attribute '%s'" % (filename, attr))
self.name = ''
self.desc = ''
self.parse()
# Links; there are no methods to set these, set them directly.
# These contain references to Entry objects.
# Use the *_link() methods to retrieve the relative links.
self.prev = None
self.next = None
self.up = None
self.index = None
self.contents = None
def should_have_desc(self):
"Return true for all entries except for the contents entry."
return self.attr != "contents"
def parse(self):
"Open file and determine name and description of the page. Set self.name and self.desc."
try:
f = open(self.filename)
except IOError, msg:
sys.exit(msg)
text = f.read()
self.name = self.find_name(text)
if self.should_have_desc():
self.desc = self.find_desc(text)
f.close()
def find_name(self, text):
"Find the name of the page in the file by looking at the H1 tag."
match = self.name_regexp.search(text)
if match:
return self.strip_html(match.group(1))
else:
sys.exit("%s: No H1 tag to determine the name of the page\n" % self.filename)
def find_desc(self, text):
"Find the description of the page in the file by looking at the corresponding META tag."
match = self.desc_regexp.search(text)
if match:
return self.strip_html(match.group(2))
else:
sys.exit("%s: No META tag to determine the description of the page\n" % self.filename)
def strip_html(self, fragm):
"Strip a fragment of HTML code of all tags and collapse whitespace."
# Remove tags
fragm = self.tag_regexp.sub('', fragm)
# Collapse whitespace.
fragm = string.join(string.split(fragm))
# Remove leading and trailing space, if any.
fragm = string.strip(fragm)
return fragm
def __str__(self):
if verbose > 3:
return "%s (%s: %d): %s -- %s..." % (self.filename, self.attr, self.level, self.name, self.desc[:15])
else:
return self.filename
def prev_link(self):
return self.link_to(self.prev)
def next_link(self):
return self.link_to(self.next)
def up_link(self):
return self.link_to(self.up)
def contents_link(self):
return self.link_to(self.contents)
def index_link(self):
return self.link_to(self.index)
def link_to(self, other):
"Return the relativ link to the other entry."
# When creating a forward link between two absolute filenames,
# you have to keep all excess directories in the to-file, or
# you must prefix the to-file with ../ for each excess
# directory in the from-file.
if other == None:
return None
src = string.split(self.filename, '/')
dest = string.split(other.filename, '/')
link = ''
# If the to-file file is in a directory ABOVE the from-file,
# eg. the from-file is 'a/b/c/d/source.html' and the to-file
# is 'a/b/c/target.html', the link is '../target.html'.
# If the to-file file is in a directory BELOW the from-file,
# eg. the from-file is 'a/b/c/source.html' and the to-file is
# 'a/b/c/d/target.html', the link is 'd/target.html'.
# Start by stripping directories that are equal.
while src[0] == dest[0] and len(src) > 1 and len(dest) > 1:
src = src[1:]
dest = dest[1:]
# If src has more elements than just the filename, the target is
# ABOVE: Replace each directory from src with '../'.
for i in src[:-1]:
link = link + '../'
# If dest has more than the filename, the target is BELOW: Add
# all the directories from dest, including the filename.
link = link + string.join(dest,'/')
return link
if __name__ == '__main__':
main()