Files
2013-04-04 08:54:25 -04:00

422 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
# (c) 2012 Sergey Mezentsev
import os
import re
from itertools import product, chain
def import_dir(name, fromlist=()):
PACKAGE_EXT = '.sublime-package'
dirname = os.path.basename(os.path.dirname(os.path.realpath(__file__)))
if dirname.endswith(PACKAGE_EXT):
dirname = dirname[:-len(PACKAGE_EXT)]
return __import__('{0}.{1}'.format(dirname, name), fromlist=fromlist)
try:
imp = import_dir('css_dict_driver', ('css_defaults', 'get_css_dict', 'get_flat_css', 'css_flat_list'))
css_defaults = imp.css_defaults
get_css_dict = imp.get_css_dict
get_flat_css = imp.get_flat_css
css_flat_list = imp.css_flat_list
except ImportError:
from css_dict_driver import css_defaults, get_css_dict, get_flat_css, css_flat_list
# TODO: Move this to dicts etc.
PRIORITY_PROPERTIES = [ 'display', 'color', 'margin', 'position', 'padding', 'width', 'background', 'zoom', 'height', 'top', 'vertical-align', 'overflow', 'left', 'margin-right', 'float', 'margin-left', 'cursor', 'text-decoration', 'font-size', 'margin-top', 'border', 'background-position', 'font', 'margin-bottom', 'padding-left', 'right', 'padding-right', 'line-height', 'white-space', 'text-align', 'border-color', 'padding-top', 'z-index', 'border-bottom', 'visibility', 'border-radius', 'padding-bottom', 'font-weight', 'clear', 'max-width', 'border-top', 'border-width', 'content', 'bottom', 'background-color', 'opacity', 'background-image', 'box-shadow', 'border-collapse', 'text-overflow', 'filter', 'border-right', 'text-indent', 'clip', 'min-width', 'min-height', 'border-left', 'max-height', 'border-right-color', 'border-top-color', 'transition', 'resize', 'overflow-x', 'list-style', 'word-wrap', 'border-left-color', 'word-spacing', 'background-repeat', 'user-select', 'border-bottom-color', 'box-sizing', 'border-top-left-radius', 'font-family', 'border-bottom-width', 'outline', 'border-bottom-right-radius', 'border-right-width', 'border-top-width', 'font-style', 'text-transform', 'border-bottom-left-radius', 'border-left-width', 'border-spacing', 'border-style', 'border-top-right-radius', 'text-shadow', 'border-image', 'overflow-y', 'table-layout', 'background-size', 'behavior', 'body', 'name', 'letter-spacing', 'background-clip', 'pointer-events', 'transform', 'counter-reset', ]
# __all__ = [
# 'extract',
# ]
STATIC_ABBR = dict([
('b', 'bottom'), # Sides consistency
('ba', 'background'), # Instead of background-attachment
('bg', 'background'), # Instead of background: linear-gradient
('bd', 'border'), # Instead of border-style: dashed;
('bbc', 'border-bottom-color'), # Instead of background-break continuous
('br', 'border-right'), # Instead of border-radius
('bt', 'border-top'), # Instead of border: thick
('bdr', 'border-right'), # Instead of border-radius
('bds', 'border-style'), # Instead of border-spacing
('bo', 'border'), # Instead of background-origin
('bos', 'border-style'), # Instead of box-shadow (?)
('ct', 'content'), # Istead of color transparent
('f', 'font'), # Istead of float (do we really need this?)
('p', 'padding'), # Instead of position (w/h/p/m consistency)
('pr', 'padding-right'), # Instead of position relative
])
PAIRS = dict([
('bg', 'background'), # Instead of border-style: groove;
('bd', 'border'), # Instead of background (Zen CSS support)
('pg', 'page'),
('lt', 'letter'),
('tf', 'transform'),
('tr', 'transition'),
])
def get_all_properties():
all_properties = list(get_css_dict().keys())
# раширить парами "свойство значение" (например "position absolute")
for prop_name in all_properties:
property_values = css_flat_list(prop_name, get_css_dict())
extends_sieve = (i for i in property_values if not i[1].startswith('<'))
unit_sieve = (i for i in extends_sieve if not i[1].startswith('.'))
all_properties.extend('{0} {1}'.format(prop_name, v[1]) for v in unit_sieve)
return all_properties
def score(a, b):
"""Оценочная функция"""
s = 0
# увеличивает вес свойству со значением (они разделены пробелом)
if a and ' ' == a[-1]:
s += 3.0
# уменьшить, если буква находится не на грницах слова
if '-' in a[1:-1] or '-' in b[1:-1]:
s += -2.0
# уменьшить, если буква находится не на грницах слова
if ' ' in a[1:-1] or ' ' in b[1:-1]:
s += -0.5
# если буква в начале слова после -
if a and a[-1] == '-':
s += 1.05
# если буквы подряд
if len(a) == 1:
s += 1.0
return s
def string_score(arr):
"""Получает оценку разбиения"""
# s = sum(score(arr[i-1], arr[i]) for i in range(1, len(arr)))
# if s >0 :
# print arr, s
return sum(score(arr[i-1], arr[i]) for i in range(1, len(arr)))
def tree(css_property, abbr):
# функция генерирует деревья (разбиения) из строки
# (abvbc, abc) -> [[a, bvb ,c], [avb, b, c]]
# print '\n', css_property
if len(css_property) < len(abbr):
return set([])
trees = [[css_property[0], css_property[1:],],]
for level in range(1, len(abbr)):
# print level, trees
for tr in trees:
if level == 1 and len(trees) == 1:
trees = []
# находит индексы букв
indexes = []
i = -1
try:
while True:
i = tr[-1].index(abbr[level], i+1)
indexes.append(i)
except ValueError:
pass
# print 'indexes len', len(indexes)
for ind in indexes:
if level == 1:
car = tr[:-1]
cdr = tr[-1]
first = cdr[:ind]
second = cdr[ind:]
add = []
add.append(car[-1] + first)
add.append(second)
# print '\t', car, '|', cdr,'|', first,'|', second, '-', add, level, '=', tr
trees.append(add)
else:
car = tr[:-1]
cdr = tr[-1]
first = cdr[:ind]
second = cdr[ind:]
add = car
add.append(first)
add.append(second)
# print '\t', car, '|', cdr,'|', first,'|', second, '-', add, level, '=', tr
# print repr(first)
trees.append(add)
# break
trees_i = set([tuple(t) for t in trees if len(t) == level+1])
trees = [list(t) for t in trees_i]
# print 'trees_i', trees_i
# break
# print
# break
# удалить разбиения с двумя "-" в шилде
ret = set([tuple(t) for t in trees])
filtered = []
for s in ret: # каждое элемент в сете
for t in s: # каждый шилд в элементе
# print '\t', t
if t.count('-') > 1:
break
else:
filtered.append(s)
# print set([tuple(t) for t in trees])
# print filtered
return filtered
def prop_value(s1, val):
"""Генератор возвращает свойства и значения разделённые пробелом
Из всех свойств выбирает только с совпадающим порядком букв"""
for pv in get_all_properties():
if ' ' not in pv.strip():
continue
prop, value = pv.split()
if sub_string(value, val):
if sub_string(prop, s1):
yield '{0} {1}'.format(prop, value).strip()
def sub_string(string, sub):
"""Функция проверяет, следуют ли буквы в нужном порядке в слове"""
index = 0
for c in sub:
try:
index += string[index:].index(c)+1
except ValueError:
return False
else:
return True
def segmentation(abbr):
"""Разбивает абрревиатуру на элементы"""
# Части аббревиатуры
parts = {
'abbr': abbr # todo: выкинуть, используется только в тестах
}
# Проверка на important свойство
if '!' == abbr[-1]:
abbr = abbr[:-1]
parts['important'] = True
else:
parts['important'] = False
# TODO: вынести regex в compile
# todo: начать тестировать regex
m = re.search(r'^([a-z]?[a-z-]*[a-z]).*$', abbr)
property_ = m if m is None else m.group(1)
if property_ is None:
# Аббревиатура не найдена
return parts
# del m
parts['property-value'] = property_
# удалить из аббревиатуры property
abbr = abbr[len(property_):]
if abbr:
parts['property-name'] = property_
del parts['property-value']
# убрать zen-style разделитель
if abbr and ':' == abbr[0]:
abbr = abbr[1:]
if not abbr:
return parts
parts.update(value_parser(abbr))
if 'value' in parts:
assert parts['value'] is None
del parts['value']
elif ('type-value' not in parts and 'type-name' not in parts):
parts['keyword-value'] = abbr
# TODO: сохранять принимаемые значения, например parts['allow'] = ['<color_values>']
return parts
def value_parser(abbr):
# todo: поддержка аббревиатур "w-.e" то есть "width -|em"
parts = {}
# Checking the color
# Better to replace with regex to simplify it
dot_index = 0
if '.' in abbr:
dot_index = abbr.index('.')
if abbr[0] == '#':
parts['color'] = (abbr[1:dot_index or 99])
if dot_index:
parts['color_alpha'] = (abbr[dot_index:])
parts['value'] = None
try:
if all((c.isupper() or c.isdigit() or c == '.') for c in abbr) and 0 <= int(abbr[:dot_index or 99], 16) <= 0xFFFFFF:
parts['color'] = abbr[:dot_index or 99]
if dot_index:
parts['color_alpha'] = (abbr[dot_index:])
parts['value'] = None
except ValueError:
pass
# Проверка на цифровое значение
val = None
numbers = re.sub("[a-z%]+$", "", abbr)
try:
val = float(numbers)
val = int(numbers)
except ValueError:
pass
if val is not None:
parts['type-value'] = val
if abbr != numbers:
parts['type-name'] = abbr[len(numbers):]
return parts
def extract(s1):
"""В зависимости от найденных компонент в аббревиатуре применяет функцию extract"""
# print repr(s1)
prop_iter = []
parts = segmentation(s1)
abbr_value = False
if 'property-name' in parts:
if parts['important']:
s1 = s1[:-1]
if s1[-1] != ':' and s1 != parts['property-name']:
abbr_value = True
if 'color' in parts:
prop_iter.extend(prop for prop, val in get_flat_css() if val == '<color_values>')
if isinstance(parts.get('type-value'), int):
prop_iter.extend(prop for prop, val in get_flat_css() if val == '<integer>')
if isinstance(parts.get('type-value'), float):
# TODO: добавить deg, grad, time
prop_iter.extend(prop for prop, val in get_flat_css() if val in ('<length>', '<number>', 'percentage'))
if 'keyword-value' in parts and not parts['keyword-value']:
prop_iter.extend(get_all_properties())
if 'keyword-value' in parts:
prop_iter.extend(prop_value(parts['property-name'], parts['keyword-value']))
elif 'color' not in parts or 'type-value' in parts:
prop_iter.extend(get_all_properties())
assert parts.get('property-name', '') or parts.get('property-value', '')
abbr = ' '.join([
parts.get('property-name', '') or parts.get('property-value', ''),
parts.get('keyword-value', ''),
])
# предустановленные правила
abbr = abbr.strip()
if abbr in STATIC_ABBR:
property_ = STATIC_ABBR[abbr]
else:
starts_properties = []
# todo: переделать механизм PAIRS
# надо вынести константы в css-dict
# по две буквы (bd, bg, ba)
pair = PAIRS.get(abbr[:2], None)
if pair is not None:
starts_properties = [prop for prop in prop_iter if prop.startswith(pair) and sub_string(prop, abbr)]
if not starts_properties:
starts_properties = [prop for prop in prop_iter if prop[0] == abbr[0] and sub_string(prop, abbr)]
if 'type-value' in parts:
starts_properties = [i for i in starts_properties if ' ' not in i]
property_ = hayaku_extract(abbr, starts_properties, PRIORITY_PROPERTIES, string_score)
property_, value = property_.split(' ') if ' ' in property_ else (property_, None)
# print property_, value
if not property_:
return {}
parts['property-name'] = property_
if value is not None:
parts['keyword-value'] = value
# Проверка соответствия свойства и значения
allow_values = [val for prop, val in get_flat_css() if prop == parts['property-name']]
if 'color' in parts and '<color_values>' not in allow_values:
del parts['color']
if 'type-value' in parts and not any((t in allow_values) for t in ['<integer>', 'percentage', '<length>', '<number>', '<alphavalue>']):
del parts['type-value']
if 'keyword-value' in parts and parts['keyword-value'] not in allow_values:
del parts['keyword-value']
if all([
'keyword-value' not in parts,
'type-value' not in parts,
'color' not in parts,
]) and abbr_value:
return {}
# Добавить значение по-умолчанию
if parts['property-name'] in get_css_dict():
default_value = css_defaults(parts['property-name'], get_css_dict())
if default_value is not None:
parts['default-value'] = default_value
obj = get_css_dict()[parts['property-name']]
if 'prefixes' in obj:
parts['prefixes'] = obj['prefixes']
if 'no-unprefixed-property' in obj:
parts['no-unprefixed-property'] = obj['no-unprefixed-property']
if parts['abbr'] == parts.get('property-value'):
del parts['property-value']
return parts
def hayaku_extract(abbr, filtered, priority=None, score_func=None):
# выбирает только те правила куда входят все буквы в нужном порядке
# все возможные разбиения
trees_filtered = []
for property_ in filtered:
trees_filtered.extend(tree(property_, abbr))
# оценки к разбиениям
if score_func is not None:
scores = [(score_func(i), i) for i in trees_filtered]
# выбрать с максимальной оценкой
if scores:
max_score = max(s[0] for s in scores)
filtered_scores = (i for s, i in scores if s == max_score)
filtered = [''.join(t) for t in filtered_scores]
if len(filtered) == 1:
return ''.join(filtered[0])
# выбрать более приоритетные
if len(filtered) == 1:
return filtered[0]
elif len(filtered) > 1 and priority is not None:
# выбирает по приоритету
prior = []
for f in filtered:
p = f.split(' ')[0] if ' ' in f else f
try:
prior.append((priority.index(p), f))
except ValueError:
prior.append((len(priority)+1, f))
prior.sort()
try:
return prior[0][1]
except IndexError:
return ''
else:
return ''