mirror of
https://github.com/kennethreitz-archive/kcode.tmbundle.git
synced 2026-06-05 15:40:17 +00:00
1239 lines
31 KiB
Python
1239 lines
31 KiB
Python
#!/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"
|
|
|
|
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)
|
|
|
|
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@\!\#\.][a-z0-9:\-]*)((?:(?:[#\.][\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 tx[pos + 1] == '{':
|
|
# it's a variable, skip it
|
|
return False
|
|
|
|
# replace sequense of $ symbols with padded number
|
|
j = pos + 1
|
|
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 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.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())
|