463 lines
17 KiB
Python
463 lines
17 KiB
Python
# -*- coding: utf-8 -*-
|
||
import json
|
||
import os
|
||
import re
|
||
|
||
import sublime
|
||
|
||
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:
|
||
get_flat_css = import_dir('css_dict_driver', ('get_flat_css',)).get_flat_css
|
||
except ImportError:
|
||
from css_dict_driver import get_flat_css
|
||
|
||
try:
|
||
imp = import_dir('probe', ('hayaku_extract', 'sub_string'))
|
||
hayaku_extract, sub_string = imp.hayaku_extract, imp.sub_string
|
||
except ImportError:
|
||
from probe import hayaku_extract, sub_string
|
||
|
||
|
||
COLOR_REGEX = re.compile(r'#([0-9a-fA-F]{3,6})')
|
||
COMPLEX_COLOR_REGEX = re.compile(r'^\s*(#?([a-fA-F\d]{3}|[a-fA-F\d]{6})|(rgb|hsl)a?\([^\)]+\))\s*$')
|
||
IMAGE_REGEX = re.compile(r'^\s*([^\s]+\.(jpg|jpeg|gif|png))\s*$')
|
||
|
||
CAPTURING_GROUPS = re.compile(r'(?<!\\)\((?!\?[^<])')
|
||
CAPTURES = re.compile(r'(\(\?|\$)(\d+)|^(\d)')
|
||
|
||
def align_prefix(property_name, prefix_list, no_unprefixed_property, aligned_prefixes, use_only):
|
||
"""Если есть префиксы, сделать шаблон с правильными отступами"""
|
||
|
||
# if no_unprefixed_property:
|
||
# prefix_list = ('-{0}-{1}'.format(prefix_list[0], property_name),)
|
||
|
||
# skip if `use_only` is empty
|
||
if use_only:
|
||
prefix_list = [p for p in prefix_list if p in use_only]
|
||
|
||
if prefix_list:
|
||
prefix_list = ['-{0}-{1}'.format(p, property_name) for p in prefix_list]
|
||
if not no_unprefixed_property:
|
||
prefix_list.append(property_name)
|
||
if not aligned_prefixes:
|
||
return prefix_list
|
||
max_length = max(len(p) for p in prefix_list)
|
||
# TODO: сделать сортировку по размеру значений в prefix_list
|
||
return tuple((' '*(max_length-len(p))) + p for p in prefix_list)
|
||
return (property_name,)
|
||
|
||
def hex_to_coloralpha(hex):
|
||
if len(hex) == 1:
|
||
hex = hex*2
|
||
return round(float(int(hex, 16)) / 255, 2)
|
||
|
||
def color_expand(color,alpha):
|
||
if not color:
|
||
return '#'
|
||
if len(color) == 1:
|
||
if color == '#':
|
||
color = ''
|
||
else:
|
||
color = color * 3
|
||
elif len(color) == 2:
|
||
if color[0] == '#':
|
||
color = color[1] * 3
|
||
else:
|
||
color = color * 3
|
||
elif len(color) == 3:
|
||
if color[0] == '#':
|
||
color = color[1:] * 3
|
||
else:
|
||
color = color
|
||
elif len(color) == 4:
|
||
if color[0] != '#' and alpha == 1:
|
||
alpha = hex_to_coloralpha(color[3])
|
||
color = color[:3]
|
||
else:
|
||
return color
|
||
elif len(color) == 5:
|
||
if color[0] != '#':
|
||
alpha = hex_to_coloralpha(color[3:5])
|
||
color = color[:3]
|
||
else:
|
||
alpha = hex_to_coloralpha(color[4]*2)
|
||
color = color[1:4]
|
||
elif len(color) == 6:
|
||
if color[0] != '#':
|
||
pass
|
||
else:
|
||
alpha = hex_to_coloralpha(color[4:5])
|
||
color = color[1:4]
|
||
elif len(color) == 7:
|
||
color = color[1:]
|
||
else:
|
||
return color
|
||
|
||
# Convert color to rgba if there is some alpha
|
||
if alpha == '.' or float(alpha) < 1:
|
||
if alpha == '.':
|
||
alpha = '.${1:5}' # adding caret for entering alpha value
|
||
if alpha == '.0' or alpha == 0:
|
||
alpha = '0'
|
||
if len(color) == 3:
|
||
color = color[0] * 2 + color[1] * 2 + color[2] * 2
|
||
return "rgba({0},{1},{2},{3})".format(int(color[:2],16), int(color[2:4],16), int(color[4:],16), alpha)
|
||
|
||
return '#{0}'.format(color)
|
||
|
||
def length_expand(name, value, unit, options=None):
|
||
if options is None:
|
||
options = {}
|
||
|
||
if unit and 'percents'.startswith(unit):
|
||
unit = '%'
|
||
|
||
if isinstance(value, float):
|
||
full_unit = options.get('CSS_default_unit_decimal', 'em')
|
||
else:
|
||
full_unit = options.get('CSS_default_unit', 'px')
|
||
|
||
if '<number>' in [val for prop, val in get_flat_css() if prop == name] and not options.get('CSS_units_for_unitless_numbers'):
|
||
full_unit = ''
|
||
|
||
if value == 0:
|
||
return '0'
|
||
if value == '':
|
||
return ''
|
||
|
||
if unit:
|
||
units = (val[1:] for key, val in get_flat_css() if key == name and val.startswith('.'))
|
||
req_units = [u for u in units if sub_string(u, unit)]
|
||
|
||
PRIORITY = ("em", "ex", "vw", "vh", "vmin", "vmax" "vm", "ch", "rem",
|
||
"px", "cm", "mm", "in", "pt", "pc")
|
||
full_unit = hayaku_extract(unit, req_units, PRIORITY)
|
||
if not full_unit:
|
||
return
|
||
|
||
|
||
return '{0}{1}'.format(value, full_unit)
|
||
|
||
def expand_value(args, options=None):
|
||
if 'keyword-value' in args:
|
||
return args['keyword-value']
|
||
if args['property-name'] in set(p for p, v in get_flat_css() if v == '<color_values>'):
|
||
if 'color' in args and not args['color']:
|
||
return '#'
|
||
return color_expand(args.get('color', ''),args.get('color_alpha', 1))
|
||
elif args['property-name'] in set(p for p, v in get_flat_css() if v.startswith('.')) and 'keyword-value' not in args:
|
||
ret = length_expand(args['property-name'], args.get('type-value', ''), args.get('type-name', ''), options)
|
||
return ret
|
||
elif 'type-value' in args:
|
||
return str(args['type-value'])
|
||
return args.get('keyword-value', '')
|
||
|
||
def split_for_snippet(values, offset=0):
|
||
split_lefts = [[]]
|
||
split_rights = [[]]
|
||
parts = 0
|
||
new_offset = offset
|
||
|
||
for value in (v for v in values if len(v) > 1):
|
||
for i in range(1, len(value)):
|
||
if value[:i] not in [item for sublist in split_lefts for item in sublist] + values:
|
||
if len(split_lefts[parts]) > 98:
|
||
parts += 1
|
||
split_lefts.append([])
|
||
split_rights.append([])
|
||
split_lefts[parts].append(value[:i])
|
||
split_rights[parts].append(value[i:])
|
||
new_offset += 1
|
||
|
||
for index in range(0, parts + 1):
|
||
split_lefts[index] = ''.join('({0}$)?'.format(re.escape(i)) for i in split_lefts[index])
|
||
split_rights[index] = ''.join('(?{0}:{1})'.format(i+1+offset,re.escape(f)) for i,f in enumerate(split_rights[index]))
|
||
|
||
return (split_lefts, split_rights, new_offset)
|
||
|
||
def convert_to_parts(parts):
|
||
matches = []
|
||
inserts = []
|
||
parts_count = 1
|
||
|
||
# Function for offsetting the captured groups in inserts
|
||
def offset_captures(match):
|
||
if match.group(3):
|
||
return '()' + match.group(3)
|
||
else:
|
||
number = int(match.group(2))
|
||
return match.group(1) + str(number + parts_count)
|
||
|
||
for part in parts:
|
||
matches.append(''.join([
|
||
'(?=(',
|
||
part['match'],
|
||
')?)',
|
||
]))
|
||
inserts.append(''.join([
|
||
'(?',
|
||
str(parts_count),
|
||
':',
|
||
CAPTURES.sub(offset_captures, part['insert']),
|
||
')',
|
||
]))
|
||
# Incrementing the counter, adding the number of internal capturing groups
|
||
parts_count += 1 + len(CAPTURING_GROUPS.findall(part['match'] ))
|
||
return { "matches": matches, "inserts": inserts }
|
||
|
||
def generate_snippet(data):
|
||
value = data.get('value')
|
||
before = ''.join([
|
||
'_PROPERTY_',
|
||
data.get('colon'),
|
||
data.get('space'),
|
||
])
|
||
after = ''
|
||
importance = ''
|
||
if data.get('important'):
|
||
importance = ' !important'
|
||
|
||
if value:
|
||
after = importance + data.get('semicolon')
|
||
else:
|
||
if not importance:
|
||
importance_splitted = split_for_snippet(["!important"])
|
||
importance = ''.join([
|
||
'${1/.*?',
|
||
importance_splitted[0][0],
|
||
'$/',
|
||
importance_splitted[1][0],
|
||
'/}',
|
||
])
|
||
|
||
befores = convert_to_parts(data["before"])
|
||
before = ''.join([
|
||
'${1/^',
|
||
''.join(befores["matches"]),
|
||
'.+$|.*/',
|
||
before,
|
||
''.join(befores["inserts"]),
|
||
'/m}',
|
||
])
|
||
|
||
|
||
if data.get('semicolon') == '':
|
||
data['semicolon'] = ' '
|
||
|
||
afters = convert_to_parts(data["after"])
|
||
after = ''.join([
|
||
'${1/^',
|
||
''.join(afters["matches"]),
|
||
'.+$|.*/',
|
||
''.join(afters["inserts"]),
|
||
'/m}',
|
||
data.get('autovalues'),
|
||
importance,
|
||
data.get('semicolon'),
|
||
])
|
||
value = ''.join([
|
||
'${1:',
|
||
data.get('default'),
|
||
'}',
|
||
])
|
||
return (before + value + after).replace('{','{{').replace('}','}}').replace('_PROPERTY_','{0}')
|
||
|
||
|
||
def make_template(args, options):
|
||
whitespace = options.get('CSS_whitespace_after_colon', '')
|
||
disable_semicolon = options.get('CSS_syntax_no_semicolons', False)
|
||
disable_colon = options.get('CSS_syntax_no_colons', False)
|
||
disable_prefixes = options.get('CSS_prefixes_disable', False)
|
||
clipboard = sublime.get_clipboard()
|
||
|
||
if not whitespace and disable_colon:
|
||
whitespace = ' '
|
||
|
||
value = expand_value(args, options)
|
||
if value is None:
|
||
return
|
||
|
||
if value.startswith('[') and value.endswith(']'):
|
||
value = False
|
||
|
||
semicolon = ';'
|
||
colon = ':'
|
||
|
||
if disable_semicolon:
|
||
semicolon = ''
|
||
if disable_colon:
|
||
colon = ''
|
||
|
||
snippet_parts = {
|
||
'colon': colon,
|
||
'semicolon': semicolon,
|
||
'space': whitespace,
|
||
'default': args.get('default-value',''),
|
||
'important': args.get('important'),
|
||
'before': [],
|
||
'after': [],
|
||
'autovalues': '',
|
||
}
|
||
|
||
# Handling prefixes
|
||
property_ = (args['property-name'],)
|
||
if not disable_prefixes:
|
||
property_ = align_prefix(
|
||
args['property-name'],
|
||
args.get('prefixes', []),
|
||
args.get('no-unprefixed-property', False) or options.get('CSS_prefixes_no_unprefixed', False),
|
||
options.get('CSS_prefixes_align', True),
|
||
options.get('CSS_prefixes_only', []),
|
||
)
|
||
|
||
# Replace the parens with a tabstop snippet
|
||
# TODO: Move the inside snippets to the corresponding snippets dict
|
||
if value and '()' in value:
|
||
if value.replace('()', '') in ['rotate','rotateX','rotateY','rotateZ','skew','skewX','skewY']:
|
||
value = value.replace('()', '($1${1/^((?!0$)-?(\d*.)?\d+)?.*$/(?1:deg)/m})')
|
||
else:
|
||
value = value.replace('()', '($1)')
|
||
|
||
# Do things when there is no value expanded
|
||
if not value or value == "#":
|
||
if not options.get('CSS_disable_postexpand', False):
|
||
auto_values = [val for prop, val in get_flat_css() if prop == args['property-name']]
|
||
if auto_values:
|
||
units = []
|
||
values = []
|
||
|
||
for p_value in (v for v in auto_values if len(v) > 1):
|
||
if p_value.startswith('.'):
|
||
units.append(p_value[1:])
|
||
elif not p_value.startswith('<'):
|
||
values.append(p_value)
|
||
|
||
values_splitted = split_for_snippet(values)
|
||
snippet_values = ''
|
||
for index in range(0,len(values_splitted[0])):
|
||
snippet_values += ''.join([
|
||
'${1/^\s*',
|
||
values_splitted[0][index],
|
||
'.*/',
|
||
values_splitted[1][index],
|
||
'/m}',
|
||
])
|
||
snippet_parts['autovalues'] += snippet_values
|
||
|
||
snippet_units = ''
|
||
# TODO: find out when to use units or colors
|
||
# TODO: Rewrite using after
|
||
if units and value != "#":
|
||
units_splitted = split_for_snippet(units, 4)
|
||
snippet_parts['before'].append({
|
||
"match": "%$",
|
||
"insert": "100"
|
||
})
|
||
# If there can be `number` in value, don't add `em` automatically
|
||
optional_unit_for_snippet = '(?2:(?3::0)em:px)'
|
||
if '<number>' in auto_values and not options.get('CSS_units_for_unitless_numbers'):
|
||
optional_unit_for_snippet = '(?2:(?3::0):)'
|
||
snippet_units = ''.join([
|
||
'${1/^\s*((?!0$)(?=.)[\d\-]*(\.)?(\d+)?((?=.)',
|
||
units_splitted[0][0],
|
||
')?$)?.*/(?4:',
|
||
units_splitted[1][0],
|
||
':(?1:' + optional_unit_for_snippet + '))/m}',
|
||
])
|
||
snippet_parts['autovalues'] += snippet_units
|
||
|
||
# Adding snippets for colors
|
||
if value == "#":
|
||
value = ''
|
||
# Insert hash and doubling letters
|
||
snippet_parts['before'].append({
|
||
"match": "([0-9a-fA-F]{1,6}|[0-9a-fA-F]{3,6}\s*(!\w*\s*)?)$",
|
||
"insert": "#"
|
||
})
|
||
snippet_parts['after'].append({
|
||
"match": "#?([0-9a-fA-F]{1,2})$",
|
||
"insert": "(?1:$1$1)"
|
||
})
|
||
# Insert `rgba` thingies
|
||
snippet_parts['before'].append({
|
||
"match": "(\d{1,3}%?),(\.)?.*$",
|
||
"insert": "rgba\((?2:$1,$1,)"
|
||
})
|
||
snippet_parts['after'].append({
|
||
"match": "(\d{1,3}%?),(\.)?(.+)?$",
|
||
"insert": "(?2:(?3::5):(?3::$1,$1,1))\)"
|
||
})
|
||
|
||
# Getting the value from the clipboard
|
||
# TODO: Move to the whole clipboard2default function
|
||
check_clipboard_for_color = COMPLEX_COLOR_REGEX.match(clipboard)
|
||
if check_clipboard_for_color and 'colors' in options.get('CSS_clipboard_defaults'):
|
||
snippet_parts['default'] = check_clipboard_for_color.group(1)
|
||
# TODO: move this out of `if not value`,
|
||
# so we could use it for found `url()` values
|
||
if '<url>' in auto_values:
|
||
snippet_parts['before'].append({
|
||
"match": "[^\s]+\.(jpg|jpeg|gif|png)$",
|
||
"insert": "url\("
|
||
})
|
||
snippet_parts['after'].append({
|
||
"match": "[^\s]+\.(jpg|jpeg|gif|png)$",
|
||
"insert": "\)"
|
||
})
|
||
check_clipboard_for_image = IMAGE_REGEX.match(clipboard)
|
||
if check_clipboard_for_image and 'images' in options.get('CSS_clipboard_defaults'):
|
||
quote_symbol = ''
|
||
if options.get('CSS_syntax_url_quotes'):
|
||
quote_symbol = options.get('CSS_syntax_quote_symbol')
|
||
snippet_parts['default'] = 'url(' + quote_symbol + check_clipboard_for_image.group(1) + quote_symbol + ')'
|
||
|
||
|
||
snippet_parts['value'] = value or ''
|
||
|
||
snippet = generate_snippet(snippet_parts)
|
||
|
||
# Apply settings to the colors in the values
|
||
def restyle_colors(match):
|
||
color = match.group(1)
|
||
# Change case of the colors in the value
|
||
if options.get('CSS_colors_case').lower() in ('uppercase' 'upper'):
|
||
color = color.upper()
|
||
elif options.get('CSS_colors_case').lower() in ('lowercase' 'lower'):
|
||
color = color.lower()
|
||
# Make colors short or longhand
|
||
if options.get('CSS_colors_length').lower() in ('short' 'shorthand') and len(color) == 6:
|
||
if color[0] == color[1] and color[2] == color[3] and color[4] == color[5]:
|
||
color = color[0] + color[2] + color[4]
|
||
elif options.get('CSS_colors_length').lower() in ('long' 'longhand') and len(color) == 3:
|
||
color = color[0] * 2 + color[1] * 2 + color[2] * 2
|
||
return '#' + color
|
||
snippet = COLOR_REGEX.sub(restyle_colors, snippet)
|
||
|
||
# Apply setting of the prefered quote symbol
|
||
|
||
if options.get('CSS_syntax_quote_symbol') == "'" and '"' in snippet:
|
||
snippet = snippet.replace('"',"'")
|
||
if options.get('CSS_syntax_quote_symbol') == '"' and "'" in snippet:
|
||
snippet = snippet.replace("'",'"')
|
||
|
||
newline_ending = ''
|
||
if options.get('CSS_newline_after_expand'):
|
||
newline_ending = '\n'
|
||
return '\n'.join(snippet.format(prop) for prop in property_) + newline_ending
|
||
|
||
# TODO
|
||
# display: -moz-inline-box;
|
||
# display: inline-block;
|
||
|
||
# background-image: -webkit-linear-gradient(top,rgba(255,255,255,0.6),rgba(255,255,255,0));
|
||
# background-image: -moz-linear-gradient(top,rgba(255,255,255,0.6),rgba(255,255,255,0));
|
||
# background-image: -o-linear-gradient(top,rgba(255,255,255,0.6),rgba(255,255,255,0));
|
||
# background-image: linear-gradient(top,rgba(255,255,255,0.6),rgba(255,255,255,0));
|