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 <img src=""> 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