1import re
2
3
4def serialize(nodes):
5 """Serialize nodes to CSS syntax.
6
7 This should be used for :term:`component values`
8 instead of just :meth:`tinycss2.ast.Node.serialize` on each node
9 as it takes care of corner cases such as ``;`` between declarations,
10 and consecutive identifiers
11 that would otherwise parse back as the same token.
12
13 :type nodes: :term:`iterable`
14 :param nodes: An iterable of :class:`tinycss2.ast.Node` objects.
15 :returns: A :obj:`string <str>` representing the nodes.
16
17 """
18 chunks = []
19 _serialize_to(nodes, chunks.append)
20 return ''.join(chunks)
21
22
23def serialize_identifier(value):
24 """Serialize any string as a CSS identifier
25
26 :type value: :obj:`str`
27 :param value: A string representing a CSS value.
28 :returns:
29 A :obj:`string <str>` that would parse as an
30 :class:`tinycss2.ast.IdentToken` whose
31 :attr:`tinycss2.ast.IdentToken.value` attribute equals the passed
32 ``value`` argument.
33
34 """
35 if value == '-':
36 return r'\-'
37
38 if value[:2] == '--':
39 return '--' + serialize_name(value[2:])
40
41 if value[0] == '-':
42 result = '-'
43 value = value[1:]
44 else:
45 result = ''
46 c = value[0]
47 result += (
48 c if c in ('abcdefghijklmnopqrstuvwxyz_'
49 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') or ord(c) > 0x7F else
50 r'\A ' if c == '\n' else
51 r'\D ' if c == '\r' else
52 r'\C ' if c == '\f' else
53 '\\%X ' % ord(c) if c in '0123456789' else
54 '\\' + c
55 )
56 result += serialize_name(value[1:])
57 return result
58
59
60def serialize_name(value):
61 return ''.join(
62 c if c in ('abcdefghijklmnopqrstuvwxyz-_0123456789'
63 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') or ord(c) > 0x7F else
64 r'\A ' if c == '\n' else
65 r'\D ' if c == '\r' else
66 r'\C ' if c == '\f' else
67 '\\' + c
68 for c in value
69 )
70
71
72_replacement_string_value = {
73 '"': r'\"',
74 '\\': r'\\',
75 '\n': r'\A ',
76 '\r': r'\D ',
77 '\f': r'\C ',
78}
79_re_string_value = ''.join(re.escape(char) for char in _replacement_string_value)
80_re_string_value = re.compile(f'[{_re_string_value}]', re.MULTILINE)
81def _serialize_string_value_match(match):
82 return _replacement_string_value[match.group(0)]
83def serialize_string_value(value):
84 return _re_string_value.sub(_serialize_string_value_match, value)
85
86
87def serialize_url(value):
88 return ''.join(
89 r"\'" if c == "'" else
90 r'\"' if c == '"' else
91 r'\\' if c == '\\' else
92 r'\ ' if c == ' ' else
93 r'\9 ' if c == '\t' else
94 r'\A ' if c == '\n' else
95 r'\D ' if c == '\r' else
96 r'\C ' if c == '\f' else
97 r'\(' if c == '(' else
98 r'\)' if c == ')' else
99 c
100 for c in value
101 )
102
103
104# https://drafts.csswg.org/css-syntax/#serialization-tables
105def _serialize_to(nodes, write):
106 """Serialize an iterable of nodes to CSS syntax.
107
108 White chunks as a string by calling the provided :obj:`write` callback.
109
110 """
111 bad_pairs = BAD_PAIRS
112 previous_type = None
113 for node in nodes:
114 serialization_type = (node.type if node.type != 'literal'
115 else node.value)
116 if (previous_type, serialization_type) in bad_pairs:
117 write('/**/')
118 elif previous_type == '\\' and not (
119 serialization_type == 'whitespace' and
120 node.value.startswith('\n')):
121 write('\n')
122 node._serialize_to(write)
123 if serialization_type == 'declaration':
124 write(';')
125 previous_type = serialization_type
126
127
128BAD_PAIRS = set(
129 [(a, b)
130 for a in ('ident', 'at-keyword', 'hash', 'dimension', '#', '-', 'number')
131 for b in ('ident', 'function', 'url', 'number', 'percentage',
132 'dimension', 'unicode-range')] +
133 [(a, b)
134 for a in ('ident', 'at-keyword', 'hash', 'dimension')
135 for b in ('-', '-->')] +
136 [(a, b)
137 for a in ('#', '-', 'number', '@')
138 for b in ('ident', 'function', 'url')] +
139 [(a, b)
140 for a in ('unicode-range', '.', '+')
141 for b in ('number', 'percentage', 'dimension')] +
142 [('@', b) for b in ('ident', 'function', 'url', 'unicode-range', '-')] +
143 [('unicode-range', b) for b in ('ident', 'function', '?')] +
144 [(a, '=') for a in '$*^~|'] +
145 [('ident', '() block'), ('|', '|'), ('/', '*')]
146)