├── .github
└── workflows
│ └── run_tests.yml
├── LICENSE
├── README.md
├── chope
├── __init__.py
├── css.py
├── element.py
├── functions
│ ├── __init__.py
│ ├── color.py
│ ├── function.py
│ ├── shape.py
│ └── transform.py
└── variable.py
├── pyproject.toml
└── tests
├── __init__.py
├── test_css.py
├── test_element.py
└── test_function.py
/.github/workflows/run_tests.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: CI
4 |
5 | # Controls when the workflow will run
6 | on:
7 | # Triggers the workflow on push or pull request events but only for the "main" branch
8 | push:
9 | branches: [ "main" ]
10 | pull_request:
11 | branches: [ "main" ]
12 |
13 | # Allows you to run this workflow manually from the Actions tab
14 | workflow_dispatch:
15 |
16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
17 | jobs:
18 | # This workflow contains a single job called "build"
19 | build:
20 | # The type of runner that the job will run on
21 | runs-on: ubuntu-latest
22 | strategy:
23 | matrix:
24 | python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
25 |
26 | # Steps represent a sequence of tasks that will be executed as part of the job
27 | steps:
28 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
29 | - uses: actions/checkout@v3
30 | - name: Set up Python ${{ matrix.python-version }}
31 | uses: actions/setup-python@v4
32 | with:
33 | python-version: ${{ matrix.python-version }}
34 | - name: Install dependencies
35 | run: |
36 | python -m pip install --upgrade pip
37 | pip install ruff pytest
38 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
39 | - name: Test with pytest
40 | run: |
41 | pytest tests/
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 hanstjua
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Chope
2 |
3 | CSS & HTML on Python Easily.
4 |
5 | 
6 | 
7 | 
8 | 
9 | 
10 |
11 | Chope is a library that aims to provide a HTML and CSS domain-specific language (DSL) for Python.
12 | It draws inspiration from Clojure's [Hiccup](https://github.com/weavejester/hiccup) and JavaScript's [Emotion](https://emotion.sh/docs/introduction).
13 |
14 | ## Table of Contents
15 | * [Install](#install)
16 | * [Syntax](#syntax)
17 | * [HTML](#html)
18 | * [Creating Custom Elements](#creating-custom-elements)
19 | * [CSS](#css)
20 | * [Units](#units)
21 | * [Render](#render)
22 | * [Building a Template](#building-a-template)
23 | * [Factory Function](#factory-function)
24 | * [Variable Object](#variable-object)
25 |
26 |
27 |
28 | ## Install
29 |
30 | Chope can be installed through pip.
31 |
32 | `pip install chope`
33 |
34 |
35 |
36 | ## Syntax
37 |
38 | Here is a basic example of Chope syntax:
39 |
40 | ```python
41 | from chope import *
42 | from chope.css import *
43 |
44 | page = html[
45 | head[
46 | style[
47 | Css[
48 | 'body': dict(
49 | background_color='linen',
50 | font_size=pt/12
51 | ),
52 | '.inner-div': dict(
53 | color='maroon',
54 | margin_left=px/40
55 | )
56 | ]
57 | ]
58 | ],
59 | body[
60 | h1['Title'],
61 | div(class_='outer-div')[
62 | div(class_='inner-div')[
63 | 'Some content.'
64 | ]
65 | ]
66 | ]
67 | ]
68 | ```
69 |
70 |
71 |
72 | ### HTML
73 |
74 | Declaring an element is as simple as this:
75 |
76 | ```python
77 | #
content
78 |
79 | div['content']
80 | ```
81 |
82 | Element attributes can be specified like so:
83 |
84 | ```python
85 | # This is key-value style.
86 |
87 | div(id='my-id', class_='my-class your-class')['This is key-value style.']
88 | ```
89 |
90 | Notice the `_` suffix in the `class_` attribute. This suffix is necessary for any attribute names that clashes with any Python keyword.
91 |
92 | You can also define `id` and `class` using CSS selector syntax:
93 |
94 | ```python
95 | # This is selector style.
96 |
97 | div('#my-id.my-class.your-class', title='Title')['This is selector style.']
98 | ```
99 |
100 | For attributes with names that cannot be declared using the *key-value* style, you can use the *tuple* style.
101 |
102 | ```python
103 | # This is tuple style.
104 |
105 | div('my:attr', 'x',
106 | 'ur-attr', 'y',
107 | 'their[attr]', 'z')[
108 | 'This is tuple style.'
109 | ]
110 | ```
111 |
112 | The different styles can be mixed as long as there is no duplicate attribute definition.
113 |
114 | ```python
115 | # acceptable mixed style
116 |
117 | div('#my-id.class1.class2',
118 | 'my:attr', 'x',
119 | 'ur-attr', 'y',
120 | 'their[attr]', 'z',
121 | title="Mix 'em up",
122 | subtitle="But don't get mixed up"
123 | )[
124 | 'This mixed style is OK.'
125 | ]
126 |
127 | # NOT acceptable mixed style
128 |
129 | div('#my-id.class1.class2',
130 | 'id', 'x', # conflicts with 'id' defined in selector style
131 | 'title', 'y',
132 | 'their[attr]', 'z',
133 | title="Mix 'em up", # conflicts with 'title' defined in tuple style
134 | subtitle="But don't get mixed up"
135 | )[
136 | 'This mixed style is NOT OK.'
137 | ]
138 | ```
139 |
140 | Iterables can be used to generate a sequence of elements in the body of an element.
141 |
142 | ```python
143 | #
144 |
145 | ul[
146 | [li[str(i)] for i in range(3)]
147 | ]
148 | ```
149 |
150 |
151 |
152 | #### Creating Custom Elements
153 |
154 | If you want to create a custom element with a custom tag, simply inherit from the `Element` class.
155 |
156 | ```python
157 | from chope import Element
158 |
159 |
160 | class custom(Element): ## class name will be used as tag name during rendering
161 | pass
162 |
163 |
164 | custom['some content.'] ## some content.
165 | ```
166 |
167 | Normally, you don't need to override any method of `Element`, but if you want to change how your element is rendered, you can override the `render()` method.
168 |
169 |
170 |
171 | ### CSS
172 |
173 | The CSS syntax in Chope is simply a mapping between CSS selector strings and declarations dictionaries.
174 |
175 | Here's how a simple CSS stylesheet looks like in Chope:
176 |
177 | ```python
178 | '''
179 | h1 {
180 | color: blue;
181 | }
182 |
183 | .my-class {
184 | background-color: linen;
185 | text-align: center;
186 | }
187 | '''
188 |
189 | Css[
190 | 'h1': dict(
191 | color='blue'
192 | ),
193 | '.my-class': dict(
194 | background_color='linen',
195 | text_align='center'
196 | )
197 | ]
198 |
199 | # OR
200 |
201 | Css[
202 | 'h1': {
203 | 'color': 'blue'
204 | },
205 | '.my-class': {
206 | 'background-color': 'linen',
207 | 'text-align': 'center'
208 | }
209 | ]
210 | ```
211 |
212 | When you do declarations using the `dict` constructor, any `_` will be converted to `-` automatically.
213 |
214 | If your attribute name actually contains an `_`, declare using dictionary literal instead.
215 |
216 |
217 |
218 | #### Units
219 |
220 | Declaring size properties is very simple:
221 |
222 | ```python
223 | '''
224 | .my-class {
225 | font-size: 14px;
226 | margin: 20%;
227 | }
228 | '''
229 |
230 | Css[
231 | '.my-class': dict(
232 | font_size=px/14,
233 | margin=percent/20
234 | )
235 | ]
236 | ```
237 |
238 | Chope supports standard HTML units. (e.g.`em`, `rem`, `pt`, etc.)
239 |
240 | To set properties with multiple values, simply pass an iterable or a string.
241 |
242 | ```python
243 | '''
244 | .my-class {
245 | padding: 58px 0 0;
246 | }
247 | '''
248 |
249 | Css[
250 | '.my-class': dict(
251 | padding=(px/58, 0, 0)
252 | )
253 | ]
254 |
255 | # OR
256 |
257 | Css[
258 | '.my-class': dict(
259 | padding='58px 0 0'
260 | )
261 | ]
262 | ```
263 |
264 |
265 |
266 | ## Render
267 |
268 | Once you are done defining your CSS and HTML, you can render them into string using the `render()` method.
269 |
270 | ```python
271 | >>> page = html[
272 | head[
273 | style[
274 | Css[
275 | '.item': dict(font_size=px/14)
276 | ]
277 | ]
278 | ],
279 | body[
280 | div('#my-item.item')['My content.']
281 | ]
282 | ]
283 | >>> print(page.render())
284 | '
285 |
286 |
291 |
292 |
293 |
294 | My content.
295 |
296 |
297 | '
298 | ```
299 |
300 | By default, `render()` will add indentations with 2 spaces. You can modify this using the `indent` keyword argument.
301 |
302 | ```python
303 | >>> print(page.render(indent=4))
304 | '
305 |
306 |
311 |
312 |
313 |
314 | My content.
315 |
316 |
317 | '
318 | >>> print(page.render(indent=0)) ## render flat string
319 | 'My content.
'
320 | ```
321 |
322 | CSS objects can also be rendered the same way.
323 |
324 | ```python
325 | >>> css = Css[
326 | 'h1': dict(font_size=px/14),
327 | '.my-class': dict(
328 | color='blue',
329 | padding=(0,0,px/20)
330 | )
331 | ]
332 | >>> print(css.render())
333 | 'h1 {
334 | font-size: 14px;
335 | }
336 |
337 | .my-class {
338 | color: blue;
339 | padding: 0 0 20px;
340 | }'
341 | ```
342 |
343 |
344 |
345 | ## Building a Template
346 |
347 | There are different ways you can construct a HTML template with `chope`, two of which are *Factory Function* and *Variable Object*.
348 |
349 |
350 |
351 | ### Factory Function
352 |
353 | Factory function is probably the simplest way to build reusable templates.
354 |
355 | ```python
356 | def my_list(title: str, items: List[str], ordered: bool = False) -> div:
357 | list_tag = ol if ordered else ul
358 | return div[
359 | f'{title}
',
360 | list_tag[
361 | [li[i] for i in items]
362 | ]
363 | ]
364 |
365 | def my_content(items: List[Component], attrs: dict) -> div:
366 | return div(**attrs)[
367 | items
368 | ]
369 |
370 | result = my_content(
371 | [
372 | my_list('Grocery', ['Soap', 'Shampoo', 'Carrots']),
373 | my_list(
374 | 'Egg Cooking',
375 | ['Crack egg.', 'Fry egg.', 'Eat egg.'],
376 | ordered=True
377 | )
378 | ],
379 | {
380 | 'id': 'my-content',
381 | 'class': 'list styled-list'
382 | }
383 | )
384 | ```
385 |
386 | Factory function is a simple, elegant solution to construct a group of small, independent reusable templates. However, when your templates group grows in size and complexity, the factory functions can get unwieldy, as we will see at the end of the next section.
387 |
388 |
389 |
390 | ### Variable Object
391 |
392 | Another way to build a HTML template is to use the *Variable Object*, `Var`.
393 |
394 |
395 | ```python
396 | from chope import *
397 | from chope.variable import Var
398 |
399 | # declaring element with Var content
400 | template = html[
401 | div[Var('my-content')]
402 | ]
403 |
404 | # setting value to Var object
405 |
406 | final_html = template.set_vars({'my-content': 'This is my content.'}) ## dict style
407 |
408 | ## OR
409 |
410 | final_html = template.set_vars(my_content='This is my content.') ## key-value style
411 |
412 | print(final_html.render(indent=0)) ## This is my content.
413 | ```
414 |
415 | You can combine both _dict_ and _key-value_ style when setting variable values, but note that **values defined using _kwargs_ take priority over those defined using _dict_**.
416 |
417 | A variable object can have a default value.
418 |
419 | ```python
420 | >>> print(div[Var('content', 'This is default content.')].render(indent=0))
421 | 'This is default content.
'
422 | ```
423 |
424 | A variable object's value can be set to an element.
425 |
426 | ```python
427 | >>> content = div[Var('inner')]
428 | >>> new_content = content.set_vars(inner=div['This is inner content.'])
429 | >>> print(new_content.render())
430 | '
431 |
432 | This is inner content.
433 |
434 |
'
435 | ```
436 |
437 | `Var` works in an element attribute as well.
438 |
439 | ```python
440 | >>> content = div(name=Var('name'))['My content.']
441 | >>> new_content = content.set_vars(name='my-content')
442 | >>> print(new_content.render())
443 | '
444 | My content.
445 |
'
446 | ```
447 |
448 | You can use `Var` in CSS too.
449 |
450 | ```python
451 | >>> css = Css[
452 | # CSS rule as a variable
453 | 'h1': dict(font_size=Var('h1.font-size')),
454 |
455 | # CSS declaration as a variable
456 | '.my-class': Var('my-class')
457 | ]
458 | >>> new_css = css.set_vars({'h1.font-size': px/1, 'my-class': {'color': 'blue'}})
459 | >>> print(new_css.render())
460 | 'h1 {
461 | font-size: 1px;
462 | }
463 |
464 | .my-class {
465 | color: blue;
466 | }'
467 | ```
468 |
469 | The `set` of all variable names in an element/CSS can be retrieved using the `get_vars()` method.
470 |
471 | ```python
472 | >>> template = html[
473 | style[
474 | Css[
475 | 'h1': dict(font_size=Var('css.h1.font-size'))
476 | ]
477 | ],
478 | div[
479 | Var('main-content'),
480 | div[
481 | Var('inner-content')
482 | ]
483 | ]
484 | ]
485 | >>> print(template.get_vars())
486 | {'main-content', 'inner-content', 'css.h1.font-size'}
487 | ```
488 |
489 | An advantage of using variable object is that it allows for easy deferment of variable value settings, which makes combining templates simple.
490 |
491 | ```python
492 | navs_template = ul('.nav')[
493 | Var('navs.items')
494 | ]
495 |
496 | pagination_template = nav[
497 | ul('.pagination')[
498 | li('.page-item', class_=Var(
499 | 'pagination.previous.disabled',
500 | 'disabled'
501 | ))['Previous'],
502 | Var('nav.pages'),
503 | li('.page-item', class_=Var(
504 | 'pagination.next.disabled',
505 | 'disabled'
506 | ))['Next']
507 | ]
508 | ]
509 |
510 | body_template = body[
511 | navs_template,
512 | div('.main-content')[Var('body.main-content')],
513 | pagination_template
514 | ]
515 | ```
516 |
517 | Compare that to the equivalent factory function implementation.
518 |
519 | ```python
520 | def navs_template(items: List[li]) -> ul:
521 | return ul('.nav')[
522 | items
523 | ]
524 |
525 | def pagination_template(
526 | pages: List[li],
527 | previous_disabled: bool = True,
528 | next_disabled: bool = True
529 | ) -> nav:
530 | return nav[
531 | ul('.pagination')[
532 | li(f'.page-item{" disabled" if previous_disabled else ""}')['Previous'],
533 | pages,
534 | li(f'.page-item{" disabled" if next_disabled else ""}')['Next']
535 | ]
536 | ]
537 |
538 | def body_template(
539 | navs_items: List[li],
540 | pagination_pages: List[li],
541 | body_main_content: Element,
542 | pagination_previous_disabled: bool = True,
543 | pagination_next_disabled: bool = True
544 | ) -> body:
545 | return body[
546 | navs_template(navs_items),
547 | body_main_content,
548 | pagination_template(
549 | pagination_pages,
550 | pagination_previous_disabled,
551 | pagination_next_disabled
552 | )
553 | ]
554 | ```
555 |
556 | As you may have observed, the number of parameters for upstream template's factory function can easily explode when you start combining more downstream templates.
557 |
--------------------------------------------------------------------------------
/chope/__init__.py:
--------------------------------------------------------------------------------
1 | from chope.element import Element
2 |
3 |
4 | class a(Element):
5 | pass
6 |
7 |
8 | class abbr(Element):
9 | pass
10 |
11 |
12 | class acronym(Element):
13 | pass
14 |
15 |
16 | class address(Element):
17 | pass
18 |
19 |
20 | class applet(Element):
21 | pass
22 |
23 |
24 | class area(Element):
25 | pass
26 |
27 |
28 | class article(Element):
29 | pass
30 |
31 |
32 | class aside(Element):
33 | pass
34 |
35 |
36 | class audio(Element):
37 | pass
38 |
39 |
40 | class b(Element):
41 | pass
42 |
43 |
44 | class base(Element):
45 | pass
46 |
47 |
48 | class basefont(Element):
49 | pass
50 |
51 |
52 | class bdi(Element):
53 | pass
54 |
55 |
56 | class bdo(Element):
57 | pass
58 |
59 |
60 | class big(Element):
61 | pass
62 |
63 |
64 | class blockquote(Element):
65 | pass
66 |
67 |
68 | class body(Element):
69 | pass
70 |
71 |
72 | class br(Element):
73 | pass
74 |
75 |
76 | class button(Element):
77 | pass
78 |
79 |
80 | class canvas(Element):
81 | pass
82 |
83 |
84 | class caption(Element):
85 | pass
86 |
87 |
88 | class center(Element):
89 | pass
90 |
91 |
92 | class cite(Element):
93 | pass
94 |
95 |
96 | class code(Element):
97 | pass
98 |
99 |
100 | class col(Element):
101 | pass
102 |
103 |
104 | class colgroup(Element):
105 | pass
106 |
107 |
108 | class data(Element):
109 | pass
110 |
111 |
112 | class datalist(Element):
113 | pass
114 |
115 |
116 | class dd(Element):
117 | pass
118 |
119 |
120 | class details(Element):
121 | pass
122 |
123 |
124 | class dfn(Element):
125 | pass
126 |
127 |
128 | class dialog(Element):
129 | pass
130 |
131 |
132 | class dir(Element):
133 | pass
134 |
135 |
136 | class div(Element):
137 | pass
138 |
139 |
140 | class dl(Element):
141 | pass
142 |
143 |
144 | class dt(Element):
145 | pass
146 |
147 |
148 | class em(Element):
149 | pass
150 |
151 |
152 | class embed(Element):
153 | pass
154 |
155 |
156 | class fieldset(Element):
157 | pass
158 |
159 |
160 | class figcaption(Element):
161 | pass
162 |
163 |
164 | class figure(Element):
165 | pass
166 |
167 |
168 | class font(Element):
169 | pass
170 |
171 |
172 | class footer(Element):
173 | pass
174 |
175 |
176 | class form(Element):
177 | pass
178 |
179 |
180 | class frame(Element):
181 | pass
182 |
183 |
184 | class frameset(Element):
185 | pass
186 |
187 |
188 | class h1(Element):
189 | pass
190 |
191 |
192 | class h2(Element):
193 | pass
194 |
195 |
196 | class h3(Element):
197 | pass
198 |
199 |
200 | class h4(Element):
201 | pass
202 |
203 |
204 | class h5(Element):
205 | pass
206 |
207 |
208 | class h6(Element):
209 | pass
210 |
211 |
212 | class head(Element):
213 | pass
214 |
215 |
216 | class header(Element):
217 | pass
218 |
219 |
220 | class hr(Element):
221 | pass
222 |
223 |
224 | class html(Element):
225 | pass
226 |
227 |
228 | class i(Element):
229 | pass
230 |
231 |
232 | class iframe(Element):
233 | pass
234 |
235 |
236 | class img(Element):
237 | pass
238 |
239 |
240 | class input(Element):
241 | pass
242 |
243 |
244 | class ins(Element):
245 | pass
246 |
247 |
248 | class kbd(Element):
249 | pass
250 |
251 |
252 | class label(Element):
253 | pass
254 |
255 |
256 | class legend(Element):
257 | pass
258 |
259 |
260 | class li(Element):
261 | pass
262 |
263 |
264 | class link(Element):
265 | pass
266 |
267 |
268 | class main(Element):
269 | pass
270 |
271 |
272 | class map(Element):
273 | pass
274 |
275 |
276 | class mark(Element):
277 | pass
278 |
279 |
280 | class meta(Element):
281 | pass
282 |
283 |
284 | class meter(Element):
285 | pass
286 |
287 |
288 | class nav(Element):
289 | pass
290 |
291 |
292 | class noframes(Element):
293 | pass
294 |
295 |
296 | class noscript(Element):
297 | pass
298 |
299 |
300 | class object(Element):
301 | pass
302 |
303 |
304 | class ol(Element):
305 | pass
306 |
307 |
308 | class optgroup(Element):
309 | pass
310 |
311 |
312 | class option(Element):
313 | pass
314 |
315 |
316 | class output(Element):
317 | pass
318 |
319 |
320 | class p(Element):
321 | pass
322 |
323 |
324 | class param(Element):
325 | pass
326 |
327 |
328 | class picture(Element):
329 | pass
330 |
331 |
332 | class pre(Element):
333 | pass
334 |
335 |
336 | class progress(Element):
337 | pass
338 |
339 |
340 | class q(Element):
341 | pass
342 |
343 |
344 | class rp(Element):
345 | pass
346 |
347 |
348 | class rt(Element):
349 | pass
350 |
351 |
352 | class ruby(Element):
353 | pass
354 |
355 |
356 | class s(Element):
357 | pass
358 |
359 |
360 | class samp(Element):
361 | pass
362 |
363 |
364 | class script(Element):
365 | pass
366 |
367 |
368 | class section(Element):
369 | pass
370 |
371 |
372 | class select(Element):
373 | pass
374 |
375 |
376 | class small(Element):
377 | pass
378 |
379 |
380 | class source(Element):
381 | pass
382 |
383 |
384 | class span(Element):
385 | pass
386 |
387 |
388 | class strike(Element):
389 | pass
390 |
391 |
392 | class strong(Element):
393 | pass
394 |
395 |
396 | class style(Element):
397 | pass
398 |
399 |
400 | class sub(Element):
401 | pass
402 |
403 |
404 | class summary(Element):
405 | pass
406 |
407 |
408 | class sup(Element):
409 | pass
410 |
411 |
412 | class svg(Element):
413 | pass
414 |
415 |
416 | class table(Element):
417 | pass
418 |
419 |
420 | class tbody(Element):
421 | pass
422 |
423 |
424 | class td(Element):
425 | pass
426 |
427 |
428 | class template(Element):
429 | pass
430 |
431 |
432 | class textarea(Element):
433 | pass
434 |
435 |
436 | class tfoot(Element):
437 | pass
438 |
439 |
440 | class th(Element):
441 | pass
442 |
443 |
444 | class thead(Element):
445 | pass
446 |
447 |
448 | class time(Element):
449 | pass
450 |
451 |
452 | class title(Element):
453 | pass
454 |
455 |
456 | class tr(Element):
457 | pass
458 |
459 |
460 | class track(Element):
461 | pass
462 |
463 |
464 | class tt(Element):
465 | pass
466 |
467 |
468 | class u(Element):
469 | pass
470 |
471 |
472 | class ul(Element):
473 | pass
474 |
475 |
476 | class var(Element):
477 | pass
478 |
479 |
480 | class video(Element):
481 | pass
482 |
483 |
484 | class wbr(Element):
485 | pass
486 |
--------------------------------------------------------------------------------
/chope/css.py:
--------------------------------------------------------------------------------
1 | from functools import reduce
2 | from itertools import chain
3 | from typing import Any, Dict, Iterable, List, Set, Union
4 |
5 | from chope.variable import Var
6 |
7 |
8 | class RenderError(Exception):
9 | pass
10 |
11 |
12 | class Rule:
13 | def __init__(self, name: str, declarations: List[dict]):
14 | self.__declarations = declarations
15 | self.__name = name
16 |
17 | def __eq__(self, __value: object) -> bool:
18 | return (
19 | isinstance(__value, Rule)
20 | and self.__declarations == __value.__declarations
21 | and self.__name == __value.__name
22 | )
23 |
24 | def render(self, indent: int = 2) -> str:
25 | nl = "\n"
26 | indented = indent > 0
27 | declarations_str = ""
28 |
29 | def get_value(var: Any) -> Any:
30 | if isinstance(var, Dict):
31 | return var
32 | elif isinstance(var, Var):
33 | return get_value(var.value)
34 | else:
35 | return var
36 |
37 | declarations = get_value(self.__declarations)
38 |
39 | if not isinstance(declarations, dict):
40 | raise RenderError(
41 | f"Invalid declaration {declarations} in rule '{self.__name}'. Declarations must be a dict object."
42 | )
43 |
44 | for property, value in declarations.items():
45 | value = get_value(value)
46 | if isinstance(value, Iterable) and not isinstance(value, str):
47 | value = " ".join(value)
48 |
49 | declarations_str += (
50 | f'{" " * indent}{property.replace("_", "-")}: {value};{nl * indented}'
51 | )
52 |
53 | return f"{self.__name} {{{nl * indented}{declarations_str}}}"
54 |
55 | def get_vars(self) -> Set[str]:
56 | def get_var(var: Var) -> Set[str]:
57 | if isinstance(var.value, Var):
58 | return {var.name}.union(get_var(var.value))
59 | else:
60 | return {var.name}
61 |
62 | if isinstance(self.__declarations, Var):
63 | return get_var(self.__declarations)
64 | elif isinstance(self.__declarations, dict):
65 | return reduce(
66 | lambda out, s: out.union(s),
67 | (get_var(d) for d in self.__declarations.values()),
68 | set(),
69 | )
70 | else:
71 | return set()
72 |
73 | def set_vars(self, values: Dict[str, Any]) -> "Rule":
74 | def set_var(comp: Any, values: Dict[str, Any]) -> Any:
75 | if isinstance(comp, Var):
76 | new_var = comp.set_value(values)
77 | if new_var == comp and isinstance(new_var.value, Var):
78 | return Var(new_var.name, new_var.value.set_vars(values))
79 | else:
80 | return new_var
81 | else:
82 | return comp
83 |
84 | if isinstance(self.__declarations, dict):
85 | new_declarations = {
86 | attr: set_var(value, values)
87 | for attr, value in self.__declarations.items()
88 | }
89 | elif isinstance(self.__declarations, Var):
90 | old_var = self.__declarations
91 | new_var = set_var(old_var, values)
92 | new_declarations = (
93 | self if new_var == old_var else Var(old_var.name, new_var.value)
94 | )
95 | else:
96 | new_declarations = self.__declarations
97 |
98 | return (
99 | self
100 | if new_declarations == self.__declarations
101 | else Rule(self.__name, new_declarations)
102 | )
103 |
104 |
105 | class Css:
106 | def __init__(self, rules: List[Rule]):
107 | self._rules = rules
108 |
109 | def __eq__(self, __value: object) -> bool:
110 | return isinstance(__value, Css) and self._rules == __value._rules
111 |
112 | def __class_getitem__(cls, items: Union[slice, Iterable[slice]]) -> "Css":
113 | if isinstance(items, slice):
114 | rules = [Rule(items.start.replace("_", "-"), items.stop)]
115 | else:
116 | rules = [Rule(item.start.replace("_", "-"), item.stop) for item in items]
117 |
118 | return cls(rules)
119 |
120 | def render(self, indent: int = 2) -> str:
121 | indented = indent > 0
122 | return ("\n\n" * indented).join((rule.render(indent) for rule in self._rules))
123 |
124 | def get_vars(self) -> Set[str]:
125 | return reduce(
126 | lambda out, s: out.union(s),
127 | (rule.get_vars() for rule in self._rules),
128 | set(),
129 | )
130 |
131 | def set_vars(self, values_: Dict[str, Any], **kwargs) -> "Css":
132 | combined_values = {k: v for k, v in chain(values_.items(), kwargs.items())}
133 |
134 | # shoutout to Dua Lipa
135 | new_rules = [rule.set_vars(combined_values) for rule in self._rules]
136 | return self if new_rules == self._rules else Css(new_rules)
137 |
138 |
139 | class Unit:
140 | def __init__(self, name: str):
141 | self.__name = name
142 |
143 | def __truediv__(self, value) -> str:
144 | return str(value) + self.__name
145 |
146 |
147 | cm = Unit("cm")
148 | ch = Unit("ch")
149 | em = Unit("em")
150 | ex = Unit("ex")
151 | in_ = Unit("in")
152 | mm = Unit("mm")
153 | pc = Unit("pc")
154 | percent = Unit("%")
155 | pt = Unit("pt")
156 | px = Unit("px")
157 | rem = Unit("rem")
158 | vh = Unit("vh")
159 | vmax = Unit("vmax")
160 | vmin = Unit("vmin")
161 | vw = Unit("vw")
162 |
--------------------------------------------------------------------------------
/chope/element.py:
--------------------------------------------------------------------------------
1 | import re
2 | from functools import reduce
3 | from itertools import chain
4 | from typing import Any, Dict, Iterable, Set, Tuple, Union
5 |
6 | from chope.css import Css
7 | from chope.variable import Var
8 |
9 |
10 | class DuplicateAttributeError(Exception):
11 | pass
12 |
13 |
14 | class Element:
15 | def __init__(self, *args, **kwargs):
16 | self._components: Tuple[Component, ...] = ()
17 |
18 | self._classes = ""
19 | self._id = ""
20 | self._attributes = {}
21 |
22 | if args:
23 | selector_id, selector_classes = (
24 | self.__get_id_classes_from_selector(args[0])
25 | if len(args) % 2
26 | else ("", "")
27 | )
28 | self._classes = selector_classes
29 |
30 | tuple_id, tuple_classes, tuple_attrs = (
31 | self.__get_id_classes_attrs_from_tuple(args[len(args) % 2 :])
32 | )
33 |
34 | if selector_id and tuple_id:
35 | raise DuplicateAttributeError(
36 | f"id declared twice: id={selector_id} and id={tuple_id}"
37 | )
38 |
39 | self._id = selector_id or tuple_id
40 | self._classes = (
41 | f"{self._classes} {tuple_classes}".strip()
42 | if tuple_classes
43 | else self._classes
44 | )
45 |
46 | self._attributes.update(tuple_attrs)
47 |
48 | if self._id and "id" in kwargs:
49 | raise DuplicateAttributeError(
50 | f'id declared twice: id={self._id} and id={kwargs["id"]}'
51 | )
52 |
53 | self._classes = (
54 | f'{self._classes} {kwargs.pop("class_")}'.strip()
55 | if "class_" in kwargs
56 | else self._classes
57 | )
58 | self._id = kwargs.pop("id", self._id)
59 | self._attributes.update(
60 | {key.replace("_", "-"): value for key, value in kwargs.items()}
61 | )
62 |
63 | @staticmethod
64 | def __get_id_classes_from_selector(selector: str) -> Tuple[str, str]:
65 | selector_pattern = r"^(?:#([^\s\.#]+))?(?:\.([^\s#]+))?" # id.class1.class2
66 |
67 | results = re.findall(selector_pattern, selector)[0]
68 | if results:
69 | id, classes = results
70 | return id, f'{classes.replace(".", " ")}'.strip()
71 | else:
72 | return "", ""
73 |
74 | @staticmethod
75 | def __get_id_classes_attrs_from_tuple(
76 | tuple_: Tuple[str, ...],
77 | ) -> Tuple[str, str, dict]:
78 | attrs = {tuple_[i - 1]: tuple_[i] for i in range(1, len(tuple_), 2)}
79 |
80 | id = attrs.pop("id", "")
81 | classes = f'{attrs.pop("class")}'.strip() if "class" in attrs else ""
82 |
83 | return id, classes, attrs
84 |
85 | def __eq__(self, __value: object) -> bool:
86 | return (
87 | isinstance(__value, Element)
88 | and self._components == __value._components
89 | and self._attributes == __value._attributes
90 | and self._classes == __value._classes
91 | and self._id == __value._id
92 | )
93 |
94 | def __class_getitem__(
95 | cls, comps: Union["Component", Iterable["Component"], Tuple[Any, ...]]
96 | ) -> "Element":
97 | return cls()[comps]
98 |
99 | def __getitem__(
100 | self, comps: Union["Component", Iterable["Component"], Tuple[Any, ...]]
101 | ) -> "Element":
102 | if isinstance(comps, Component.__args__):
103 | self._components = (comps,)
104 | else:
105 |
106 | def as_tuple(x):
107 | return (
108 | (x,)
109 | if isinstance(x, str) or not isinstance(x, Iterable)
110 | else tuple(x)
111 | )
112 |
113 | comps_tuples = (as_tuple(comp) for comp in comps)
114 | self._components = reduce(lambda l1, l2: l1 + l2, comps_tuples, ())
115 |
116 | return self
117 |
118 | def __call__(self, *args, **kwargs) -> 'Element':
119 | ret = self.__class__()
120 | updated_element = self.__class__(*args, **kwargs)
121 | ret._components = self._components
122 | ret._id = updated_element._id if updated_element._id else self._id
123 | ret._classes = updated_element._classes if updated_element._classes else self._classes
124 | ret._attributes = dict(**self._attributes)
125 | ret._attributes.update(updated_element._attributes)
126 |
127 | return ret
128 |
129 | def render(self, indent: int = 2) -> str:
130 | nl = "\n"
131 | indented = indent > 0
132 |
133 | def render_var(value, quote_str=False) -> str:
134 | if isinstance(value, (Element, Css)):
135 | return value.render(indent=indent).replace(nl, f'{nl}{" " * indent}')
136 | elif isinstance(value, Var):
137 | return render_var(value.value, quote_str=quote_str)
138 | elif isinstance(value, str):
139 | return (
140 | f"'{value}'"
141 | if '"' in value
142 | else f'"{value}"'
143 | if quote_str
144 | else value
145 | )
146 | elif isinstance(value, Iterable):
147 | return f'{nl * indented}{" " * indent}'.join((render_var(i) for i in value))
148 | else:
149 | return str(value)
150 |
151 | comp_str = nl * indented
152 | for comp in self._components:
153 | if isinstance(comp, str):
154 | _comp = comp.replace("\n", "
")
155 | comp_str += f'{" " * indent}{_comp}{nl * indented}'
156 | elif isinstance(comp, Var):
157 | comp_str += f'{" " * indent}{render_var(comp)}{nl * indented}'
158 | else:
159 | _comp_str = comp.render(indent).replace(
160 | nl, f'{nl * indented}{" " * indent}'
161 | )
162 | comp_str += f'{" " * indent}{_comp_str}{nl * indented}'
163 |
164 | name = self.__class__.__name__
165 |
166 | attrs_str = f" id={render_var(self._id, True)}" if self._id else ""
167 | attrs_str += (
168 | f" class={render_var(self._classes, True)}" if self._classes else ""
169 | )
170 |
171 | for attr, val in self._attributes.items():
172 | attrs_str += (
173 | f" {attr}"
174 | if isinstance(val, bool)
175 | else f" {attr}={render_var(val, True)}"
176 | )
177 |
178 | return f"<{name}{attrs_str}>{comp_str}{name}>"
179 |
180 | def get_vars(self) -> Set[str]:
181 | ret = set()
182 | if isinstance(self._id, Var):
183 | ret.add(self._id.name)
184 |
185 | if isinstance(self._classes, Var):
186 | ret.add(self._classes.name)
187 |
188 | ret.update(
189 | {val.name for val in self._attributes.values() if isinstance(val, Var)}
190 | )
191 |
192 | def get_var(comp):
193 | if isinstance(comp, (Element, Css)):
194 | return comp.get_vars()
195 | elif isinstance(comp, Var):
196 | if isinstance(comp.value, Var):
197 | return {comp.name}.union(get_var(comp.value))
198 | elif isinstance(comp.value, (Element, Css)):
199 | return {comp.name}.union(comp.value.get_vars())
200 | else:
201 | return {comp.name}
202 | else:
203 | return set()
204 |
205 | return ret.union(
206 | reduce(lambda res, comp: res.union(get_var(comp)), self._components, set())
207 | )
208 |
209 | def set_vars(self, values_: Dict[str, Any] = {}, **kwargs) -> "Component":
210 | combined_values = {k: v for k, v in chain(values_.items(), kwargs.items())}
211 |
212 | def set_var(comp: Component, values: Dict[str, Any]) -> Component:
213 | if isinstance(comp, (Element, Css)):
214 | return comp.set_vars(values)
215 | elif isinstance(comp, Var):
216 | new_var = comp.set_value(values)
217 | if new_var == comp and isinstance(new_var.value, (Element, Css)):
218 | return Var(new_var.name, new_var.value.set_vars(values))
219 | else:
220 | return new_var
221 | else:
222 | return comp
223 |
224 | id = set_var(self._id, combined_values)
225 | classes = set_var(self._classes, combined_values)
226 | attributes = {
227 | key: set_var(value, combined_values)
228 | for key, value in self._attributes.items()
229 | }
230 | components = tuple(set_var(comp, combined_values) for comp in self._components)
231 |
232 | if (
233 | id == self._id
234 | and classes == self._classes
235 | and attributes == self._attributes
236 | and components == self._components
237 | ):
238 | return self
239 |
240 | else:
241 | return self.__class__(id=id, class_=classes, **attributes)[components]
242 |
243 | def __str__(self) -> str:
244 | return self.render(0)
245 |
246 | def __repr__(self) -> str:
247 | return self.__str__()
248 |
249 |
250 | Component = Union[str, Element, Css, Var]
251 |
--------------------------------------------------------------------------------
/chope/functions/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hanstjua/chope/0f9197ce89a1057b565b2da30961b96e5a3bda00/chope/functions/__init__.py
--------------------------------------------------------------------------------
/chope/functions/color.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, Union
2 | from chope.functions.function import Function
3 |
4 |
5 | class rgb(Function):
6 | def __init__(self, red: int, green: int, blue: int,
7 | alpha: Optional[Union[int, str]] = None):
8 | if isinstance(alpha, str) and '%' not in alpha:
9 | raise ValueError('alpha must be an integer or a percentage.')
10 |
11 | if alpha is not None:
12 | super().__init__(red, green, blue, alpha)
13 | else:
14 | super().__init__(red, green, blue)
15 |
16 |
17 | class hsl(Function):
18 | def __init__(self, hue: Union[int, str], saturation: str, lightness: str,
19 | alpha: Optional[Union[int, str]] = None):
20 | if isinstance(hue, str) and \
21 | 'deg' not in hue and \
22 | 'rad' not in hue and \
23 | 'grad' not in hue and \
24 | 'turn' not in hue:
25 | raise ValueError('hue must be an integer or an angle.')
26 |
27 | if '%' not in saturation:
28 | raise ValueError('saturation must be a percentage.')
29 |
30 | if '%' not in lightness:
31 | raise ValueError('lightness must be a percentage.')
32 |
33 | if isinstance(alpha, str) and \
34 | '%' not in alpha:
35 | raise ValueError('alpha must be an integer or a percentage.')
36 |
37 | if alpha is not None:
38 | super().__init__(hue, saturation, lightness, alpha)
39 | else:
40 | super().__init__(hue, saturation, lightness)
41 |
42 |
43 | class hwb(Function):
44 | def __init__(self, hue: Union[int, str], whiteness: str, blackness: str,
45 | alpha: Optional[Union[int, str]] = None):
46 | if isinstance(hue, str) and \
47 | 'deg' not in hue and \
48 | 'rad' not in hue and \
49 | 'grad' not in hue and \
50 | 'turn' not in hue:
51 | raise ValueError('hue must be an integer or an angle.')
52 |
53 | if '%' not in whiteness:
54 | raise ValueError('whiteness must be a percentage.')
55 |
56 | if '%' not in blackness:
57 | raise ValueError('blackness must be a percentage.')
58 |
59 | if isinstance(alpha, str) and \
60 | '%' not in alpha:
61 | raise ValueError('alpha must be an integer or a percentage.')
62 |
63 | if alpha is not None:
64 | super().__init__(hue, whiteness, blackness, alpha)
65 | else:
66 | super().__init__(hue, whiteness, blackness)
67 |
--------------------------------------------------------------------------------
/chope/functions/function.py:
--------------------------------------------------------------------------------
1 | class Function:
2 | def __init__(self, *args):
3 | self.__args = args
4 |
5 | def render(self) -> str:
6 | name = self.__class__.__name__
7 |
8 | args = ', '.join(map(str, self.__args))
9 |
10 | return f'{name}({args})'
11 |
--------------------------------------------------------------------------------
/chope/functions/shape.py:
--------------------------------------------------------------------------------
1 | from typing import Optional
2 | from chope.functions.function import Function
3 |
4 |
5 | class circle(Function):
6 | def __init__(self, length: str, position_1: Optional[str] = None,
7 | position_2: Optional[str] = None):
8 | if (position_1 is not None and position_2 is None) or \
9 | (position_1 is None and position_2 is not None):
10 | raise ValueError(
11 | 'position_1 and position_2 must be either both empty or have values.')
12 |
13 | if position_1 is None:
14 | super().__init__(length)
15 | else:
16 | super().__init__(length, 'at', position_1, position_2)
17 |
18 |
19 | class ellipse(Function):
20 | def __init__(self, x_radius: str, y_radius: str, position_1: Optional[str] = None,
21 | position_2: Optional[str] = None):
22 | if (position_1 is not None and position_2 is None) or \
23 | (position_1 is None and position_2 is not None):
24 | raise ValueError(
25 | 'position_1 and position_2 must be either both empty or have values.')
26 |
27 | if position_1 is None:
28 | super().__init__(x_radius, y_radius)
29 | else:
30 | super().__init__(x_radius, y_radius, 'at', position_1, position_2)
31 |
32 |
33 | class polygon(Function):
34 | def __init__(self, *args):
35 | func_args = []
36 | if isinstance(args[0], str):
37 | if args[0] == 'nonzero' or args[0] == 'evenodd':
38 | func_args += args[0]
39 | args = args[1:]
40 | else:
41 | raise ValueError('fill-rule can only "nonzero" or "evenodd".')
42 |
43 | try:
44 | for x, y in args:
45 | func_args += 'x y'
46 | except TypeError:
47 | raise ValueError('argument for polygon must be a pair of values')
48 |
49 | super().__init__(*func_args)
50 |
51 |
52 |
53 | class path(Function):
54 | def __init__(self, *args):
55 | if len(args) > 1:
56 | if not (args[0] == 'nonzero' or args[0] == 'evenodd'):
57 | raise ValueError('fill-rule can only "nonzero" or "evenodd".')
58 |
59 | if not isinstance(args[-1], str):
60 | raise ValueError('argument for path must be an SVG string.')
61 |
62 |
63 | super().__init__(*args)
64 |
--------------------------------------------------------------------------------
/chope/functions/transform.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, Union
2 | from chope.functions.function import Function
3 |
4 |
5 | class transformX(Function):
6 | def __init__(self, length: Union[int, str]):
7 | super().__init__(length)
8 |
9 |
10 | class transformY(Function):
11 | def __init__(self, length: Union[int, str]):
12 | super().__init__(length)
13 |
14 |
15 | class transformZ(Function):
16 | def __init__(self, length: Union[int, str]):
17 | super().__init__(length)
18 |
19 |
20 | class translate(Function):
21 | def __init__(self, length_1: Union[int, str],
22 | length_2: Optional[Union[int, str]] = None):
23 | if length_2 is not None:
24 | super().__init__(length_1, length_2)
25 | else:
26 | super().__init__(length_1)
27 |
28 |
29 | class translate3d(Function):
30 | def __init__(self, tx: Union[int, str], ty: Optional[Union[int, str]] = None,
31 | tz: Optional[Union[int, str]] = None):
32 | if (ty is None and tz is not None) or (tz is None and ty is not None):
33 | raise ValueError(
34 | 'ty and tz must either be both None or both with values.')
35 |
36 | if '%' in tz:
37 | raise ValueError('tz cannot be percentage.')
38 |
39 | if ty is None and tz is None:
40 | super().__init__(tx)
41 | else:
42 | super().__init__(tx, ty, tz)
43 |
--------------------------------------------------------------------------------
/chope/variable.py:
--------------------------------------------------------------------------------
1 | from typing import Any, Dict
2 |
3 |
4 | class Var:
5 | def __init__(self, name: str, value: Any = None) -> None:
6 | self._name = name
7 | self._value = value
8 |
9 | def __eq__(self, __value: object) -> bool:
10 | return (
11 | isinstance(__value, Var)
12 | and self._name == __value._name
13 | and self._value == __value._value
14 | )
15 |
16 | @property
17 | def name(self) -> str:
18 | return self._name
19 |
20 | @property
21 | def value(self) -> Any:
22 | return self._value if self._value is not None else f'[{self.name} is not set]'
23 |
24 | def set_value(self, values: Dict[str, Any]) -> "Var":
25 | if self._name in values:
26 | return Var(self._name, values[self._name])
27 | else:
28 | new_value = (
29 | self._value.set_value(values)
30 | if isinstance(self._value, Var)
31 | else self._value
32 | )
33 | if new_value != self._value:
34 | return Var(self._name, new_value)
35 | else:
36 | return self
37 |
38 | def __str__(self) -> str:
39 | return f'Var["{self.name}","{self.value}"]'
40 |
41 | def __repr__(self) -> str:
42 | return self.__str__()
43 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools", "setuptools-scm"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "chope"
7 | version = "0.5.2"
8 | authors = [
9 | {name = "Johan Tjuatja", email = "hanstjua@yahoo.co.id"},
10 | ]
11 | description = "CSS & HTML on Python Easily"
12 | readme = "README.md"
13 | requires-python = ">=3.7"
14 | classifiers = [
15 | "Programming Language :: Python :: 3.7",
16 | "Programming Language :: Python :: 3.8",
17 | "Programming Language :: Python :: 3.9",
18 | "Programming Language :: Python :: 3.10",
19 | "Programming Language :: Python :: 3.11",
20 | "License :: OSI Approved :: MIT License",
21 | "Operating System :: OS Independent",
22 | ]
23 | dependencies = ["pytest"]
24 |
25 | [project.urls]
26 | "Homepage" = "https://github.com/hanstjua/chope"
27 | "Bug Tracker" = "https://github.com/hanstjua/chope/issues"
28 |
29 | [tool.setuptools]
30 | packages = ["chope"]
31 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hanstjua/chope/0f9197ce89a1057b565b2da30961b96e5a3bda00/tests/__init__.py
--------------------------------------------------------------------------------
/tests/test_css.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from chope.css import Css, RenderError, in_, percent, px, rem
4 | from chope.variable import Var
5 |
6 | expected = '''h1 {
7 | color: red;
8 | font-size: 1.2rem;
9 | padding: 1in;
10 | margin: 2%;
11 | outline: 1px dotted green;
12 | }
13 |
14 | .my-class {
15 | background: black;
16 | }'''
17 |
18 |
19 | def test_should_render_css_correctly():
20 | style = Css[
21 | 'h1': dict(
22 | color='red',
23 | font_size=rem/1.2,
24 | padding=in_/1,
25 | margin=percent/2,
26 | outline=(px/1, 'dotted', 'green')
27 | ),
28 | '.my-class': dict(
29 | background='black'
30 | )
31 | ]
32 |
33 | assert style.render(3) == expected
34 |
35 | def test_when_indent_is_zero_should_render_flat_string():
36 | expected = 'a {b: c;}d {e: f;}'
37 |
38 | style = Css['a': dict(b='c'), 'd': dict(e='f')]
39 |
40 | assert style.render(0) == expected
41 |
42 | def test_set_variable_values():
43 | expected_render = 'h1 {color: red;padding: 1in;size: 5px;margin: 1%;}.my-class {background: black;}'
44 | expected_css = Css[
45 | 'h1': dict(
46 | color=Var('color', 'red'),
47 | padding=Var('padding', in_/1),
48 | size=Var('size', Var('size_nested', px/5)),
49 | margin=Var('margin', percent/1)
50 | ),
51 | '.my-class': Var('my-class', {'background': 'black'})
52 | ]
53 |
54 | css = Css[
55 | # declaration variables
56 | 'h1': dict(
57 | color=Var('color'),
58 | padding=Var('padding', in_/1),
59 |
60 | # nested variables
61 | size=Var('size', Var('size_nested', px/10)),
62 | margin=Var('margin', Var('margin_nested', percent/2))
63 | ),
64 |
65 | # rule variable
66 | '.my-class': Var('my-class')
67 | ]
68 |
69 | values = {
70 | 'color': 'blue',
71 | 'size_nested': px/50,
72 | 'margin': percent/1,
73 | 'my-class': {'background': 'black'}
74 | }
75 |
76 | new_css = css.set_vars(values, color='red', size_nested=px/5)
77 |
78 | assert new_css == expected_css
79 | assert new_css.render(indent=0) == expected_render
80 |
81 | def test_handle_unset_rule_variable():
82 | css = Css[
83 | '.my-class': Var('my-class')
84 | ]
85 |
86 | with pytest.raises(RenderError):
87 | css.render()
88 |
89 | def test_get_variable_names():
90 | expected = {'color', 'size', 'size_nested', 'my-class'}
91 |
92 | css = Css[
93 | 'h1': dict(
94 | color=Var('color'),
95 | size=Var('size', Var('size_nested', px/10)),
96 | ),
97 | '.my-class': Var('my-class')
98 | ]
99 |
100 | assert css.get_vars() == expected
101 |
102 | @pytest.mark.parametrize(
103 | 'input1, input2, expected',
104 | ((Css['h1': {'asdf': 123}], Css['h1': {'asdf': 123}], True),
105 | (Css['h1': {'asdf': 123}], Css['h1': {'asdf': 234}], False),
106 | (Css['h1': {'asdf': 123}], 'some_str', False))
107 | )
108 | def test_comparison(input1, input2, expected):
109 | assert (input1 == input2) == expected
110 |
--------------------------------------------------------------------------------
/tests/test_element.py:
--------------------------------------------------------------------------------
1 | from typing import Tuple
2 |
3 | import pytest
4 |
5 | from chope import Element
6 | from chope.css import Css
7 | from chope.element import DuplicateAttributeError
8 | from chope.variable import Var
9 |
10 |
11 | class a(Element):
12 | pass
13 |
14 |
15 | class b(Element):
16 | pass
17 |
18 |
19 | def test_should_render_nested_components_correctly():
20 | class e1(Element):
21 | pass
22 |
23 | class e2(Element):
24 | pass
25 |
26 | class e3(Element):
27 | pass
28 |
29 | expected = """
30 | text
31 |
32 | word
33 |
34 |
35 | letter
36 | space
37 | symbols
38 |
39 | """
40 |
41 | component = e1(
42 | class_="my-class", color="yellow", size=123, my_attr="yes", autofocus=True
43 | )["text", e2["word\n"], e3["letter", ["space", "symbols"]]]
44 |
45 | assert component.render(4) == expected
46 |
47 |
48 | @pytest.mark.parametrize(
49 | "selectors, tuple, kv, expected",
50 | (
51 | ("#id.class1.class2", (), {}, 'id="id" class="class1 class2"'),
52 | ("", ("my[attr]", "x"), {}, 'my[attr]="x"'),
53 | ("", (), {"named": 1.0}, "named=1.0"),
54 | ("#id", ("my-attr", "x"), {}, 'id="id" my-attr="x"'),
55 | ("", ("my@attr", 1), {"named": 1.0}, "my@attr=1 named=1.0"),
56 | ("#id", (), {"named": 1.0}, 'id="id" named=1.0'),
57 | ("#id", ("my.attr", 1), {"named": 1.0}, 'id="id" my.attr=1 named=1.0'),
58 | ),
59 | ids=[
60 | "selectors only",
61 | "tuple only",
62 | "key-value only",
63 | "selector + tuple",
64 | "tuple + key-value",
65 | "selector + key-value",
66 | "all",
67 | ],
68 | )
69 | def test_attribute_definitions_styles(
70 | selectors: str, tuple: Tuple[str, ...], kv: dict, expected: str
71 | ):
72 | args = (selectors,) + tuple if selectors else tuple
73 | assert a(*args, **kv).render(0) == f""
74 |
75 |
76 | @pytest.mark.parametrize(
77 | "selector, tuple, kv",
78 | (
79 | ("#id", ("id", "di"), {}),
80 | ("#id", (), {"id": "di"}),
81 | ("", ("id", "id"), {"id": "di"}),
82 | ("#id", ("id", "di"), {"id": "di"}),
83 | ),
84 | ids=["selector + tuple", "selector + key-value", "tuple + key-value", "all"],
85 | )
86 | def test_conflicting_attribute_definitions_should_raise_error(
87 | selector: str, tuple: Tuple[str, ...], kv: dict
88 | ):
89 | with pytest.raises(DuplicateAttributeError):
90 | args = (selector,) + tuple if selector else tuple
91 | a(*args, **kv).render(0)
92 |
93 |
94 | def test_zero_indent_should_render_flat_string():
95 | expected = "text"
96 |
97 | assert a["text"].render(0) == expected
98 |
99 |
100 | def test_when_negative_number_is_passed_to_render_should_render_with_zero_indent():
101 | assert a["text"].render(-1) == a["text"].render(0)
102 |
103 |
104 | def test_able_to_render_css():
105 | expected = "\n b {\n prop: text;\n }\n"
106 |
107 | comp = a[Css["b" : dict(prop="text")]]
108 |
109 | assert comp.render(2) == expected
110 |
111 |
112 | def test_infer_id_and_classes_through_css_selector():
113 | expected = 'text'
114 |
115 | assert a("#id.class1.class2", class_="class3 class4")["text"].render(0) == expected
116 |
117 |
118 | def test_should_raise_exception_if_id_detected_in_both_kwargs_and_css_selector():
119 | with pytest.raises(DuplicateAttributeError):
120 | a("#a", id="a")
121 |
122 |
123 | def test_override_element_attributes():
124 | expected_comp = a(name='content', id='overriden', some_attr='new')['Content']
125 |
126 | comp = a(name='content', id='original', some_attr='old')['Content']
127 |
128 | assert comp(id='overriden', some_attr='new').render() == expected_comp.render()
129 |
130 |
131 | def test_set_variable_values():
132 | expected_render = 'OuterInnerset_nestedset_nestedset_nested'
133 | expected_comp = a(id=Var("id", "id"), count=Var("count", 1))[
134 | Var("outer", "Outer"),
135 | a(name=Var("name", "default"))[
136 | Var("inner", b(name="inner")[
137 | "Inner"
138 | ])
139 | ],
140 | Var("unset_nested", Var("set_nested", a["set_nested"])),
141 | Var("unset_nested", a[Var("set_nested", a["set_nested"])]),
142 | Var("set_nested", a["set_nested"]),
143 | ]
144 |
145 | comp = a(id=Var("id"), count=Var("count"))[
146 | # variable in element
147 | Var("outer"),
148 |
149 | # variable in inner element
150 | a(name=Var("name", "default"))[
151 | Var("inner")
152 | ],
153 |
154 | # nested variables
155 | Var("unset_nested", Var("set_nested")),
156 |
157 | # nested variable inside element
158 | Var("unset_nested", a[Var("set_nested")]),
159 | Var("set_nested", a[Var("unset_nested")]),
160 | ]
161 |
162 | values = {
163 | "id": "id",
164 | "count": 1,
165 | "outer": "Outer",
166 | "inner": b(name="inner")["Inner"],
167 | "set_nested": a["set_nested"],
168 | }
169 |
170 | new_comp = comp.set_vars(values)
171 |
172 | assert new_comp == expected_comp
173 | assert new_comp.render(indent=0) == expected_render
174 |
175 |
176 | def test_set_variable_values_using_kwargs():
177 | expected_comp = a(name=Var("name", "my-name"))[
178 | Var("content", "My content."),
179 | b[Var("inner", "Inner content.")]
180 | ]
181 |
182 | comp = a(name=Var("name"))[
183 | Var("content"),
184 | b[Var("inner")]
185 | ]
186 |
187 | new_comp = comp.set_vars(
188 | {"name": "not-my-name"},
189 | name="my-name",
190 | content="My content.",
191 | inner="Inner content.",
192 | )
193 |
194 | assert new_comp == expected_comp
195 |
196 |
197 | def test_set_variables_to_iterables():
198 | equivalent_comp = a[
199 | b['0'],
200 | b['1'],
201 | b['2'],
202 | b['3'],
203 | b['4']
204 | ]
205 |
206 | expected = equivalent_comp.render()
207 |
208 | comp = a[
209 | Var('content'),
210 | (b['3'], b['4'])
211 | ]
212 |
213 | result = comp.set_vars(content=(b[str(i)] for i in range(3))).render()
214 |
215 | assert result == expected
216 |
217 |
218 | def test_get_variable_names():
219 | vars_count = 5
220 | vars = [Var(str(i)) for i in range(vars_count)]
221 |
222 | comp = a(var=vars[0])[
223 | vars[1],
224 | a(var=vars[1])[
225 | vars[2],
226 | # nested variables
227 | Var("0", vars[3]),
228 | Var("0", a[vars[4]]),
229 | ],
230 | ]
231 |
232 | expected = {str(i) for i in range(vars_count)}
233 |
234 | assert comp.get_vars() == expected
235 |
236 | def test_able_to_accept_empty_iterable_as_a_child():
237 | comp = a[
238 | b[[]],
239 | b[()],
240 | b[(i for i in range(0))]
241 | ]
242 | expected = ''
243 |
244 | assert comp.render(0) == expected
245 |
--------------------------------------------------------------------------------
/tests/test_function.py:
--------------------------------------------------------------------------------
1 | from chope.css import px
2 | from chope.functions.function import Function
3 |
4 |
5 | def test_should_render_correctly():
6 | class test_func(Function):
7 | def __init__(self, arg_1: str, arg_2: int, arg_3: float):
8 | super().__init__(arg_1, arg_2, arg_3)
9 |
10 | assert test_func(px/1, 2, 3.0).render() == 'test_func(1px, 2, 3.0)'
11 |
--------------------------------------------------------------------------------