def set_style_property(tag, property_name, value, editor): ''' Set a style property, i.e. a CSS property inside the style attribute of the tag. Any existing style attribute is updated or a new attribute is inserted. ''' block, offset = find_attribute_in_tag(tag.start_block, tag.start_offset + 1, 'style') c = editor.textCursor() def css(d): return css_text(d).replace('\n', ' ') if block is None or offset is None: d = parseStyle('') d.setProperty(property_name, value) c.setPosition(tag.end_block.position() + tag.end_offset) c.insertText(' style="%s"' % css(d)) else: c.setPosition(block.position() + offset - 1) end_block, end_offset = find_end_of_attribute(block, offset + 1) if end_block is None: return error_dialog(editor, _('Invalid markup'), _( 'The current block tag has an existing unclosed style attribute. Run the Fix HTML' ' tool first.'), show=True) c.setPosition(end_block.position() + end_offset, c.KeepAnchor) d = parseStyle(editor.selected_text_from_cursor(c)[1:-1]) d.setProperty(property_name, value) c.insertText('"%s"' % css(d))
def set_style_property(tag, property_name, value, editor): ''' Set a style property, i.e. a CSS property inside the style attribute of the tag. Any existing style attribute is updated or a new attribute is inserted. ''' block, offset = find_attribute_in_tag(tag.start_block, tag.start_offset + 1, 'style') c = editor.textCursor() def css(d): return css_text(d).replace('\n', ' ') if block is None or offset is None: d = parseStyle('') d.setProperty(property_name, value) c.setPosition(tag.end_block.position() + tag.end_offset) c.insertText(' style="%s"' % css(d)) else: c.setPosition(block.position() + offset - 1) end_block, end_offset = find_end_of_attribute(block, offset + 1) if end_block is None: return error_dialog( editor, _('Invalid markup'), _('The current block tag has an existing unclosed style attribute. Run the Fix HTML' ' tool first.'), show=True) c.setPosition(end_block.position() + end_offset, c.KeepAnchor) d = parseStyle(editor.selected_text_from_cursor(c)[1:-1]) d.setProperty(property_name, value) c.insertText('"%s"' % css(d))
def test_border_condensation(self): vals = 'red solid 5px' css = '; '.join('border-%s-%s: %s' % (edge, p, v) for edge in EDGES for p, v in zip(BORDER_PROPS, vals.split())) style = parseStyle(css) condense_rule(style) for e, p in product(EDGES, BORDER_PROPS): self.assertFalse(style.getProperty('border-%s-%s' % (e, p))) self.assertFalse(style.getProperty('border-%s' % e)) self.assertFalse(style.getProperty('border-%s' % p)) self.assertEqual(style.getProperty('border').value, vals) css = '; '.join('border-%s-%s: %s' % (edge, p, v) for edge in ('top', ) for p, v in zip(BORDER_PROPS, vals.split())) style = parseStyle(css) condense_rule(style) self.assertEqual(css_text(style), 'border-top: %s' % vals) css += ';' + '; '.join( 'border-%s-%s: %s' % (edge, p, v) for edge in ('right', 'left', 'bottom') for p, v in zip(BORDER_PROPS, vals.replace('red', 'green').split())) style = parseStyle(css) condense_rule(style) self.assertEqual(len(style.getProperties()), 4) self.assertEqual(style.getProperty('border-top').value, vals) self.assertEqual( style.getProperty('border-left').value, vals.replace('red', 'green'))
def test_list_style_normalization(self): def ls_dict(expected): ans = { 'list-style-%s' % x: DEFAULTS['list-style-%s' % x] for x in ('type', 'image', 'position') } for k, v in iteritems(expected): ans['list-style-%s' % k] = v return ans for raw, expected in iteritems({ 'url(http://www.example.com/images/list.png)': { 'image': 'url(http://www.example.com/images/list.png)' }, 'inside square': { 'position': 'inside', 'type': 'square' }, 'upper-roman url(img) outside': { 'position': 'outside', 'type': 'upper-roman', 'image': 'url(img)' }, }): cval = tuple(parseStyle('list-style: %s' % raw, validate=False))[0].propertyValue self.assertDictEqual( ls_dict(expected), normalizers['list-style']('list-style', cval))
def test_edge_condensation(self): for s, v in iteritems({ (1, 1, 3): None, (1, 2, 3, 4): '2pt 3pt 4pt 1pt', (1, 2, 3, 2): '2pt 3pt 2pt 1pt', (1, 2, 1, 3): '2pt 1pt 3pt', (1, 2, 1, 2): '2pt 1pt', (1, 1, 1, 1): '1pt', ('2%', '2%', '2%', '2%'): '2%', tuple('0 0 0 0'.split()): '0', }): for prefix in ('margin', 'padding'): css = { '%s-%s' % (prefix, x): unicode_type(y) + 'pt' if isinstance(y, numbers.Number) else y for x, y in zip(('left', 'top', 'right', 'bottom'), s) } css = '; '.join( ('%s:%s' % (k, v) for k, v in iteritems(css))) style = parseStyle(css) condense_rule(style) val = getattr(style.getProperty(prefix), 'value', None) self.assertEqual(v, val) if val is not None: for edge in EDGES: self.assertFalse( getattr( style.getProperty('%s-%s' % (prefix, edge)), 'value', None))
def test_replaceUrls(self): "css_parser.replaceUrls()" css_parser.ser.prefs.keepAllProperties = True css = r''' @import "im1"; @import url(im2); a { background-image: url(c) !important; background-\image: url(b); background: url(a) no-repeat !important; }''' s = css_parser.parseString(css) css_parser.replaceUrls(s, lambda old: "NEW" + old) self.assertEqual('@import "NEWim1";', s.cssRules[0].cssText) self.assertEqual('NEWim2', s.cssRules[1].href) self.assertEqual( '''background-image: url(NEWc) !important; background-\\image: url(NEWb); background: url(NEWa) no-repeat !important''', s.cssRules[2].style.cssText) css_parser.ser.prefs.keepAllProperties = False # CSSStyleDeclaration style = css_parser.parseStyle('''color: red; background-image: url(1.png), url('2.png')''') css_parser.replaceUrls(style, lambda url: 'prefix/' + url) self.assertEqual( style.cssText, '''color: red; background-image: url(prefix/1.png), url(prefix/2.png)''')
def test_parseStyle(self): "css_parser.parseStyle()" s = css_parser.parseStyle('x:0; y:red') self.assertEqual(type(s), css_parser.css.CSSStyleDeclaration) self.assertEqual(s.cssText, 'x: 0;\ny: red') s = css_parser.parseStyle('@import "x";') self.assertEqual(type(s), css_parser.css.CSSStyleDeclaration) self.assertEqual(s.cssText, '') tests = [('content: "ä"', 'iso-8859-1'), ('content: "€"', 'utf-8')] for v, e in tests: s = css_parser.parseStyle(v.encode(e), encoding=e) self.assertEqual(s.cssText, v) self.assertRaises(UnicodeDecodeError, css_parser.parseStyle, 'content: "ä"'.encode('utf-8'), 'ascii')
def test_font_normalization(self): def font_dict(expected): ans = {k: DEFAULTS[k] for k in font_composition} if expected else {} ans.update(expected) return ans for raw, expected in iteritems({ 'some_font': { 'font-family': 'some_font' }, 'inherit': {k: 'inherit' for k in font_composition}, '1.2pt/1.4 A_Font': { 'font-family': 'A_Font', 'font-size': '1.2pt', 'line-height': '1.4' }, 'bad font': { 'font-family': '"bad font"' }, '10% serif': { 'font-family': 'serif', 'font-size': '10%' }, '12px "My Font", serif': { 'font-family': '"My Font", serif', 'font-size': '12px' }, 'normal 0.6em/135% arial,sans-serif': { 'font-family': 'arial, sans-serif', 'font-size': '0.6em', 'line-height': '135%', 'font-style': 'normal' }, 'bold italic large serif': { 'font-family': 'serif', 'font-weight': 'bold', 'font-style': 'italic', 'font-size': 'large' }, 'bold italic small-caps larger/normal serif': { 'font-family': 'serif', 'font-weight': 'bold', 'font-style': 'italic', 'font-size': 'larger', 'line-height': 'normal', 'font-variant': 'small-caps' }, '2em A B': { 'font-family': '"A B"', 'font-size': '2em' }, }): val = tuple(parseStyle('font: %s' % raw, validate=False))[0].propertyValue style = normalizers['font']('font', val) self.assertDictEqual(font_dict(expected), style, raw)
def test_border_normalization(self): def border_edge_dict(expected, edge='right'): ans = {'border-%s-%s' % (edge, x): DEFAULTS['border-%s-%s' % (edge, x)] for x in ('style', 'width', 'color')} for x, v in iteritems(expected): ans['border-%s-%s' % (edge, x)] = v return ans def border_dict(expected): ans = {} for edge in EDGES: ans.update(border_edge_dict(expected, edge)) return ans def border_val_dict(expected, val='color'): ans = {'border-%s-%s' % (edge, val): DEFAULTS['border-%s-%s' % (edge, val)] for edge in EDGES} for edge in EDGES: ans['border-%s-%s' % (edge, val)] = expected return ans for raw, expected in iteritems({ 'solid 1px red': {'color':'red', 'width':'1px', 'style':'solid'}, '1px': {'width': '1px'}, '#aaa': {'color': '#aaa'}, '2em groove': {'width':'2em', 'style':'groove'}, }): for edge in EDGES: br = 'border-%s' % edge val = tuple(parseStyle('%s: %s' % (br, raw), validate=False))[0].cssValue self.assertDictEqual(border_edge_dict(expected, edge), normalizers[br](br, val)) for raw, expected in iteritems({ 'solid 1px red': {'color':'red', 'width':'1px', 'style':'solid'}, '1px': {'width': '1px'}, '#aaa': {'color': '#aaa'}, 'thin groove': {'width':'thin', 'style':'groove'}, }): val = tuple(parseStyle('%s: %s' % ('border', raw), validate=False))[0].cssValue self.assertDictEqual(border_dict(expected), normalizers['border']('border', val)) for name, val in iteritems({ 'width': '10%', 'color': 'rgb(0, 1, 1)', 'style': 'double', }): cval = tuple(parseStyle('border-%s: %s' % (name, val), validate=False))[0].cssValue self.assertDictEqual(border_val_dict(val, name), normalizers['border-'+name]('border-'+name, cval))
def test_border_condensation(self): vals = 'red solid 5px' css = '; '.join('border-%s-%s: %s' % (edge, p, v) for edge in EDGES for p, v in zip(BORDER_PROPS, vals.split())) style = parseStyle(css) condense_rule(style) for e, p in product(EDGES, BORDER_PROPS): self.assertFalse(style.getProperty('border-%s-%s' % (e, p))) self.assertFalse(style.getProperty('border-%s' % e)) self.assertFalse(style.getProperty('border-%s' % p)) self.assertEqual(style.getProperty('border').value, vals) css = '; '.join('border-%s-%s: %s' % (edge, p, v) for edge in ('top',) for p, v in zip(BORDER_PROPS, vals.split())) style = parseStyle(css) condense_rule(style) self.assertEqual(css_text(style), 'border-top: %s' % vals) css += ';' + '; '.join('border-%s-%s: %s' % (edge, p, v) for edge in ('right', 'left', 'bottom') for p, v in zip(BORDER_PROPS, vals.replace('red', 'green').split())) style = parseStyle(css) condense_rule(style) self.assertEqual(len(style.getProperties()), 4) self.assertEqual(style.getProperty('border-top').value, vals) self.assertEqual(style.getProperty('border-left').value, vals.replace('red', 'green'))
def test_edge_normalization(self): def edge_dict(prefix, expected): return {'%s-%s' % (prefix, edge) : x for edge, x in zip(EDGES, expected)} for raw, expected in iteritems({ '2px': ('2px', '2px', '2px', '2px'), '1em 2em': ('1em', '2em', '1em', '2em'), '1em 2em 3em': ('1em', '2em', '3em', '2em'), '1 2 3 4': ('1', '2', '3', '4'), }): for prefix in ('margin', 'padding'): cval = tuple(parseStyle('%s: %s' % (prefix, raw), validate=False))[0].cssValue self.assertDictEqual(edge_dict(prefix, expected), normalizers[prefix](prefix, cval))
def test_parsevalidation(self): style = 'color: 1' t = 'a { %s }' % style css_parser.log.setLevel(logging.DEBUG) # sheet s = self._setHandler() css_parser.parseString(t) self.assertNotEqual(len(s.getvalue()), 0) s = self._setHandler() css_parser.parseString(t, validate=False) self.assertEqual(s.getvalue(), '') # style s = self._setHandler() css_parser.parseStyle(style) self.assertNotEqual(len(s.getvalue()), 0) s = self._setHandler() css_parser.parseStyle(style, validate=True) self.assertNotEqual(len(s.getvalue()), 0) s = self._setHandler() css_parser.parseStyle(style, validate=False) self.assertEqual(s.getvalue(), '')
def test_list_style_normalization(self): def ls_dict(expected): ans = {'list-style-%s' % x : DEFAULTS['list-style-%s' % x] for x in ('type', 'image', 'position')} for k, v in iteritems(expected): ans['list-style-%s' % k] = v return ans for raw, expected in iteritems({ 'url(http://www.example.com/images/list.png)': {'image': 'url(http://www.example.com/images/list.png)'}, 'inside square': {'position':'inside', 'type':'square'}, 'upper-roman url(img) outside': {'position':'outside', 'type':'upper-roman', 'image':'url(img)'}, }): cval = tuple(parseStyle('list-style: %s' % raw, validate=False))[0].cssValue self.assertDictEqual(ls_dict(expected), normalizers['list-style']('list-style', cval))
def test_children(self): "CSSStyleDeclaration.children()" style = '/*1*/color: red; color: green; @x;' types = [ css_parser.css.CSSComment, css_parser.css.Property, css_parser.css.Property, css_parser.css.CSSUnknownRule ] def t(s): for i, x in enumerate(s.children()): self.assertEqual(types[i], type(x)) self.assertEqual(x.parent, s) t(css_parser.parseStyle(style)) t(css_parser.parseString('a {' + style + '}').cssRules[0].style) t( css_parser.parseString('@media all {a {' + style + '}}').cssRules[0].cssRules[0].style) s = css_parser.parseStyle(style) s['x'] = '0' self.assertEqual(s, s.getProperty('x').parent) s.setProperty('y', '1') self.assertEqual(s, s.getProperty('y').parent)
def _apply_style_attr(self, url_replacer=None): attrib = self._element.attrib if 'style' not in attrib: return css = attrib['style'].split(';') css = filter(None, (x.strip() for x in css)) css = [y.strip() for y in css] css = [y for y in css if self.MS_PAT.match(y) is None] css = '; '.join(css) try: style = parseStyle(css, validate=False) except CSSSyntaxError: return if url_replacer is not None: replaceUrls(style, url_replacer, ignoreImportRules=True) self._style.update(self._stylizer.flatten_style(style))
def backgroundColor(self): ''' Return the background color by parsing both the background-color and background shortcut properties. Note that inheritance/default values are not used. None is returned if no background color is set. ''' def validate_color(col): return cssprofiles.validateWithProfile('color', col, profiles=[profiles.Profiles.CSS_LEVEL_2])[1] if self._bgcolor is None: col = None val = self._style.get('background-color', None) if val and validate_color(val): col = val else: val = self._style.get('background', None) if val is not None: try: style = parseStyle('background: '+val, validate=False) val = style.getProperty('background').cssValue try: val = list(val) except: # val is CSSPrimitiveValue val = [val] for c in val: c = c.cssText if isinstance(c, bytes): c = c.decode('utf-8', 'replace') if validate_color(c): col = c break except: pass if col is None: self._bgcolor = False else: self._bgcolor = col return self._bgcolor if self._bgcolor else None
def test_font_normalization(self): def font_dict(expected): ans = {k:DEFAULTS[k] for k in font_composition} if expected else {} ans.update(expected) return ans for raw, expected in iteritems({ 'some_font': {'font-family':'some_font'}, 'inherit':{k:'inherit' for k in font_composition}, '1.2pt/1.4 A_Font': {'font-family':'A_Font', 'font-size':'1.2pt', 'line-height':'1.4'}, 'bad font': {'font-family':'"bad font"'}, '10% serif': {'font-family':'serif', 'font-size':'10%'}, '12px "My Font", serif': {'font-family':'"My Font", serif', 'font-size': '12px'}, 'normal 0.6em/135% arial,sans-serif': {'font-family': 'arial, sans-serif', 'font-size': '0.6em', 'line-height':'135%', 'font-style':'normal'}, 'bold italic large serif': {'font-family':'serif', 'font-weight':'bold', 'font-style':'italic', 'font-size':'large'}, 'bold italic small-caps larger/normal serif': {'font-family':'serif', 'font-weight':'bold', 'font-style':'italic', 'font-size':'larger', 'line-height':'normal', 'font-variant':'small-caps'}, '2em A B': {'font-family': '"A B"', 'font-size': '2em'}, }): val = tuple(parseStyle('font: %s' % raw, validate=False))[0].cssValue style = normalizers['font']('font', val) self.assertDictEqual(font_dict(expected), style, raw)
def test_edge_condensation(self): for s, v in iteritems({ (1, 1, 3) : None, (1, 2, 3, 4) : '2pt 3pt 4pt 1pt', (1, 2, 3, 2) : '2pt 3pt 2pt 1pt', (1, 2, 1, 3) : '2pt 1pt 3pt', (1, 2, 1, 2) : '2pt 1pt', (1, 1, 1, 1) : '1pt', ('2%', '2%', '2%', '2%') : '2%', tuple('0 0 0 0'.split()) : '0', }): for prefix in ('margin', 'padding'): css = {'%s-%s' % (prefix, x) : str(y)+'pt' if isinstance(y, numbers.Number) else y for x, y in zip(('left', 'top', 'right', 'bottom'), s)} css = '; '.join(('%s:%s' % (k, v) for k, v in iteritems(css))) style = parseStyle(css) condense_rule(style) val = getattr(style.getProperty(prefix), 'value', None) self.assertEqual(v, val) if val is not None: for edge in EDGES: self.assertFalse(getattr(style.getProperty('%s-%s' % (prefix, edge)), 'value', None))
def test_remove_property_value(self): style = parseStyle( 'background-image: url(b.png); background: black url(a.png) fixed') for prop in style.getProperties(all=True): remove_property_value(prop, lambda val: 'png' in val.cssText) self.assertEqual('background: black fixed', style.cssText)
def test_validate(self): """CSSParser(validate)""" style = 'color: red' t = 'a { %s }' % style # helper s = css_parser.parseString(t) self.assertEqual(s.validating, True) s = css_parser.parseString(t, validate=False) self.assertEqual(s.validating, False) s = css_parser.parseString(t, validate=True) self.assertEqual(s.validating, True) d = css_parser.parseStyle(style) self.assertEqual(d.validating, True) d = css_parser.parseStyle(style, validate=True) self.assertEqual(d.validating, True) d = css_parser.parseStyle(style, validate=False) self.assertEqual(d.validating, False) # parser p = css_parser.CSSParser() s = p.parseString(t) self.assertEqual(s.validating, True) s = p.parseString(t, validate=False) self.assertEqual(s.validating, False) s = p.parseString(t, validate=True) self.assertEqual(s.validating, True) d = p.parseStyle(style) self.assertEqual(d.validating, True) p = css_parser.CSSParser(validate=True) s = p.parseString(t) self.assertEqual(s.validating, True) s = p.parseString(t, validate=False) self.assertEqual(s.validating, False) s = p.parseString(t, validate=True) self.assertEqual(s.validating, True) d = p.parseStyle(style) self.assertEqual(d.validating, True) p = css_parser.CSSParser(validate=False) s = p.parseString(t) self.assertEqual(s.validating, False) s = p.parseString(t, validate=False) self.assertEqual(s.validating, False) s = p.parseString(t, validate=True) self.assertEqual(s.validating, True) d = p.parseStyle(style) self.assertEqual(d.validating, False) # url p = css_parser.CSSParser(validate=False) p.setFetcher(self._make_fetcher('utf-8', t)) u = 'url' s = p.parseUrl(u) self.assertEqual(s.validating, False) s = p.parseUrl(u, validate=False) self.assertEqual(s.validating, False) s = p.parseUrl(u, validate=True) self.assertEqual(s.validating, True)
def test_remove_property_value(self): style = parseStyle('background-image: url(b.png); background: black url(a.png) fixed') for prop in style.getProperties(all=True): remove_property_value(prop, lambda val:'png' in val.cssText) self.assertEqual('background: black fixed', style.cssText)
def test_keys(self): "CSSStyleDeclaration.keys()" s = css_parser.parseStyle('x:1; x:2; y:1') self.assertEqual(['x', 'y'], s.keys()) self.assertEqual(s['x'], '2') self.assertEqual(s['y'], '1')
def test_refs(self): "CSSStyleRule references" s = css_parser.css.CSSStyleRule() sel, style = s.selectorList, s.style self.assertEqual(s, sel.parentRule) self.assertEqual(s, style.parentRule) s.cssText = 'a { x:1 }' self.assertNotEqual(sel, s.selectorList) self.assertEqual('a', s.selectorList.selectorText) self.assertNotEqual(style, s.style) self.assertEqual('1', s.style.getPropertyValue('x')) sel, style = s.selectorList, s.style invalids = ( '$b { x:2 }', # invalid selector 'c { $x3 }', # invalid style '/b { 2 }' # both invalid ) for invalid in invalids: try: s.cssText = invalid except xml.dom.DOMException as e: pass self.assertEqual(sel, s.selectorList) self.assertEqual('a', s.selectorList.selectorText) self.assertEqual(style, s.style) self.assertEqual('1', s.style.getPropertyValue('x')) # CHANGING s = css_parser.parseString('a {s1: 1}') r = s.cssRules[0] sel1 = r.selectorList st1 = r.style # selectorList r.selectorText = 'b' self.assertNotEqual(sel1, r.selectorList) self.assertEqual('b', r.selectorList.selectorText) self.assertEqual('b', r.selectorText) sel1b = r.selectorList sel1b.selectorText = 'c' self.assertEqual(sel1b, r.selectorList) self.assertEqual('c', r.selectorList.selectorText) self.assertEqual('c', r.selectorText) sel2 = css_parser.css.SelectorList('sel2') s.selectorList = sel2 self.assertEqual(sel2, s.selectorList) self.assertEqual('sel2', s.selectorList.selectorText) sel2.selectorText = 'sel2b' self.assertEqual('sel2b', sel2.selectorText) self.assertEqual('sel2b', s.selectorList.selectorText) s.selectorList.selectorText = 'sel2c' self.assertEqual('sel2c', sel2.selectorText) self.assertEqual('sel2c', s.selectorList.selectorText) # style r.style = 's1: 2' self.assertNotEqual(st1, r.style) self.assertEqual('s1: 2', r.style.cssText) st2 = css_parser.parseStyle('s2: 1') r.style = st2 self.assertEqual(st2, r.style) self.assertEqual('s2: 1', r.style.cssText) # cssText sl, st = r.selectorList, r.style # fails try: r.cssText = '$ {content: "new"}' except xml.dom.SyntaxErr as e: pass self.assertEqual(sl, r.selectorList) self.assertEqual(st, r.style) r.cssText = 'a {content: "new"}' self.assertNotEqual(sl, r.selectorList) self.assertNotEqual(st, r.style)