Coverage for gws-app/gws/lib/style/parser.py: 52%
142 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
1"""Style parser."""
3import re
5import gws
6from . import icon
9class Options(gws.Data):
10 """Options about an icon object"""
11 trusted: bool
12 """Indicates whether icon is created by us or someone else."""
13 strict: bool
14 """Indicates whether Exceptions should be raised."""
15 imageDirs: list[str]
16 """Paths to directories in which parsing is allowed"""
19def parse_dict(d: dict, opts: Options) -> dict:
20 """Adds a dictionary describing style features to a default dictionary.
22 Args:
23 d: A dictionary with new features.
24 opts: Dictionary options.
26 Returns:
27 New dictionary of features.
29 Raises:
30 ``Exception``: If an invalid css property or value is used.
32 """
33 res = dict(_DEFAULTS)
35 for key, val in d.items():
36 if val is None:
37 continue
38 k = key.replace('-', '_')
39 if k.startswith('__'):
40 k = k[2:]
42 fn = gws.u.get(_ParseFunctions, k)
44 if not fn:
45 err = f'style: invalid css property {key!r}'
46 if opts.strict:
47 raise gws.Error(err)
48 else:
49 gws.log.error(err)
50 continue
52 try:
53 v = fn(val, opts)
54 if v is not None:
55 res[k] = v
56 except Exception as exc:
57 err = f'style: invalid css value for {key!r}: {val!r}'
58 if opts.strict:
59 raise gws.Error(err) from exc
60 else:
61 gws.log.error(err)
63 return res
66# @TODO use a real CSS parser
68def parse_text(text: str, opts: Options) -> dict:
69 """Parses a text of features to a dict containing the new features.
71 Args:
72 text: Options String formatted like ``'a:b;c:d;...'``
73 opts: Text options.
75 Returns:
76 New dictionary of features.
78 """
79 d = {}
80 for r in text.split(';'):
81 r = r.strip()
82 if not r:
83 continue
84 a, _, b = r.partition(':')
85 d[a.strip()] = b.strip()
86 return parse_dict(d, opts)
89##
92_DEFAULTS: dict = dict(
93 fill=None,
95 stroke=None,
96 stroke_dasharray=[],
97 stroke_dashoffset=0,
98 stroke_linecap='butt',
99 stroke_linejoin='miter',
100 stroke_miterlimit=0,
101 stroke_width=0,
103 marker_size=0,
104 marker_stroke_dasharray=[],
105 marker_stroke_dashoffset=0,
106 marker_stroke_linecap='butt',
107 marker_stroke_linejoin='miter',
108 marker_stroke_miterlimit=0,
109 marker_stroke_width=0,
111 with_geometry='all',
112 with_label='all',
114 label_align='center',
115 label_font_family='sans-serif',
116 label_font_size=12,
117 label_font_style='normal',
118 label_font_weight='normal',
119 label_line_height=1,
120 label_max_scale=1000000000,
121 label_min_scale=0,
122 label_offset_x=None,
123 label_offset_y=None,
124 label_placement='middle',
125 label_stroke_dasharray=[],
126 label_stroke_dashoffset=0,
127 label_stroke_linecap='butt',
128 label_stroke_linejoin='miter',
129 label_stroke_miterlimit=0,
130 label_stroke_width=0,
132 point_size=10,
133 icon=None,
134 parsed_icon=None,
136 offset_x=0,
137 offset_y=0,
138)
140_ENUMS = dict(
141 stroke_linecap=['butt', 'round', 'square'],
142 stroke_linejoin=['bevel', 'round', 'miter'],
143 marker=['circle', 'square', 'arrow', 'cross'],
144 with_geometry=['all', 'none'],
145 with_label=['all', 'none'],
146 label_align=['left', 'right', 'center'],
147 label_font_style=['normal', 'italic'],
148 label_font_weight=['normal', 'bold'],
149 label_padding=[int],
150 label_placement=['start', 'end', 'middle'],
151 label_stroke_dasharray=[int],
152)
154_COLOR_PATTERNS = (
155 r'^#[0-9a-fA-F]{3}$',
156 r'^#[0-9a-fA-F]{6}$',
157 r'^#[0-9a-fA-F]{8}$',
158 r'^rgb\(\d{1,3},\d{1,3},\d{1,3}\)$',
159 r'^rgba\(\d{1,3},\d{1,3},\d{1,3},\d?(\.\d{1,3})?\)$',
160 r'^[a-z]{3,50}$',
161)
164##
166def _parse_color(val, opts):
167 val = re.sub(r'\s+', '', str(val))
168 if any(re.match(p, val) for p in _COLOR_PATTERNS):
169 return val
170 raise ValueError
173def _parse_intlist(val, opts):
174 return [int(x) for x in _make_list(val)]
177def _parse_icon(val, opts):
178 return icon.parse(val, opts)
181def _parse_unitint(val, opts):
182 return _unitint(val)
185def _parse_unitintquad(val, opts):
186 val = [_unitint(x) for x in _make_list(val)]
187 if any(x is None for x in val):
188 return None
189 if len(val) == 4:
190 return val
191 if len(val) == 2:
192 return [val[0], val[1], val[0], val[1]]
193 if len(val) == 1:
194 return [val[0], val[0], val[0], val[0]]
195 raise ValueError
198def _parse_enum_fn(cls):
199 def _check(val, opts):
200 vals = _ENUMS.get(cls)
201 if vals and val in vals:
202 return val
204 return _check
207def _parse_int(val, opts):
208 return int(val)
211def _parse_str(val, opts):
212 val = str(val).strip()
213 return val or None
216##
218def _unitint(val):
219 if isinstance(val, int):
220 return val
222 if val.isdigit():
223 # unitless = assume pixels
224 return int(val)
226 m = re.match(r'^(-?\d+)([a-z]*)', str(val))
227 if not m:
228 return
230 # @TODO support other units (need to pass dpi here)
231 if m.group(2) != 'px':
232 return
234 return int(m.group(1))
237def _make_list(val):
238 if isinstance(val, (list, tuple)):
239 return val
240 return re.split(r'[,\s]+', str(val))
243##
246class _ParseFunctions:
247 fill = _parse_color
249 stroke = _parse_color
250 stroke_dasharray = _parse_intlist
251 stroke_dashoffset = _parse_unitint
252 stroke_linecap = _parse_enum_fn('stroke_linecap')
253 stroke_linejoin = _parse_enum_fn('stroke_linejoin')
254 stroke_miterlimit = _parse_unitint
255 stroke_width = _parse_unitint
257 marker = _parse_enum_fn('marker')
258 marker_type = _parse_enum_fn('marker')
259 marker_fill = _parse_color
260 marker_size = _parse_unitint
261 marker_stroke = _parse_color
262 marker_stroke_dasharray = _parse_intlist
263 marker_stroke_dashoffset = _parse_unitint
264 marker_stroke_linecap = _parse_enum_fn('stroke_linecap')
265 marker_stroke_linejoin = _parse_enum_fn('stroke_linejoin')
266 marker_stroke_miterlimit = _parse_unitint
267 marker_stroke_width = _parse_unitint
269 with_geometry = _parse_enum_fn('with_geometry')
270 with_label = _parse_enum_fn('with_label')
272 label_align = _parse_enum_fn('label_align')
273 label_background = _parse_color
274 label_fill = _parse_color
275 label_font_family = _parse_str
276 label_font_size = _parse_unitint
277 label_font_style = _parse_enum_fn('label_font_style')
278 label_font_weight = _parse_enum_fn('label_font_weight')
279 label_line_height = _parse_int
280 label_max_scale = _parse_int
281 label_min_scale = _parse_int
282 label_offset_x = _parse_unitint
283 label_offset_y = _parse_unitint
284 label_padding = _parse_unitintquad
285 label_placement = _parse_enum_fn('label_placement')
286 label_stroke = _parse_color
287 label_stroke_dasharray = _parse_intlist
288 label_stroke_dashoffset = _parse_unitint
289 label_stroke_linecap = _parse_enum_fn('stroke_linecap')
290 label_stroke_linejoin = _parse_enum_fn('stroke_linejoin')
291 label_stroke_miterlimit = _parse_unitint
292 label_stroke_width = _parse_unitint
294 point_size = _parse_unitint
295 icon = _parse_icon
297 offset_x = _parse_unitint
298 offset_y = _parse_unitint