#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
Core Zen Coding library. Contains various text manipulation functions:

== Expand abbreviation
Expands abbreviation like ul#nav>li*5>a into a XHTML string.
=== How to use
First, you have to extract current string (where cursor is) from your test 
editor and use <code>find_abbr_in_line()</code> method to extract abbreviation. 
If abbreviation was found, this method will return it as well as position index
of abbreviation inside current line. If abbreviation wasn't 
found, method returns empty string. With abbreviation found, you should call
<code>parse_into_tree()</code> method to transform abbreviation into a tag tree. 
This method returns <code>Tag</code> object on success, None on failure. Then
simply call <code>to_string()</code> method of returned <code>Tag</code> object
to transoform tree into a XHTML string

You can setup output profile using <code>setup_profile()</code> method 
(see <code>default_profile</code> definition for available options) 

 
Created on Apr 17, 2009

@author: Sergey Chikuyonok (http://chikuyonok.ru)
'''
from zen_settings import zen_settings
import re
import stparser

newline = '\n'
"Newline symbol"

caret_placeholder = '{%::zen-caret::%}'

default_tag = 'div'

re_tag = re.compile(r'<\/?[\w:\-]+(?:\s+[\w\-:]+(?:\s*=\s*(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>\s]+))?)*\s*(\/?)>$')

profiles = {}
"Available output profiles"

default_profile = {
	'tag_case': 'lower',         # values are 'lower', 'upper'
	'attr_case': 'lower',        # values are 'lower', 'upper'
	'attr_quotes': 'double',     # values are 'single', 'double'
	
	'tag_nl': 'decide',          # each tag on new line, values are True, False, 'decide'
	
	'place_cursor': True,        # place cursor char — | (pipe) — in output
	
	'indent': True,              # indent tags
	
	'inline_break': 3,           # how many inline elements should be to force line break (set to 0 to disable)
	
	'self_closing_tag': 'xhtml'  # use self-closing style for writing empty elements, e.g. <br /> or <br>. 
                                 # values are True, False, 'xhtml'
}

basic_filters = 'html';
"Filters that will be applied for unknown syntax"

max_tabstop = 0
"Maximum tabstop index for current session"

def char_at(text, pos):
	"""
	Returns character at specified index of text.
	If index if out of range, returns empty string
	"""
	return text[pos] if pos < len(text) else ''

def has_deep_key(obj, key):
	"""
	Check if <code>obj</code> dictionary contains deep key. For example,
	example, it will allow you to test existance of my_dict[key1][key2][key3],
	testing existance of my_dict[key1] first, then my_dict[key1][key2], 
	and finally my_dict[key1][key2][key3]
	@param obj: Dictionary to test
	@param obj: dict
	@param key: Deep key to test. Can be list (like ['key1', 'key2', 'key3']) or
	string (like 'key1.key2.key3')
	@type key: list, tuple, str
	@return: bool
	"""
	if isinstance(key, str):
		key = key.split('.')
		
	last_obj = obj
	for v in key:
		if hasattr(last_obj, v):
			last_obj = getattr(last_obj, v)
		elif last_obj.has_key(v):
			last_obj = last_obj[v]
		else:
			return False
	
	return True
		

def is_allowed_char(ch):
	"""
	Test if passed symbol is allowed in abbreviation
	@param ch: Symbol to test
	@type ch: str
	@return: bool
	"""
	return ch.isalnum() or ch in "#.>+*:$-_!@[]()|"

def split_by_lines(text, remove_empty=False):
	"""
	Split text into lines. Set <code>remove_empty</code> to true to filter out
	empty lines
	@param text: str
	@param remove_empty: bool
	@return list
	"""
	lines = text.splitlines()
	
	return remove_empty and [line for line in lines if line.strip()] or lines

def make_map(prop):
	"""
	Helper function that transforms string into dictionary for faster search
	@param prop: Key name in <code>zen_settings['html']</code> dictionary
	@type prop: str
	"""
	obj = {}
	for a in zen_settings['html'][prop].split(','):
		obj[a] = True
		
	zen_settings['html'][prop] = obj

def create_profile(options):
	"""
	Create profile by adding default values for passed optoin set
	@param options: Profile options
	@type options: dict
	"""
	for k, v in default_profile.items():
		options.setdefault(k, v)
	
	return options

def setup_profile(name, options = {}):
	"""
	@param name: Profile name
	@type name: str
	@param options: Profile options
	@type options: dict
	"""
	profiles[name.lower()] = create_profile(options);

def get_newline():
	"""
	Returns newline symbol which is used in editor. This function must be 
	redefined to return current editor's settings 
	@return: str
	"""
	return newline

def set_newline(char):
	"""
	Sets newline character used in Zen Coding
	"""
	global newline
	newline = char

def string_to_hash(text):
	"""
	Helper function that transforms string into hash
	@return: dict
	"""
	obj = {}
	items = text.split(",")
	for i in items:
		obj[i] = True
		
	return obj

def pad_string(text, pad):
	"""
	Indents string with space characters (whitespace or tab)
	@param text: Text to indent
	@type text: str
	@param pad: Indentation level (number) or indentation itself (string)
	@type pad: int, str
	@return: str
	"""
	pad_str = ''
	result = ''
	if isinstance(pad, basestring):
		pad_str = pad
	else:
		pad_str = get_indentation() * pad
		
	nl = get_newline()
	
	lines = split_by_lines(text)
	
	if lines:
		result += lines[0]
		for line in lines[1:]:
			result += nl + pad_str + line
			
	return result

def is_snippet(abbr, doc_type = 'html'):
	"""
	Check is passed abbreviation is a snippet
	@return bool
	"""
	return get_snippet(doc_type, abbr) and True or False

def is_ends_with_tag(text):
	"""
	Test is string ends with XHTML tag. This function used for testing if '<'
	symbol belogs to tag or abbreviation 
	@type text: str
	@return: bool
	"""
	return re_tag.search(text) != None

def get_elements_collection(resource, type):
	"""
	Returns specified elements collection (like 'empty', 'block_level') from
	<code>resource</code>. If collections wasn't found, returns empty object
	@type resource: dict
	@type type: str
	@return: dict
	"""
	if 'element_types' in resource and type in resource['element_types']:
		return resource['element_types'][type]
	else:
		return {}
	
def replace_variables(text, vars=zen_settings['variables']):
	"""
	Replace variables like ${var} in string
	@param text: str
	@return: str
	"""
	return re.sub(r'\$\{([\w\-]+)\}', lambda m: m.group(1) in vars and vars[m.group(1)] or m.group(0), text)

def get_abbreviation(res_type, abbr):
	"""
	Returns abbreviation value from data set
	@param res_type: Resource type (html, css, ...)
	@type res_type: str
	@param abbr: Abbreviation name
	@type abbr: str
	@return dict, None
	"""
	return get_settings_resource(res_type, abbr, 'abbreviations')

def get_snippet(res_type, snippet_name):
	"""
	Returns snippet value from data set
	@param res_type: Resource type (html, css, ...)
	@type res_type: str
	@param snippet_name: Snippet name
	@type snippet_name: str
	@return dict, None
	"""
	return get_settings_resource(res_type, snippet_name, 'snippets');

def get_variable(name):
	"""
	Returns variable value
	 @return: str
	"""
	return zen_settings['variables'][name]

def set_variable(name, value):
	"""
	Set variable value
	"""
	zen_settings['variables'][name] = value

def get_indentation():
	"""
	Returns indentation string
	@return {String}
	"""
	return get_variable('indentation');

def create_resource_chain(syntax, name):
	"""
	Creates resource inheritance chain for lookups
	@param syntax: Syntax name
	@type syntax: str
	@param name: Resource name
	@type name: str
	@return: list
	"""
	result = []
	
	if syntax in zen_settings:
		resource = zen_settings[syntax]
		if name in resource:
			result.append(resource[name])
		if 'extends' in resource:
			# find resource in ancestors
			for type in resource['extends']:
				if  has_deep_key(zen_settings, [type, name]):
					result.append(zen_settings[type][name])
				
	return result

def get_resource(syntax, name):
	"""
	Get resource collection from settings file for specified syntax. 
	It follows inheritance chain if resource wasn't directly found in
	syntax settings
	@param syntax: Syntax name
	@type syntax: str
	@param name: Resource name
	@type name: str
	"""
	chain = create_resource_chain(syntax, name)
	return chain[0] if chain else None

def get_settings_resource(syntax, abbr, name):
	"""
	Returns resurce value from data set with respect of inheritance
	@param syntax: Resource syntax (html, css, ...)
	@type syntax: str
	@param abbr: Abbreviation name
	@type abbr: str
	@param name: Resource name ('snippets' or 'abbreviation')
	@type name: str
	@return dict, None
	"""
	for item in create_resource_chain(syntax, name):
		if abbr in item:
			return item[abbr]
		
	return None

def get_word(ix, text):
	"""
	Get word, starting at <code>ix</code> character of <code>text</code>
	@param ix: int
	@param text: str
	"""
	m = re.match(r'^[\w\-:\$]+', text[ix:])
	return m.group(0) if m else ''
	
def extract_attributes(attr_set):
	"""
	Extract attributes and their values from attribute set 
 	@param attr_set: str
	"""
	attr_set = attr_set.strip()
	loop_count = 100 # endless loop protection
	re_string = r'^(["\'])((?:(?!\1)[^\\]|\\.)*)\1'
	result = []
		
	while attr_set and loop_count:
		loop_count -= 1
		attr_name = get_word(0, attr_set)
		attr = None
		if attr_name:
			attr = {'name': attr_name, 'value': ''}
			
			# let's see if attribute has value
			ch = attr_set[len(attr_name)] if len(attr_set) > len(attr_name) else ''
			if ch == '=':
				ch2 = attr_set[len(attr_name) + 1]
				if ch2 in '"\'':
					# we have a quoted string
					m = re.match(re_string, attr_set[len(attr_name) + 1:])
					if m:
						attr['value'] = m.group(2)
						attr_set = attr_set[len(attr_name) + len(m.group(0)) + 1:].strip()
					else:
						# something wrong, break loop
						attr_set = ''
				else:
					# unquoted string
					m = re.match(r'^(.+?)(\s|$)', attr_set[len(attr_name) + 1:])
					if m:
						attr['value'] = m.group(1)
						attr_set = attr_set[len(attr_name) + len(m.group(1)) + 1:].strip()
					else:
						# something wrong, break loop
						attr_set = ''
				
			else:
				attr_set = attr_set[len(attr_name):].strip()
		else:
			# something wrong, can't extract attribute name
			break
		
		if attr: result.append(attr)
		
	return result

def parse_attributes(text):
	"""
	Parses tag attributes extracted from abbreviation
	"""
	
#	Example of incoming data:
#	#header
#	.some.data
#	.some.data#header
#	[attr]
#	#item[attr=Hello other="World"].class

	result = []
	class_name = None
	char_map = {'#': 'id', '.': 'class'}
	
	# walk char-by-char
	i = 0
	il = len(text)
		
	while i < il:
		ch = text[i]
		
		if ch == '#': # id
			val = get_word(i, text[1:])
			result.append({'name': char_map[ch], 'value': val})
			i += len(val) + 1
			
		elif ch == '.': #class
			val = get_word(i, text[1:])
			if not class_name:
				# remember object pointer for value modification
				class_name = {'name': char_map[ch], 'value': ''}
				result.append(class_name)
			
			if class_name['value']:
				class_name['value'] += ' ' + val
			else:
				class_name['value'] = val
			
			i += len(val) + 1
				
		elif ch == '[': # begin attribute set
			# search for end of set
			end_ix = text.find(']', i)
			if end_ix == -1:
				# invalid attribute set, stop searching
				i = len(text)
			else:
				result.extend(extract_attributes(text[i + 1:end_ix]))
				i = end_ix
		else:
			i += 1
		
		
	return result

class AbbrGroup(object):
	"""
	Abreviation's group element
	"""
	def __init__(self, parent=None):
		"""
		@param parent: Parent group item element
		@type parent: AbbrGroup
		"""
		self.expr = ''
		self.parent = parent
		self.children = []
		
	def add_child(self):
		child = AbbrGroup(self)
		self.children.append(child)
		return child
	
	def clean_up(self):
		for item in self.children:
			expr = item.expr
			if not expr:
				self.children.remove(item)
			else:
				# remove operators at the and of expression
				item.clean_up()

def split_by_groups(abbr):
	"""
	Split abbreviation by groups
	@type abbr: str
	@return: AbbrGroup
	"""
	root = AbbrGroup()
	last_parent = root
	cur_item = root.add_child()
	stack = []
	i = 0
	il = len(abbr)
	
	while i < il:
		ch = abbr[i]
		if ch == '(':
			# found new group
			operator = i and abbr[i - 1] or ''
			if operator == '>':
				stack.append(cur_item)
				last_parent = cur_item
			else:
				stack.append(last_parent)
			cur_item = None
		elif ch == ')':
			last_parent = stack.pop()
			cur_item = None
			next_char = char_at(abbr, i + 1)
			if next_char == '+' or next_char == '>': 
				# next char is group operator, skip it
				i += 1
		else:
			if ch == '+' or ch == '>':
				# skip operator if it's followed by parenthesis
				next_char = char_at(abbr, i + 1)
				if next_char == '(':
					i += 1 
					continue
			
			if not cur_item:
				cur_item = last_parent.add_child()
			cur_item.expr += ch
			
		i += 1
	
	root.clean_up()
	return root

def rollout_tree(tree, parent=None):
	"""
	Roll outs basic Zen Coding tree into simplified, DOM-like tree.
	The simplified tree, for example, represents each multiplied element 
	as a separate element sets with its own content, if exists.
	 
	The simplified tree element contains some meta info (tag name, attributes, 
	etc.) as well as output strings, which are exactly what will be outputted
	after expanding abbreviation. This tree is used for <i>filtering</i>:
	you can apply filters that will alter output strings to get desired look
	of expanded abbreviation.
	 
	@type tree: Tag
	@param parent: ZenNode
	"""
	if not parent:
		parent = ZenNode(tree)
		
	how_many = 1
	tag_content = ''
	
	for child in tree.children:
		how_many = child.count
		
		if child.repeat_by_lines:
			# it's a repeating element
			tag_content = split_by_lines(child.get_content(), True)
			how_many = max(len(tag_content), 1)
		else:
			tag_content = child.get_content()
		
		for j in range(how_many):
			tag = ZenNode(child)
			parent.add_child(tag)
			tag.counter = j + 1
			
			if child.children:
				rollout_tree(child, tag)
				
			add_point = tag.find_deepest_child() or tag
			
			if tag_content:
				if isinstance(tag_content, basestring):
					add_point.content = tag_content
				else:
					add_point.content = tag_content[j] or ''
					
	return parent

def run_filters(tree, profile, filter_list):
	"""
	Runs filters on tree
	@type tree: ZenNode
	@param profile: str, object
	@param filter_list: str, list
	@return: ZenNode
	"""
	import filters
	
	if isinstance(profile, basestring) and profile in profiles:
		profile = profiles[profile];
	
	if not profile:
		profile = profiles['plain']
		
	if isinstance(filter_list, basestring):
		filter_list = re.split(r'[\|,]', filter_list)
		
	for name in filter_list:
		name = name.strip()
		if name and name in filters.filter_map:
			tree = filters.filter_map[name](tree, profile)
			
	return tree

def abbr_to_primary_tree(abbr, doc_type='html'):
	"""
	Transforms abbreviation into a primary internal tree. This tree should'n 
	be used ouside of this scope
	@param abbr: Abbreviation to transform
	@type abbr: str
	@param doc_type: Document type (xsl, html), a key of dictionary where to
	search abbreviation settings
	@type doc_type: str
	@return: Tag
	"""
	root = Tag('', 1, doc_type)
	token = re.compile(r'([\+>])?([a-z@\!\#\.][\w:\-]*)((?:(?:[#\.][\w\-\$]+)|(?:\[[^\]]+\]))+)?(\*(\d*))?(\+$)?', re.IGNORECASE)
	
	if not abbr:
		return None
	
	def expando_replace(m):
		ex = m.group(0)
		a = get_abbreviation(doc_type, ex)
		return a and a.value or ex
		
	def token_expander(operator, tag_name, attrs, has_multiplier, multiplier, has_expando):
		multiply_by_lines = (has_multiplier and not multiplier)
		multiplier = multiplier and int(multiplier) or 1
		
		tag_ch = tag_name[0]
		if tag_ch == '#' or tag_ch == '.':
			if attrs: attrs = tag_name + attrs
			else: attrs = tag_name
			tag_name = default_tag
		
		if has_expando:
			tag_name += '+'
		
		current = is_snippet(tag_name, doc_type) and Snippet(tag_name, multiplier, doc_type) or Tag(tag_name, multiplier, doc_type)
		
		if attrs:
			attrs = parse_attributes(attrs)
			for attr in attrs:
				current.add_attribute(attr['name'], attr['value'])
			
		# dive into tree
		if operator == '>' and token_expander.last:
			token_expander.parent = token_expander.last;
			
		token_expander.parent.add_child(current)
		token_expander.last = current
		
		if multiply_by_lines:
			root.multiply_elem = current
		
		return ''
		
	# replace expandos
	abbr = re.sub(r'([a-z][a-z0-9]*)\+$', expando_replace, abbr)
	
	token_expander.parent = root
	token_expander.last = None
	
	
#	abbr = re.sub(token, lambda m: token_expander(m.group(1), m.group(2), m.group(3), m.group(4), m.group(5), m.group(6), m.group(7)), abbr)
	# Issue from Einar Egilsson
	abbr = token.sub(lambda m: token_expander(m.group(1), m.group(2), m.group(3), m.group(4), m.group(5), m.group(6)), abbr)
	
	root.last = token_expander.last
	
	# empty 'abbr' variable means that abbreviation was expanded successfully, 
	# non-empty variable means there was a syntax error
	return not abbr and root or None;

def expand_group(group, doc_type, parent):
	"""
	Expand single group item 
	@param group: AbbrGroup
	@param doc_type: str
	@param parent: Tag
	"""
	tree = abbr_to_primary_tree(group.expr, doc_type)
	last_item = None
		
	if tree:
		for item in tree.children:
			last_item = item
			parent.add_child(last_item)
	else:
		raise Exception('InvalidGroup')
	
	
	# set repeating element to the topmost node
	root = parent
	while root.parent:
		root = root.parent
	
	root.last = tree.last
	if tree.multiply_elem:
		root.multiply_elem = tree.multiply_elem
		
	# process child groups
	if group.children:
		add_point = last_item.find_deepest_child() or last_item
		for child in group.children:
			expand_group(child, doc_type, add_point)

def replace_unescaped_symbol(text, symbol, replace):
	"""
	Replaces unescaped symbols in <code>text</code>. For example, the '$' symbol
	will be replaced in 'item$count', but not in 'item\$count'.
	@param text: Original string
	@type text: str
	@param symbol: Symbol to replace
	@type symbol: st
	@param replace: Symbol replacement
	@type replace: str, function 
	@return: str
	"""
	i = 0
	il = len(text)
	sl = len(symbol)
	match_count = 0
		
	while i < il:
		if text[i] == '\\':
			# escaped symbol, skip next character
			i += sl + 1
		elif text[i:i + sl] == symbol:
			# have match
			cur_sl = sl
			match_count += 1
			new_value = replace
			if callable(new_value):
				replace_data = replace(text, symbol, i, match_count)
				if replace_data:
					cur_sl = len(replace_data[0])
					new_value = replace_data[1]
				else:
					new_value = False
			
			if new_value is False: # skip replacement
				i += 1
				continue
			
			text = text[0:i] + new_value + text[i + cur_sl:]
			# adjust indexes
			il = len(text)
			i += len(new_value)
		else:
			i += 1
	
	return text
	
def run_action(name, *args, **kwargs):
	"""
	 Runs Zen Coding action. For list of available actions and their
	 arguments see zen_actions.py file.
	 @param name: Action name 
	 @type name: str 
	 @param args: Additional arguments. It may be array of arguments
	 or inline arguments. The first argument should be <code>zen_editor</code> instance
	 @type args: list
	 @example
	 zen_coding.run_actions('expand_abbreviation', zen_editor)
	 zen_coding.run_actions('wrap_with_abbreviation', zen_editor, 'div')  
	"""
	import zen_actions
	
	try:
		if hasattr(zen_actions, name):
			return getattr(zen_actions, name)(*args, **kwargs)
	except:
		return False

def expand_abbreviation(abbr, syntax='html', profile_name='plain'):
	"""
	Expands abbreviation into a XHTML tag string
	@type abbr: str
	@return: str
	"""
	tree_root = parse_into_tree(abbr, syntax);
	if tree_root:
		tree = rollout_tree(tree_root)
		apply_filters(tree, syntax, profile_name, tree_root.filters)
		return replace_variables(tree.to_string())
	
	return ''

def extract_abbreviation(text):
	"""
	Extracts abbreviations from text stream, starting from the end
	@type text: str
	@return: Abbreviation or empty string
	"""
	cur_offset = len(text)
	start_index = -1
	brace_count = 0
	
	while True:
		cur_offset -= 1
		if cur_offset < 0:
			# moved at string start
			start_index = 0
			break
		
		ch = text[cur_offset]
		
		if ch == ']':
			brace_count += 1
		elif ch == '[':
			brace_count -= 1
		else:
			if brace_count: 
				# respect all characters inside attribute sets
				continue
			if not is_allowed_char(ch) or (ch == '>' and is_ends_with_tag(text[0:cur_offset + 1])):
				# found stop symbol
				start_index = cur_offset + 1
				break
		
	return text[start_index:] if start_index != -1 else ''

def parse_into_tree(abbr, doc_type='html'):
	"""
	Parses abbreviation into a node set
	@param abbr: Abbreviation to transform
	@type abbr: str
	@param doc_type: Document type (xsl, html), a key of dictionary where to
	search abbreviation settings
	@type doc_type: str
	@return: Tag
	"""
	# remove filters from abbreviation
	filter_list = []
	
	def filter_replace(m):
		filter_list.append(m.group(1))
		return ''
	
	re_filter = re.compile(r'\|([\w\|\-]+)$')
	abbr = re_filter.sub(filter_replace, abbr)
	
	# split abbreviation by groups
	group_root = split_by_groups(abbr)
	tree_root = Tag('', 1, doc_type)
	
	# then recursively expand each group item
	try:
		for item in group_root.children:
			expand_group(item, doc_type, tree_root)
	except:
		# there's invalid group, stop parsing
		return None
	
	tree_root.filters = ''.join(filter_list)
	return tree_root

def is_inside_tag(html, cursor_pos):
	re_tag = re.compile(r'^<\/?\w[\w\:\-]*.*?>')
	
	# search left to find opening brace
	pos = cursor_pos
	while pos > -1:
		if html[pos] == '<': break
		pos -= 1
	
	
	if pos != -1:
		m = re_tag.match(html[pos:]);
		if m and cursor_pos > pos and cursor_pos < pos + len(m.group(0)):
			return True

	return False

def wrap_with_abbreviation(abbr, text, doc_type='html', profile='plain'):
	"""
	Wraps passed text with abbreviation. Text will be placed inside last
	expanded element
	@param abbr: Abbreviation
	@type abbr: str
	
	@param text: Text to wrap
	@type text: str
	
	@param doc_type: Document type (html, xml, etc.)
	@type doc_type: str
	
	@param profile: Output profile's name.
	@type profile: str
	@return {String}
	"""
	tree_root = parse_into_tree(abbr, doc_type)
	if tree_root:
		repeat_elem = tree_root.multiply_elem or tree_root.last
		repeat_elem.set_content(text)
		repeat_elem.repeat_by_lines = bool(tree_root.multiply_elem)
		
		tree = rollout_tree(tree_root)
		apply_filters(tree, doc_type, profile, tree_root.filters);
		return replace_variables(tree.to_string())
	
	return None

def get_caret_placeholder():
	"""
	Returns caret placeholder
	@return: str
	"""
	if callable(caret_placeholder):
		return caret_placeholder()
	else:
		return caret_placeholder

def set_caret_placeholder(value):
	"""
	Set caret placeholder: a string (like '|') or function.
	You may use a function as a placeholder generator. For example,
	TextMate uses ${0}, ${1}, ..., ${n} natively for quick Tab-switching
	between them.
	@param {String|Function}
	"""
	global caret_placeholder
	caret_placeholder = value

def apply_filters(tree, syntax, profile, additional_filters=None):
	"""
	Applies filters to tree according to syntax
	@param tree: Tag tree to apply filters to
	@type tree: ZenNode
	@param syntax: Syntax name ('html', 'css', etc.)
	@type syntax: str
	@param profile: Profile or profile's name
	@type profile: str, object
	@param additional_filters: List or pipe-separated string of additional filters to apply
	@type additional_filters: str, list 
	 
	@return: ZenNode
	"""
	_filters = get_resource(syntax, 'filters') or basic_filters
		
	if additional_filters:
		_filters += '|'
		if isinstance(additional_filters, basestring):
			_filters += additional_filters
		else:
			_filters += '|'.join(additional_filters)
		
	if not _filters:
		# looks like unknown syntax, apply basic filters
		_filters = basic_filters
		
	return run_filters(tree, profile, _filters)

def replace_counter(text, value):
	"""
	 Replaces '$' character in string assuming it might be escaped with '\'
	 @type text: str
	 @type value: str, int
	 @return: str
	"""
	symbol = '$'
	value = str(value)
	
	def replace_func(tx, symbol, pos, match_num):
		if char_at(tx, pos + 1) == '{' or char_at(tx, pos + 1).isdigit():
			# it's a variable, skip it
			return False
		
		# replace sequense of $ symbols with padded number  
		j = pos + 1
		if j < len(text):
			while tx[j] == '$' and char_at(tx, j + 1) != '{': j += 1
		
		return (tx[pos:j], value.zfill(j - pos))
	
	return replace_unescaped_symbol(text, symbol, replace_func)

def upgrade_tabstops(node):
	"""
	Upgrades tabstops in zen node in order to prevent naming conflicts
	@type node: ZenNode
	@param offset: Tab index offset
	@type offset: int
	@returns Maximum tabstop index in element
	"""
	max_num = [0]
	props = ('start', 'end', 'content')
	
	def _replace(m):
		num = int(m.group(1) or m.group(2))
		if num > max_num[0]: max_num[0] = num
		return re.sub(r'\d+', str(num + max_tabstop), m.group(0), 1)
	
	for prop in props:
		node.__setattr__(prop, re.sub(r'\$(\d+)|\$\{(\d+):[^\}]+\}', _replace, node.__getattribute__(prop)))
		
	globals()['max_tabstop'] += max_num[0] + 1
		
	return max_num[0]

def unescape_text(text):
	"""
	Unescapes special characters used in Zen Coding, like '$', '|', etc.
	@type text: str
	@return: str
	"""
	return re.sub(r'\\(.)', r'\1', text)


def get_profile(name):
	"""
	Get profile by it's name. If profile wasn't found, returns 'plain' profile
	"""
	return profiles[name] if name in profiles else profiles['plain']

def update_settings(settings):
	globals()['zen_settings'] = settings
	
class Tag(object):
	def __init__(self, name, count=1, doc_type='html'):
		"""
		@param name: Tag name
		@type name: str
		@param count:  How many times this tag must be outputted
		@type count: int
		@param doc_type: Document type (xsl, html)
		@type doc_type: str
		"""
		name = name.lower()
		
		abbr = get_abbreviation(doc_type, name)
		
		if abbr and abbr.type == stparser.TYPE_REFERENCE:
			abbr = get_abbreviation(doc_type, abbr.value)
		
		self.name = abbr and abbr.value['name'] or name.replace('+', '')
		self.count = count
		self.children = []
		self.attributes = []
		self.multiply_elem = None
		self.__attr_hash = {}
		self._abbr = abbr
		self.__content = ''
		self.repeat_by_lines = False
		self._res = zen_settings.has_key(doc_type) and zen_settings[doc_type] or {}
		self.parent = None
		
		# add default attributes
		if self._abbr and 'attributes' in self._abbr.value:
			for a in self._abbr.value['attributes']:
				self.add_attribute(a['name'], a['value'])
		
	def add_child(self, tag):
		"""
		Add new child
		@type tag: Tag
		"""
		tag.parent = self
		self.children.append(tag)
		
	def add_attribute(self, name, value):
		"""
		Add attribute to tag. If the attribute with the same name already exists,
		it will be overwritten, but if it's name is 'class', it will be merged
		with the existed one
		@param name: Attribute nama
		@type name: str
		@param value: Attribute value
		@type value: str
		"""
		
		# the only place in Tag where pipe (caret) character may exist
		# is the attribute: escape it with internal placeholder
		value = replace_unescaped_symbol(value, '|', get_caret_placeholder());
		
		if name in self.__attr_hash:
#			attribue already exists
			a = self.__attr_hash[name]
			if name == 'class':
#				'class' is a magic attribute
				if a['value']:
					value = ' ' + value
				a['value'] += value
			else:
				a['value'] = value
		else:
			a = {'name': name, 'value': value}
			self.__attr_hash[name] = a
			self.attributes.append(a)
	
	def has_tags_in_content(self):
		"""
		This function tests if current tags' content contains XHTML tags. 
	 	This function is mostly used for output formatting
		"""
		return self.get_content() and re_tag.search(self.get_content())
	
	def get_content(self):
		return self.__content
	
	def set_content(self, value):
		self.__content = value
		
	def set_content(self, content): #@DuplicatedSignature
		self.__content = content
		
	def get_content(self): #@DuplicatedSignature
		return self.__content
	
	def find_deepest_child(self):
		"""
		Search for deepest and latest child of current element.
		Returns None if there's no children
	 	@return Tag or None 
		"""
		if not self.children:
			return None
			
		deepest_child = self
		while True:
			deepest_child = deepest_child.children[-1]
			if not deepest_child.children:
				break
		
		return deepest_child
	
class Snippet(Tag):
	def __init__(self, name, count=1, doc_type='html'):
		super(Snippet, self).__init__(name, count, doc_type)
		self.value = replace_unescaped_symbol(get_snippet(doc_type, name), '|', get_caret_placeholder())
		self.attributes = {'id': get_caret_placeholder(), 'class': get_caret_placeholder()}
		self._res = zen_settings[doc_type]		
	
	def is_block(self):
		return True
	
class ZenNode(object):
	"""
	Creates simplified tag from Zen Coding tag
	"""
	def __init__(self, tag):
		"""
		@type tag: Tag
		"""
		self.type = 'snippet' if isinstance(tag, Snippet) else 'tag'
		self.name = tag.name
		self.attributes = tag.attributes
		self.children = [];
		self.counter = 1
		
		self.source = tag
		"Source element from which current tag was created"
		
		# relations
		self.parent = None
		self.next_sibling = None
		self.previous_sibling = None
		
		# output params
		self.start = ''
		self.end = ''
		self.content = ''
		self.padding = ''

	def add_child(self, tag):
		"""
		@type tag: ZenNode
		"""
		tag.parent = self
		
		if self.children:
			last_child = self.children[-1]
			tag.previous_sibling = last_child
			last_child.next_sibling = tag
		
		self.children.append(tag)
		
	def get_attribute(self, name):
		"""
		Get attribute's value.
		@type name: str
		@return: None if attribute wasn't found
		"""
		name = name.lower()
		for attr in self.attributes:
			if attr['name'].lower() == name:
				return attr['value']
		
		return None
	
	def is_unary(self):
		"""
		Test if current tag is unary (no closing tag)
		@return: bool
		"""
		if self.type == 'snippet':
			return False
			
		return (self.source._abbr and self.source._abbr.value['is_empty']) or (self.name in get_elements_collection(self.source._res, 'empty'))
	
	def is_inline(self):
		"""
		Test if current tag is inline-level (like <strong>, <img>)
		@return: bool
		"""
		return self.name in get_elements_collection(self.source._res, 'inline_level')
	
	def is_block(self):
		"""
		Test if current element is block-level
		@return: bool
		"""
		return self.type == 'snippet' or not self.is_inline()
	
	def has_tags_in_content(self):
		"""
		This function tests if current tags' content contains xHTML tags. 
		This function is mostly used for output formatting
		"""
		return self.content and re_tag.search(self.content)
	
	def has_children(self):
		"""
		Check if tag has child elements
		@return: bool
		"""
		return bool(self.children)
	
	def has_block_children(self):
		"""
		Test if current tag contains block-level children
		@return: bool
		"""
		if self.has_tags_in_content() and self.is_block():
			return True
		
		for item in self.children:
			if item.is_block():
				return True
			
		return False
	
	def find_deepest_child(self):
		"""
		Search for deepest and latest child of current element
		Returns None if there's no children
		@return: ZenNode|None 
		"""
		if not self.children:
			return None
			
		deepest_child = self
		while True:
			deepest_child = deepest_child.children[-1]
			if not deepest_child.children:
				break
		
		return deepest_child
	
	def to_string(self):
		"@return {String}"
		content = ''.join([item.to_string() for item in self.children])
		return self.start + self.content + content + self.end
		
# create default profiles
setup_profile('xhtml');
setup_profile('html', {'self_closing_tag': False});
setup_profile('xml', {'self_closing_tag': True, 'tag_nl': True});
setup_profile('plain', {'tag_nl': False, 'indent': False, 'place_cursor': False});

# This method call explicity loads default settings from zen_settings.py on start up
# Comment this line if you want to load data from other resources (like editor's 
# native snippet) 
update_settings(stparser.get_settings())
