"""Classes to read and write @file nodes."""


from leoGlobals import *
import leoColor,leoNodes
import filecmp,os,time

# These constants must be global to this module because they are shared by several classes.

# The kind of at_directives.
noDirective		   =  1 # not an at-directive.
# not used      =  2
docDirective	   =  3 # @doc.
atDirective		   =  4 # @<space> or @<newline>
codeDirective	  =  5 # @code
cDirective		    =  6 # @c<space> or @c<newline>
othersDirective	=  7 # at-others
miscDirective	  =  8 # All other directives
rawDirective    =  9 # @raw
endRawDirective = 10 # @end_raw

# The kind of sentinel line.
noSentinel		 = 20 # Not a sentinel
# not used   = 21
endAt			 = 22 # @-at
endBody			 = 23 # @-body
endDoc			 = 24 # @-doc
endLeo			 = 25 # @-leo
endNode			 = 26 # @-node
endOthers		  = 27 # @-others

# not used     = 40
startAt			   = 41 # @+at
startBody		    = 42 # @+body
startDoc		     = 43 # @+doc
startLeo		     = 44 # @+leo
startNode		    = 45 # @+node
startOthers		  = 46 # @+others

startComment   = 60 # @comment
startDelims		  = 61 # @delims
startDirective	= 62 # @@
startRef		     = 63 # @< < ... > >
startVerbatim	 = 64 # @verbatim
startVerbatimAfterRef = 65 # @verbatimAfterRef (3.0 only)

# New in 4.0...
startAfterRef  = 70 # @afterref (4.0)
startNl        = 71 # @nl (4.0)
startNonl      = 72 # @nonl (4.0)
	
sentinelDict = {
	# Unpaired sentinels: 3.x and 4.x.
	"@comment" : startComment,
	"@delims" :  startDelims,
	"@verbatim": startVerbatim,
	# Unpaired sentinels: 3.x only.
	"@verbatimAfterRef": startVerbatimAfterRef,
	# Unpaired sentinels: 4.x only.
	"@afterref" : startAfterRef,
	"@nl"       : startNl,
	"@nonl"     : startNonl,
	# Paired sentinels: 3.x only.
	"@+body":   startBody,   "@-body":   endBody,
	# Paired sentinels: 3.x and 4.x.
	"@+at":     startAt,     "@-at":     endAt,
	"@+doc":    startDoc,    "@-doc":    endDoc,
	"@+leo":    startLeo,    "@-leo":    endLeo,
	"@+node":   startNode,   "@-node":   endNode,
	"@+others": startOthers, "@-others": endOthers }

class baseAtFile:
	"""The base class for the top-level atFile subcommander."""
	def __init__(self,c):
		
		self.c = c
		self.fileCommands = self.c.fileCommands
		
		# Create subcommanders to handler old and new format derived files.
		self.old_df = oldDerivedFile(c)
		self.new_df = newDerivedFile(c)
		
		self.initIvars()
		
	def initIvars(self):
		
		# Set by scanDefaultDirectory.
		self.default_directory = None
		self.errors = 0
	
		# Set by scanHeader when reading. Set by scanAllDirectives...
		self.encoding = app.config.default_derived_file_encoding
		self.endSentinelComment = None
		self.startSentinelComment = None
	def error(self,message):
	
		es(message,color="red")
		print message
		self.errors += 1
	def readAll(self,root,partialFlag=false):
		
		"""Scan vnodes, looking for @file nodes to read."""
	
		at = self ; c = at.c
		c.endEditing() # Capture the current headline.
		anyRead = false
		at.initIvars()
		v = root
		if partialFlag: after = v.nodeAfterTree()
		else: after = None
		while v and v != after:
			if v.isAtIgnoreNode():
				v = v.nodeAfterTree()
			elif v.isAtFileNode() or v.isAtRawFileNode():
				anyRead = true
				if partialFlag:
					# We are forcing the read.
					at.read(v)
				else:
					# if v is an orphan, we don't expect to see a derived file,
					# and we shall read a derived file if it exists.
					wasOrphan = v.isOrphan()
					ok = at.read(v)
					if wasOrphan and not ok:
						# Remind the user to fix the problem.
						v.setDirty()
						c.setChanged(true)
				v = v.nodeAfterTree()
			else: v = v.threadNext()
		# Clear all orphan bits.
		v = root
		while v:
			v.clearOrphan()
			v = v.threadNext()
			
		if partialFlag and not anyRead:
			es("no @file nodes in the selected tree")
	# The caller has enclosed this code in beginUpdate/endUpdate.
	
	def read(self,root,importFileName=None):
		
		"""Common read logic for any derived file."""
		
		at = self ; c = at.c
		at.errors = 0
		at.scanDefaultDirectory(root)
		if at.errors: return
		if importFileName:
			fileName = importFileName
		elif root.isAtFileNode():
			fileName = root.atFileNodeName()
		else:
			fileName = root.atRawFileNodeName()
			
		if not fileName:
			at.error("Missing file name.  Restoring @file tree from .leo file.")
			return false
		fn = os_path_join(at.default_directory,fileName)
		fn = os_path_normpath(fn)
		
		try:
			# 11/4/03: open the file in binary mode to allow 0x1a in bodies & headlines.
			file = open(fn,'rb')
			if file:
				try:
					read_only = not os.access(fn,os.W_OK)
					if read_only:
						es("read only: " + fn,color="red")
				except:
					pass # os.access() may not exist on all platforms.
			else: return false
		except:
			at.error("Can not open: " + '"@file ' + fn + '"')
			root.setDirty()
			return false
		es("reading: " + root.headString())
		firstLines,read_new = at.scanHeader(file,fileName)
		df = choose(read_new,at.new_df,at.old_df)
		df.importing = importFileName != None
		df.importRootSeen = false
		
		# Set by scanHeader.
		df.encoding = at.encoding
		df.endSentinelComment = at.endSentinelComment
		df.startSentinelComment = at.startSentinelComment
		
		# Set other common ivars.
		df.errors = 0
		df.file = file
		df.targetFileName = fileName
		df.indent = 0
		df.raw = false
		df.root = root
		df.root_seen = false
		root.clearVisitedInTree()
		try:
			df.readOpenFile(root,file,firstLines)
		except:
			at.error("Unexpected exception while reading derived file")
			es_exception()
		file.close()
		root.clearDirty() # May be set dirty below.
		after = root.nodeAfterTree()
		v = root
		while v and v != after:
			try: s = v.t.tempBodyString
			except: s = ""
			if s and not v.t.isVisited():
				at.error("Not in derived file:" + v.headString())
				v.t.setVisited() # One message is enough.
			v = v.threadNext()
		if df.errors == 0:
			if not df.importing:
				v = root
				while v and v != after:
					try: s = v.t.tempBodyString
					except: s = ""
					if s != v.bodyString():
						es("changed: " + v.headString(),color="blue")
						if 0: # For debugging.
							print ; print "changed: " + v.headString()
							print ; print "new:",`s`
							print ; print "old:",`v.bodyString()`
						v.setBodyStringOrPane(s) # Sets v and v.c dirty.
						v.setMarked()
					v = v.threadNext()
		v = root
		while v and v != after:
			if hasattr(v.t,"tempBodyString"):
				delattr(v.t,"tempBodyString")
			v = v.threadNext()
		return df.errors == 0
	def scanDefaultDirectory(self,v):
		
		"""Set default_directory ivar by looking for @path directives."""
	
		at = self ; c = at.c
		at.default_directory = None
		# An absolute path in an @file node over-rides everything else.
		# A relative path gets appended to the relative path by the open logic.
		
		# Bug fix: 10/16/02
		if v.isAtFileNode():
			name = v.atFileNodeName()
		elif v.isAtRawFileNode():
			name = v.atRawFileNodeName()
		elif v.isAtNoSentinelsFileNode():
			name = v.atNoSentinelsFileNodeName()
		else:
			name = ""
		
		dir = choose(name,os_path_dirname(name),None)
		
		if dir and os_path_isabs(dir):
			if os_path_exists(dir):
				at.default_directory = dir
			else:
				at.default_directory = makeAllNonExistentDirectories(dir)
				if not at.default_directory:
					at.error("Directory \"" + dir + "\" does not exist")
		if at.default_directory:
			return
	
		while v:
			s = v.t.bodyString
			dict = get_directives_dict(s)
			if dict.has_key("path"):
				# We set the current director to a path so future writes will go to that directory.
				
				k = dict["path"]
				j = i = k + len("@path")
				i = skip_to_end_of_line(s,i)
				path = string.strip(s[j:i])
				
				# Remove leading and trailing delims if they exist.
				if len(path) > 2 and (
					(path[0]=='<' and path[-1] == '>') or
					(path[0]=='"' and path[-1] == '"') ):
					path = path[1:-1]
				
				path = path.strip()
				
				if path and len(path) > 0:
					base = getBaseDirectory() # returns "" on error.
					path = os_path_join(base,path)
					
					if os_path_isabs(path):
						# path is an absolute path.
						
						if os_path_exists(path):
							at.default_directory = path
						else:
							at.default_directory = makeAllNonExistentDirectories(path)
							if not at.default_directory:
								at.error("invalid @path: " + path)
					else:
						at.error("ignoring bad @path: " + path)
				else:
					at.error("ignoring empty @path")
				
				return
			v = v.parent()
	
		# This code is executed if no valid absolute path was specified in the @file node or in an @path directive.
		
		assert(not at.default_directory)
		
		if c.frame :
			base = getBaseDirectory() # returns "" on error.
			for dir in (c.tangle_directory,c.frame.openDirectory,c.openDirectory):
				if dir and len(dir) > 0:
					dir = os_path_join(base,dir)
					if os_path_isabs(dir): # Errors may result in relative or invalid path.
						if os_path_exists(dir):
							at.default_directory = dir ; break
						else:
							at.default_directory = makeAllNonExistentDirectories(dir)
		if not at.default_directory:
			# This should never happen: c.openDirectory should be a good last resort.
			at.error("No absolute directory specified anywhere.")
			at.default_directory = ""
	def scanHeader(self,file,fileName):
		
		"""Scan the @+leo sentinel.
		
		Sets self.encoding, and self.start/endSentinelComment.
		
		Returns (firstLines,new_df) where:
		firstLines contains all @first lines,
		new_df is true if we are reading a new-format derived file."""
		
		at = self
		new_df = false # Set default.
		firstLines = [] # The lines before @+leo.
		version_tag = "-ver="
		tag = "@+leo" ; encoding_tag = "-encoding="
		valid = true
		# Queue up the lines before the @+leo.  These will be used to add as 
		# parameters to the @first directives, if any.  Empty lines are 
		# ignored (because empty @first directives are ignored). NOTE: the 
		# function now returns a list of the lines before @+leo.
		# 
		# We can not call sentinelKind here because that depends on the 
		# comment delimiters we set here.  @first lines are written 
		# "verbatim", so nothing more needs to be done!
		
		s = at.readLine(file)
		while len(s) > 0:
			j = s.find(tag)
			if j != -1: break
			firstLines.append(s) # Queue the line
			s = at.readLine(file)
		n = len(s)
		valid = n > 0
		# s contains the tag
		i = j = skip_ws(s,0)
		# The opening comment delim is the initial non-whitespace.
		# 7/8/02: The opening comment delim is the initial non-tag
		while i < n and not match(s,i,tag) and not is_nl(s,i):
			i += 1
		if j < i:
			at.startSentinelComment = s[j:i]
		else: valid = false
		# REM hack: leading whitespace is significant before the @+leo.  We do 
		# this so that sentinelKind need not skip whitespace following 
		# self.startSentinelComment.  This is correct: we want to be as 
		# restrictive as possible about what is recognized as a sentinel.  
		# This minimizes false matches.
		
		if 0:# 7/8/02: make leading whitespace significant.
			i = skip_ws(s,i)
		
		if match(s,i,tag):
			i += len(tag)
		else: valid = false
		new_df = match(s,i,version_tag)
		
		if new_df:
			# Skip to the next minus sign or end-of-line
			i += len(version_tag)
			j = i
			while i < len(s) and not is_nl(s,i) and s[i] != '-':
				i += 1
		
			if j < i:
				pass # version = s[j:i]
			else:
				valid = false
		# Set the default encoding
		at.encoding = app.config.default_derived_file_encoding
		
		if match(s,i,encoding_tag):
			# Read optional encoding param, e.g., -encoding=utf-8,
			i += len(encoding_tag)
			# Skip to the next comma
			j = i
			while i < len(s) and not is_nl(s,i) and s[i] not in (',','.'):
				i += 1
			if match(s,i,',') or match(s,i,'.'):
				encoding = s[j:i]
				i += 1
				# print "@+leo-encoding=",encoding
				if isValidEncoding(encoding):
					at.encoding = encoding
				else:
					print "bad encoding in derived file:",encoding
					es("bad encoding in derived file:",encoding)
			else:
				valid = false
		# The closing comment delim is the trailing non-whitespace.
		i = j = skip_ws(s,i)
		while i < n and not is_ws(s[i]) and not is_nl(s,i):
			i += 1
		at.endSentinelComment = s[j:i]
		if not valid:
			at.error("Bad @+leo sentinel in " + fileName)
		return firstLines, new_df
	def readLine (self,file):
	
		"""Reads one line from file using the present encoding"""
		
		s = readlineForceUnixNewline(file)
		u = toUnicode(s,self.encoding)
		return u
	def writeAll(self,writeAtFileNodesFlag=false,writeDirtyAtFileNodesFlag=false):
		
		"""Write @file nodes in all or part of the outline"""
	
		at = self ; c = at.c
		write_new = not app.config.write_old_format_derived_files
		df = choose(write_new,at.new_df,at.old_df)
		df.initIvars()
		changedFiles = [] # Files that were actually changed.
		writtenFiles = [] # Files that might be written again.
	
		if writeAtFileNodesFlag:
			# Write all nodes in the selected tree.
			v = c.currentVnode()
			after = v.nodeAfterTree()
		else:
			# Write dirty nodes in the entire outline.
			v = c.rootVnode()
			after = None
	
		# We must clear these bits because they may have been set on a 
		# previous write.  Calls to atFile::write may set the orphan bits in 
		# @file nodes.  If so, write_LEO_file will write the entire @file 
		# tree.
		
		v2 = v
		while v2 and v2 != after:
			v2.clearOrphan()
			v2 = v2.threadNext()
		while v and v != after:
			if v.isAnyAtFileNode() or v.isAtIgnoreNode():
				# This code is a little tricky: @ignore not recognised in @silentfile nodes.
				
				if v.isDirty() or writeAtFileNodesFlag or v.t in writtenFiles:
				
					if v.isAtSilentFileNode():
						at.silentWrite(v)
					elif v.isAtIgnoreNode():
						pass
					elif v.isAtRawFileNode():
						at.rawWrite(v)
					elif v.isAtNoSentinelsFileNode():
						at.write(v,nosentinels=true)
					elif v.isAtFileNode():
						at.write(v)
				
					if not v.isAtIgnoreNode():
						writtenFiles.append(v.t)
				
					if df.fileChangedFlag: # Set by replaceTargetFileIfDifferent.
						changedFiles.append(v.t)
				v = v.nodeAfterTree()
			else:
				v = v.threadNext()
	
		if writeAtFileNodesFlag or writeDirtyAtFileNodesFlag:
			if len(writtenFiles) > 0:
				es("finished")
			elif writeAtFileNodesFlag:
				es("no @file nodes in the selected tree")
			else:
				es("no dirty @file nodes")
		return len(changedFiles) > 0 # So caller knows whether to do an auto-save.
	def rawWrite (self,v):
		at = self
		write_new = not app.config.write_old_format_derived_files
		df = choose(write_new,at.new_df,at.old_df)
		try:    df.rawWrite(v)
		except: at.writeException(v)
		
	def silentWrite (self,v):
		at = self
		try: at.old_df.silentWrite(v) # No new_df.silentWrite method.
		except: at.writeException(v)
		
	def write (self,v,nosentinels=false):
		at = self
		write_new = not app.config.write_old_format_derived_files
		df = choose(write_new,at.new_df,at.old_df)
		try:    df.write(v,nosentinels)
		except: at.writeException(v)
			
	def writeException(self,v):
		self.error("Unexpected exception while writing " + v.headString())
		es_exception()
	def writeOldDerivedFiles (self):
		
		self.writeDerivedFiles(write_old=true)
	
	def writeNewDerivedFiles (self):
	
		self.writeDerivedFiles(write_old=false)
		
	def writeDerivedFiles (self,write_old):
		
		config = app.config
		old = config.write_old_format_derived_files
		config.write_old_format_derived_files = write_old
		self.writeAll(writeAtFileNodesFlag=true)
		config.write_old_format_derived_files = old
	def writeMissing(self,v):
	
		at = self
		if at.trace: trace("old_df",v)
	
		write_new = not app.config.write_old_format_derived_files
		df = choose(write_new,at.new_df,at.old_df)
		df.initIvars()
		writtenFiles = false ; changedFiles = false
		after = v.nodeAfterTree()
		while v and v != after:
			if v.isAtSilentFileNode() or (v.isAnyAtFileNode() and not v.isAtIgnoreNode()):
				missing = false ; valid = true
				df.targetFileName = v.anyAtFileNodeName()
				# This is similar, but not the same as, the logic in openWriteFile.
				
				valid = df.targetFileName and len(df.targetFileName) > 0
				
				if valid:
					try:
						# Creates missing directives if option is enabled.
						df.scanAllDirectives(v)
						valid = df.errors == 0
					except:
						es("exception in atFile.scanAllDirectives")
						es_exception()
						valid = false
				
				if valid:
					try:
						fn = df.targetFileName
						df.shortFileName = fn # name to use in status messages.
						df.targetFileName = os_path_join(df.default_directory,fn)
						df.targetFileName = os_path_normpath(df.targetFileName)
				
						path = df.targetFileName # Look for the full name, not just the directory.
						valid = path and len(path) > 0
						if valid:
							missing = not os_path_exists(path)
					except:
						es("exception creating path:" + fn)
						es_exception()
						valid = false
				if valid and missing:
					try:
						df.outputFileName = df.targetFileName + ".leotmp"
						df.outputFile = open(df.outputFileName,'wb')
						if df.outputFile == None:
							es("can not open " + df.outputFileName)
					except:
						es("exception opening:" + df.outputFileName)
						es_exception()
						df.outputFile = None
					if at.outputFile:
						if v.isAtSilentFileNode():
							at.silentWrite(v)
						elif v.isAtRawFileNode():
							at.rawWrite(v)
						elif v.isAtNoSentinelsFileNode():
							at.write(v,nosentinels=true)
						elif v.isAtFileNode():
							at.write(v)
						else: assert(0)
						
						writtenFiles = true
						
						if df.fileChangedFlag: # Set by replaceTargetFileIfDifferent.
							changedFiles = true
				v = v.nodeAfterTree()
			elif v.isAtIgnoreNode():
				v = v.nodeAfterTree()
			else:
				v = v.threadNext()
		
		if writtenFiles > 0:
			es("finished")
		else:
			es("no missing @file node in the selected tree")
			
		return changedFiles # So caller knows whether to do an auto-save.
	
class atFile (baseAtFile):
	pass # May be overridden in plugins.
	
class baseOldDerivedFile:
	"""The base class to read and write 3.x derived files."""
	def __init__(self,c):
	
		self.c = c # The commander for the current window.
		self.fileCommands = self.c.fileCommands
	
		self.initIvars()
	
	def initIvars(self):
	
		# errors is the number of errors seen while reading and writing.
		self.errors = 0
		
		# Initialized by atFile.scanAllDirectives.
		self.default_directory = None
		self.page_width = None
		self.tab_width  = None
		self.startSentinelComment = None
		self.endSentinelComment = None
		self.language = None
		
		# The files used by the output routines.  When tangling, we first 
		# write to a temporary output file.  After tangling is temporary 
		# file.  Otherwise we delete the old target file and rename the 
		# temporary file to be the target file.
		self.shortFileName = "" # short version of file name used for messages.
		self.targetFileName = u"" # EKR 1/21/03: now a unicode string
		self.outputFileName = u"" # EKR 1/21/03: now a unicode string
		self.outputFile = None # The temporary output file.
		
		# The indentation used when outputting section references or at-others 
		# sections.  We add the indentation of the line containing the at-node 
		# directive and restore the old value when the
		# expansion is complete.
		self.indent = 0  # The unit of indentation is spaces, not tabs.
		
		# The root of tree being written.
		self.root = None
		
		# Ivars used to suppress newlines between sentinels.
		self.suppress_newlines = true # true: enable suppression of newlines.
		self.newline_pending = false # true: newline is pending on read or write.
		
		# Support of output_newline option
		self.output_newline = getOutputNewline()
		
		# Support of @raw
		self.raw = false # true: in @raw mode
		self.sentinels = true # true: output sentinels while expanding refs.
		
		# Enables tracing (debugging only).
		self.trace = false
		
		# The encoding used to convert from unicode to a byte stream.
		self.encoding = app.config.default_derived_file_encoding
		
		# For interface between 3.x and 4.x read code.
		self.file = None
		self.importing = false
		self.importRootSeen = false
		
		# Set when a file has actually been updated.
		self.fileChangedFlag = false
	def createImportedNode (self,root,c,headline):
		
		at = self
	
		if at.importRootSeen:
			v = root.insertAsLastChild()
			v.initHeadString(headline)
		else:
			# Put the text into the already-existing root node.
			v = root
			at.importRootSeen = true
			
		v.t.setVisited() # Suppress warning about unvisited node.
		return v
	def readOpenFile(self,root,file,firstLines):
		
		"""Read an open 3.x derived file."""
		
		at = self
		if at.trace: trace("old_df",root)
	
		# Scan the file buffer
		at.scanAllDirectives(root)
		lastLines = at.scanText(file,root,[],endLeo)
		root.t.setVisited() # Disable warning about set nodes.
	
		# Handle first and last lines.
		try: body = root.t.tempBodyString
		except: body = ""
		lines = body.split('\n')
		at.completeFirstDirectives(lines,firstLines)
		at.completeLastDirectives(lines,lastLines)
		s = '\n'.join(lines).replace('\r', '')
		root.t.tempBodyString = s
	# 14-SEP-2002 DTHEIN: added for use by atFile.read()
	
	# this function scans the lines in the list 'out' for @first directives
	# and appends the corresponding line from 'firstLines' to each @first 
	# directive found.  NOTE: the @first directives must be the very first
	# lines in 'out'.
	def completeFirstDirectives(self,out,firstLines):
	
		tag = "@first"
		foundAtFirstYet = 0
		outRange = range(len(out))
		j = 0
		for k in outRange:
			# skip leading whitespace lines
			if (not foundAtFirstYet) and (len(out[k].strip()) == 0): continue
			# quit if something other than @first directive
			i = 0
			if not match(out[k],i,tag): break;
			foundAtFirstYet = 1
			# quit if no leading lines to apply
			if j >= len(firstLines): break
			# make the new @first directive
			#18-SEP-2002 DTHEIN: remove trailing newlines because they are inserted later
			# 21-SEP-2002 DTHEIN: no trailing whitespace on empty @first directive
			leadingLine = " " + firstLines[j]
			out[k] = tag + leadingLine.rstrip() ; j += 1
	# 14-SEP-2002 DTHEIN: added for use by atFile.read()
	
	# this function scans the lines in the list 'out' for @last directives
	# and appends the corresponding line from 'lastLines' to each @last 
	# directive found.  NOTE: the @last directives must be the very last
	# lines in 'out'.
	def completeLastDirectives(self,out,lastLines):
	
		tag = "@last"
		foundAtLastYet = 0
		outRange = range(-1,-len(out),-1)
		j = -1
		for k in outRange:
			# skip trailing whitespace lines
			if (not foundAtLastYet) and (len(out[k].strip()) == 0): continue
			# quit if something other than @last directive
			i = 0
			if not match(out[k],i,tag): break;
			foundAtLastYet = 1
			# quit if no trailing lines to apply
			if j < -len(lastLines): break
			# make the new @last directive
			#18-SEP-2002 DTHEIN: remove trailing newlines because they are inserted later
			# 21-SEP-2002 DTHEIN: no trailing whitespace on empty @last directive
			trailingLine = " " + lastLines[j]
			out[k] = tag + trailingLine.rstrip() ; j -= 1
	# Sections appear in the derived file in reference order, not tree order.  
	# Therefore, when we insert the nth child of the parent there is no 
	# guarantee that the previous n-1 children have already been inserted. And 
	# it won't work just to insert the nth child as the last child if there 
	# aren't n-1 previous siblings.  For example, if we insert the third child 
	# followed by the second child followed by the first child the second and 
	# third children will be out of order.
	# 
	# To ensure that nodes are placed in the correct location we create 
	# "dummy" children as needed as placeholders.  In the example above, we 
	# would insert two dummy children when inserting the third child.  When 
	# inserting the other two children we replace the previously inserted 
	# dummy child with the actual children.
	# 
	# vnode child indices are zero-based.  Here we use 1-based indices.
	# 
	# With the "mirroring" scheme it is a structure error if we ever have to 
	# create dummy vnodes.  Such structure errors cause a second pass to be 
	# made, with an empty root.  This second pass will generate other 
	# structure errors, which are ignored.
	def createNthChild(self,n,parent,headline):
		
		"""Create the nth child of the parent."""
	
		at = self
		assert(n > 0)
		
		if at.importing:
			return at.createImportedNode(at.root,at.c,headline)
	
		# Create any needed dummy children.
		dummies = n - parent.numberOfChildren() - 1
		if dummies > 0:
			if 0: # CVS produces to many errors for this to be useful.
				es("dummy created")
			self.errors += 1
		while dummies > 0:
			dummies -= 1
			dummy = parent.insertAsLastChild(leoNodes.tnode())
			# The user should never see this headline.
			dummy.initHeadString("Dummy")
	
		if n <= parent.numberOfChildren():
			# 1/24/03: A kludgy fix to the problem of headlines containing comment delims.
			
			result = parent.nthChild(n-1)
			resulthead = result.headString()
			
			if headline.strip() != resulthead.strip():
				start = self.startSentinelComment
				end = self.endSentinelComment
				if end and len(end) > 0:
					# 1/25/03: The kludgy fix.
					# Compare the headlines without the delims.
					h1 =   headline.replace(start,"").replace(end,"")
					h2 = resulthead.replace(start,"").replace(end,"")
					if h1.strip() == h2.strip():
						# 1/25/03: Another kludge: use the headline from the outline, not the derived file.
						headline = resulthead
					else:
						self.errors += 1
				else:
					self.errors += 1
		else:
			# This is using a dummy; we should already have bumped errors.
			result = parent.insertAsLastChild(leoNodes.tnode())
		result.initHeadString(headline)
		
		result.setVisited() # Suppress all other errors for this node.
		result.t.setVisited() # Suppress warnings about unvisited nodes.
		return result
	def handleLinesFollowingSentinel (self,lines,sentinel,comments = true):
		
		"""convert lines following a sentinel to a single line"""
		
		m = " following" + sentinel + " sentinel"
		start = self.startSentinelComment
		end   = self.endSentinelComment
		
		if len(lines) == 1: # The expected case.
			s = lines[0]
		elif len(lines) == 5:
			self.readError("potential cvs conflict" + m)
			s = lines[1]
			es("using " + s)
		else:
			self.readError("unexpected lines" + m)
			es(len(lines), " lines" + m)
			s = "bad " + sentinel
			if comments: s = start + ' ' + s
	
		if comments:
			# Remove the starting comment and the blank.
			# 5/1/03: The starting comment now looks like a sentinel, to warn users from changing it.
			comment = start + '@ '
			if match(s,0,comment):
				s = s[len(comment):]
			else:
				self.readError("expecting comment" + m)
			
			# Remove the trailing comment.
			if len(end) == 0:
				s = string.strip(s[:-1])
			else:
				k = s.rfind(end)
				s = string.strip(s[:k]) # works even if k == -1
			
		# Undo the cweb hack: undouble @ signs if the opening comment delim ends in '@'.
		if start[-1:] == '@':
			s = s.replace('@@','@')
	
		return s
	def readLine (self,file):
		"""Reads one line from file using the present encoding"""
		
		s = readlineForceUnixNewline(file)
		u = toUnicode(s,self.encoding)
		return u
	
	# We expect only a single line, and more may exist if cvs detects a conflict.
	# We accept the first line even if it looks like a sentinel.
	# 5/1/03: The starting comment now looks like a sentinel, to warn users from changing it.
	
	def readLinesToNextSentinel (self,file):
		
		"""	read lines following multiline sentinels"""
		
		lines = []
		start = self.startSentinelComment + '@ '
		nextLine = self.readLine(file)
		while nextLine and len(nextLine) > 0:
			if len(lines) == 0:
				lines.append(nextLine)
				nextLine = self.readLine(file)
			else:
				# 5/1/03: looser test then calling sentinelKind.
				s = nextLine ; i = skip_ws(s,0)
				if match(s,i,start):
					lines.append(nextLine)
					nextLine = self.readLine(file)
				else: break
	
		return nextLine,lines
	# Scans the doc part and appends the text out.
	# s,i point to the present line on entry.
	
	def scanDoc(self,file,s,i,out,kind):
	
		endKind = choose(kind ==startDoc,endDoc,endAt)
		single = len(self.endSentinelComment) == 0
		assert(match(s,i,choose(kind == startDoc, "+doc", "+at")))
		
		out.append(choose(kind == startDoc, "@doc", "@"))
		s = self.readLine(file)
		if not single:
			j = skip_ws(s,0)
			if match(s,j,self.startSentinelComment):
				s = self.readLine(file)
		nextLine = None ; kind = noSentinel
		while len(s) > 0:
			# For non-sentinel lines we look ahead to see whether the next 
			# line is a sentinel.
			
			assert(nextLine==None)
			
			kind = self.sentinelKind(s)
			
			if kind == noSentinel:
				j = skip_ws(s,0)
				blankLine = s[j] == '\n'
				nextLine = self.readLine(file)
				nextKind = self.sentinelKind(nextLine)
				if blankLine and nextKind == endKind:
					kind = endKind # stop the scan now
			if kind == endKind: break
			# Point i to the start of the real line.
			
			if single: # Skip the opening comment delim and a blank.
				i = skip_ws(s,0)
				if match(s,i,self.startSentinelComment):
					i += len(self.startSentinelComment)
					if match(s,i," "): i += 1
			else:
				i = self.skipIndent(s,0, self.indent)
			# Append the line with a newline if it is real
			
			line = s[i:-1] # remove newline for rstrip.
			
			if line == line.rstrip():
				# no trailing whitespace: the newline is real.
				out.append(line + '\n')
			else:
				# trailing whitespace: the newline is not real.
				out.append(line)
			if nextLine:
				s = nextLine ; nextLine = None
			else: s = self.readLine(file)
		if kind != endKind:
			self.readError("Missing " + self.sentinelName(endKind) + " sentinel")
		# This code will typically only be executed for HTML files.
		
		if not single:
		
			delim = self.endSentinelComment
			n = len(delim)
			
			# Remove delim and possible a leading newline.
			s = string.join(out,"")
			s = s.rstrip()
			if s[-n:] == delim:
				s = s[:-n]
			if s[-1] == '\n':
				s = s[:-1]
				
			# Rewrite out in place.
			del out[:]
			out.append(s)
	def scanText (self,file,v,out,endSentinelKind,nextLine=None):
		
		"""Scan a 3.x derived file recursively."""
	
		lastLines = [] # The lines after @-leo
		lineIndent = 0 ; linep = 0 # Changed only for sentinels.
		while 1:
			if nextLine:
				s = nextLine ; nextLine = None
			else:
				s = self.readLine(file)
				if len(s) == 0: break
			# For non-sentinel lines we look ahead to see whether the next 
			# line is a sentinel.  If so, the newline that ends a non-sentinel 
			# line belongs to the next sentinel.
			
			assert(nextLine==None)
			
			kind = self.sentinelKind(s)
			
			if kind == noSentinel:
				nextLine = self.readLine(file)
				nextKind = self.sentinelKind(nextLine)
			else:
				nextLine = nextKind = None
			
			# nextLine != None only if we have a non-sentinel line.
			# Therefore, nextLine == None whenever scanText returns.
			if kind != noSentinel:
				# lineIndent is the total indentation on a sentinel line.  The 
				# first "self.indent" portion of that must be removed when 
				# recreating text.  leading_ws is the remainder of the leading 
				# whitespace.  linep points to the first "real" character of a 
				# line, the character following the "indent" whitespace.
				
				# Point linep past the first self.indent whitespace characters.
				if self.raw: # 10/15/02
					linep =0
				else:
					linep = self.skipIndent(s,0,self.indent)
				
				# Set lineIndent to the total indentation on the line.
				lineIndent = 0 ; i = 0
				while i < len(s):
					if s[i] == '\t': lineIndent += (abs(self.tab_width) - (lineIndent % abs(self.tab_width)))
					elif s[i] == ' ': lineIndent += 1
					else: break
					i += 1
				# trace("lineIndent:" +`lineIndent` + ", " + `s`)
				
				# Set leading_ws to the additional indentation on the line.
				leading_ws = s[linep:i]
				i = self.skipSentinelStart(s,0)
			if kind == noSentinel:
				# We don't output the trailing newline if the next line is a sentinel.
				if self.raw: # 10/15/02
					i = 0
				else:
					i = self.skipIndent(s,0,self.indent)
				
				assert(nextLine != None)
				
				if nextKind == noSentinel:
					line = s[i:]
					out.append(line)
				else:
					line = s[i:-1] # don't output the newline
					out.append(line)
			elif kind in (endAt, endBody,endDoc,endLeo,endNode,endOthers):
					# trace("end sentinel:", self.sentinelName(kind))
					
					if kind == endSentinelKind:
						if kind == endLeo:
							# Ignore everything after @-leo.
							# Such lines were presumably written by @last.
							while 1:
								s = self.readLine(file)
								if len(s) == 0: break
								lastLines.append(s) # Capture all trailing lines, even if empty.
						elif kind == endBody:
							self.raw = false
						# nextLine != None only if we have a non-sentinel line.
						# Therefore, nextLine == None whenever scanText returns.
						assert(nextLine==None)
						return lastLines # End the call to scanText.
					else:
						# Tell of the structure error.
						name = self.sentinelName(kind)
						expect = self.sentinelName(endSentinelKind)
						self.readError("Ignoring " + name + " sentinel.  Expecting " + expect)
			elif kind == startBody:
				assert(match(s,i,"+body"))
				
				child_out = [] ; child = v # Do not change out or v!
				oldIndent = self.indent ; self.indent = lineIndent
				self.scanText(file,child,child_out,endBody)
				
				# Set the body, removing cursed newlines.
				# This must be done here, not in the @+node logic.
				body = string.join(child_out, "")
				body = body.replace('\r', '')
				body = toUnicode(body,app.tkEncoding) # 9/28/03
				
				if self.importing:
					child.t.bodyString = body
				else:
					child.t.tempBodyString = body
				
				self.indent = oldIndent
			elif kind == startNode:
				assert(match(s,i,"+node:"))
				i += 6
				
				childIndex = 0 ; cloneIndex = 0
				i = skip_ws(s,i) ; j = i
				while i < len(s) and s[i] in string.digits:
					i += 1
				
				if j == i:
					self.readError("Implicit child index in @+node")
					childIndex = 0
				else:
					childIndex = int(s[j:i])
				
				if match(s,i,':'):
					i += 1 # Skip the ":".
				else:
					self.readError("Bad child index in @+node")
				while i < len(s) and s[i] != ':' and not is_nl(s,i):
					if match(s,i,"C="):
						# set cloneIndex from the C=nnn, field
						i += 2 ; j = i
						while i < len(s) and s[i] in string.digits:
							i += 1
						if j < i:
							cloneIndex = int(s[j:i])
					else: i += 1 # Ignore unknown status bits.
				
				if match(s,i,":"):
					i += 1
				else:
					self.readError("Bad attribute field in @+node")
				headline = ""
				# Set headline to the rest of the line.
				# 6/22/03: don't strip leading whitespace.
				if len(self.endSentinelComment) == 0:
					headline = s[i:-1].rstrip()
				else:
					# 10/24/02: search from the right, not the left.
					k = s.rfind(self.endSentinelComment,i)
					headline = s[i:k].rstrip() # works if k == -1
					
				# 10/23/02: The cweb hack: undouble @ signs if the opening comment delim ends in '@'.
				if self.startSentinelComment[-1:] == '@':
					headline = headline.replace('@@','@')
				
				# Set reference if it exists.
				i = skip_ws(s,i)
				
				if 0: # no longer used
					if match(s,i,"<<"):
						k = s.find(">>",i)
						if k != -1: ref = s[i:k+2]
				
				# print childIndex,headline
				
				if childIndex == 0: # The root node.
					if not at.importing:
						h = headline.strip()
						
						if h[:5] == "@file":
							i,junk,junk = scanAtFileOptions(h)
							fileName = string.strip(h[i:])
							if fileName != self.targetFileName:
								self.readError("File name in @node sentinel does not match file's name")
						elif h[:8] == "@rawfile":
							fileName = string.strip(h[8:])
							if fileName != self.targetFileName:
								self.readError("File name in @node sentinel does not match file's name")
						else:
							self.readError("Missing @file in root @node sentinel")
					# Put the text of the root node in the current node.
					self.scanText(file,v,out,endNode)
					v.t.setCloneIndex(cloneIndex)
					# if cloneIndex > 0: trace("clone index:" + `cloneIndex` + ", " + `v`)
				else:
					# NB: this call to createNthChild is the bottleneck!
					child = self.createNthChild(childIndex,v,headline)
					child.t.setCloneIndex(cloneIndex)
					# if cloneIndex > 0: trace("clone index:" + `cloneIndex` + ", " + `child`)
					self.scanText(file,child,out,endNode)
				
				s = self.readLine(file)
				kind = self.sentinelKind(s)
				
				if len(s) > 1 and kind == startVerbatimAfterRef:
					s = self.readLine(file)
					# trace("verbatim:"+`s`)
					out.append(s)
				elif len(s) > 1 and self.sentinelKind(s) == noSentinel:
					out.append(s)
				else:
					nextLine = s # Handle the sentinel or blank line later.
			elif kind == startRef:
				# The sentinel contains an @ followed by a section name in 
				# angle brackets.  This code is different from the code for 
				# the @@ sentinel: the expansion of the reference does not 
				# include a trailing newline.
				
				assert(match(s,i,"<<"))
				
				if len(self.endSentinelComment) == 0:
					line = s[i:-1] # No trailing newline
				else:
					k = s.find(self.endSentinelComment,i)
					line = s[i:k] # No trailing newline, whatever k is.
						
				# 10/30/02: undo cweb hack here
				start = self.startSentinelComment
				if start and len(start) > 0 and start[-1] == '@':
					line = line.replace('@@','@')
				
				out.append(line)
			elif kind == startAt:
				assert(match(s,i,"+at"))
				self.scanDoc(file,s,i,out,kind)
			elif kind == startDoc:
				assert(match(s,i,"+doc"))
				self.scanDoc(file,s,i,out,kind)
			elif kind == startOthers:
				assert(match(s,i,"+others"))
				
				# Make sure that the generated at-others is properly indented.
				out.append(leading_ws + "@others")
				
				self.scanText(file,v,out,endOthers)
			elif kind == startComment:
				assert(match(s,i,"comment"))
				
				# We need do nothing more to ignore the comment line!
			elif kind == startDelims:
				assert(match(s,i-1,"@delims"));
				
				# Skip the keyword and whitespace.
				i0 = i-1
				i = skip_ws(s,i-1+7)
					
				# Get the first delim.
				j = i
				while i < len(s) and not is_ws(s[i]) and not is_nl(s,i):
					i += 1
				
				if j < i:
					self.startSentinelComment = s[j:i]
					# print "delim1:", self.startSentinelComment
				
					# Get the optional second delim.
					j = i = skip_ws(s,i)
					while i < len(s) and not is_ws(s[i]) and not is_nl(s,i):
						i += 1
					end = choose(j<i,s[j:i],"")
					i2 = skip_ws(s,i)
					if end == self.endSentinelComment and (i2 >= len(s) or is_nl(s,i2)):
						self.endSentinelComment = "" # Not really two params.
						line = s[i0:j]
						line = line.rstrip()
						out.append(line+'\n')
					else:
						self.endSentinelComment = end
						# print "delim2:",end
						line = s[i0:i]
						line = line.rstrip()
						out.append(line+'\n')
				else:
					self.readError("Bad @delims")
					# Append the bad @delims line to the body text.
					out.append("@delims")
			elif kind == startDirective:
				# The first '@' has already been eaten.
				assert(match(s,i,"@"))
				
				if match_word(s,i,"@raw"):
					self.raw = true
				elif match_word(s,i,"@end_raw"):
					self.raw = false
				
				e = self.endSentinelComment
				s2 = s[i:]
				if len(e) > 0:
					k = s.rfind(e,i)
					if k != -1:
						s2 = s[i:k] + '\n'
					
				start = self.startSentinelComment
				if start and len(start) > 0 and start[-1] == '@':
					s2 = s2.replace('@@','@')
				out.append(s2)
				# trace(`s2`)
			elif kind == startLeo:
				assert(match(s,i,"+leo"))
				self.readError("Ignoring unexpected @+leo sentinel")
			elif kind == startVerbatim:
				assert(match(s,i,"verbatim"))
				
				# Skip the sentinel.
				s = self.readLine(file) 
				
				# Append the next line to the text.
				i = self.skipIndent(s,0,self.indent)
				out.append(s[i:])
			else:
				j = i
				i = skip_line(s,i)
				line = s[j:i]
				self.readError("Unknown sentinel: " + line)
		# Issue the error.
		name = self.sentinelName(endSentinelKind)
		self.readError("Unexpected end of file. Expecting " + name + "sentinel" )
		assert(len(s)==0 and nextLine==None) # We get here only if readline fails.
		return lastLines # We get here only if there are problems.
	# 4/5/03: config.write_clone_indices no longer used.
	
	def nodeSentinelText(self,v):
		
		if v == self.root or not v.parent():
			index = 0
		else:
			index = v.childIndex() + 1
	
		h = v.headString()
		# Bug fix 1/24/03:
		# 
		# If the present @language/@comment settings do not specify a 
		# single-line comment we remove all block comment delims from h.  This 
		# prevents headline text from interfering with the parsing of node 
		# sentinels.
		
		start = self.startSentinelComment
		end = self.endSentinelComment
		
		if end and len(end) > 0:
			h = h.replace(start,"")
			h = h.replace(end,"")
	
		return str(index) + '::' + h
	def putCloseNodeSentinel(self,v):
	
		s = self.nodeSentinelText(v)
		self.putSentinel("@-node:" + s)
	# root is an ancestor of v, or root == v.
	
	def putCloseSentinels(self,root,v):
		
		"""call putCloseSentinel for v up to, but not including, root."""
	
		self.putCloseNodeSentinel(v)
		while 1:
			v = v.parent()
			assert(v) # root must be an ancestor of v.
			if  v == root: break
			self.putCloseNodeSentinel(v)
	# This method is the same as putSentinel except we don't put an opening newline and leading whitespace.
	
	def putOpenLeoSentinel(self,s):
		
		"""Put a +leo sentinel containing s."""
		
		if not self.sentinels:
			return # Handle @nosentinelsfile.
	
		self.os(self.startSentinelComment)
		self.os(s)
		encoding = self.encoding.lower()
		if encoding != "utf-8":
			self.os("-encoding=")
			self.os(encoding)
			self.os(".")
		self.os(self.endSentinelComment)
		if self.suppress_newlines: # 9/27/02
			self.newline_pending = true # Schedule a newline.
		else:
			self.onl() # End of sentinel.
	def putOpenNodeSentinel(self,v):
	
		"""Put an open node sentinel for node v."""
	
		if v.isAtFileNode() and v != self.root:
			self.writeError("@file not valid in: " + v.headString())
			return
		
		s = self.nodeSentinelText(v)
		self.putSentinel("@+node:" + s)
	# root is an ancestor of v, or root == v.
	
	def putOpenSentinels(self,root,v):
	
		"""Call putOpenNodeSentinel on all the descendents of root which are the ancestors of v."""
	
		last = root
		while last != v:
			# Set node to v or the ancestor of v that is a child of last.
			node = v
			while node and node.parent() != last:
				node = node.parent()
			assert(node)
			self.putOpenNodeSentinel(node)
			last = node
	# All sentinels are eventually output by this method.
	# 
	# Sentinels include both the preceding and following newlines. This rule 
	# greatly simplies the code and has several important benefits:
	# 
	# 1. Callers never have to generate newlines before or after sentinels.  
	# Similarly, routines that expand code and doc parts never have to add 
	# "extra" newlines.
	# 2. There is no need for a "no-newline" directive.  If text follows a 
	# section reference, it will appear just after the newline that ends 
	# sentinel at the end of the expansion of the reference.  If no 
	# significant text follows a reference, there will be two newlines 
	# following the ending sentinel.
	# 
	# The only exception is that no newline is required before the opening 
	# "leo" sentinel. The putLeoSentinel and isLeoSentinel routines handle 
	# this minor exception.
	def putSentinel(self,s):
		
		"""Put a sentinel containing s."""
		
		if not self.sentinels:
			return # Handle @nosentinelsfile.
	
		self.newline_pending = false # discard any pending newline.
		self.onl() ; self.putIndent(self.indent) # Start of sentinel.
		self.os(self.startSentinelComment)
	
		# 11/1/02: The cweb hack: if the opening comment delim ends in '@',
		# double all '@' signs except the first, which is "doubled" by the
		# trailing '@' in the opening comment delimiter.
		start = self.startSentinelComment
		if start and len(start) > 0 and start[-1] == '@':
			assert(s and len(s)>0 and s[0]=='@')
			s = s.replace('@','@@')[1:]
	
		self.os(s)
		self.os(self.endSentinelComment)
		if self.suppress_newlines:
			self.newline_pending = true # Schedule a newline.
		else:
			self.onl() # End of sentinel.
	def sentinelKind(self,s):
	
		"""This method tells what kind of sentinel appears in line s.
		
		Typically s will be an empty line before the actual sentinel,
		but it is also valid for s to be an actual sentinel line.
		
		Returns (kind, s, emptyFlag), where emptyFlag is true if
		kind == noSentinel and s was an empty line on entry."""
	
		i = skip_ws(s,0)
		if match(s,i,self.startSentinelComment):
			i += len(self.startSentinelComment)
		else:
			return noSentinel
	
		# 10/30/02: locally undo cweb hack here
		start = self.startSentinelComment
		if start and len(start) > 0 and start[-1] == '@':
			s = s[:i] + string.replace(s[i:],'@@','@')
	
		# Do not skip whitespace here!
		if match(s,i,"@<<"): return startRef
		if match(s,i,"@@"):   return startDirective
		if not match(s,i,'@'): return noSentinel
		j = i # start of lookup
		i += 1 # skip the at sign.
		if match(s,i,'+') or match(s,i,'-'):
			i += 1
		i = skip_c_id(s,i)
		key = s[j:i]
		if len(key) > 0 and sentinelDict.has_key(key):
			# trace("found:",key)
			return sentinelDict[key]
		else:
			# trace("not found:",key)
			return noSentinel
	# Returns the name of the sentinel for warnings.
	
	def sentinelName(self, kind):
	
		sentinelNameDict = {
			noSentinel:  "<no sentinel>",
			startAt:     "@+at",     endAt:     "@-at",
			startBody:   "@+body",   endBody:   "@-body", # 3.x only.
			startDoc:    "@+doc",    endDoc:    "@-doc",
			startLeo:    "@+leo",    endLeo:    "@-leo",
			startNode:   "@+node",   endNode:   "@-node",
			startOthers: "@+others", endOthers: "@-others",
			startAfterRef:  "@afterref", # 4.x
			startComment:   "@comment",
			startDelims:    "@delims",
			startDirective: "@@",
			startNl:        "@nl",   # 4.x
			startNonl:      "@nonl", # 4.x
			startRef:       "@<<",
			startVerbatim:  "@verbatim",
			startVerbatimAfterRef: "@verbatimAfterRef" } # 3.x only.
	
		return sentinelNameDict.get(kind,"<unknown sentinel!>")
	def skipSentinelStart(self,s,i):
	
		start = self.startSentinelComment
		assert(start and len(start)>0)
	
		if is_nl(s,i): i = skip_nl(s,i)
		i = skip_ws(s,i)
		assert(match(s,i,start))
		i += len(start)
		# 7/8/02: Support for REM hack
		i = skip_ws(s,i)
		assert(i < len(s) and s[i] == '@')
		return i + 1
	# Returns the kind of at-directive or noDirective.
	
	def directiveKind(self,s,i):
	
		n = len(s)
		if i >= n or s[i] != '@':
			return noDirective
	
		table = (
			("@c",cDirective),
			("@code",codeDirective),
			("@doc",docDirective),
			("@end_raw",endRawDirective),
			("@others",othersDirective),
			("@raw",rawDirective))
	
		# This code rarely gets executed, so simple code suffices.
		if i+1 >= n or match(s,i,"@ ") or match(s,i,"@\t") or match(s,i,"@\n"):
			# 10/25/02: @space is not recognized in cweb mode.
			# 11/15/02: Noweb doc parts are _never_ scanned in cweb mode.
			return choose(self.language=="cweb",
				noDirective,atDirective)
	
		# 10/28/02: @c and @(nonalpha) are not recognized in cweb mode.
		# We treat @(nonalpha) separately because @ is in the colorizer table.
		if self.language=="cweb" and (
			match_word(s,i,"@c") or
			i+1>= n or s[i+1] not in string.ascii_letters):
			return noDirective
	
		for name,directive in table:
			if match_word(s,i,name):
				return directive
		# 10/14/02: return miscDirective only for real directives.
		for name in leoColor.leoKeywords:
			if match_word(s,i,name):
				return miscDirective
	
		return noDirective
	def error(self,message):
	
		es_error(message)
		self.errors += 1
	def readError(self,message):
	
		# This is useful now that we don't print the actual messages.
		if self.errors == 0:
			es_error("----- error reading @file " + self.targetFileName)
			self.error(message) # 9/10/02: we must increment self.errors!
			
		print message
	
		if 0: # CVS conflicts create too many messages.
			self.error(message)
		
		self.root.setOrphan()
		self.root.setDirty()
	# Once a directive is seen, no other related directives in nodes further 
	# up the tree have any effect.  For example, if an @color directive is 
	# seen in node v, no @color or @nocolor directives are examined in any 
	# ancestor of v.
	# 
	# This code is similar to Commands::scanAllDirectives, but it has been 
	# modified for use by the atFile class.
	
	def scanAllDirectives(self,v):
		
		"""Scan vnode v and v's ancestors looking for directives,
		setting corresponding atFile ivars.
		"""
	
		c = self.c
		self.page_width = self.c.page_width
		self.tab_width  = self.c.tab_width
		
		self.default_directory = None # 8/2: will be set later.
		
		delim1, delim2, delim3 = set_delims_from_language(c.target_language)
		self.language = c.target_language
		
		self.encoding = app.config.default_derived_file_encoding
		self.output_newline = getOutputNewline() # 4/24/03: initialize from config settings.
		# An absolute path in an @file node over-rides everything else.
		# A relative path gets appended to the relative path by the open logic.
		
		# Bug fix: 10/16/02
		if v.isAtFileNode():
			name = v.atFileNodeName()
		elif v.isAtRawFileNode():
			name = v.atRawFileNodeName()
		elif v.isAtNoSentinelsFileNode():
			name = v.atNoSentinelsFileNodeName()
		else:
			name = ""
		
		dir = choose(name,os_path_dirname(name),None)
		
		if dir and len(dir) > 0 and os_path_isabs(dir):
			if os_path_exists(dir):
				self.default_directory = dir
			else: # 9/25/02
				self.default_directory = makeAllNonExistentDirectories(dir)
				if not self.default_directory:
					self.error("Directory \"" + dir + "\" does not exist")
					
		old = {}
		while v:
			s = v.t.bodyString
			dict = get_directives_dict(s)
			# We set the current director to a path so future writes will go to that directory.
			
			if not self.default_directory and not old.has_key("path") and dict.has_key("path"):
			
				k = dict["path"]
				j = i = k + len("@path")
				i = skip_to_end_of_line(s,i)
				path = string.strip(s[j:i])
				
				# Remove leading and trailing delims if they exist.
				if len(path) > 2 and (
					(path[0]=='<' and path[-1] == '>') or
					(path[0]=='"' and path[-1] == '"') ):
					path = path[1:-1]
				path = path.strip()
				
				if 0: # 11/14/02: we want a _relative_ path, not an absolute path.
					path = os_path_join(app.loadDir,path)
				if path and len(path) > 0:
					base = getBaseDirectory() # returns "" on error.
					path = os_path_join(base,path)
					if os_path_isabs(path):
						# path is an absolute path.
						
						if os_path_exists(path):
							self.default_directory = path
						else: # 9/25/02
							self.default_directory = makeAllNonExistentDirectories(path)
							if not self.default_directory:
								self.error("invalid @path: " + path)
					else:
						self.error("ignoring bad @path: " + path)
				else:
					self.error("ignoring empty @path")
			if not old.has_key("encoding") and dict.has_key("encoding"):
				
				e = scanAtEncodingDirective(s,dict)
				if e:
					self.encoding = e
			# 10/17/02: @language and @comment may coexist in @file trees.
			# For this to be effective the @comment directive should follow the @language directive.
			
			if not old.has_key("comment") and dict.has_key("comment"):
				k = dict["comment"]
				# 11/14/02: Similar to fix below.
				delim1, delim2, delim3 = set_delims_from_string(s[k:])
			
			# Reversion fix: 12/06/02: We must use elif here, not if.
			elif not old.has_key("language") and dict.has_key("language"):
				k = dict["language"]
				# 11/14/02: Fix bug reported by J.M.Gilligan.
				self.language,delim1,delim2,delim3 = set_language(s,k)
			# EKR: 10/10/02: perform the sames checks done by tangle.scanAllDirectives.
			if dict.has_key("header") and dict.has_key("noheader"):
				es("conflicting @header and @noheader directives")
			if not old.has_key("lineending") and dict.has_key("lineending"):
				
				lineending = scanAtLineendingDirective(s,dict)
				if lineending:
					self.output_newline = lineending
			if dict.has_key("pagewidth") and not old.has_key("pagewidth"):
				
				w = scanAtPagewidthDirective(s,dict,issue_error_flag=true)
				if w and w > 0:
					self.page_width = w
			if dict.has_key("tabwidth") and not old.has_key("tabwidth"):
				
				w = scanAtTabwidthDirective(s,dict,issue_error_flag=true)
				if w and w != 0:
					self.tab_width = w
			
			old.update(dict)
			v = v.parent()
		# This code is executed if no valid absolute path was specified in the @file node or in an @path directive.
		
		if c.frame and not self.default_directory:
			base = getBaseDirectory() # returns "" on error.
			for dir in (c.tangle_directory,c.frame.openDirectory,c.openDirectory):
				if dir and len(dir) > 0:
					dir = os_path_join(base,dir)
					if os_path_isabs(dir): # Errors may result in relative or invalid path.
						if os_path_exists(dir):
							self.default_directory = dir ; break
						else: # 9/25/02
							self.default_directory = makeAllNonExistentDirectories(dir)
		
		if not self.default_directory:
			# This should never happen: c.openDirectory should be a good last resort.
			self.error("No absolute directory specified anywhere.")
			self.default_directory = ""
		# Use single-line comments if we have a choice.
		# 8/2/01: delim1,delim2,delim3 now correspond to line,start,end
		if delim1:
			self.startSentinelComment = delim1
			self.endSentinelComment = "" # Must not be None.
		elif delim2 and delim3:
			self.startSentinelComment = delim2
			self.endSentinelComment = delim3
		else: # Emergency!
			# assert(0)
			es("Unknown language: using Python comment delimiters")
			es("c.target_language:"+`c.target_language`)
			es("delim1,delim2,delim3:" + `delim1`+":"+`delim2`+":"+`delim3`)
			self.startSentinelComment = "#" # This should never happen!
			self.endSentinelComment = ""
	# Skip past whitespace equivalent to width spaces.
	
	def skipIndent(self,s,i,width):
	
		ws = 0 ; n = len(s)
		while i < n and ws < width:
			if   s[i] == '\t': ws += (abs(self.tab_width) - (ws % abs(self.tab_width)))
			elif s[i] == ' ':  ws += 1
			else: break
			i += 1
		return i
	def writeError(self,message):
	
		if self.errors == 0:
			es_error("errors writing: " + self.targetFileName)
	
		self.error(message)
		self.root.setOrphan()
		self.root.setDirty()
	def rawWrite(self,root):
	
		if self.trace: trace("old_df",root)
		
		c = self.c ; self.root = root
		self.errors = 0
		self.sentinels = true # 10/1/03
		c.endEditing() # Capture the current headline.
		try:
			self.targetFileName = root.atRawFileNodeName()
			ok = self.openWriteFile(root)
			if not ok: return
			next = root.nodeAfterTree()
			# Write any @first lines.  These lines are also converted to 
			# @verbatim lines, so the read logic simply ignores lines 
			# preceding the @+leo sentinel.
			
			s = root.t.bodyString
			tag = "@first"
			i = 0
			while match(s,i,tag):
				i += len(tag)
				i = skip_ws(s,i)
				j = i
				i = skip_to_end_of_line(s,i)
				# 21-SEP-2002 DTHEIN: write @first line, whether empty or not
				line = s[j:i]
				self.putBuffered(line) ; self.onl()
				i = skip_nl(s,i)
			self.putOpenLeoSentinel("@+leo")
			s2 = app.config.output_initial_comment
			if s2:
				lines = string.split(s2,"\\n")
				for line in lines:
					line = line.replace("@date",time.asctime())
					if len(line)> 0:
						self.putSentinel("@comment " + line)
			
			v = root
			while v and v != next:
				self.putOpenNodeSentinel(v)
					
				s = v.bodyString()
				if s and len(s) > 0:
					self.putSentinel("@+body")
					if self.newline_pending:
						self.newline_pending = false
						self.onl()
					s = toEncodedString(s,self.encoding,reportErrors=true) # 3/7/03
					self.outputStringWithLineEndings(s)
					self.putSentinel("@-body")
					
				self.putCloseNodeSentinel(v)
				v = v.threadNext()
			
			self.putSentinel("@-leo")
			# Write any @last lines.  These lines are also converted to 
			# @verbatim lines, so the read logic simply ignores lines 
			# following the @-leo sentinel.
			
			tag = "@last"
			lines = string.split(root.t.bodyString,'\n')
			n = len(lines) ; j = k = n - 1
			# Don't write an empty last line.
			if j >= 0 and len(lines[j])==0:
				j = k = n - 2
			# Scan backwards for @last directives.
			while j >= 0:
				line = lines[j]
				if match(line,0,tag): j -= 1
				else: break
			# Write the @last lines.
			for line in lines[j+1:k+1]:
				i = len(tag) ; i = skip_ws(line,i)
				self.putBuffered(line[i:]) ; self.onl()
			self.closeWriteFile()
			self.replaceTargetFileIfDifferent()
			root.clearOrphan() ; root.clearDirty()
		except:
			self.handleWriteException(root)
	def silentWrite(self,root):
	
		if self.trace: trace("old_df",root)
	
		c = self.c ; self.root = root
		self.errors = 0
		c.endEditing() # Capture the current headline.
		try:
			self.targetFileName = root.atSilentFileNodeName()
			ok = self.openWriteFile(root)
			if not ok: return
			next = root.nodeAfterTree()
			v = root
			while v and v != next:
				s = v.headString()
				if match(s,0,"@@"):
					s = s[2:]
					if s and len(s) > 0:
						s = toEncodedString(s,self.encoding,reportErrors=true) # 3/7/03
						self.outputFile.write(s)
				s = v.bodyString()
				if s and len(s) > 0:
					s = toEncodedString(s,self.encoding,reportErrors=true) # 3/7/03
					self.outputStringWithLineEndings(s)
				v = v.threadNext()
			self.closeWriteFile()
			self.replaceTargetFileIfDifferent()
			root.clearOrphan() ; root.clearDirty()
		except:
			self.handleWriteException(root)
	# This is the entry point to the write code.  root should be an @file vnode.
	
	def write(self,root,nosentinels=false):
		
		if self.trace: trace("old_df",root)
		
		# Remove any old tnodeList.
		if hasattr(root,"tnodeList"):
			if self.trace: trace("removing tnodeList for " + `root`)
			delattr(root,"tnodeList")
	
		c = self.c
		self.sentinels = not nosentinels
		self.errors = 0 # 9/26/02
		c.setIvarsFromPrefs()
		self.root = root
		self.raw = false
		c.endEditing() # Capture the current headline.
		try:
			if nosentinels:
				self.targetFileName = root.atNoSentinelsFileNodeName()
			else:
				self.targetFileName = root.atFileNodeName()
			
			ok = self.openWriteFile(root)
			if not ok: return
			root.clearVisitedInTree()
			next = root.nodeAfterTree()
			
			# Write any @first lines.  These lines are also converted to 
			# @verbatim lines, so the read logic simply ignores lines 
			# preceding the @+leo sentinel.
			
			s = root.t.bodyString
			tag = "@first"
			i = 0
			while match(s,i,tag):
				i += len(tag)
				i = skip_ws(s,i)
				j = i
				i = skip_to_end_of_line(s,i)
				# 21-SEP-2002 DTHEIN: write @first line, whether empty or not
				line = s[j:i]
				self.putBuffered(line) ; self.onl()
				i = skip_nl(s,i)
			tag1 = "@+leo"
			
			self.putOpenLeoSentinel(tag1)
			self.putInitialComment()
			self.putOpenNodeSentinel(root)
			self.putBodyPart(root)
			self.putCloseNodeSentinel(root)
			self.putSentinel("@-leo")
			# Write any @last lines.  These lines are also converted to 
			# @verbatim lines, so the read logic simply ignores lines 
			# following the @-leo sentinel.
			
			tag = "@last"
			lines = string.split(root.t.bodyString,'\n')
			n = len(lines) ; j = k = n - 1
			# Don't write an empty last line.
			if j >= 0 and len(lines[j])==0:
				j = k = n - 2
			# Scan backwards for @last directives.
			while j >= 0:
				line = lines[j]
				if match(line,0,tag): j -= 1
				else: break
			# Write the @last lines.
			for line in lines[j+1:k+1]:
				i = len(tag) ; i = skip_ws(line,i)
				self.putBuffered(line[i:]) ; self.onl()
			
			root.setVisited()
			self.closeWriteFile()
			if not nosentinels:
				# 10/26/02: Always warn, even when language=="cweb"
				
				next = root.nodeAfterTree()
				v = root
				while v and v != next:
					if not v.isVisited():
						self.writeError("Orphan node:  " + v.headString())
					if v.isAtIgnoreNode():
						self.writeError("@ignore node: " + v.headString())
					v = v.threadNext()
			# We set the orphan and dirty flags if there are problems writing 
			# the file to force Commands::write_LEO_file to write the tree to 
			# the .leo file.
			
			if self.errors > 0 or self.root.isOrphan():
				root.setOrphan()
				root.setDirty() # 2/9/02: make _sure_ we try to rewrite this file.
				os.remove(self.outputFileName) # Delete the temp file.
				es("Not written: " + self.outputFileName)
			else:
				root.clearOrphan()
				root.clearDirty()
				self.replaceTargetFileIfDifferent()
		except:
			self.handleWriteException()
	def closeWriteFile (self):
		
		if self.outputFile:
			if self.suppress_newlines and self.newline_pending:
				self.newline_pending = false
				self.onl() # Make sure file ends with a newline.
			self.outputFile.flush()
			self.outputFile.close()
			self.outputFile = None
	def handleWriteException (self,root=None):
		
		es("exception writing:" + self.targetFileName,color="red")
		es_exception()
		
		if self.outputFile:
			self.outputFile.flush()
			self.outputFile.close()
			self.outputFile = None
		
		if self.outputFileName != None:
			try: # Just delete the temp file.
				os.remove(self.outputFileName)
			except:
				es("exception deleting:" + self.outputFileName,color="red")
				es_exception()
	
		if root:
			# Make sure we try to rewrite this file.
			root.setOrphan()
			root.setDirty()
	# Open files.  Set root.orphan and root.dirty flags and return on errors.
	
	def openWriteFile (self,root):
	
		try:
			self.scanAllDirectives(root)
			valid = self.errors == 0
		except:
			self.writeError("exception in atFile.scanAllDirectives")
			es_exception()
			valid = false
	
		if valid:
			try:
				fn = self.targetFileName
				self.shortFileName = fn # name to use in status messages.
				self.targetFileName = os_path_join(self.default_directory,fn)
				self.targetFileName = os_path_normpath(self.targetFileName)
				path = os_path_dirname(self.targetFileName)
				if not path or not os_path_exists(path):
					self.writeError("path does not exist: " + path)
					valid = false
			except:
				self.writeError("exception creating path:" + fn)
				es_exception()
				valid = false
	
		if valid and os_path_exists(self.targetFileName):
			try:
				if not os.access(self.targetFileName,os.W_OK):
					self.writeError("read only: " + self.targetFileName)
					valid = false
			except:
				pass # os.access() may not exist on all platforms.
			
		if valid:
			try:
				self.outputFileName = self.targetFileName + ".tmp"
				self.outputFile = open(self.outputFileName,'wb')
				if self.outputFile is None:
					self.writeError("can not open " + self.outputFileName)
					valid = false
			except:
				es("exception opening:" + self.outputFileName)
				es_exception()
				valid = false
		
		if not valid:
			root.setOrphan()
			root.setDirty()
		
		return valid
	def putInitialComment (self):
		
		s2 = app.config.output_initial_comment
		if s2:
			lines = string.split(s2,"\\n")
			for line in lines:
				line = line.replace("@date",time.asctime())
				if len(line)> 0:
					self.putSentinel("@comment " + line)
	def replaceTargetFileIfDifferent (self):
		
		assert(self.outputFile == None)
		
		self.fileChangedFlag = false
		if os_path_exists(self.targetFileName):
			if filecmp.cmp(self.outputFileName,self.targetFileName):
				try: # Just delete the temp file.
					os.remove(self.outputFileName)
				except:
					es("exception deleting:" + self.outputFileName)
					es_exception()
				
				es("unchanged: " + self.shortFileName)
			else:
				try:
					# 10/6/02: retain the access mode of the previous file,
					# removing any setuid, setgid, and sticky bits.
					mode = (os.stat(self.targetFileName))[0] & 0777
				except:
					mode = None
				
				try: # Replace target file with temp file.
					os.remove(self.targetFileName)
					try:
						utils_rename(self.outputFileName,self.targetFileName)
						if mode != None: # 10/3/02: retain the access mode of the previous file.
							try:
								os.chmod(self.targetFileName,mode)
							except:
								es("exception in os.chmod(%s)" % (self.targetFileName))
						es("writing: " + self.shortFileName)
						self.fileChangedFlag = true
					except:
						# 6/28/03
						self.writeError("exception renaming: %s to: %s" % (self.outputFileName,self.targetFileName))
						es_exception()
				except:
					self.writeError("exception removing:" + self.targetFileName)
					es_exception()
					try: # Delete the temp file when the deleting the target file fails.
						os.remove(self.outputFileName)
					except:
						es("exception deleting:" + self.outputFileName)
						es_exception()
		else:
			try:
				utils_rename(self.outputFileName,self.targetFileName)
				es("creating: " + self.targetFileName)
				self.fileChangedFlag = true
			except:
				self.writeError("exception renaming:" + self.outputFileName +
					" to " + self.targetFileName)
				es_exception()
	# Write the string s as-is except that we replace '\n' with the proper line ending.
	
	def outputStringWithLineEndings (self,s):
	
		# Calling self.onl() runs afoul of queued newlines.
		self.os(s.replace('\n',self.output_newline))
	def putBodyPart(self,v):
		
		""" Generate the body enclosed in sentinel lines."""
	
		s = v.t.bodyString
		i = skip_ws_and_nl(s, 0)
		if i >= len(s): return
	
		s = removeTrailingWs(s) # don't use string.rstrip!
		self.putSentinel("@+body")
		i = 0 ; n = len(s)
		firstLastHack = 1
		
		if firstLastHack:
			# 14-SEP-2002 DTHEIN: If this is the root node, then handle all @first directives here
			lookingForLast = 0
			lookingForFirst = 0
			initialLastDirective = -1
			lastDirectiveCount = 0
			if (v == self.root):
				lookingForLast = 1
				lookingForFirst = 1
		while i < n:
			kind = self.directiveKind(s,i)
			if firstLastHack:
				# 14-SEP-2002 DTHEIN: If first directive isn't @first, then stop looking for @first
				if lookingForFirst:
					if kind != miscDirective:
						lookingForFirst = 0
					elif not match_word(s,i,"@first"):
						lookingForFirst = 0
				
				if lookingForLast:
					if initialLastDirective == -1:
						if (kind == miscDirective) and match_word(s,i,"@last"):
							# mark the point where the last directive was found
							initialLastDirective = i
					else:
						if (kind != miscDirective) or (not match_word(s,i,"@last")):
							# found something after @last, so process the @last directives
							# in 'ignore them' mode
							i, initialLastDirective = initialLastDirective, -1
							lastDirectiveCount = 0
							kind = self.directiveKind(s,i)
			j = i
			if kind == docDirective or kind == atDirective:
				i = self.putDoc(s,i,kind)
			elif ( # 10/16/02
				kind == miscDirective or
				kind == rawDirective or
				kind == endRawDirective ):
				if firstLastHack:
					if lookingForFirst: # DTHEIN: can only be true if it is @first directive
						i = self.putEmptyDirective(s,i)
					elif (initialLastDirective != -1) and match_word(s,i,"@last"):
						# DTHEIN: can only be here if lookingForLast is true
						# skip the last directive ... we'll output it at the end if it
						# is truly 'last'
						lastDirectiveCount += 1
						i = skip_line(s,i)
					else:
						i = self.putDirective(s,i)
				else:
					i = self.putDirective(s,i)
			elif kind == noDirective or kind == othersDirective:
				i = self.putCodePart(s,i,v)
			elif kind == cDirective or kind == codeDirective:
				i = self.putDirective(s,i)
				i = self.putCodePart(s,i,v)
			else: assert(false) # We must handle everything that directiveKind returns
			assert(n == len(s))
			assert(j < i) # We must make progress.
		
		if firstLastHack:
			# 14-SEP-2002 DTHEIN
			if initialLastDirective != -1:
				d = initialLastDirective
				for k in range(lastDirectiveCount):
					d = self.putEmptyDirective(s,d)
		self.putSentinel("@-body")
	def putDoc(self,s,i,kind):
	
		"""Outputs a doc section terminated by @code or end-of-text.
		
		All other interior directives become part of the doc part."""
	
		if kind == atDirective:
			i += 1 ; tag = "at"
		elif kind == docDirective:
			i += 4 ; tag = "doc"
		else: assert(false)
		# Set j to the end of the doc part.
		n = len(s) ; j = i
		while j < n:
			j = skip_line(s, j)
			kind = self.directiveKind(s, j)
			if kind == codeDirective or kind == cDirective:
				break
		self.putSentinel("@+" + tag)
		self.putDocPart(s[i:j])
		self.putSentinel("@-" + tag)
		return j
	# Puts a comment part in comments.
	# Note: this routine is _never_ called in cweb mode,
	# so noweb section references are _valid_ in cweb doc parts!
	
	def putDocPart(self,s):
	
		# j = skip_line(s,0) ; trace(`s[:j]`)
		single = len(self.endSentinelComment) == 0
		if not single:
			self.putIndent(self.indent)
			self.os(self.startSentinelComment) ; self.onl()
		# Put all lines.
		i = 0 ; n = len(s)
		while i < n:
			self.putIndent(self.indent)
			leading = self.indent
			if single:
				self.os(self.startSentinelComment) ; self.oblank()
				leading += len(self.startSentinelComment) + 1
			# We remove trailing whitespace from lines that have _not_ been 
			# split so that a newline has been inserted by this routine if and 
			# only if it is preceded by whitespace.
			
			line = i # Start of the current line.
			while i < n:
				word = i # Start of the current word.
				# Skip the next word and trailing whitespace.
				i = skip_ws(s, i)
				while i < n and not is_nl(s,i) and not is_ws(s[i]):
					i += 1
				i = skip_ws(s,i)
				# Output the line if no more is left.
				if i < n and is_nl(s,i):
					break
				# Split the line before the current word if needed.
				lineLen = i - line
				if line == word or leading + lineLen < self.page_width:
					word = i # Advance to the next word.
				else:
					# Write the line before the current word and insert a newline.
					theLine = s[line:word]
					self.os(theLine)
					self.onl() # This line must contain trailing whitespace.
					line = i = word  # Put word on the next line.
					break
			# Remove trailing whitespace and output the remainder of the line.
			theLine = string.rstrip(s[line:i]) # from right.
			self.os(theLine)
			if i < n and is_nl(s,i):
				i = skip_nl(s,i)
				self.onl() # No inserted newline and no trailing whitespace.
		if not single:
			# This comment is like a sentinel.
			self.onl() ; self.putIndent(self.indent)
			self.os(self.endSentinelComment)
			self.onl() # Note: no trailing whitespace.
	def putCodePart(self,s,i,v):
	
		"""Expands a code part, terminated by any at-directive except at-others.
		
		It expands references and at-others and outputs @verbatim sentinels as needed."""
	
		atOthersSeen = false # true: at-others has been expanded.
		while i < len(s):
			# The at-others directive is the only directive that is recognized 
			# following leading whitespace, so it is just a little tricky to 
			# recognize it.
			
			leading_nl = (s[i] == body_newline) # 9/27/02: look ahead before outputting newline.
			if leading_nl:
				i = skip_nl(s,i)
				self.onl() # 10/15/02: simpler to do it here.
			
			#leading_ws1 = i # 1/27/03
			j,delta = skip_leading_ws_with_indent(s,i,self.tab_width)
			#leading_ws2 = j # 1/27/03
			kind1 = self.directiveKind(s,i)
			kind2 = self.directiveKind(s,j)
			if self.raw:
				if kind1 == endRawDirective:
					self.raw = false
					self.putSentinel("@@end_raw")
					i = skip_line(s,i)
			else:
				if kind1 == othersDirective or kind2 == othersDirective:
					# This skips all indent and delta whitespace, so putAtOthers must generate it all.
					
					if 0: # 9/27/02: eliminates the newline preceeding the @+others sentinel.
						# This does not seem to be a good idea.
						i = skip_line(s,i) 
					else:
						i = skip_to_end_of_line(s,i)
					
					if atOthersSeen:
						self.writeError("@others already expanded in: " + v.headString())
					else:
						atOthersSeen = true
						self.putAtOthers(v, delta)
						
						# 12/8/02: Skip the newline _after_ the @others.
						if not self.sentinels and is_nl(s,i):
							i = skip_nl(s,i)
				elif kind1 == rawDirective:
					self.raw = true
					self.putSentinel("@@raw")
					i = skip_line(s,i)
				elif kind1 == noDirective:
					if match (s,i,self.startSentinelComment + '@'):
						self.putSentinel("@verbatim") # Bug fix (!!): 9/20/03
				else:
					break # all other directives terminate the code part.
			if not self.raw:
				# 12/8/02: Don't write trailing indentation if not writing sentinels.
				if self.sentinels or j < len(s):
					self.putIndent(self.indent)
			
			newlineSeen = false
			# 12/8/02: we buffer characters here for two reasons:
			# 1) to make traces easier to read and 2) to increase speed.
			buf = i # Indicate the start of buffered characters.
			while i < len(s) and not newlineSeen:
				ch = s[i]
				if ch == body_newline:
					break
				elif ch == body_ignored_newline:
					i += 1
				elif ch == '<' and not self.raw:
					isSection, j = self.isSectionName(s, i)
					
					if isSection:
						# Output the buffered characters and clear the buffer.
						s2 = s[buf:i] ; buf = i
						# 7/9/03: don't output trailing indentation if we aren't generating sentinels.
						if not self.sentinels:
							while len(s2) and s2[-1] in (' ','\t'):
								s2 = s2[:-1]
						self.putBuffered(s2)
						# Output the expansion.
						name = s[i:j]
						j,newlineSeen = self.putRef(name,v,s,j,delta)
						assert(j > i) # isSectionName must have made progress
						i = j ; buf = i
					else:
						# This is _not_ an error.
						i += 1
				else:
					i += 1
			# Output any buffered characters.
			self.putBuffered(s[buf:i])
	
		# Raw code parts can only end at the end of body text.
		self.raw = false
		return i
	def inAtOthers(self,v):
	
		"""Returns true if v should be included in the expansion of the at-others directive in the body text of v's parent.
		
		v will not be included if it is a definition node or if its body text contains an @ignore directive.
		Previously, a "nested" @others directive would also inhibit the inclusion of v."""
	
		# Return false if this has been expanded previously.
		if  v.isVisited(): return false
	
		# Return false if this is a definition node.
		h = v.headString()
		i = skip_ws(h,0)
		isSection, j = self.isSectionName(h,i)
		if isSection: return false
	
		# Return false if v's body contains an @ignore or at-others directive.
		if 1: # 7/29/02: New code.  Amazingly, this appears to work!
			return not v.isAtIgnoreNode()
		else: # old & reliable code
			return not v.isAtIgnoreNode() and not v.isAtOthersNode()
	# returns (flag, end). end is the index of the character after the section name.
	
	def isSectionName(self,s,i):
	
		if not match(s,i,"<<"):
			return false, -1
		i = find_on_line(s,i,">>")
		if i:
			return true, i + 2
		else:
			return false, -1
	# The at-others directive is recognized only at the start of the line.  
	# This code must generate all leading whitespace for the opening sentinel.
	def putAtOthers(self,v,delta):
		
		"""Output code corresponding to an @others directive."""
	
		self.indent += delta
		self.putSentinel("@+others")
	
		child = v.firstChild()
		while child:
			if self.inAtOthers( child ):
				self.putAtOthersChild( child )
			child = child.next()
	
		self.putSentinel("@-others")
		self.indent -= delta
	def putAtOthersChild(self,v):
		
		# trace("%d %s" % (self.indent,`v`))
		self.putOpenNodeSentinel(v)
		
		# Insert the expansion of v.
		v.setVisited() # Make sure it is never expanded again.
		self.putBodyPart(v)
	
		# Insert expansions of all children.
		child = v.firstChild()
		while child:
			if self.inAtOthers( child ):
				self.putAtOthersChild( child )
			child = child.next()
	
		self.putCloseNodeSentinel(v)
	def putRef (self,name,v,s,i,delta):
	
		newlineSeen = false
		ref = findReference(name, v)
		if not ref:
			self.writeError("undefined section: " + name +
				"\n\treferenced from: " + v.headString())
			return i,newlineSeen
	
		# trace(self.indent,delta,s[i:])
		# Adjust indent here so sentinel looks better.
		self.indent += delta
		
		self.putSentinel("@" + name)
		self.putOpenSentinels(v,ref)
		self.putBodyPart(ref)
		self.putCloseSentinels(v,ref)
		j = skip_ws(s,i)
		if j < len(s) and match(s,j,self.startSentinelComment + '@'):
			self.putSentinel("@verbatimAfterRef")
			# 9/27/02: Put the line immediately, before the @-node sentinel.
			k = skip_to_end_of_line(s,i)
			self.putBuffered(s[i:k])
			i = k ; newlineSeen = false
		
		self.indent -= delta
		ref.setVisited()
	
		# The newlineSeen allows the caller to break out of the loop.
		return i,newlineSeen
	def putBuffered (self,s):
		
		"""Put s, converting all tabs to blanks as necessary."""
		
		if s:
			w = self.tab_width
			if w < 0:
				lines = s.split('\n')
				for i in xrange(len(lines)):
					line = lines[i]
					line2 = ""
					for j in xrange(len(line)):
						ch = line[j]
						if ch == '\t':
							w2 = computeWidth(s[:j],w)
							w3 = (abs(w) - (w2 % abs(w)))
							line2 += ' ' * w3
						else:
							line2 += ch
					lines[i] = line2
				s = string.join(lines,'\n')
			self.os(s)
	def oblank(self):
		self.os(' ')
	
	def oblanks(self,n):
		self.os(' ' * abs(n))
	
	def onl(self):
		self.os(self.output_newline)
	
	def os(self,s):
		if s is None or len(s) == 0: return
		if self.suppress_newlines and self.newline_pending:
			self.newline_pending = false
			s = self.output_newline + s
		if self.outputFile:
			try:
				s = toEncodedString(s,self.encoding,reportErrors=true)
				self.outputFile.write(s)
			except:
				es("exception writing:" + `s`)
				es_exception()
	
	def otabs(self,n):
		self.os('\t' * abs(n))
	# This method outputs s, a directive or reference, in a sentinel.
	
	def putDirective(self,s,i):
	
		tag = "@delims"
		assert(i < len(s) and s[i] == '@')
		k = i
		j = skip_to_end_of_line(s,i)
		directive = s[i:j]
	
		if match_word(s,k,tag):
			# Put a space to protect the last delim.
			self.putSentinel(directive + " ") # 10/23/02: put @delims, not @@delims
			
			# Skip the keyword and whitespace.
			j = i = skip_ws(s,k+len(tag))
			
			# Get the first delim.
			while i < len(s) and not is_ws(s[i]) and not is_nl(s,i):
				i += 1
			if j < i:
				self.startSentinelComment = s[j:i]
				# Get the optional second delim.
				j = i = skip_ws(s,i)
				while i < len(s) and not is_ws(s[i]) and not is_nl(s,i):
					i += 1
				self.endSentinelComment = choose(j<i, s[j:i], "")
			else:
				self.writeError("Bad @delims directive")
		else:
			self.putSentinel("@" + directive)
	
		i = skip_line(s,k)
		return i
	# 14-SEP-2002 DTHEIN
	# added for use by putBodyPart()
	
	# This method outputs the directive without the parameter text
	def putEmptyDirective(self,s,i):
	
		assert(i < len(s) and s[i] == '@')
		
		endOfLine = s.find('\n',i)
		# 21-SEP-2002 DTHEIN: if no '\n' then just use line length
		if endOfLine == -1:
			endOfLine = len(s)
		token = s[i:endOfLine].split()
		directive = token[0]
		self.putSentinel("@" + directive)
	
		i = skip_line(s,i)
		return i
	def putIndent(self,n):
		
		"""Put tabs and spaces corresponding to n spaces, assuming that we are at the start of a line."""
	
		if n != 0:
			w = self.tab_width
			if w > 1:
				q,r = divmod(n,w) 
				self.otabs(q) 
				self.oblanks(r)
			else:
				self.oblanks(n)
	
class oldDerivedFile(baseOldDerivedFile):
	pass # May be overridden in plugins.
	
class baseNewDerivedFile(oldDerivedFile):
	"""The base class to read and write 4.x derived files."""
	def __init__(self,c):
		
		"""Ctor for 4.x atFile class."""
		
		at = self
	
		# Initialize the base class.
		oldDerivedFile.__init__(self,c) 
	
		# For 4.x reading & writing...
		at.inCode = true
		## at.nodeIndices = app.nodeIndices
	
		# For 4.x writing...
		at.docKind = None
		at.pending = [] # Doc part that remains to be written.
	
		# For 4.x reading...
		at.docOut = [] # The doc part being accumulated.
		at.done = false # true when @-leo seen.
		at.endSentinelStack = []
		at.importing = false
		at.indent = 0 ; at.indentStack = []
		at.lastLines = [] # The lines after @-leo
		at.leadingWs = ""
		at.out = None ; at.outStack = []
		at.root_seen = false # true: root vnode has been handled in this file.
		at.tnodeList = [] ; at.tnodeListIndex = 0
		at.t = None ; at.tStack = []
	
		# The dispatch dictionary used by scanText4.
		at.dispatch_dict = {
			# Plain line.
			noSentinel: at.readNormalLine,
			# Starting sentinels...
			startAt:     at.readStartAt,
			startDoc:    at.readStartDoc,
			startLeo:    at.readStartLeo,
			startNode:   at.readStartNode,
			startOthers: at.readStartOthers,
			# Ending sentinels...
			endAt:     at.readEndAt,
			endDoc:    at.readEndDoc,
			endLeo:    at.readEndLeo,
			endNode:   at.readEndNode,
			endOthers: at.readEndOthers,
			# Non-paired sentinels.
			startAfterRef:  at.readAfterRef,
			startComment:   at.readComment,
			startDelims:    at.readDelims,
			startDirective: at.readDirective,
			startNl:        at.readNl,
			startNonl:      at.readNonl,
			startRef:       at.readRef,
			startVerbatim:  at.readVerbatim,
			# Ignored 3.x sentinels
			endBody:               at.ignoreOldSentinel,
			startBody:             at.ignoreOldSentinel,
			startVerbatimAfterRef: at.ignoreOldSentinel }
	def readOpenFile(self,root,file,firstLines):
		
		"""Read an open 4.x derived file."""
		
		at = self
		if at.trace: trace("new_df",root)
	
		# Scan the 4.x file.
		at.scanAllDirectives(root)
		at.tnodeListIndex = 0
		lastLines = at.scanText4(file,root)
		root.t.setVisited() # Disable warning about set nodes.
		
		# Handle first and last lines.
		try: body = root.t.tempBodyString
		except: body = ""
		lines = body.split('\n')
		at.completeFirstDirectives(lines,firstLines)
		at.completeLastDirectives(lines,lastLines)
		s = '\n'.join(lines).replace('\r', '')
		root.t.tempBodyString = s
	def findChild (self,headline):
		
		"""Return the next tnode in the at.tnodeList."""
	
		at = self
	
		if at.importing:
			v = at.createImportedNode(at.root,at.c,headline)
			return v.t
	
		if not hasattr(at.root,"tnodeList"):
			at.readError("no tnodeList for " + `at.root`)
			es("Write the @file node or use the Import Derived File command")
			trace("no tnodeList for ",at.root)
			return None
			
		if at.tnodeListIndex >= len(at.root.tnodeList):
			at.readError("bad tnodeList index: %d, %s" % (at.tnodeListIndex,`at.root`))
			trace("bad tnodeList index",at.tnodeListIndex,len(at.root.tnodeList),at.root)
			return None
			
		t = at.root.tnodeList[at.tnodeListIndex]
		assert(t)
		at.tnodeListIndex += 1
	
		# Get any vnode joined to t.
		# To do: cut/paste may cause problems here...
		try:
			v = t.joinList[0]
		except:
			at.readError("No joinList for tnode: %s" % `t`)
			trace(at.tnodeListIndex,len(at.root.tnodeList))
			return None
	
		# Check the headline.
		if headline.strip() == v.headString().strip():
			t.setVisited() # Supress warning about unvisited node.
			return t
		else:
			at.readError(
				"Mismatched headline.\nExpecting: %s\ngot: %s" %
				(headline,v.headString()))
			trace("Mismatched headline",headline,v.headString())
			trace(at.tnodeListIndex,len(at.root.tnodeList))
			return None
	def scanText4 (self,file,v):
		
		"""Scan a 4.x derived file non-recursively."""
	
		at = self
		# Unstacked ivars...
		at.done = false
		at.inCode = true
		at.lastLines = [] # The lines after @-leo
		at.leadingWs = ""
		at.indent = 0 # Changed only for sentinels.
		at.rootSeen = false
		
		# Stacked ivars...
		at.endSentinelStack = [endLeo] # We have already handled the @+leo sentinel.
		at.out = [] ; at.outStack = []
		at.t = v.t ; at.tStack = []
		
		if 0: # Useful for debugging.
			if hasattr(v,"tnodeList"):
				trace("len(v.tnodeList)",len(v.tnodeList),v)
			else:
				trace("no tnodeList",v)
		while at.errors == 0 and not at.done:
			s = at.readLine(file)
			if len(s) == 0: break
			kind = at.sentinelKind(s)
			# trace(at.sentinelName(kind),`s`)
			if kind == noSentinel:
				i = 0
			else:
				i = at.skipSentinelStart(s,0)
			func = at.dispatch_dict[kind]
			func(s,i)
	
		if at.errors == 0 and not at.done:
			assert(at.endSentinelStack)
			
			at.readError(
				"Unexpected end of file. Expecting %s sentinel" %
				at.sentinelName(at.endSentinelStack[-1]))
	
		return at.lastLines
	def readNormalLine (self,s,i):
	
		at = self
		
		if at.inCode:
			if not at.raw:
				s = removeLeadingWhitespace(s,at.indent,at.tab_width)
			at.out.append(s)
		else:
			if len(at.endSentinelComment) == 0:
				# Skip the single comment delim and a blank.
				i = skip_ws(s,0)
				if match(s,i,at.startSentinelComment):
					i += len(at.startSentinelComment)
					if match(s,i," "): i += 1
			else:
				i = at.skipIndent(s,0,at.indent)
			
			line = s[i:-1] # remove newline for rstrip.
			
			if line == line.rstrip():
				# no trailing whitespace: the newline is real.
				at.docOut.append(line + '\n')
			else:
				# trailing whitespace: the newline is fake.
				at.docOut.append(line)
	def readStartAt (self,s,i):
		"""Read an @+at sentinel."""
		at = self ; assert(match(s,i,"+at"))
		if 0:# new code: append whatever follows the sentinel.
			i += 3 ; j = self.skipToEndSentinel(s,i) ; follow = s[i:j]
			at.out.append('@' + follow) ; at.docOut = []
		else:
			i += 3 ; j = skip_ws(s,i) ; ws = s[i:j]
			at.docOut = ['@' + ws + '\n'] # This newline may be removed by a following @nonl
		at.inCode = false
		at.endSentinelStack.append(endAt)
		
	def readStartDoc (self,s,i):
		"""Read an @+doc sentinel."""
		at = self ; assert(match(s,i,"+doc"))
		if 0: # new code: append whatever follows the sentinel.
			i += 4 ; j = self.skipToEndSentinel(s,i) ; follow = s[i:j]
			at.out.append('@' + follow) ; at.docOut = []
		else:
			i += 4 ; j = skip_ws(s,i) ; ws = s[i:j]
			at.docOut = ["@doc" + ws + '\n'] # This newline may be removed by a following @nonl
		at.inCode = false
		at.endSentinelStack.append(endDoc)
		
	def skipToEndSentinel(self,s,i):
		end = self.endSentinelComment
		if end:
			j = s.find(end,i)
			if j == -1:
				return skip_to_end_of_line(s,i)
			else:
				return j
		else:
			return skip_to_end_of_line(s,i)
	def readStartLeo (self,s,i):
		
		"""Read an unexpected @+leo sentinel."""
	
		at = self
		assert(match(s,i,"+leo"))
		at.readError("Ignoring unexpected @+leo sentinel")
	def readStartNode (self,s,i):
		
		"""Read an @node sentinel."""
		
		at = self ; assert(match(s,i,"+node:"))
		i += 6
		
		# Set headline to the rest of the line.
		# Don't strip leading whitespace."
		
		if len(at.endSentinelComment) == 0:
			headline = s[i:-1].rstrip()
		else:
			k = s.rfind(at.endSentinelComment,i)
			headline = s[i:k].rstrip() # works if k == -1
		
		# Undo the CWEB hack: undouble @ signs if the opening comment delim ends in '@'.
		if at.startSentinelComment[-1:] == '@':
			headline = headline.replace('@@','@')
		if not at.root_seen:
			at.root_seen = true
			if not at.importing:
				h = headline.strip()
				
				if h[:5] == "@file":
					i,junk,junk = scanAtFileOptions(h)
					fileName = string.strip(h[i:])
					if fileName != at.targetFileName:
						at.readError("File name in @node sentinel does not match file's name")
				elif h[:8] == "@rawfile":
					fileName = string.strip(h[8:])
					if fileName != at.targetFileName:
						at.readError("File name in @node sentinel does not match file's name")
				else:
					at.readError("Missing @file in root @node sentinel")
	
		i,newIndent = skip_leading_ws_with_indent(s,0,at.tab_width)
		at.indentStack.append(at.indent) ; at.indent = newIndent
		
		at.outStack.append(at.out) ; at.out = []
		at.tStack.append(at.t) ; at.t = at.findChild(headline)
		
		at.endSentinelStack.append(endNode)
	def readStartOthers (self,s,i):
		
		"""Read an @+others sentinel."""
	
		at = self
		j = skip_ws(s,i)
		leadingWs = s[i:j]
		if leadingWs:
			assert(match(s,j,"@+others"))
		else:
			assert(match(s,j,"+others"))
	
		# Make sure that the generated at-others is properly indented.
		at.out.append(leadingWs + "@others\n")
		
		at.endSentinelStack.append(endOthers)
	def readEndAt (self,s,i):
		
		"""Read an @-at sentinel."""
	
		at = self
		at.readLastDocLine("@")
		at.popSentinelStack(endAt)
		at.inCode = true
			
	def readEndDoc (self,s,i):
		
		"""Read an @-doc sentinel."""
	
		at = self
		at.readLastDocLine("@doc")
		at.popSentinelStack(endDoc)
		at.inCode = true
	def readEndLeo (self,s,i):
		
		"""Read an @-leo sentinel."""
		
		at = self
	
		# Ignore everything after @-leo.
		# Such lines were presumably written by @last.
		while 1:
			s = at.readLine(at.file)
			if len(s) == 0: break
			at.lastLines.append(s) # Capture all trailing lines, even if empty.
	
		at.done = true
	def readEndNode (self,s,i):
		
		"""Handle end-of-node processing for @-others and @-ref sentinels."""
	
		at = self
		
		# End raw mode.
		at.raw = false
		
		# Set the temporary body text.
		s = ''.join(at.out)
		s = toUnicode(s,app.tkEncoding) # 9/28/03
	
		if at.importing:
			at.t.bodyString = s
		else:
			at.t.tempBodyString = s
				
		# Indicate that the tnode has been set in the derived file.
		at.t.setVisited()
	
		# End the previous node sentinel.
		at.indent = at.indentStack.pop()
		at.out = at.outStack.pop()
		at.t = at.tStack.pop()
	
		at.popSentinelStack(endNode)
	def readEndOthers (self,s,i):
		
		"""Read an @-others sentinel."""
		
		at = self
		at.popSentinelStack(endOthers)
	def readLastDocLine (self,tag):
		
		"""Read the @c line that terminates the doc part.
		tag is @doc or @."""
		
		at = self
		end = at.endSentinelComment
		start = at.startSentinelComment
		s = ''.join(at.docOut)
		
		if 0: # new code.
			if end:
				# Remove opening block delim.
				if match(s,0,start):
					s = s[len(start):]
				else:
					at.readError("Missing open block comment")
					trace(s)
					return
					
				# Remove trailing newline.
				if s[-1] == '\n':
					s = s[:-1]
			
				# Remove closing block delim.
				if s[-len(end):] == end:
					s = s[:-len(end)]
				else:
					at.readError("Missing close block comment")
					return
			
			at.out.append(s) # The tag has already been removed.
			at.docOut = []
		else:
			# Remove the @doc or @space.  We'll add it back at the end.
			if match(s,0,tag):
				s = s[len(tag):]
			else:
				at.readError("Missing start of doc part")
				return
			
			if end:
				# Remove opening block delim.
				if match(s,0,start):
					s = s[len(start):]
				else:
					at.readError("Missing open block comment")
					trace(s)
					return
					
				# Remove trailing newline.
				if s[-1] == '\n':
					s = s[:-1]
			
				# Remove closing block delim.
				if s[-len(end):] == end:
					s = s[:-len(end)]
				else:
					at.readError("Missing close block comment")
					return
			
			at.out.append(tag + s)
			at.docOut = []
	def  ignoreOldSentinel (self,s,i):
		
		"""Ignore an 3.x sentinel."""
		
		es("Ignoring 3.x sentinel: " + s.strip(), color="blue")
	def  readAfterRef (self,s,i):
		
		"""Read an @afterref sentinel."""
		
		at = self
		assert(match(s,i,"afterref"))
		
		# Append the next line to the text.
		s = at.readLine(at.file)
		at.out.append(s)
	def readComment (self,s,i):
		
		"""Read an @comment sentinel."""
	
		assert(match(s,i,"comment"))
	
		# Just ignore the comment line!
	def readDelims (self,s,i):
		
		"""Read an @delims sentinel."""
		
		at = self
		assert(match(s,i-1,"@delims"));
	
		# Skip the keyword and whitespace.
		i0 = i-1
		i = skip_ws(s,i-1+7)
			
		# Get the first delim.
		j = i
		while i < len(s) and not is_ws(s[i]) and not is_nl(s,i):
			i += 1
		
		if j < i:
			at.startSentinelComment = s[j:i]
			# print "delim1:", at.startSentinelComment
		
			# Get the optional second delim.
			j = i = skip_ws(s,i)
			while i < len(s) and not is_ws(s[i]) and not is_nl(s,i):
				i += 1
			end = choose(j<i,s[j:i],"")
			i2 = skip_ws(s,i)
			if end == at.endSentinelComment and (i2 >= len(s) or is_nl(s,i2)):
				at.endSentinelComment = "" # Not really two params.
				line = s[i0:j]
				line = line.rstrip()
				at.out.append(line+'\n')
			else:
				at.endSentinelComment = end
				# print "delim2:",end
				line = s[i0:i]
				line = line.rstrip()
				at.out.append(line+'\n')
		else:
			at.readError("Bad @delims")
			# Append the bad @delims line to the body text.
			at.out.append("@delims")
	def readDirective (self,s,i):
		
		"""Read an @@sentinel."""
		
		at = self
		assert(match(s,i,"@")) # The first '@' has already been eaten.
		
		if match_word(s,i,"@raw"):
			at.raw = true
		elif match_word(s,i,"@end_raw"):
			at.raw = false
		
		e = at.endSentinelComment
		s2 = s[i:]
		if len(e) > 0:
			k = s.rfind(e,i)
			if k != -1:
				s2 = s[i:k] + '\n'
			
		start = at.startSentinelComment
		if start and len(start) > 0 and start[-1] == '@':
			s2 = s2.replace('@@','@')
	
		at.out.append(s2)
	def readNl (self,s,i):
		
		"""Handle an @nonl sentinel."""
		
		at = self
		assert(match(s,i,"nl"))
		
		if at.inCode:
			at.out.append('\n')
		else:
			at.docOut.append('\n')
	def readNonl (self,s,i):
		
		"""Handle an @nonl sentinel."""
		
		at = self
		assert(match(s,i,"nonl"))
		
		if at.inCode:
			s = ''.join(at.out)
			if s and s[-1] == '\n':
				at.out = [s[:-1]]
			else:
				trace("out:",`s`)
				at.readError("unexpected @nonl directive in code part")	
		else:
			s = ''.join(at.pending)
			if s:
				if s and s[-1] == '\n':
					at.pending = [s[:-1]]
				else:
					trace("docOut:",`s`)
					at.readError("unexpected @nonl directive in pending doc part")
			else:
				s = ''.join(at.docOut)
				if s and s[-1] == '\n':
					at.docOut = [s[:-1]]
				else:
					trace("docOut:",`s`)
					at.readError("unexpected @nonl directive in doc part")
	# The sentinel contains an @ followed by a section name in angle 
	# brackets.  This code is different from the code for the @@ sentinel: the 
	# expansion of the reference does not include a trailing newline.
	
	def readRef (self,s,i):
		
		"""Handle an @<< sentinel."""
		
		at = self
		j = skip_ws(s,i)
		assert(match(s,j,"<<"))
		
		if len(at.endSentinelComment) == 0:
			line = s[i:-1] # No trailing newline
		else:
			k = s.find(at.endSentinelComment,i)
			line = s[i:k] # No trailing newline, whatever k is.
				
		# Undo the cweb hack.
		start = at.startSentinelComment
		if start and len(start) > 0 and start[-1] == '@':
			line = line.replace('@@','@')
	
		at.out.append(line)
	def readVerbatim (self,s,i):
		
		"""Read an @verbatim sentinel."""
		
		at = self
		assert(match(s,i,"verbatim"))
		
		# Append the next line to the text.
		s = at.readLine(at.file) 
		i = at.skipIndent(s,0,at.indent)
		at.out.append(s[i:])
	def badEndSentinel (self,expectedKind):
		
		"""Handle a mismatched ending sentinel."""
	
		at = self
		assert(at.endSentinelStack)
		at.readError("Ignoring %s sentinel.  Expecting %s" %
			(at.sentinelName(at.endSentinelStack[-1]),
			 at.sentinelName(expectedKind)))
			 
	def popSentinelStack (self,expectedKind):
		
		"""Pop an entry from endSentinelStack and check it."""
		
		at = self
		if at.endSentinelStack and at.endSentinelStack[-1] == expectedKind:
			at.endSentinelStack.pop()
		else:
			at.badEndSentinel(expectedKind)
	def nodeSentinelText(self,v):
		
		"""Return the text of a @+node or @-node sentinel for v."""
		
		at = self ; h = v.headString()
		# Bug fix 1/24/03:
		# 
		# If the present @language/@comment settings do not specify a 
		# single-line comment we remove all block comment delims from h.  This 
		# prevents headline text from interfering with the parsing of node 
		# sentinels.
		
		start = at.startSentinelComment
		end = at.endSentinelComment
		
		if end and len(end) > 0:
			h = h.replace(start,"")
			h = h.replace(end,"")
		return h
	def putLeadInSentinel (self,s,i,j,delta):
		
		"""Generate @nonl sentinels as needed to ensure a newline before a group of sentinels.
		
		Set at.leadingWs as needed for @+others and @+<< sentinels.
	
		i points at the start of a line.
		j points at @others or a section reference.
		delta is the change in at.indent that is about to happen and hasn't happened yet."""
	
		at = self
		at.leadingWs = "" # Set the default.
		if i == j:
			return # The @others or ref starts a line.
	
		k = skip_ws(s,i)
		if j == k:
			# Only whitespace before the @others or ref.
			at.leadingWs = s[i:j] # Remember the leading whitespace, including its spelling.
		else:
			at.os(s[i:j]) ; at.onl_sent() # 10/21/03
			at.indent += delta # Align the @nonl with the following line.
			at.putSentinel("@nonl")
			at.indent -= delta # Let the caller set at.indent permanently.
	def putOpenLeoSentinel(self,s):
		
		"""Write @+leo sentinel."""
	
		at = self
		
		if not at.sentinels:
			return # Handle @nosentinelsfile.
	
		encoding = at.encoding.lower()
		if encoding != "utf-8":
			s = s + "-encoding=%s." % (encoding)
		
		at.putSentinel(s)
	def putOpenNodeSentinel(self,v):
		
		"""Write @+node sentinel for v."""
		
		at = self
	
		if v.isAtFileNode() and v != at.root:
			at.writeError("@file not valid in: " + v.headString())
			return
		
		s = at.nodeSentinelText(v)
		at.putSentinel("@+node:" + s)
	
		# Append the n'th tnode to the root's tnode list.
	
		# trace("%3d %3d" % (len(at.root.tnodeList),v.t.fileIndex),v)
		at.root.tnodeList.append(v.t)
	# This method outputs all sentinels.
	
	def putSentinel(self,s):
	
		"Write a sentinel whose text is s, applying the CWEB hack if needed."
		
		at = self
	
		if not at.sentinels:
			return # Handle @file-nosent
	
		at.putIndent(at.indent)
		at.os(at.startSentinelComment)
		# The cweb hack:
		# 
		# If the opening comment delim ends in '@', double all '@' signs 
		# except the first, which is "doubled" by the trailing '@' in the 
		# opening comment delimiter.
		
		start = at.startSentinelComment
		if start and start[-1] == '@':
			assert(s and s[0]=='@')
			s = s.replace('@','@@')[1:]
		at.os(s)
		if at.endSentinelComment:
			at.os(at.endSentinelComment)
		at.onl()
	def skipSentinelStart(self,s,i):
		
		"""Skip the start of a sentinel."""
	
		start = self.startSentinelComment
		assert(start and len(start)>0)
	
		i = skip_ws(s,i)
		assert(match(s,i,start))
		i += len(start)
	
		# 7/8/02: Support for REM hack
		i = skip_ws(s,i)
		assert(i < len(s) and s[i] == '@')
		return i + 1
	def sentinelKind(self,s):
		
		"""Return the kind of sentinel at s."""
		
		at = self
	
		i = skip_ws(s,0)
		if match(s,i,at.startSentinelComment): 
			i += len(at.startSentinelComment)
		else:
			return noSentinel
	
		# Locally undo cweb hack here
		start = at.startSentinelComment
		if start and len(start) > 0 and start[-1] == '@':
			s = s[:i] + string.replace(s[i:],'@@','@')
			
		# 4.0: Look ahead for @[ws]@others and @[ws]<<
		if match(s,i,"@"):
			j = skip_ws(s,i+1)
			if j > i+1:
				# trace(`ws`,`s`)
				if match(s,j,"@+others"):
					return startOthers
				elif match(s,j,"<<"):
					return startRef
				else:
					# No other sentinels allow whitespace following the '@'
					return noSentinel
	
		# Do not skip whitespace here!
		if match(s,i,"@<<"): return startRef
		if match(s,i,"@@"):   return startDirective
		if not match(s,i,'@'): return noSentinel
		j = i # start of lookup
		i += 1 # skip the at sign.
		if match(s,i,'+') or match(s,i,'-'):
			i += 1
		i = skip_c_id(s,i)
		key = s[j:i]
		if len(key) > 0 and sentinelDict.has_key(key):
			return sentinelDict[key]
		else:
			return noSentinel
	# 4.0: Don't use newline-pending logic.
	
	def closeWriteFile (self):
		
		at = self
		if at.outputFile:
			at.outputFile.flush()
			at.outputFile.close()
			at.outputFile = None
	# This is the entry point to the write code.  root should be an @file vnode.
	
	def write(self,root,nosentinels=false,scriptFile=None):
		
		"""Write a 4.x derived file."""
		
		at = self ; c = at.c
		if at.trace: trace("new_df",root)
	
		at.sentinels = not nosentinels
		at.errors = 0
		c.setIvarsFromPrefs()
		at.root = root
		at.root.tnodeList = []
		at.raw = false
		c.endEditing() # Capture the current headline.
		try:
			if scriptFile:
				at.targetFileName = "<script>"
			if nosentinels:
				at.targetFileName = root.atNoSentinelsFileNodeName()
			else:
				at.targetFileName = root.atFileNodeName()
			
			if scriptFile:
				ok = true
				at.outputFileName = "<script>"
				at.outputFile = scriptFile
			else:
				ok = at.openWriteFile(root)
				
			if not ok:
				return
			root.clearVisitedInTree()
			# unvisited nodes will be orphans, except in cweb trees.
			root.clearVisitedInTree()
			
			# Write any @first lines.  These lines are also converted to 
			# @verbatim lines, so the read logic simply ignores lines 
			# preceding the @+leo sentinel.
			
			s = root.t.bodyString
			tag = "@first"
			i = 0
			while match(s,i,tag):
				i += len(tag)
				i = skip_ws(s,i)
				j = i
				i = skip_to_end_of_line(s,i)
				# Write @first line, whether empty or not
				line = s[j:i]
				self.os(line) ; self.onl()
				i = skip_nl(s,i)
			
			# Put the main part of the file.
			at.putOpenLeoSentinel("@+leo-ver=4")
			at.putInitialComment()
			at.putBody(root)
			at.putSentinel("@-leo")
			root.setVisited()
			
			# Write any @last lines.  These lines are also converted to 
			# @verbatim lines, so the read logic simply ignores lines 
			# following the @-leo sentinel.
			
			tag = "@last"
			lines = string.split(root.t.bodyString,'\n')
			n = len(lines) ; j = k = n - 1
			# Don't write an empty last line.
			if j >= 0 and len(lines[j])==0:
				j = k = n - 2
			# Scan backwards for @last directives.
			while j >= 0:
				line = lines[j]
				if match(line,0,tag): j -= 1
				else: break
			# Write the @last lines.
			for line in lines[j+1:k+1]:
				i = len(tag) ; i = skip_ws(line,i)
				self.os(line[i:]) ; self.onl()
			
			
			if not scriptFile:
				at.closeWriteFile()
				if not nosentinels:
					# 10/26/02: Always warn, even when language=="cweb"
					
					next = root.nodeAfterTree()
					v = root
					while v and v != next:
						if not v.isVisited():
							at.writeError("Orphan node:  " + v.headString())
						if v.isAtIgnoreNode():
							at.writeError("@ignore node: " + v.headString())
						v = v.threadNext()
				# We set the orphan and dirty flags if there are problems 
				# writing the file to force Commands::write_LEO_file to write 
				# the tree to the .leo file.
				
				if at.errors > 0 or at.root.isOrphan():
					root.setOrphan()
					root.setDirty() # 2/9/02: make _sure_ we try to rewrite this file.
					os.remove(at.outputFileName) # Delete the temp file.
					es("Not written: " + at.outputFileName)
				else:
					root.clearOrphan()
					root.clearDirty()
					at.replaceTargetFileIfDifferent()
		except:
			if scriptFile:
				es("exception preprocessing script",color="blue")
				es_exception(full=false)
				scriptFile.clear()
			else:
				at.handleWriteException()
	def rawWrite(self,root):
	
		at = self
		if at.trace: trace("new_df",root)
	
		c = at.c ; at.root = root
		at.errors = 0
		at.root.tnodeList = [] # 9/26/03: after beta 1 release.
		at.sentinels = true # 10/1/03
		c.endEditing() # Capture the current headline.
		try:
			at.targetFileName = root.atRawFileNodeName()
			ok = at.openWriteFile(root)
			if not ok: return
			# Write any @first lines.  These lines are also converted to 
			# @verbatim lines, so the read logic simply ignores lines 
			# preceding the @+leo sentinel.
			
			s = root.t.bodyString
			tag = "@first"
			i = 0
			while match(s,i,tag):
				i += len(tag)
				i = skip_ws(s,i)
				j = i
				i = skip_to_end_of_line(s,i)
				# Write @first line, whether empty or not
				line = s[j:i]
				at.putBuffered(line) ; at.onl()
				i = skip_nl(s,i)
			at.putOpenLeoSentinel("@+leo-ver=4")
			s2 = app.config.output_initial_comment
			if s2:
				lines = string.split(s2,"\\n")
				for line in lines:
					line = line.replace("@date",time.asctime())
					if len(line)> 0:
						at.putSentinel("@comment " + line)
			
			next = root.nodeAfterTree()
			v = root
			while v and v != next:
				at.putOpenNodeSentinel(v)
				
				s = v.bodyString()
				if s and len(s) > 0:
					s = toEncodedString(s,at.encoding,reportErrors=true) # 3/7/03
					at.outputStringWithLineEndings(s)
					
				# Put an @nonl sentinel if s does not end in a newline.
				if s and s[-1] != '\n':
					at.onl_sent() ; at.putSentinel("@nonl")
				
				at.putCloseNodeSentinel(v)
				v = v.threadNext()
			
			at.putSentinel("@-leo")
			# Write any @last lines.  These lines are also converted to 
			# @verbatim lines, so the read logic simply ignores lines 
			# following the @-leo sentinel.
			
			tag = "@last"
			lines = string.split(root.t.bodyString,'\n')
			n = len(lines) ; j = k = n - 1
			# Don't write an empty last line.
			if j >= 0 and len(lines[j])==0:
				j = k = n - 2
			# Scan backwards for @last directives.
			while j >= 0:
				line = lines[j]
				if match(line,0,tag): j -= 1
				else: break
			# Write the @last lines.
			for line in lines[j+1:k+1]:
				i = len(tag) ; i = skip_ws(line,i)
				at.putBuffered(line[i:]) ; at.onl()
			at.closeWriteFile()
			at.replaceTargetFileIfDifferent()
			root.clearOrphan() ; root.clearDirty()
		except:
			at.handleWriteException(root)
	def putBody(self,v):
		
		""" Generate the body enclosed in sentinel lines."""
	
		at = self ; s = v.bodyString()
		
		v.setVisited() # Mark the node for the orphans check.
		if not s: return
	
		inCode = true
		
		# Make _sure_ all lines end in a newline (11/20/03: except in nosentinel mode).
		# If we add a trailing newline, we'll generate an @nonl sentinel below.
		trailingNewlineFlag = s and s[-1] == '\n'
		if at.sentinels and not trailingNewlineFlag:
			s = s + '\n'
	
		at.putOpenNodeSentinel(v)
		i = 0
		while i < len(s):
			next_i = skip_line(s,i)
			assert(next_i > i)
			kind = at.directiveKind(s,i)
			if kind == noDirective:
				if inCode:
					hasRef,n1,n2 = at.findSectionName(s,i)
					if hasRef and not at.raw:
						at.putRefLine(s,i,n1,n2,v)
					else:
						at.putCodeLine(s,i)
				else:
					at.putDocLine(s,i)
			elif kind in (docDirective,atDirective):
				assert(not at.pending)
				at.putStartDocLine(s,i,kind)
				inCode = false
			elif kind in (cDirective,codeDirective):
				# Only @c and @code end a doc part.
				if not inCode:
					at.putEndDocLine() 
				at.putDirective(s,i)
				inCode = true
			elif kind == othersDirective:
				if inCode: at.putAtOthersLine(s,i,v)
				else: at.putDocLine(s,i) # 12/7/03
			elif kind == rawDirective:
				at.raw = true
				at.putSentinel("@@raw")
			elif kind == endRawDirective:
				at.raw = false
				at.putSentinel("@@end_raw")
				i = skip_line(s,i)
			elif kind == miscDirective:
				at.putDirective(s,i)
			else:
				assert(0) # Unknown directive.
			i = next_i
		if not inCode:
			at.putEndDocLine()
		if at.sentinels and not trailingNewlineFlag:
			at.putSentinel("@nonl")
		at.putCloseNodeSentinel(v)
	def inAtOthers(self,v):
		
		"""Returns true if v should be included in the expansion of the at-others directive
		
		in the body text of v's parent."""
	
		# Return false if this has been expanded previously.
		if  v.isVisited():
			# trace("previously visited",v)
			return false
		
		# Return false if this is a definition node.
		h = v.headString() ; i = skip_ws(h,0)
		isSection,junk = self.isSectionName(h,i)
		if isSection:
			# trace("is section",v)
			return false
	
		# Return false if v's body contains an @ignore directive.
		if v.isAtIgnoreNode():
			# trace("is @ignore",v)
			return false
		else:
			# trace("ok",v)
			return true
	def putAtOthersChild(self,v):
	
		v.setVisited() # Make sure v is never expanded again.
		self.putBody(v) # Insert the expansion of v.
	
		# Insert expansions of all children.
		child = v.firstChild()
		while child:
			if self.inAtOthers( child ):
				self.putAtOthersChild( child )
			child = child.next()
	def putAtOthersLine (self,s,i,v):
		
		"""Put the expansion of @others."""
		
		at = self
		j,delta = skip_leading_ws_with_indent(s,i,at.tab_width)
		at.putLeadInSentinel(s,i,j,delta)
	
		at.indent += delta
		if at.leadingWs:
			at.putSentinel("@" + at.leadingWs + "@+others")
		else:
			at.putSentinel("@+others")
		
		child = v.firstChild()
		while child:
			if at.inAtOthers(child):
				at.putAtOthersChild(child)
			child = child.next()
	
		at.putSentinel("@-others")
		at.indent -= delta
	def putCodeLine (self,s,i):
		
		"""Put a normal code line."""
		
		at = self
		
		# Put @verbatim sentinel if required.
		k = skip_ws(s,i)
		if match(s,k,self.startSentinelComment + '@'):
			self.putSentinel("@verbatim")
	
		j = skip_line(s,i)
		if not at.raw:
			at.putIndent(at.indent)
		line = s[i:j]
		# at.os(line)
		if line[-1:]=="\n": # 12/2/03: emakital
			at.os(line[:-1])
			at.onl()
		else:
			at.os(line)
	def putRefLine(self,s,i,n1,n2,v):
		
		"""Put a line containing one or more references."""
		
		at = self
		
		# Compute delta only once.
		delta = self.putRefAt(s,i,n1,n2,v,delta=None)
		if delta is None: return # 11/23/03
		
		while 1:
			i = n2 + 2
			hasRef,n1,n2 = at.findSectionName(s,i)
			if hasRef:
				self.putAfterMiddleRef(s,i,n1,delta)
				self.putRefAt(s,n1,n1,n2,v,delta)
			else:
				break
		
		self.putAfterLastRef(s,i,delta)
	def putRefAt (self,s,i,n1,n2,v,delta):
		
		"""Put a reference at s[n1:n2+2] from v."""
		
		at = self ; name = s[n1:n2+2]
	
		ref = findReference(name,v)
		if not ref:
			at.writeError(
				"undefined section: %s\n\treferenced from: %s" %
				( name,v.headString()))
			return None
		
		# Expand the ref.
		if not delta:
			junk,delta = skip_leading_ws_with_indent(s,i,at.tab_width)
		at.putLeadInSentinel(s,i,n1,delta)
	
		at.indent += delta
		if at.leadingWs:
			at.putSentinel("@" + at.leadingWs + name)
		else:
			at.putSentinel("@" + name)
		at.putBody(ref)
		at.indent -= delta
		
		return delta
	def putAfterLastRef (self,s,start,delta):
		
		"""Handle whatever follows the last ref of a line."""
		
		at = self
		
		j = skip_ws(s,start)
		
		if j < len(s) and s[j] != '\n':
			end = skip_line(s,start)
			after = s[start:end] # Ends with a newline only if the line did.
			# Temporarily readjust delta to make @afterref look better.
			at.indent += delta
			at.putSentinel("@afterref")
			at.os(after)
			if at.sentinels and after and after[-1] != '\n':
				at.onl() # Add a newline if the line didn't end with one.
			at.indent -= delta
		else:
			# Temporarily readjust delta to make @nl look better.
			at.indent += delta
			at.putSentinel("@nl")
			at.indent -= delta
	def putAfterMiddleRef (self,s,start,end,delta):
		
		"""Handle whatever follows a ref that is not the last ref of a line."""
		
		at = self
		
		if start < end:
			after = s[start:end]
			at.indent += delta
			at.putSentinel("@afterref")
			at.os(after) ; at.onl_sent() # Not a real newline.
			at.putSentinel("@nonl")
			at.indent -= delta
	def putBlankDocLine (self):
		
		at = self
		
		at.putPending(split=false)
	
		if not at.endSentinelComment:
			at.putIndent(at.indent)
			at.os(at.startSentinelComment) ; at.oblank()
	
		at.onl()
	def putStartDocLine (self,s,i,kind):
		
		"""Write the start of a doc part."""
		
		at = self ; at.docKind = kind
		
		sentinel = choose(kind == docDirective,"@+doc","@+at")
		directive = choose(kind == docDirective,"@doc","@")
		
		if 0: # New code: put whatever follows the directive in the sentinel
			# Skip past the directive.
			i += len(directive)
			j = skip_to_end_of_line(s,i)
			follow = s[i:j]
		
			# Put the opening @+doc or @-doc sentinel, including whatever follows the directive.
			at.putSentinel(sentinel + follow)
	
			# Put the opening comment if we are using block comments.
			if at.endSentinelComment:
				at.putIndent(at.indent)
				at.os(at.startSentinelComment) ; at.onl()
		else: # old code.
			# Skip past the directive.
			i += len(directive)
		
			# Get the trailing whitespace.
			j = skip_ws(s,i)
			ws = s[i:j]
			
			# Put the opening @+doc or @-doc sentinel, including trailing whitespace.
			at.putSentinel(sentinel + ws)
		
			# Put the opening comment.
			if at.endSentinelComment:
				at.putIndent(at.indent)
				at.os(at.startSentinelComment) ; at.onl()
		
			# Put an @nonl sentinel if there is significant text following @doc or @.
			if not is_nl(s,j):
				# Doesn't work if we are using block comments.
				at.putSentinel("@nonl")
				at.putDocLine(s,j)
	def putDocLine (self,s,i):
		
		"""Handle one line of a doc part.
		
		Output complete lines and split long lines and queue pending lines.
		Inserted newlines are always preceded by whitespace."""
		
		at = self
		j = skip_line(s,i)
		s = s[i:j]
	
		if at.endSentinelComment:
			leading = at.indent
		else:
			leading = at.indent + len(at.startSentinelComment) + 1
	
		if not s or s[0] == '\n':
			# A blank line.
			at.putBlankDocLine()
		else:
			# All inserted newlines are preceeded by whitespace:
			# we remove trailing whitespace from lines that have not been 
			# split.
			
			i = 0
			while i < len(s):
			
				# Scan to the next word.
				word1 = i # Start of the current word.
				word2 = i = skip_ws(s,i)
				while i < len(s) and s[i] not in (' ','\t'):
					i += 1
				word3 = i = skip_ws(s,i)
				# trace(s[word1:i])
				
				if leading + word3 - word1 + len(''.join(at.pending)) >= at.page_width:
					if at.pending:
						# trace("splitting long line.")
						# Ouput the pending line, and start a new line.
						at.putPending(split=true)
						at.pending = [s[word2:word3]]
					else:
						# Output a long word on a line by itself.
						# trace("long word:",s[word2:word3])
						at.pending = [s[word2:word3]]
						at.putPending(split=true)
				else:
					# Append the entire word to the pending line.
					# trace("appending",s[word1:word3])
					at.pending.append(s[word1:word3])
						
			# Output the remaining line: no more is left.
			at.putPending(split=false)
	def putEndDocLine (self):
		
		"""Write the conclusion of a doc part."""
		
		at = self
		
		at.putPending(split=false)
		
		# Put the closing delimiter if we are using block comments.
		if at.endSentinelComment:
			at.putIndent(at.indent)
			at.os(at.endSentinelComment)
			at.onl() # Note: no trailing whitespace.
	
		sentinel = choose(at.docKind == docDirective,"@-doc","@-at")
		at.putSentinel(sentinel)
	def putPending (self,split):
		
		"""Write the pending part of a doc part.
		
		We retain trailing whitespace iff the split flag is true."""
		
		at = self ; s = ''.join(at.pending) ; at.pending = []
		
		# trace("split",`s`)
		
		# Remove trailing newline temporarily.  We'll add it back later.
		if s and s[-1] == '\n':
			s = s[:-1]
	
		if not split:
			s = s.rstrip()
			if not s:
				return
	
		at.putIndent(at.indent)
	
		if not at.endSentinelComment:
			at.os(at.startSentinelComment) ; at.oblank()
	
		at.os(s) ; at.onl()
	# Returns the kind of at-directive or noDirective.
	
	def directiveKind(self,s,i):
	
		at = self
		n = len(s)
		if i >= n or s[i] != '@':
			j = skip_ws(s,i)
			if match_word(s,j,"@others"):
				return othersDirective
			else:
				return noDirective
	
		table = (
			("@c",cDirective),
			("@code",codeDirective),
			("@doc",docDirective),
			("@end_raw",endRawDirective),
			("@others",othersDirective),
			("@raw",rawDirective))
	
		# This code rarely gets executed, so simple code suffices.
		if i+1 >= n or match(s,i,"@ ") or match(s,i,"@\t") or match(s,i,"@\n"):
			# 10/25/02: @space is not recognized in cweb mode.
			# 11/15/02: Noweb doc parts are _never_ scanned in cweb mode.
			return choose(at.language=="cweb",
				noDirective,atDirective)
	
		# 10/28/02: @c and @(nonalpha) are not recognized in cweb mode.
		# We treat @(nonalpha) separately because @ is in the colorizer table.
		if at.language=="cweb" and (
			match_word(s,i,"@c") or
			i+1>= n or s[i+1] not in string.ascii_letters):
			return noDirective
	
		for name,directive in table:
			if match_word(s,i,name):
				return directive
	
		# 10/14/02: return miscDirective only for real directives.
		for name in leoColor.leoKeywords:
			if match_word(s,i,name):
				return miscDirective
	
		return noDirective
	def findSectionName(self,s,i):
		
		end = s.find('\n',i)
		if end == -1:
			n1 = s.find("<<",i)
			n2 = s.find(">>",i)
		else:
			n1 = s.find("<<",i,end)
			n2 = s.find(">>",i,end)
	
		return -1 < n1 < n2, n1, n2
	def oblank(self):
		self.os(' ')
	
	def oblanks(self,n):
		self.os(' ' * abs(n))
	
	def onl(self):
		self.os(self.output_newline)
		
	def onl_sent(self):
		if self.sentinels:
			self.onl()
		
	def os (self,s):
		if s and self.outputFile:
			try:
				s = toEncodedString(s,self.encoding,reportErrors=true)
				self.outputFile.write(s)
			except:
				es("exception writing:" + `s`)
				es_exception()
	
	def otabs(self,n):
		self.os('\t' * abs(n))
	# It is important for PHP and other situations that @first and @last 
	# directives get translated to verbatim lines that do _not_ include what 
	# follows the @first & @last directives.
	
	def putDirective(self,s,i):
		
		"""Output a sentinel a directive or reference s."""
	
		tag = "@delims"
		assert(i < len(s) and s[i] == '@')
		k = i
		j = skip_to_end_of_line(s,i)
		directive = s[i:j]
	
		if match_word(s,k,"@delims"):
			# Put a space to protect the last delim.
			self.putSentinel(directive + " ") # 10/23/02: put @delims, not @@delims
			
			# Skip the keyword and whitespace.
			j = i = skip_ws(s,k+len(tag))
			
			# Get the first delim.
			while i < len(s) and not is_ws(s[i]) and not is_nl(s,i):
				i += 1
			if j < i:
				self.startSentinelComment = s[j:i]
				# Get the optional second delim.
				j = i = skip_ws(s,i)
				while i < len(s) and not is_ws(s[i]) and not is_nl(s,i):
					i += 1
				self.endSentinelComment = choose(j<i, s[j:i], "")
			else:
				self.writeError("Bad @delims directive")
		elif match_word(s,k,"@last"):
			self.putSentinel("@@last") # 10/27/03: Convert to an verbatim line _without_ anything else.
		elif match_word(s,k,"@first"):
			self.putSentinel("@@first") # 10/27/03: Convert to an verbatim line _without_ anything else.
		else:
			self.putSentinel("@" + directive)
	
		i = skip_line(s,k)
		return i
	
class newDerivedFile(baseNewDerivedFile):
	pass # May be overridden in plugins.
