1# -*- coding: utf-8 -*-
2# cython: language_level=3, always_allow_keywords=True
3
4## Copyright 1999-2018 by LivingLogic AG, Bayreuth/Germany
5## Copyright 1999-2018 by Walter Dörwald
6##
7## All Rights Reserved
8##
9## See ll/xist/__init__.py for the license
10
11
12"""
13This module contains functions related to the handling of CSS.
14"""
15
16import os, contextlib, operator
17
18try:
19	import cssutils
20	from cssutils import css, stylesheets, codec
21except ImportError:
22	cssutils = None
23else:
24	import logging
25	cssutils.log.setLevel(logging.FATAL)
26
27from ll import misc, url
28from ll.xist import xsc, xfind
29from ll.xist.ns import html
30
31
32__docformat__ = "reStructuredText"
33
34
35def _isstyle(path):
36	if path:
37		node = path[-1]
38		return isinstance(node, (html.style, html.link)) and str(node.attrs.type) == "text/css"
39	return False
40
41
42def replaceurls(stylesheet, replacer):
43	"""
44	Replace all URLs appearing in the :class:`CSSStyleSheet` :obj:`stylesheet`.
45	For each URL the function :obj:`replacer` will be called and the URL will
46	be replaced with the result.
47	"""
48	def newreplacer(u):
49		return str(replacer(url.URL(u)))
50	cssutils.replaceUrls(stylesheet, newreplacer)
51
52
53def geturls(stylesheet):
54	"""
55	Return a list of all URLs appearing in the :class:`CSSStyleSheet`
56	:obj:`stylesheet`.
57	"""
58	return [url.URL(u) for u in cssutils.getUrls(stylesheet)] # This requires cssutils 0.9.5b1
59
60
61def _getmedia(stylesheet):
62	while stylesheet is not None:
63		if stylesheet.media is not None:
64			return {mq.value.mediaType for mq in stylesheet.media}
65		stylesheet = stylesheet.parentStyleSheet
66	return None
67
68
69def _doimport(wantmedia, parentsheet, base):
70	def prependbase(u):
71		if base is not None:
72			u = base/u
73		return u
74
75	havemedia = _getmedia(parentsheet)
76	if wantmedia is None or not havemedia or wantmedia in havemedia:
77		replaceurls(parentsheet, prependbase)
78		for rule in parentsheet.cssRules:
79			if rule.type == css.CSSRule.IMPORT_RULE:
80				href = url.URL(rule.href)
81				if base is not None:
82					href = base/href
83				havemedia = rule.media
84				with contextlib.closing(href.open("rb")) as r:
85					href = r.finalurl()
86					text = r.read()
87				sheet = css.CSSStyleSheet(href=str(href), media=havemedia, parentStyleSheet=parentsheet)
88				sheet.cssText = text
89				yield from _doimport(wantmedia, sheet, href)
90			elif rule.type == css.CSSRule.MEDIA_RULE:
91				if wantmedia in (mq.value.mediaType for mq in rule.media):
92					yield from rule.cssRules
93			elif rule.type == css.CSSRule.STYLE_RULE:
94				yield rule
95
96
97def iterrules(node, base=None, media=None, title=None):
98	"""
99	Return an iterator for all CSS rules defined in the HTML tree :obj:`node`.
100	This will parse the CSS defined in any :class:`html.style` or
101	:class:`html.link` element (and recursively in those stylesheets imported
102	via the ``@import`` rule). The rules will be returned as
103	:class:`CSSStyleRule` objects from the :mod:`cssutils` package (so this
104	requires :mod:`cssutils`).
105
106	The :obj:`base` argument will be used as the base URL for parsing the
107	stylesheet references in the tree (so :const:`None` means the URLs will be
108	used exactly as they appear in the tree). All URLs in the style properties
109	will be resolved.
110
111	If :obj:`media` is given, only rules that apply to this media type will
112	be produced.
113
114	:obj:`title` can be used to specify which stylesheet group should be used.
115	If :obj:`title` is :const:`None` only the persistent and preferred
116	stylesheets will be used. If :obj:`title` is a string only the persistent
117	stylesheets and alternate stylesheets with that style name will be used.
118
119	For a description of "persistent", "preferred" and "alternate" stylesheets
120	see <http://www.w3.org/TR/2002/WD-xhtml2-20020805/mod-styleSheet.html#sec_20.1.2.>
121	"""
122	if base is not None:
123		base = url.URL(base)
124
125	def matchlink(node):
126		if node.attrs.media.hasmedia(media):
127			if title is None:
128				if "title" not in node.attrs and "alternate" not in str(node.attrs.rel).split():
129					return True
130			elif not node.attrs.title.isfancy() and str(node.attrs.title) == title and "alternate" in str(node.attrs.rel).split():
131				return True
132		return False
133
134	def matchstyle(node):
135		if node.attrs.media.hasmedia(media):
136			if title is None:
137				if "title" not in node.attrs:
138					return True
139			elif str(node.attrs.title) == title:
140				return True
141		return False
142
143	def doiter(node):
144		for cssnode in node.walknodes(_isstyle):
145			if isinstance(cssnode, html.style):
146				href = str(base) if base is not None else None
147				if matchstyle(cssnode):
148					stylesheet = cssutils.parseString(str(cssnode.content), href=href, media=str(cssnode.attrs.media))
149					yield from _doimport(media, stylesheet, base)
150			else: # link
151				if "href" in cssnode.attrs:
152					href = cssnode.attrs.href.asURL()
153					if base is not None:
154						href = base/href
155					if matchlink(cssnode):
156						stylesheet = cssutils.parseUrl(str(href), media=str(cssnode.attrs.media))
157						yield from _doimport(media, stylesheet, href)
158	return misc.Iterator(doiter(node))
159
160
161def applystylesheets(node, base=None, media=None, title=None):
162	"""
163	:func:`applystylesheets` modifies the XIST tree :obj:`node` by removing all
164	CSS (from :class:`html.link` and :class:`html.style` elements and their
165	``@import``\ed stylesheets) and putting the resulting style properties into
166	the ``style`` attribute of every affected element instead.
167
168	For the meaning of :obj:`base`, :obj:`media` and :obj:`title` see
169	:func:`iterrules`.
170	"""
171
172	def iterstyles(node, rules):
173		yield from rules
174		# According to CSS 2.1 (http://www.w3.org/TR/CSS21/cascade.html#specificity)
175		# style attributes have the highest weight, so we yield it last
176		# (CSS 3 uses the same weight)
177		if "style" in node.attrs:
178			style = node.attrs.style
179			if not style.isfancy():
180				yield (
181					(1, 0, 0, 0),
182					xfind.IsSelector(node),
183					cssutils.parseStyle(str(style)) # parse the style out of the style attribute
184				)
185
186	rules = []
187	for rule in iterrules(node, base=base, media=media, title=title):
188		for sel in rule.selectorList:
189			rules.append((sel.specificity, selector(sel), rule.style))
190	rules.sort(key=operator.itemgetter(0))
191	count = 0
192	for cursor in node.walk(xsc.Element):
193		del cursor.node[_isstyle] # drop style sheet nodes
194		if cursor.node.Attrs.isdeclared("style"):
195			styles = {}
196			for (spec, sel, style) in iterstyles(cursor.node, rules):
197				if cursor.path in sel:
198					for prop in style:
199						# Properties from later rules overwrite those from earlier ones
200						# We're storing the count so that sorting keeps the order
201						styles[prop.name] = (count, prop.cssText)
202						count += 1
203			style = " ".join(f"{value};" for (count, value) in sorted(styles.values()))
204			if style:
205				cursor.node.attrs.style = style
206
207
208###
209### Selector helper functions
210###
211
212def _is_nth_node(iterator, node, index):
213	# Return whether :obj:`node` is the :obj:`index`'th node in :obj:`iterator` (starting at 1)
214	# :obj:`index` is an int or int string or "even" or "odd"
215	if index == "even":
216		for (i, child) in enumerate(iterator):
217			if child is node:
218				return i % 2 == 1
219		return False
220	elif index == "odd":
221		for (i, child) in enumerate(iterator):
222			if child is node:
223				return i % 2 == 0
224		return False
225	else:
226		if not isinstance(index, int):
227			try:
228				index = int(index)
229			except ValueError:
230				raise ValueError(f"illegal argument {index!r}")
231			else:
232				if index < 1:
233					return False
234		try:
235			return iterator[index-1] is node
236		except IndexError:
237			return False
238
239
240def _is_nth_last_node(iterator, node, index):
241	# Return whether :obj:`node` is the :obj:`index`'th last node in :obj:`iterator`
242	# :obj:`index` is an int or int string or "even" or "odd"
243	if index == "even":
244		pos = None
245		for (i, child) in enumerate(iterator):
246			if child is node:
247				pos = i
248		return pos is None or (i-pos) % 2 == 1
249	elif index == "odd":
250		pos = None
251		for (i, child) in enumerate(iterator):
252			if child is node:
253				pos = i
254		return pos is None or (i-pos) % 2 == 0
255	else:
256		if not isinstance(index, int):
257			try:
258				index = int(index)
259			except ValueError:
260				raise ValueError(f"illegal argument {index!r}")
261			else:
262				if index < 1:
263					return False
264		try:
265			return iterator[-index] is node
266		except IndexError:
267			return False
268
269
270def _children_of_type(node, type):
271	for child in node:
272		if isinstance(child, xsc.Element) and child.xmlname == type:
273			yield child
274
275
276###
277### Selectors
278###
279
280class CSSWeightedSelector(xfind.Selector):
281	"""
282	Base class for all CSS pseudo-class selectors.
283	"""
284
285
286class CSSHasAttributeSelector(CSSWeightedSelector):
287	"""
288	A :class:`CSSHasAttributeSelector` selector selects all element nodes
289	that have an attribute with the specified XML name.
290	"""
291	def __init__(self, attributename):
292		self.attributename = attributename
293
294	def __contains__(self, path):
295		if path:
296			node = path[-1]
297			if isinstance(node, xsc.Element):
298				return node.attrs.has(self.attributename)
299		return False
300
301	def __str__(self):
302		return f"{self.__class___.__qualname__}({self.attributename!r})"
303
304
305class CSSAttributeListSelector(CSSWeightedSelector):
306	"""
307	A :class:`CSSAttributeListSelector` selector selects all element nodes
308	where an attribute with the specified XML name has the specified word
309	among the white space-separated list of words in the attribute value.
310	"""
311	def __init__(self, attributename, attributevalue):
312		self.attributename = attributename
313		self.attributevalue = attributevalue
314
315	def __contains__(self, path):
316		if path:
317			node = path[-1]
318			if isinstance(node, xsc.Element):
319				attr = node.attrs.get(self.attributename)
320				return self.attributevalue in str(attr).split()
321		return False
322
323	def __str__(self):
324		return f"{self.__class__.__qualname__}({self.attributename!r}, {self.attributevalue!r})"
325
326
327class CSSAttributeLangSelector(CSSWeightedSelector):
328	"""
329	A :class:`CSSAttributeLangSelector` selector selects all element nodes
330	where an attribute with the specified XML name either is exactly the
331	specified value or starts with the specified value followed by ``"-"``.
332	"""
333	def __init__(self, attributename, attributevalue):
334		self.attributename = attributename
335		self.attributevalue = attributevalue
336
337	def __contains__(self, path):
338		if path:
339			node = path[-1]
340			if isinstance(node, xsc.Element):
341				attr = node.attrs.get(self.attributename)
342				parts = str(attr).split("-", 1)
343				if parts:
344					return parts[0] == self.attributevalue
345		return False
346
347	def __str__(self):
348		return f"{self.__class__.__qualname__}({self.attributename!r}, {self.attributevalue!r})"
349
350
351class CSSFirstChildSelector(CSSWeightedSelector):
352	"""
353	A :class:`CSSFirstChildSelector` selector selects all element nodes
354	that are the first child of its parent.
355	"""
356	def __contains__(self, path):
357		return len(path) >= 2 and _is_nth_node(path[-2][xsc.Element], path[-1], 1)
358
359	def __str__(self):
360		return "CSSFirstChildSelector()"
361
362
363class CSSLastChildSelector(CSSWeightedSelector):
364	"""
365	A :class:`CSSLastChildSelector` selector selects all element nodes
366	that are the last child of its parent.
367	"""
368	def __contains__(self, path):
369		return len(path) >= 2 and _is_nth_last_node(path[-2][xsc.Element], path[-1], 1)
370
371	def __str__(self):
372		return "CSSLastChildSelector()"
373
374
375class CSSFirstOfTypeSelector(CSSWeightedSelector):
376	"""
377	A :class:`CSSLastChildSelector` selector selects all element nodes
378	that are the first of its type among their siblings.
379	"""
380	def __contains__(self, path):
381		if len(path) >= 2:
382			node = path[-1]
383			return isinstance(node, xsc.Element) and _is_nth_node(misc.Iterator(_children_of_type(path[-2], node.xmlname)), node, 1)
384		return False
385
386	def __str__(self):
387		return "CSSFirstOfTypeSelector()"
388
389
390class CSSLastOfTypeSelector(CSSWeightedSelector):
391	"""
392	A :class:`CSSLastChildSelector` selector selects all element nodes
393	that are the last of its type among their siblings.
394	"""
395	def __contains__(self, path):
396		if len(path) >= 2:
397			node = path[-1]
398			return isinstance(node, xsc.Element) and _is_nth_last_node(misc.Iterator(_children_of_type(path[-2], node.xmlname)), node, 1)
399		return False
400
401	def __str__(self):
402		return "CSSLastOfTypeSelector()"
403
404
405class CSSOnlyChildSelector(CSSWeightedSelector):
406	"""
407	A :class:`CSSOnlyChildSelector` selector selects all element nodes that are
408	the only element among its siblings.
409	"""
410	def __contains__(self, path):
411		if len(path) >= 2:
412			node = path[-1]
413			if isinstance(node, xsc.Element):
414				for child in path[-2][xsc.Element]:
415					if child is not node:
416						return False
417				return True
418		return False
419
420	def __str__(self):
421		return "CSSOnlyChildSelector()"
422
423
424class CSSOnlyOfTypeSelector(CSSWeightedSelector):
425	"""
426	A :class:`CSSOnlyOfTypeSelector` selector selects all element nodes that are
427	the only element of its type among its siblings.
428	"""
429	def __contains__(self, path):
430		if len(path) >= 2:
431			node = path[-1]
432			if isinstance(node, xsc.Element):
433				for child in _children_of_type(path[-2], node.xmlname):
434					if child is not node:
435						return False
436				return True
437		return False
438
439	def __str__(self):
440		return "CSSOnlyOfTypeSelector()"
441
442
443class CSSEmptySelector(CSSWeightedSelector):
444	"""
445	A :class:`CSSEmptySelector` selector selects all element nodes that are
446	empty (i.e. they contain no elements or non-whitespace text).
447	"""
448	def __contains__(self, path):
449		if path:
450			node = path[-1]
451			if isinstance(node, xsc.Element):
452				for child in path[-1].content:
453					if isinstance(child, xsc.Element) or (isinstance(child, xsc.Text) and child):
454						return False
455				return True
456		return False
457
458	def __str__(self):
459		return "CSSEmptySelector()"
460
461
462class CSSRootSelector(CSSWeightedSelector):
463	"""
464	A :class:`CSSRootSelector` selector selects the root element.
465	"""
466	def __contains__(self, path):
467		return len(path) == 1 and isinstance(path[-1], xsc.Element)
468
469	def __str__(self):
470		return "CSSRootSelector()"
471
472
473class CSSLinkSelector(CSSWeightedSelector):
474	"""
475	A :class:`CSSLinkSelector` selector selects all HTML links.
476	"""
477	def __contains__(self, path):
478		if path:
479			node = path[-1]
480			return isinstance(node, xsc.Element) and node.xmlns=="http://www.w3.org/1999/xhtml" and node.xmlname=="a" and "href" in node.attrs
481		return False
482
483	def __str__(self):
484		return f"{self.__class__.__qualname__}()"
485
486
487class CSSInvalidPseudoSelector(CSSWeightedSelector):
488	def __contains__(self, path):
489		return False
490
491	def __str__(self):
492		return f"{self.__class__.__qualname__}()"
493
494
495class CSSHoverSelector(CSSInvalidPseudoSelector):
496	pass
497
498
499class CSSActiveSelector(CSSInvalidPseudoSelector):
500	pass
501
502
503class CSSVisitedSelector(CSSInvalidPseudoSelector):
504	pass
505
506
507class CSSFocusSelector(CSSInvalidPseudoSelector):
508	pass
509
510
511class CSSAfterSelector(CSSInvalidPseudoSelector):
512	pass
513
514
515class CSSBeforeSelector(CSSInvalidPseudoSelector):
516	pass
517
518
519class CSSFunctionSelector(CSSWeightedSelector):
520	"""
521	Base class of all CSS selectors that require an argument.
522	"""
523	def __init__(self, value=None):
524		self.value = value
525
526	def __str__(self):
527		return f"{self.__class__.__qualname__}({self.value!r})"
528
529
530class CSSNthChildSelector(CSSFunctionSelector):
531	"""
532	A :class:`CSSNthChildSelector` selector selects all element nodes that are
533	the n-th element among their siblings.
534	"""
535	def __contains__(self, path):
536		if len(path) >= 2:
537			node = path[-1]
538			if isinstance(node, xsc.Element):
539				return _is_nth_node(path[-2][xsc.Element], node, self.value)
540		return False
541
542
543class CSSNthLastChildSelector(CSSFunctionSelector):
544	"""
545	A :class:`CSSNthLastChildSelector` selector selects all element nodes that are
546	the n-th last element among their siblings.
547	"""
548	def __contains__(self, path):
549		if len(path) >= 2:
550			node = path[-1]
551			if isinstance(node, xsc.Element):
552				return _is_nth_last_node(path[-2][xsc.Element], node, self.value)
553		return False
554
555
556class CSSNthOfTypeSelector(CSSFunctionSelector):
557	"""
558	A :class:`CSSNthOfTypeSelector` selector selects all element nodes that are
559	the n-th of its type among their siblings.
560	"""
561	def __contains__(self, path):
562		if len(path) >= 2:
563			node = path[-1]
564			if isinstance(node, xsc.Element):
565				return _is_nth_node(misc.Iterator(_children_of_type(path[-2], node.xmlname)), node, self.value)
566		return False
567
568
569class CSSNthLastOfTypeSelector(CSSFunctionSelector):
570	"""
571	A :class:`CSSNthOfTypeSelector` selector selects all element nodes that are
572	the n-th last of its type among their siblings.
573	"""
574	def __contains__(self, path):
575		if len(path) >= 2:
576			node = path[-1]
577			if isinstance(node, xsc.Element):
578				return _is_nth_last_node(misc.Iterator(_children_of_type(path[-2], node.xmlname)), node, self.value)
579		return False
580
581
582class CSSTypeSelector(xfind.Selector):
583	def __init__(self, type=None, xmlns=None, *selectors):
584		self.type = type
585		self.xmlns = xsc.nsname(xmlns)
586		self.selectors = [] # id, class, attribute etc. selectors for this node
587
588	def __contains__(self, path):
589		if path:
590			node = path[-1]
591			if self.type is None or node.xmlname == self.type:
592				if self.xmlns is None or node.xmlns == self.xmlns:
593					for selector in self.selectors:
594						if path not in selector:
595							return False
596					return True
597		return False
598
599	def __str__(self):
600		v = [self.__class__.__name__, "("]
601		if self.type is not None or self.xmlns is not None or self.selectors:
602			v.append(repr(self.type))
603		if self.xmlns is not None or self.selectors:
604			v.append(", ")
605			v.append(repr(self.xmlns))
606		for selector in self.selectors:
607			v.append(", ")
608			v.append(str(selector))
609		v.append(")")
610		return "".join(v)
611
612
613class CSSAdjacentSiblingCombinator(xfind.BinaryCombinator):
614	"""
615	A :class:`CSSAdjacentSiblingCombinator` works similar to an
616	:class:`~ll.xist.xfind.AdjacentSiblingCombinator` except that only preceding
617	*elements* are considered.
618	"""
619
620	def __contains__(self, path):
621		if len(path) >= 2 and path in self.right:
622			# Find sibling
623			node = path[-1]
624			sibling = None
625			for child in path[-2][xsc.Element]:
626				if child is node:
627					break
628				sibling = child
629			if sibling is not None:
630				return path[:-1]+[sibling] in self.left
631		return False
632
633	def __str__(self):
634		return f"{self.__class__.__qualname__}({self.left}, {self.right})"
635
636
637class CSSGeneralSiblingCombinator(xfind.BinaryCombinator):
638	"""
639	A :class:`CSSGeneralSiblingCombinator` works similar to an
640	:class:`xfind.GeneralSiblingCombinator` except that only preceding *elements*
641	are considered.
642	"""
643
644	def __contains__(self, path):
645		if len(path) >= 2 and path in self.right:
646			node = path[-1]
647			for child in path[-2][xsc.Element]:
648				if child is node: # no previous element siblings
649					return False
650				if path[:-1]+[child] in self.left:
651					return True
652		return False
653
654	def __str__(self):
655		return f"{self.__class__.__qualname__}({self.left}, {self.right})"
656
657
658_pseudoname2class = {
659	"first-child": CSSFirstChildSelector,
660	"last-child": CSSLastChildSelector,
661	"first-of-type": CSSFirstOfTypeSelector,
662	"last-of-type": CSSLastOfTypeSelector,
663	"only-child": CSSOnlyChildSelector,
664	"only-of-type": CSSOnlyOfTypeSelector,
665	"empty": CSSEmptySelector,
666	"root": CSSRootSelector,
667	"hover": CSSHoverSelector,
668	"focus": CSSFocusSelector,
669	"link": CSSLinkSelector,
670	"visited": CSSVisitedSelector,
671	"active": CSSActiveSelector,
672	"after": CSSAfterSelector,
673	"before": CSSBeforeSelector,
674}
675
676_function2class = {
677	"nth-child": CSSNthChildSelector,
678	"nth-last-child": CSSNthLastChildSelector,
679	"nth-of-type": CSSNthOfTypeSelector,
680	"nth-last-of-type": CSSNthLastOfTypeSelector,
681}
682
683
684def selector(selectors, prefixes=None):
685	"""
686	Create a :class:`xfind.Selector` object that matches all nodes that match
687	the specified CSS selector expression. :obj:`selectors` can be a string or a
688	:class:`cssutils.css.selector.Selector` object. :obj:`prefixes`
689	may be a mapping mapping namespace prefixes to namespace names.
690	"""
691
692	if isinstance(selectors, str):
693		if prefixes is not None:
694			prefixes = dict((key, xsc.nsname(value)) for (key, value) in prefixes.items())
695			namespaces = "\n".join(f"@namespace {key if key is not None else ''} {value!r};" for (key, value) in prefixes.items())
696			selectors = f"{namespaces}\n{selectors}{{}}"
697		else:
698			selectors = f"{selectors}{{}}"
699		for rule in cssutils.CSSParser().parseString(selectors).cssRules:
700			if isinstance(rule, css.CSSStyleRule):
701				selectors = rule.selectorList.seq
702				break
703		else:
704			raise ValueError("can't happen")
705	elif isinstance(selectors, css.CSSStyleRule):
706		selectors = selectors.selectorList.seq
707	elif isinstance(selectors, css.Selector):
708		selectors = [selectors]
709	else:
710		raise TypeError(f"can't handle {type(selectors)!r}")
711	orcombinators = []
712	for selector in selectors:
713		rule = root = CSSTypeSelector()
714		attributename = None
715		attributevalue = None
716		combinator = None
717		for item in selector.seq:
718			t = item.type
719			v = item.value
720			if t == "type-selector":
721				rule.xmlns = v[0] if v[0] != -1 else None
722				rule.type = v[1]
723			if t == "universal":
724				rule.xmlns = v[0] if v[0] != -1 else None
725				rule.type = None
726			elif t == "id":
727				rule.selectors.append(xfind.hasid(v.lstrip("#")))
728			elif t == "class":
729				rule.selectors.append(xfind.hasclass(v.lstrip(".")))
730			elif t == "attribute-start":
731				combinator = None
732			elif t == "attribute-end":
733				if combinator is None:
734					rule.selectors.append(CSSHasAttributeSelector(attributename))
735				else:
736					rule.selectors.append(combinator(attributename, attributevalue))
737			elif t == "attribute-selector":
738				attributename = v
739			elif t == "equals":
740				combinator = xfind.attrhasvalue
741			elif t == "includes":
742				combinator = CSSAttributeListSelector
743			elif t == "dashmatch":
744				combinator = CSSAttributeLangSelector
745			elif t == "prefixmatch":
746				combinator = xfind.attrstartswith
747			elif t == "suffixmatch":
748				combinator = xfind.attrendswith
749			elif t == "substringmatch":
750				combinator = xfind.attrcontains
751			elif t == "pseudo-class":
752				if v.endswith("("):
753					try:
754						rule.selectors.append(_function2class[v.lstrip(":").rstrip("(")]())
755					except KeyError:
756						raise ValueError(f"unknown function {v}")
757					rule.function = v
758				else:
759					try:
760						rule.selectors.append(_pseudoname2class[v.lstrip(":")]())
761					except KeyError:
762						raise ValueError(f"unknown pseudo-class {v}")
763			elif t == "NUMBER":
764				# can only appear in a function => set the function value
765				rule.selectors[-1].value = v
766			elif t == "STRING":
767				# can only appear in a attribute selector => set the attribute value
768				attributevalue = v
769			elif t == "child":
770				rule = CSSTypeSelector()
771				root = xfind.ChildCombinator(root, rule)
772			elif t == "descendant":
773				rule = CSSTypeSelector()
774				root = xfind.DescendantCombinator(root, rule)
775			elif t == "adjacent-sibling":
776				rule = CSSTypeSelector()
777				root = CSSAdjacentSiblingCombinator(root, rule)
778			elif t == "following-sibling":
779				rule = CSSTypeSelector()
780				root = CSSGeneralSiblingCombinator(root, rule)
781		orcombinators.append(root)
782	return orcombinators[0] if len(orcombinators) == 1 else xfind.OrCombinator(*orcombinators)
783
784
785def parsestring(data, base=None, encoding=None):
786	"""
787	Parse the string :obj:`data` into a :mod:`cssutils` stylesheet. :obj:`base`
788	is the base URL for the parsing process, :obj:`encoding` can be used to force
789	the parser to use the specified encoding.
790	"""
791	if encoding is None:
792		encoding = "css"
793	if base is not None:
794		base = url.URL(base)
795		href = str(base)
796	else:
797		href = None
798	stylesheet = cssutils.parseString(data.decode(encoding), href=href)
799	if base is not None:
800		def prependbase(u):
801			return base/u
802		replaceurls(stylesheet, prependbase)
803	return stylesheet
804
805
806def parsestream(stream, base=None, encoding=None):
807	"""
808	Parse a :mod:`cssutils` stylesheet from the stream :obj:`stream`. :obj:`base`
809	is the base URL for the parsing process, :obj:`encoding` can be used to force
810	the parser to use the specified encoding.
811	"""
812	return parsestring(stream.read(), base=base, encoding=None)
813
814
815def parsefile(filename, base=None, encoding=None):
816	"""
817	Parse a :mod:`cssutils` stylesheet from the file named :obj:`filename`.
818	:obj:`base` is the base URL for the parsing process (defaulting to the
819	filename itself), :obj:`encoding` can be used to force the parser to use the
820	specified encoding.
821	"""
822	filename = os.path.expanduser(filename)
823	if base is None:
824		base = filename
825	with contextlib.closing(open(filename, "rb")) as stream:
826		return parsestream(stream, base=base, encoding=encoding)
827
828
829def parseurl(name, base=None, encoding=None, *args, **kwargs):
830	"""
831	Parse a :mod:`cssutils` stylesheet from the URL :obj:`name`. :obj:`base` is
832	the base URL for the parsing process (defaulting to the final URL of the
833	response, i.e. including redirects), :obj:`encoding` can be used to force
834	the parser to use the specified encoding. :obj:`arg` and :obj:`kwargs` are
835	passed on to :meth:`URL.openread`, so you can pass POST data and request
836	headers.
837	"""
838	with contextlib.closing(url.URL(name).openread(*args, **kwargs)) as stream:
839		if base is None:
840			base = stream.finalurl()
841		return parsestream(stream, base=base, encoding=encoding)
842
843
844def write(stylesheet, stream, base=None, encoding=None):
845	if base is not None:
846		def reltobase(u):
847			return u.relative(base)
848		replaceurls(stylesheet, reltobase)
849	if encoding is not None:
850		stylesheet.encoding = encoding
851	stream.write(stylesheet.cssText)
852