#!/usr/bin/env python
#-*- coding: utf-8 -*-

'''
This module is the B{Template Macro} module. It preprocesses a web 
template in which variables and blocks are contained. This package 
plays an important role as an explicit boundary between web template 
designing and script implementation.

@note: A block can be defined with block comment:
C{<!-- block }M{q}C{ start -->}
M{c}
C{<!-- block }M{q}C{ end -->},
where M{q} is the block name and M{c} is the content of this block.

@note: It should be noted that each subblock symbol is necessary to 
differ from its superblock's name. This is owing to the block pattern.

@author: Prachya Boonkwan
@organization: NAI{i}ST Laboratory, Kasetsart University, Thailand
@copyright: October 23, 2003
'''

########################################

import copy
import re
import string
from types import *

########################################

class Template:
	'''
	Web template representation. As soon as the template instance is 
	constructed, the content is automatically analyzed and the symbol 
	table is afterward created.

	@ivar path: Path to web template file.
	@type path: str
	@ivar content: Textual content of web template.
	@type content: str
	@ivar symtab: Symbol table of web template
	@type symtab: dict

	@cvar blkpat: Block declaration pattern (regular expression). It
	should be noted that this pattern does I{not} greedily match the
	block body.
	'''

	blkpat = re.compile(
		r'\s*<!---*\s*block\s*(?P<blkname>\w+?)\s*(start|begin)\s*-*-->'
		r'(?P<blkbody>\s*.*?)'
		r'\s*<!---*\s*block\s*(?P=blkname)\s*end\s*-*-->',
		re.DOTALL
	)

	def __init__(self, path = None, content = None, symtab = None):
		'''
		Construct a web template representation.

		@param path: Path to web template file.
		@type path: str
		@param content: Textual content of web template
		@type content: str
		@param symtab: Symbol table of web template
		@type symtab: dict
		
		@note: It is recommended to specify only the path of the 
		template.
		'''

		if symtab is None: symtab = {}

		self.path = path
		self.content = content
		self.symtab = symtab

		if self.content is None or len(self.symtab) == 0:
			if self.path is not None:
				self.load()
			self.content = self.analyze(self.content)

	def __copy__(self):
		'''
		Copy a web template representation.
		'''

		return Template(
			self.path, self.content, copy.copy(self.symtab)
		)

	def load(self):
		'''
		Load the content of the template from the specified path.
		'''

		fhdl = open(self.path)
		self.content = fhdl.read()
		fhdl.close()

	def analyze(self, text, blkpath = None):
		'''
		Analyze the text to establish blocks.

		@param text: Text to be analyzed.
		@type text: str
		@param blkpath: Block path to the text.
		@type blkpath: list

		@return: Content which blocks are replaced with symbols.
		@rtype: str

		@raise TemplateError: If symbol reassignment occurs.
		'''

		if blkpath is None: blkpath = []

		result = text

		while True:
			matobj = Template.blkpat.search(result)
			if matobj is None: break
			blkname = matobj.group('blkname')
			symbol = string.join(blkpath + [blkname], '.')
			blkbody = matobj.group('blkbody')
			(result, _) = Template.blkpat.subn(
				'<!--VAR:%s-->' % symbol, result, 1
			)
			value = self.analyze(blkbody, blkpath + [blkname])

			if symbol not in self.symtab.keys():
				self.symtab[symbol] = value
			else:
				raise TemplateError, \
				'Reassigning the same symbol (%s).' % symbol

		return result

	def __getitem__(self, symbol):
		'''
		Get the value of the symbol.

		@param symbol: Symbol name.
		@type symbol: str

		@return: Value of the symbol.
		@rtype: str
		'''

		return self.symtab[symbol]

	def repr(self):
		'''
		Represent this template with a string representation.

		@return: String C{Template(path = }M{p}C{, content = }M{c}
		C{, symtab = }M{s}C{)}, where M{p} is the path, M{c} is the 
		content of that template, and M{s} is the symbol table.
		@rtype: str
		'''

		return 'Template(path = %r, content = %r, symtab = %r)' % (
			self.path, self.content, self.symtab
		)

	__repr__ = __str__ = repr

########################################

class TemplateError(Exception):
	'''
	Error occurring in the class Template.
	'''

	pass

########################################

class PageMacro:
	'''
	Page Macro Expander. It enables users to define symbols in order 
	to be expanded to templates further. Moreover, also enables users 
	to expand consecutively blocks in templates.

	@ivar symtab: Symbol table.
	@type symtab: dict

	@cvar KEEPMODE: Mode of keeping all undefined symbols.
	@type KEEPMODE: int
	@cvar DELMODE: Mode of deleting all undefined symbols.
	@type DELMODE: int

	@cvar varpat: Symbol pattern.
	@cvar unkpat: Unknown symbol pattern.
	'''

	KEEPMODE = 0
	DELMODE = 1

	varpat = re.compile(r'<!--VAR:(?P<symname>(\w+\.)*\w+?)-->')
	unkpat = re.compile(r'<!--VAR::(?P<synname>(\w+\.)*\w+?)-->')

	def __init__(self, mainsym, unkmthd = None, symtab = None):
		'''
		Construct a page macro expander.

		@param mainsym: Main symbol to be displayed.
		@type mainsym: str
		@param unkmthd: Resolution method of undefined symbols
		(C{PageMacro.KEEPMODE} or C{PageMacro.DELMODE}).
		@type unkmthd: int
		@param symtab: Symbol table.
		@type symtab: dict
		'''

		if unkmthd is None: unkmthd = PageMacro.KEEPMODE
		if symtab is None: symtab = {}

		self.mainsym = mainsym
		self.unkmthd = unkmthd
		self.symtab = symtab

	def __copy__(self):
		'''
		Copy a page macro expander.
		'''

		return PageMacro(
			self.mainsym, self.unkmthd, copy.copy(self.symtab)
		)

	def repr(self):
		'''
		Represent a page macro expander with a string representation.

		@return: String C{PageMacro(mainsym = }M{q}C{, unkmthd = }M{m}
		C{, symtab = }M{s}C{)}, where M{q} is the main symbol, M{m} is 
		the resolution method of unknown or undefined symbols and M{s} 
		is the symbol table.
		@rtype: str
		'''

		return 'PageMacro(mainsym = %s, unkmthd = %d, symtab = %r)' % (
			self.mainsym, self.unkmthd, self.symtab
		)

	__repr__ = repr

	def __getitem__(self, symbol):
		'''
		Get the value of a symbol.

		@param symbol: Symbol name.
		@type symbol: str

		@return: Value of the symbol.
		@rtype: Template
		'''

		return self.symtab[symbol]

	def __setitem__(self, symbol, value):
		'''
		Set the value of a symbol.

		@param symbol: Symbol name.
		@type symbol: str
		@param value: Value of the symbol.
		@type value: str or Template

		@note: The parameter C{value} of a symbol can be a string or a
		template. If it is a string, it will be considered as the
		content of the template and it will be wrapped with Template
		class. On the contrary, if it is a template, it will be
		directly assigned to the symbol.

		@note: If users would like to assign to a symbol with the path
		to a template file, please use C{pm[}M{s}C{] = 
		Template(path = }M{p}C{)}, where C{pm} is a C{PageMacro} 
		instance, M{s} is a symbol name, and M{p} is a path to 
		template. Users can alternatively use the method C{load} for
		ease of use.

		@note: If users would like to assign directly to a symbol with
		the template content, please use C{pm[}M{s}C{] = }M{c}, where 
		C{pm} is a C{PageMacro} instance and M{c} is a content of
		template.
		'''

		if isinstance(value, Template):
			self.symtab[symbol] = value
		elif type(value) == type('d'):
			self.symtab[symbol] = Template(content = value)
		elif type(value) == type(u'öä'):
			self.symtab[symbol] = Template(content = value.encode('utf-8'))

	def load(self, symbol, filename):
		'''
		Load a template file to a symbol.

		@param symbol: Symbol name.
		@type symbol: str
		@param filename: Template path.
		@type filename: str
		'''

		self[symbol] = Template(path = filename)

	def resolve(self, symbol):
		'''
		Resolve the specified symbol to absolute value.

		@param symbol: Symbol name.
		@type symbol: str

		@return: Absolute value of the symbol.
		@rtype: str
		'''

		if symbol not in self.symtab:
			return '<!--VAR::%s-->' % symbol

		result = self.symtab[symbol].content

		while True:
			matobj = PageMacro.varpat.search(result)
			if matobj is None: break
			symname = matobj.group('symname')
			if symname in self.symtab:
				(result, _) = PageMacro.varpat.subn(
					self.resolve(symname), result, 1
				)
			else:
				(result, _) = PageMacro.varpat.subn(
					'<!--VAR::%s-->' % symname, result, 1
				)

		return self.handle_unk(result)

	def handle_unk(self, text):
		'''
		Handle unknown symbols in the text.

		@param text: Text.
		@type text: str

		@return: Unknown-handled text.
		@rtype: str
		'''

		result = text

		while True:
			matobj = PageMacro.unkpat.search(result)
			if matobj is None: break
			if self.unkmthd == PageMacro.KEEPMODE:
				result = PageMacro.unkpat.sub(
					'<!--VAR:\g<synname>-->', result
				)
			elif self.unkmthd == PageMacro.DELMODE:
				result = PageMacro.unkpat.sub('', result)

		return result

	def envision(self, tempsym, blocksym, symdict):
		'''
		Resolve a block in a template with a symbol dictionary.

		@param tempsym: Template symbol.
		@type tempsym: str
		@param blocksym: Block symbol in the template.
		@type blocksym: str
		@param symdict: Symbol dictionary.
		@type symdict: dict

		@raise PageMacroError: If unknown switch is to be resolved.
		'''

		self.expand(tempsym, blocksym, [symdict])

	def expand(self, tempsym, blocksym, iterable):
		'''
		Expand consecutively a block in a template with an iterable.

		@param tempsym: Template symbol.
		@type tempsym: str
		@param blocksym: Block symbol in the template.
		@type blocksym: str
		@param iterable: Iterable list of dictionaries whose keys and
		values resemble correspondences among symbols and their 
		values. A value in the dictionary can be a list or an iterable
		to expand recursively the block. In this case, the key of such
		value is necessary to specify explicitly block hierarchy.
		@type iterable: list or iter

		@raise PageMacroError: If unknown block is to be resolved.
		'''

		if (
			tempsym not in self.symtab
		):
			raise PageMacroError, \
			'Unknown template (%s) cannot be resolved.' % tempsym
			
		if (
			blocksym not in self.symtab[tempsym].symtab
		):
			raise PageMacroError, \
			'Unknown block (%s) cannot be resolved.' % blocksym

		result = []

		for symdict in iterable:
			content = self.symtab[tempsym][blocksym]

			while True:
				matobj = PageMacro.varpat.search(content)
				if matobj is None: break
				symname = matobj.group('symname')
				
				if symname in symdict:
					if type(symdict[symname]) in [str,unicode]:
						self[symname] = symdict[symname]
					elif type(symdict[symname]) in [list, iter]:
						self.expand(tempsym, symname, symdict[symname])
					elif type(symdict[symname]) is dict:
						self.expand(
							tempsym, symname, [symdict[symname]]
						)

				(content, _) = PageMacro.varpat.subn(
					self.resolve(symname), content, 1
				)

			result.append(content)

		self[blocksym] = string.join(result, '')

	def str(self):
		'''
		Represent a page macro expander with the expanded value of the 
		main symbol.

		@return: Expanded value.
		@rtype: str
		'''

		return self.resolve(self.mainsym)
		
	__str__ = str

########################################

class PageMacroError(Exception):
	'''
	Error occurring in the class PageMacro.
	'''

	pass

    
    
########################################
class warningClass:
	'''Class used to memorize warnings in a form suitable for a template like this:


	  <p>
		<!-- block warninglist start -->
	  </p>
	  <p><strong><font size="2">
		<!--VAR:warningHeader-->
		</font></strong> 
		<!--VAR:warning-->
	  </p>
	  <ul>
		<!-- block messagelist start -->
		<li> 
		  <!--VAR:message-->
		</li>
		<!-- block messagelist end -->
	  </ul>
	  <!-- block warninglist end -->

	'''
	def __init__(self):
		self.warningList = []
	
	def __len__(self):
		return len(self.warningList)

	def add(self, warn, messages=None):
		'''Adds a warning to the warning list.'''
		if messages is not None and len(messages) == 0:
			pass
		elif messages is not None and len(messages)>0:
			self.warningList.append((warn, messages))
		elif warn is not None and len(warn)>0:
			self.warningList.append((warn, []))

	def getTemplateList(self):
		'''Returns a list of hashes used to fill the HTML warning template lists.'''
		warningList = []
		for (warn, messages) in self.warningList:
			warningList.append({
				'warningHeader':"Warning!", 
				'warning':"<br>"+warn, 
				'warninglist.messagelist':[{
					'message':"<i>%s</i>"%c
				} for c in messages]
			})
		return warningList
		
	def expandTemplate(self, tmpl):
		if len(self) > 0:
			tmpl.expand('Content', 'warninglist', self.getTemplateList())
	

########################################
def genOptionList(options, placeholderOption, placeholderValue=None, setValue=None, sort=1):
	'''generates a list of hashes usable for the templates to create (preselected) option lists for html forms.'''
	l = []
	selectedVal = 0
	if type(options) == ListType:
		# its a list of options.
		if sort:
			options.sort()
		if placeholderValue <> None:
			#value entries equal visual option
			for opt in options:
				if setValue <> None and opt == setValue:
					selectedVal = 1
					l.append({placeholderOption:opt, placeholderValue:' value="'+opt+'" selected'})
				else:
					l.append({placeholderOption:opt, placeholderValue:' value="'+opt+'"'})
			if selectedVal == 0 and setValue <> None:
				# the preselected value was not found. Add it to the list!
				l.append({placeholderOption:setValue, placeholderValue:' value="'+setValue+'" selected'})
		else:
			l = [{placeholderOption:opt} for opt in options]
	else:
		# its a hash of options/values. create value entries
		optKeys = options.keys()
		if sort:
			if type(sort) == ListType:
				# sort order was given
				optKeys = sort
			else:
				optKeys.sort()
		for opt in optKeys:
			val = options[opt]
			if placeholderValue is None:
				l.append({placeholderOption:opt})
			elif setValue <> None and val == setValue:
				selectedVal = 1
				l.append({placeholderOption:opt, placeholderValue:' value="'+val+'" selected'})
			else:
				l.append({placeholderOption:opt, placeholderValue:' value="'+val+'"'})
		if selectedVal == 0 and setValue <> None:
			# the preselected value was not found. Add it to the list!
			l.append({placeholderOption:setValue, placeholderValue:' value="'+setValue+'" selected'})
	return l



    
########################################
def demo():
	'''
	Demonstrate this module.
	'''
	
	input = '''
<html>
<body>
<p>Please select a database on the left to configure ...</p>
<p> 
  DBName:<!--VAR:dbname-->
  <!-- block dblist start -->
  db-<!--VAR:db-->
  <br>
	  <!-- block list2 start -->
	  NUM: <!--VAR:num-->
	  inside: <!--VAR:in-->
	  <!-- block list2 end -->
  <!-- block dblist end -->
	  <!-- block list2 start -->
	  outside: <!--VAR:in-->
	  <!-- block list2 end -->

		  <select name="animals">
			<!-- block animallist start -->
			<option <!--VAR:animal_val-->><!--VAR:animal_opt--></option>
			<!-- block animallist end -->
		  </select>

</p>
</body>
</html>
	'''

	pm = PageMacro('Content', PageMacro.KEEPMODE)
	pm['Content'] = input
	pm['dbname'] = u'Prächya Boonkwan €µ'
	inL = ['freak', 'freaky','freako','freakkk']
	animals = {'dog':'dog', 'pig':'pig', 'frog':'frog', 'bird':'bird', 'cat':'cat'}
	x = range(1000)
	pm.expand('Content', 'dblist', [{'db': u'Cöntry','dblist.list2':[{'in':i, 'num':str(x.pop())} for i in inL]},{'db': 'Kunttree','dblist.list2':[{'in':'freak', 'in':'freaky'}]}])
	pm.expand('Content', 'list2', [{'in':'asas'},{'in':'sasa'}])
	pm.expand('Content', 'animallist', genOptionList(animals, 'animal_opt', 'animal_val', 'panther') )
	print pm

if __name__ == '__main__':
	demo()
	


