Skip to content

Commit 8f5c7a8

Browse files
authored
Merge pull request #75 from arthurdarcet/css-export
add a method on the Selector class, to export back the selector to css
2 parents 02ec77c + 8d0ff3e commit 8f5c7a8

File tree

4 files changed

+109
-7
lines changed

4 files changed

+109
-7
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ Simon Sapin
1010
Stefan Behnel
1111
Thomas Grainger
1212
Varialus
13+
Arthur Darcet

cssselect/parser.py

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ def __init__(self, tree, pseudo_element=None):
7676
#: +-------------------------+----------------+--------------------------------+
7777
#: | Invalid pseudo-class | ``li:marker`` | ``None`` |
7878
#: +-------------------------+----------------+--------------------------------+
79-
#: | Functinal | ``a::foo(2)`` | ``FunctionalPseudoElement(…)`` |
79+
#: | Functional | ``a::foo(2)`` | ``FunctionalPseudoElement(…)`` |
8080
#: +-------------------------+----------------+--------------------------------+
8181
#:
8282
#: .. _Lists3: http://www.w3.org/TR/2011/WD-css3-lists-20110524/#marker-pseudoelement
@@ -92,6 +92,20 @@ def __repr__(self):
9292
return '%s[%r%s]' % (
9393
self.__class__.__name__, self.parsed_tree, pseudo_element)
9494

95+
def canonical(self):
96+
"""Return a CSS representation for this selector (a string)
97+
"""
98+
if isinstance(self.pseudo_element, FunctionalPseudoElement):
99+
pseudo_element = '::%s' % self.pseudo_element.canonical()
100+
elif self.pseudo_element:
101+
pseudo_element = '::%s' % self.pseudo_element
102+
else:
103+
pseudo_element = ''
104+
res = '%s%s' % (self.parsed_tree.canonical(), pseudo_element)
105+
if len(res) > 1:
106+
res = res.lstrip('*')
107+
return res
108+
95109
def specificity(self):
96110
"""Return the specificity_ of this selector as a tuple of 3 integers.
97111
@@ -116,6 +130,9 @@ def __repr__(self):
116130
return '%s[%r.%s]' % (
117131
self.__class__.__name__, self.selector, self.class_name)
118132

133+
def canonical(self):
134+
return '%s.%s' % (self.selector.canonical(), self.class_name)
135+
119136
def specificity(self):
120137
a, b, c = self.selector.specificity()
121138
b += 1
@@ -151,6 +168,10 @@ def __repr__(self):
151168
def argument_types(self):
152169
return [token.type for token in self.arguments]
153170

171+
def canonical(self):
172+
args = ''.join(token.css() for token in self.arguments)
173+
return '%s(%s)' % (self.name, args)
174+
154175
def specificity(self):
155176
a, b, c = self.selector.specificity()
156177
b += 1
@@ -174,6 +195,10 @@ def __repr__(self):
174195
def argument_types(self):
175196
return [token.type for token in self.arguments]
176197

198+
def canonical(self):
199+
args = ''.join(token.css() for token in self.arguments)
200+
return '%s:%s(%s)' % (self.selector.canonical(), self.name, args)
201+
177202
def specificity(self):
178203
a, b, c = self.selector.specificity()
179204
b += 1
@@ -192,6 +217,9 @@ def __repr__(self):
192217
return '%s[%r:%s]' % (
193218
self.__class__.__name__, self.selector, self.ident)
194219

220+
def canonical(self):
221+
return '%s:%s' % (self.selector.canonical(), self.ident)
222+
195223
def specificity(self):
196224
a, b, c = self.selector.specificity()
197225
b += 1
@@ -210,6 +238,12 @@ def __repr__(self):
210238
return '%s[%r:not(%r)]' % (
211239
self.__class__.__name__, self.selector, self.subselector)
212240

241+
def canonical(self):
242+
subsel = self.subselector.canonical()
243+
if len(subsel) > 1:
244+
subsel = subsel.lstrip('*')
245+
return '%s:not(%s)' % (self.selector.canonical(), subsel)
246+
213247
def specificity(self):
214248
a1, b1, c1 = self.selector.specificity()
215249
a2, b2, c2 = self.subselector.specificity()
@@ -238,7 +272,20 @@ def __repr__(self):
238272
else:
239273
return '%s[%r[%s %s %r]]' % (
240274
self.__class__.__name__, self.selector, attrib,
241-
self.operator, self.value)
275+
self.operator, self.value.value)
276+
277+
def canonical(self):
278+
if self.namespace:
279+
attrib = '%s|%s' % (self.namespace, self.attrib)
280+
else:
281+
attrib = self.attrib
282+
283+
if self.operator == 'exists':
284+
op = attrib
285+
else:
286+
op = '%s%s%s' % (attrib, self.operator, self.value.css())
287+
288+
return '%s[%s]' % (self.selector.canonical(), op)
242289

243290
def specificity(self):
244291
a, b, c = self.selector.specificity()
@@ -258,10 +305,13 @@ def __init__(self, namespace=None, element=None):
258305
self.element = element
259306

260307
def __repr__(self):
308+
return '%s[%s]' % (self.__class__.__name__, self.canonical())
309+
310+
def canonical(self):
261311
element = self.element or '*'
262312
if self.namespace:
263313
element = '%s|%s' % (self.namespace, element)
264-
return '%s[%s]' % (self.__class__.__name__, element)
314+
return element
265315

266316
def specificity(self):
267317
if self.element:
@@ -282,6 +332,9 @@ def __repr__(self):
282332
return '%s[%r#%s]' % (
283333
self.__class__.__name__, self.selector, self.id)
284334

335+
def canonical(self):
336+
return '%s#%s' % (self.selector.canonical(), self.id)
337+
285338
def specificity(self):
286339
a, b, c = self.selector.specificity()
287340
a += 1
@@ -303,6 +356,13 @@ def __repr__(self):
303356
return '%s[%r %s %r]' % (
304357
self.__class__.__name__, self.selector, comb, self.subselector)
305358

359+
def canonical(self):
360+
subsel = self.subselector.canonical()
361+
if len(subsel) > 1:
362+
subsel = subsel.lstrip('*')
363+
return '%s %s %s' % (
364+
self.selector.canonical(), self.combinator, subsel)
365+
306366
def specificity(self):
307367
a1, b1, c1 = self.selector.specificity()
308368
a2, b2, c2 = self.subselector.specificity()
@@ -546,7 +606,7 @@ def parse_attrib(selector, stream):
546606
if next != ('DELIM', ']'):
547607
raise SelectorSyntaxError(
548608
"Expected ']', got %s" % (next,))
549-
return Attrib(selector, namespace, attrib, op, value.value)
609+
return Attrib(selector, namespace, attrib, op, value)
550610

551611

552612
def parse_series(tokens):
@@ -601,6 +661,12 @@ def is_delim(self, *values):
601661
type = property(operator.itemgetter(0))
602662
value = property(operator.itemgetter(1))
603663

664+
def css(self):
665+
if self.type == 'STRING':
666+
return repr(self.value)
667+
else:
668+
return self.value
669+
604670

605671
class EOFToken(Token):
606672
def __new__(cls, pos):

cssselect/xpath.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -308,10 +308,12 @@ def xpath_attrib(self, selector):
308308
attrib = '@' + name
309309
else:
310310
attrib = 'attribute::*[name() = %s]' % self.xpath_literal(name)
311-
if self.lower_case_attribute_values:
312-
value = selector.value.lower()
311+
if selector.value is None:
312+
value = None
313+
elif self.lower_case_attribute_values:
314+
value = selector.value.value.lower()
313315
else:
314-
value = selector.value
316+
value = selector.value.value
315317
return method(self.xpath(selector.selector), attrib, value)
316318

317319
def xpath_class(self, class_selector):

tests/test_cssselect.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,39 @@ def specificity(css):
274274
assert specificity('#lorem + foo#ipsum:first-child > bar:first-line'
275275
) == (2, 1, 3)
276276

277+
def test_css_export(self):
278+
def css2css(css, res=None):
279+
selectors = parse(css)
280+
assert len(selectors) == 1
281+
assert selectors[0].canonical() == (res or css)
282+
283+
css2css('*')
284+
css2css(' foo', 'foo')
285+
css2css('Foo', 'Foo')
286+
css2css(':empty ', ':empty')
287+
css2css(':before', '::before')
288+
css2css(':beFOre', '::before')
289+
css2css('*:before', '::before')
290+
css2css(':nth-child(2)')
291+
css2css('.bar')
292+
css2css('[baz]')
293+
css2css('[baz="4"]', "[baz='4']")
294+
css2css('[baz^="4"]', "[baz^='4']")
295+
css2css("[ns|attr='4']")
296+
css2css('#lipsum')
297+
css2css(':not(*)')
298+
css2css(':not(foo)')
299+
css2css(':not(*.foo)', ':not(.foo)')
300+
css2css(':not(*[foo])', ':not([foo])')
301+
css2css(':not(:empty)')
302+
css2css(':not(#foo)')
303+
css2css('foo:empty')
304+
css2css('foo::before')
305+
css2css('foo:empty::before')
306+
css2css('::name(arg + "val" - 3)', "::name(arg+'val'-3)")
307+
css2css('#lorem + foo#ipsum:first-child > bar::first-line')
308+
css2css('foo > *')
309+
277310
def test_parse_errors(self):
278311
def get_error(css):
279312
try:

0 commit comments

Comments
 (0)