1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3
4'''
5Created on Jan 21, 2011
6
7@author: sergey
8'''
9from zencoding.actions.basic import starts_with
10from zencoding.utils import prettify_number
11import base64
12import math
13import re
14import zencoding
15import zencoding.interface.file as zen_file
16import zencoding.parser.utils as parser_utils
17
18@zencoding.action
19def reflect_css_value(editor):
20	"""
21	Reflect CSS value: takes rule's value under caret and pastes it for the same
22	rules with vendor prefixes
23	@param editor: ZenEditor
24	"""
25	if editor.get_syntax() != 'css':
26		return False
27
28	return compound_update(editor, do_css_reflection(editor))
29
30@zencoding.action
31def update_image_size(editor):
32	"""
33	Update image size: reads image from image/CSS rule under caret
34	and updates dimensions inside tag/rule
35	@type editor: ZenEditor
36	"""
37	if editor.get_syntax() == 'css':
38		result = update_image_size_css(editor)
39	else:
40		result = update_image_size_html(editor)
41
42	return compound_update(editor, result)
43
44def compound_update(editor, data):
45	if data:
46		text = data['data']
47
48		sel_start, sel_end = editor.get_selection_range()
49
50		# try to preserve caret position
51		if data['caret'] < data['start'] + len(text):
52			relative_pos = data['caret'] - data['start']
53			if relative_pos >= 0:
54				text = text[:relative_pos] + zencoding.utils.get_caret_placeholder() + text[relative_pos:]
55
56		editor.replace_content(text, data['start'], data['end'], True)
57#		editor.replace_content(zencoding.utils.unindent(editor, text), data['start'], data['end'])
58		editor.create_selection(data['caret'], data['caret'] + sel_end - sel_start)
59		return True
60
61	return False
62
63def update_image_size_html(editor):
64	"""
65	Updates image size of &lt;img src=""&gt; tag
66	@type editor: ZenEditor
67	"""
68	editor_file = editor.get_file_path()
69	caret_pos = editor.get_caret_pos()
70
71	if editor_file is None:
72		raise zencoding.utils.ZenError("You should save your file before using this action")
73
74	image = _find_image(editor)
75
76	if image:
77		# search for image path
78		m = re.search(r'src=(["\'])(.+?)\1', image['tag'], re.IGNORECASE)
79		if m:
80			src = m.group(2)
81
82		if src:
83			size = get_image_size_for_source(editor, src)
84			if size:
85				new_tag = _replace_or_append(image['tag'], 'width', size['width'])
86				new_tag = _replace_or_append(new_tag, 'height', size['height'])
87
88				return {
89					'data': new_tag,
90					'start': image['start'],
91					'end': image['end'],
92					'caret': caret_pos
93				}
94	return False
95
96def get_image_size_for_source(editor, src):
97	"""
98	Returns image dimentions for source
99	@param {zen_editor} editor
100	@param {String} src Image source (path or data:url)
101	"""
102	if src:
103		# check if it is data:url
104		if starts_with('data:', src):
105			f_content = base64.b64decode( re.sub(r'^data\:.+?;.+?,', '', src) )
106		else:
107			editor_file = editor.get_file_path()
108
109			if editor_file is None:
110				raise zencoding.utils.ZenError("You should save your file before using this action")
111
112			abs_src = zen_file.locate_file(editor_file, src)
113			if not abs_src:
114				raise zencoding.utils.ZenError("Can't locate '%s' file" % src)
115
116			f_content = zen_file.read(abs_src)
117
118		return zencoding.utils.get_image_size(f_content)
119
120def _replace_or_append(img_tag, attr_name, attr_value):
121	"""
122	Replaces or adds attribute to the tag
123	@type img_tag: str
124	@type attr_name: str
125	@type attr_value: str
126	"""
127	if attr_name in img_tag.lower():
128		# attribute exists
129		re_attr = re.compile(attr_name + r'=([\'"])(.*?)\1', re.I)
130		return re.sub(re_attr, lambda m: '%s=%s%s%s' % (attr_name, m.group(1), attr_value, m.group(1)), img_tag)
131	else:
132		return re.sub(r'\s*(\/?>)$', ' %s="%s" \\1' % (attr_name, attr_value), img_tag)
133
134def _find_image(editor):
135	"""
136	Find image tag under caret
137 	@return Image tag and its indexes inside editor source
138	"""
139	_caret = editor.get_caret_pos()
140	text = editor.get_content()
141	start_ix = -1
142	end_ix = -1
143
144	# find the beginning of the tag
145	caret_pos = _caret
146	while caret_pos >= 0:
147		if text[caret_pos] == '<':
148			if text[caret_pos:caret_pos + 4].lower() == '<img':
149				# found the beginning of the image tag
150				start_ix = caret_pos
151				break
152			else:
153				# found some other tag
154				return None
155		caret_pos -= 1
156
157	# find the end of the tag
158	caret_pos = _caret
159	ln = len(text)
160	while caret_pos <= ln:
161		if text[caret_pos] == '>':
162			end_ix = caret_pos + 1
163			break
164		caret_pos += 1
165
166
167	if start_ix != -1 and end_ix != -1:
168		return {
169			'start': start_ix,
170			'end': end_ix,
171			'tag': text[start_ix:end_ix]
172		}
173
174	return None
175
176def find_css_insertion_point(tokens, start_ix):
177	"""
178	Search for insertion point for new CSS properties
179	@param tokens: List of parsed CSS tokens
180	@param start_ix: Token index where to start searching
181	"""
182	ins_point = None
183	ins_ix = -1
184	need_col = False
185
186	for i in range(start_ix, len(tokens)):
187		t = tokens[i]
188		if t['type'] == 'value':
189			ins_point = t
190			ins_ix = i
191
192			# look ahead for rule termination
193			if i + 1 < len(tokens) and tokens[i + 1]['type'] == ';':
194				ins_point = tokens[i + 1]
195				ins_ix += 1
196			else:
197				need_col = True
198
199			break
200
201	return {
202		'token': ins_point,
203		'ix': ins_ix,
204		'need_col': need_col
205	}
206
207def update_image_size_css(editor):
208	"""
209	Updates image size of CSS rule
210 	@type editor: ZenEditor
211	"""
212	caret_pos = editor.get_caret_pos()
213	content = editor.get_content()
214	rule = parser_utils.extract_css_rule(content, caret_pos, True)
215
216	if rule:
217		css = parser_utils.parse_css(content[rule[0]:rule[1]], rule[0])
218		cur_token = find_token_from_position(css, caret_pos, 'identifier')
219		value = find_value_token(css, cur_token + 1)
220
221		if not value: return False
222
223		# find insertion point
224		ins_point = find_css_insertion_point(css, cur_token)
225
226		m = re.match(r'url\((["\']?)(.+?)\1\)', value['content'], re.I)
227		if m:
228			size = get_image_size_for_source(editor, m.group(2))
229			if size:
230				wh = {'width': None, 'height': None}
231				updates = []
232				styler = learn_css_style(css, cur_token)
233
234				for i, item in enumerate(css):
235					if item['type'] == 'identifier' and item['content'] in wh:
236						wh[item['content']] = i
237
238				def update(name, val):
239					v = None
240					if wh[name] is not None:
241						v = find_value_token(css, wh[name] + 1)
242
243					if v:
244						updates.append([v['start'], v['end'], '%spx' % val])
245					else:
246						updates.append([ins_point['token']['end'], ins_point['token']['end'], styler(name, '%spx' % val)])
247
248
249				update('width', size['width'])
250				update('height', size['height'])
251
252				if updates:
253					updates.sort(lambda a,b: a[0] - b[0])
254#					updates = sorted(updates, key=lambda a: a[0])
255
256					# some editors do not provide easy way to replace multiple code
257					# fragments so we have to squash all replace operations into one
258					offset = updates[0][0]
259					offset_end = updates[-1][1]
260					data = content[offset:offset_end]
261
262					updates.reverse()
263					for u in updates:
264						data = replace_substring(data, u[0] - offset, u[1] - offset, u[2])
265
266						# also calculate new caret position
267						if u[0] < caret_pos:
268							caret_pos += len(u[2]) - u[1] + u[0]
269
270
271					if ins_point['need_col']:
272						data = replace_substring(data, ins_point['token']['end'] - offset, ins_point['token']['end'] - offset, ';')
273
274					return {
275						'data': data,
276						'start': offset,
277						'end': offset_end,
278						'caret': caret_pos
279					};
280
281	return None
282
283def learn_css_style(tokens, pos):
284	"""
285	Learns formatting style from parsed tokens
286	@param tokens: List of tokens
287	@param pos: Identifier token position, from which style should be learned
288	@returns: Function with <code>(name, value)</code> arguments that will create
289	CSS rule based on learned formatting
290	"""
291	prefix = ''
292	glue = ''
293
294	# use original tokens instead of optimized ones
295	pos = tokens[pos]['ref_start_ix']
296	tokens = tokens.original
297
298	# learn prefix
299	for i in xrange(pos - 1, -1, -1):
300		if tokens[i]['type'] == 'white':
301			prefix = tokens[i]['content'] + prefix
302		elif tokens[i]['type'] == 'line':
303			prefix = tokens[i]['content'] + prefix
304			break
305		else:
306			break
307
308	# learn glue
309	for t in tokens[pos+1:]:
310		if t['type'] == 'white' or t['type'] == ':':
311			glue += t['content']
312		else:
313			break
314
315	if ':' not in glue:
316		glue = ':'
317
318	return lambda name, value: "%s%s%s%s;" % (prefix, name, glue, value)
319
320
321def do_css_reflection(editor):
322	content = editor.get_content()
323	caret_pos = editor.get_caret_pos()
324	css = parser_utils.extract_css_rule(content, caret_pos)
325
326	if not css or caret_pos < css[0] or caret_pos > css[1]:
327		# no matching CSS rule or caret outside rule bounds
328		return False
329
330	tokens = parser_utils.parse_css(content[css[0]:css[1]], css[0])
331	token_ix = find_token_from_position(tokens, caret_pos, 'identifier')
332
333	if token_ix != -1:
334		cur_prop = tokens[token_ix]['content']
335		value_token = find_value_token(tokens, token_ix + 1)
336		base_name = get_base_css_name(cur_prop)
337		re_name = re.compile('^(?:\\-\\w+\\-)?' + base_name + '$')
338		re_name = get_reflected_css_name(base_name)
339		values = []
340
341		if not value_token:
342			return False
343
344		# search for all vendor-prefixed properties
345		for i, token in enumerate(tokens):
346			if token['type'] == 'identifier' and re.search(re_name, token['content']) and token['content'] != cur_prop:
347				v = find_value_token(tokens, i + 1)
348				if v:
349					values.append({'name': token, 'value': v})
350
351		# some editors do not provide easy way to replace multiple code
352		# fragments so we have to squash all replace operations into one
353		if values:
354			data = content[values[0]['value']['start']:values[-1]['value']['end']]
355			offset = values[0]['value']['start']
356			value = value_token['content']
357
358			values.reverse()
359			for v in values:
360				rv = get_reflected_value(cur_prop, value, v['name']['content'], v['value']['content'])
361				data = replace_substring(data, v['value']['start'] - offset, v['value']['end'] - offset, rv)
362
363				# also calculate new caret position
364				if v['value']['start'] < caret_pos:
365					caret_pos += len(rv) - len(v['value']['content'])
366
367			return {
368				'data': data,
369				'start': offset,
370				'end': values[0]['value']['end'],
371				'caret': caret_pos
372			}
373
374	return None
375
376def get_base_css_name(name):
377	"""
378    Removes vendor prefix from CSS property
379    @param name: CSS property
380    @type name: str
381    @return: str
382	"""
383	return re.sub(r'^\s*\-\w+\-', '', name)
384
385def get_reflected_css_name(name):
386	"""
387    Returns regexp that should match reflected CSS property names
388    @param name: Current CSS property name
389    @type name: str
390    @return: RegExp
391	"""
392	name = get_base_css_name(name)
393	vendor_prefix = '^(?:\\-\\w+\\-)?'
394
395	if name == 'opacity' or name == 'filter':
396		return re.compile(vendor_prefix + '(?:opacity|filter)$')
397
398	m = re.match(r'^border-radius-(top|bottom)(left|right)', name)
399	if m:
400		# Mozilla-style border radius
401		return re.compile(vendor_prefix + '(?:%s|border-%s-%s-radius)$' % (name, m.group(1), m.group(2)) )
402
403	m = re.match(r'^border-(top|bottom)-(left|right)-radius', name)
404	if m:
405		return re.compile(vendor_prefix + '(?:%s|border-radius-%s%s)$'  % (name, m.group(1), m.group(2)) );
406
407	return re.compile(vendor_prefix + name + '$')
408
409def get_reflected_value(cur_name, cur_value, ref_name, ref_value):
410	"""
411    Returns value that should be reflected for <code>ref_name</code> CSS property
412    from <code>cur_name</code> property. This function is used for special cases,
413    when the same result must be achieved with different properties for different
414    browsers. For example: opаcity:0.5; -> filter:alpha(opacity=50);<br><br>
415
416    This function does value conversion between different CSS properties
417
418    @param cur_name: Current CSS property name
419    @type cur_name: str
420    @param cur_value: Current CSS property value
421    @type cur_value: str
422    @param ref_name: Receiver CSS property's name
423    @type ref_name: str
424    @param ref_value: Receiver CSS property's value
425    @type ref_value: str
426    @return: New value for receiver property
427	"""
428	cur_name = get_base_css_name(cur_name)
429	ref_name = get_base_css_name(ref_name)
430
431	if cur_name == 'opacity' and ref_name == 'filter':
432		return re.sub(re.compile(r'opacity=[^\)]*', re.IGNORECASE), 'opacity=' + math.floor(float(cur_value) * 100), ref_value)
433	if cur_name == 'filter' and ref_name == 'opacity':
434		m = re.search(r'opacity=([^\)]*)', cur_value, re.IGNORECASE)
435		return prettify_number(int(m.group(1)) / 100) if m else ref_value
436
437
438	return cur_value
439
440def find_value_token(tokens, pos):
441	"""
442    Find value token, staring at <code>pos</code> index and moving right
443    @type tokens: list
444    @type pos: int
445    @return: token
446	"""
447	for t in tokens[pos:]:
448		if t['type'] == 'value':
449			return t
450		elif t['type'] == 'identifier' or t['type'] == ';':
451			break
452
453	return None
454
455def replace_substring(text, start, end, new_value):
456	"""
457    Replace substring of <code>text</code>, defined by <code>start</code> and
458    <code>end</code> indexes with <code>new_value</code>
459    @type text: str
460    @type start: int
461    @type end: int
462    @type new_value: str
463    @return: str
464	"""
465	return text[0:start] + new_value + text[end:]
466
467def find_token_from_position(tokens, pos, type):
468	"""
469    Search for token with specified type left to the specified position
470    @param tokens: List of parsed tokens
471    @type tokens: list
472    @param pos: Position where to start searching
473    @type pos: int
474    @param type: Token type
475    @type type: str
476    @return: Token index
477	"""
478	# find token under caret
479	token_ix = -1;
480	for i, token in enumerate(tokens):
481		if token['start'] <= pos and token['end'] >= pos:
482			token_ix = i
483			break
484
485	if token_ix != -1:
486		# token found, search left until we find token with specified type
487		while token_ix >= 0:
488			if tokens[token_ix]['type'] == type:
489				return token_ix
490			token_ix -= 1
491
492	return -1
493