diff --git a/docs/css.md b/docs/css.md index c0d3c3a..09569d7 100644 --- a/docs/css.md +++ b/docs/css.md @@ -41,6 +41,7 @@ print(template) ## CSSObject Using `style` object is not the only way to develop CSS with PH7, You can utilise a special class called `CSSObject` to define stylesheets. +
```python @@ -96,11 +97,10 @@ print(template) A `CSSObject` can also be reused via class inheritance. You can define a base CSS class and reuse it however many times you like. - +
```python -from ph7 import CSSObject, include -from ph7.html import body, div, head, html +from ph7 import CSSObject class flex_center(CSSObject): @@ -118,49 +118,27 @@ class textbox(flex_center): width = "100vw" -template = html( - head( - include(textbox), - ), - body( - div( - "Hello, World!", - class_name=[textbox], - ) - ), -) - -print(template) +print(textbox()) ``` -```html - - - - - -
Hello, World!
- - +```css +.textbox { + display: flex; + align-items: center; + justify-content: center; + height: 100vh; + width: 100vw; +} ```
Furthermore `CSSObject` also allows for nesting by sub classes. - +
```python -from ph7 import CSSObject, include -from ph7.html import body, div, head, html +from ph7 import CSSObject class flex_center(CSSObject): @@ -185,54 +163,79 @@ class textbox(flex_center): font_family = "Lucida Console, Monaco, monospace" -template = html( - head( - include(textbox), - ), - body( - div( - div( - "Hello, World!", - class_name=[textbox.text], - ), - class_name=[textbox], - ) - ), -) - -print(template) +print(textbox()) ``` -```html - - - - - -
-
Hello, World!
-
- - +```css +.textbox { + display: flex; + align-items: center; + justify-content: center; + height: 100vh; + width: 100vw; +} +.textbox .text { + font-size: 12px; + font-weight: 500; + font-family: Lucida Console, Monaco, monospace; +} ```
To make your styles more manageable and reusable, you can define the styles in a separate module and import them throughout your codebase. Following section provides an example of a reusable stylesheet module. +## Pseudo Classes and Elements + +A pseudo class can be defined by adding `_` at the beginning of class name, for selecting a child you can set `child` parameter. For pseudo, elements add `__` at the beginning of the class name. + + +
+```python +from ph7 import CSSObject, include +from ph7.html import body, div, head, html + + +class item(CSSObject): + """Flex center.""" + + height = "30px" + width = "100%" + + margin_top = "5px" + + class _nth_child(CSSObject): + """Nth child.""" + + child = 1 + margin_top = "0px" + + class __before(CSSObject): + """Before selector.""" + + content = '">"' + + +print(item()) +``` + +```css +.item { + height: 30px; + width: 100%; + margin-top: 5px; +} +.item::before { + content: ">"; +} +.item:nth-child(1) { + margin-top: 0px; +} +``` +
+ + + ## Static Context PH7 has a runtime context object which can be used for managing the static resource. To use a static context, import `ph7.context.ctx`, add `ctx.static.view(__name__)` at the top of your template and add `ctx.static.include` in your view. In the following example we'll use an external stylesheet for styling our view. diff --git a/docs/html.md b/docs/html.md index 34f2e72..b21feb0 100644 --- a/docs/html.md +++ b/docs/html.md @@ -246,14 +246,14 @@ You can define your view as a python function create views based on conditions, ```python import typing as t -from ph7.html import body, div, html, node +from ph7.html import HtmlNode, body, div, html user = div(class_name="user") users = div(class_name="user") nousers = div("Error, Users not found", class_name="error") -def render_users(context: t.Dict) -> node: +def render_users(context: t.Dict) -> HtmlNode: """Render users.""" if "number_of_users" not in context: return nousers @@ -301,14 +301,14 @@ You can also use named arguments instead of context argument to make thing more ```python import typing as t -from ph7.html import body, div, html, node +from ph7.html import HtmlNode, body, div, html user = div(class_name="user") users = div(class_name="user") nousers = div("Error, Users not found", class_name="error") -def render_users(number_of_users: t.Optional[int] = None) -> node: +def render_users(number_of_users: t.Optional[int] = None) -> HtmlNode: """Render users.""" if number_of_users is None: return nousers @@ -361,7 +361,7 @@ import time import typing as t from functools import lru_cache -from ph7.html import body, div, html, node +from ph7.html import HtmlNode, body, div, html user = div(class_name="user") users = div(class_name="user") @@ -369,12 +369,12 @@ nousers = div("Error, Users not found", class_name="error") @lru_cache -def _render_users(n: int) -> node: +def _render_users(n: int) -> HtmlNode: """Render users.""" return users(user(f"User {i}") for i in range(n)) -def render_users(context: t.Dict) -> node: +def render_users(context: t.Dict) -> HtmlNode: """Render users.""" if "number_of_users" not in context: return nousers @@ -401,9 +401,9 @@ print(f"Third render: {time.perf_counter() - tick}") ``` ```stdout -First render: 6.87079775 -Second render: 0.3591619169999998 -Third render: 0.3592319160000006 +First render: 6.493273332999999 +Second render: 0.4277449579999999 +Third render: 0.3591492089999999 ``` diff --git a/docs/js.md b/docs/js.md index d86eb03..9b13948 100644 --- a/docs/js.md +++ b/docs/js.md @@ -92,7 +92,7 @@ If you want to render a function call with arguments you can decorate a method w ```python from ph7 import include -from ph7.html import body, button, div, head, html, img, node +from ph7.html import HtmlNode, body, button, div, head, html, img from ph7.js import document, fetch, js_callable @@ -106,7 +106,7 @@ async def fetchUserProfilePicture(user: str) -> None: document.getElementById(f"image-{user}").src = data.profile_picture -def _user(name: str) -> node: +def _user(name: str) -> HtmlNode: return div( img(src="#", style={"height": "200px", "width": "400px"}, id=f"image-{name}"), button( @@ -118,7 +118,7 @@ def _user(name: str) -> node: ) -def _users(context: dict) -> node: +def _users(context: dict) -> HtmlNode: """List users.""" return div(_user(name=name) for name in context["users"]) diff --git a/examples/css_inherit.py b/examples/css_inherit.py index e533a0d..ecdbd79 100644 --- a/examples/css_inherit.py +++ b/examples/css_inherit.py @@ -1,5 +1,4 @@ -from ph7 import CSSObject, include -from ph7.html import body, div, head, html +from ph7 import CSSObject class flex_center(CSSObject): @@ -17,16 +16,4 @@ class textbox(flex_center): width = "100vw" -template = html( - head( - include(textbox), - ), - body( - div( - "Hello, World!", - class_name=[textbox], - ) - ), -) - -print(template) +print(textbox()) diff --git a/examples/css_nesting.py b/examples/css_nesting.py index dd17ef1..2a3b1b5 100644 --- a/examples/css_nesting.py +++ b/examples/css_nesting.py @@ -1,5 +1,4 @@ -from ph7 import CSSObject, include -from ph7.html import body, div, head, html +from ph7 import CSSObject class flex_center(CSSObject): @@ -24,19 +23,4 @@ class text(CSSObject): font_family = "Lucida Console, Monaco, monospace" -template = html( - head( - include(textbox), - ), - body( - div( - div( - "Hello, World!", - class_name=[textbox.text], - ), - class_name=[textbox], - ) - ), -) - -print(template) +print(textbox()) diff --git a/examples/css_pseudo_class.py b/examples/css_pseudo_class.py new file mode 100644 index 0000000..66c6572 --- /dev/null +++ b/examples/css_pseudo_class.py @@ -0,0 +1,24 @@ +from ph7 import CSSObject + + +class item(CSSObject): + """Flex center.""" + + height = "30px" + width = "100%" + + margin_top = "5px" + + class _nth_child(CSSObject): + """Nth child.""" + + child = 1 + margin_top = "0px" + + class __before(CSSObject): + """Before selector.""" + + content = '">"' + + +print(item()) diff --git a/ph7/context.py b/ph7/context.py index 10f804c..efde8f3 100644 --- a/ph7/context.py +++ b/ph7/context.py @@ -5,8 +5,7 @@ import typing as t from pathlib import Path -from ph7.css import CSSObject -from ph7.css import render as to_css +from ph7.css import CSSObject, to_css from ph7.js import JavaScriptObject, JSCallable from ph7.js.transpile import to_js diff --git a/ph7/core/css.py b/ph7/core/css.py new file mode 100644 index 0000000..9379ab0 --- /dev/null +++ b/ph7/core/css.py @@ -0,0 +1,86 @@ +"""CSS base classes.""" + +import typing as t + +from ph7.core.tags import TAGS +from ph7.style import attributes as css_attr + + +class CSSNode: + """Renderable CSS node.""" + + __css__: bool = True + + child: t.Optional[int] = None + + @classmethod + def name(cls) -> str: + """Name string.""" + name = cls.__name__.replace("_", "-") + if name in TAGS: + return name + if name.startswith("--"): + return f"::{name[2:]}" + if name.startswith("-") and cls.child is not None: + return f":{name[1:]}({cls.child})" + if name.startswith("-"): + return f":{name[1:]}" + return f" .{name}" + + @classmethod + def format(cls) -> str: + """Name string.""" + name = cls.__name__.replace("_", "-") + if name in TAGS: + raise ValueError( + f"{name} is a HTML tag selector, Cannot use it as a classname." + ) + return name + + @classmethod + def properties(cls) -> t.Dict[str, str]: + """Returns properties mapping.""" + properties = {} + for base in cls.__bases__: + if hasattr(base, "properties"): + properties.update(base.properties()) + for prop, val in cls.__dict__.items(): + if isinstance(val, str) and not prop.startswith("__"): + properties[css_attr[prop]] = val + return properties + + @classmethod + def subclasses(cls) -> t.List["CSSNode"]: + """Returns the list of available subclasses.""" + cs = [] + for c in cls.__dict__.values(): + if hasattr(c, "__css__"): + cs.append(c) + return cs + + @classmethod + def dict(cls, parent: str, context: t.Dict) -> None: + """Render CSS object.""" + context[f"{parent}{cls.name()}"[1:]] = cls.properties() + for subc in cls.subclasses(): + subc.dict( + parent=f"{parent}{cls.name()}", + context=context, + ) + + +def to_css(*objs: t.Union[CSSNode, t.Type[CSSNode]], minify: bool = False) -> str: + """Render CSS stylesheet.""" + sheet = "" + container: t.Dict[str, t.Dict] = {} + separator = "" if minify else " " + newline = "" if minify else "\n" + tab = "" if minify else " " + for obj in objs: + obj.dict(parent="", context=container) + for cls in sorted(container): + sheet += f"{cls}" + "{" + newline + for prop, val in container[cls].items(): + sheet += f"{tab}{prop}:{separator}{val};{newline}" + sheet += "}" + newline + return sheet diff --git a/ph7/core/tags.py b/ph7/core/tags.py new file mode 100644 index 0000000..1c81b17 --- /dev/null +++ b/ph7/core/tags.py @@ -0,0 +1,109 @@ +"""List of tags.""" + +TAGS = { + "a", + "abbr", + "address", + "area", + "article", + "aside", + "audio", + "b", + "base", + "blockquote", + "body", + "br", + "button", + "canvas", + "caption", + "cite", + "code", + "col", + "colgroup", + "data", + "datalist", + "dd", + "details", + "div", + "dl", + "dt", + "em", + "embed", + "fieldset", + "figcaption", + "figure", + "footer", + "form", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "head", + "header", + "hr", + "html", + "i", + "iframe", + "img", + "input", + "ins", + "kbd", + "label", + "legend", + "li", + "link", + "main", + "map", + "mark", + "meta", + "meter", + "nav", + "noscript", + "object", + "ol", + "optgroup", + "option", + "output", + "p", + "param", + "picture", + "pre", + "progress", + "q", + "rp", + "rt", + "ruby", + "samp", + "script", + "section", + "select", + "small", + "source", + "span", + "strong", + "style", + "sub", + "summary", + "sup", + "svg", + "table", + "tbody", + "td", + "template", + "textarea", + "tfoot", + "th", + "thead", + "time", + "title", + "tr", + "track", + "u", + "ul", + "var", + "video", + "wbr", + "unpack", +} diff --git a/ph7/css.py b/ph7/css.py index 048496d..4a21f64 100644 --- a/ph7/css.py +++ b/ph7/css.py @@ -4,18 +4,14 @@ # pylint: disable=line-too-long,too-many-lines,redefined-outer-name,redefined-builtin,invalid-name,too-many-locals -import typing as t - from typing_extensions import Literal -from ph7.style import attributes as css_attr +from ph7.core.css import CSSNode, to_css -class CSSObject: +class CSSObject(CSSNode): """CSS class representation.""" - __css__ = True - animation: Literal[ # type: ignore "animation-name", "animation-duration", @@ -843,8 +839,8 @@ class CSSObject: grid: Literal["none", "initial", "inherit", str] # type: ignore """ - grid: none |grid-template-rows / grid-template-columns |grid-template-areas | - grid-template-rows / [grid-auto-flow] grid-auto-columns | + grid: none |grid-template-rows / grid-template-columns | + grid-template-areas |grid-template-rows / [grid-auto-flow] grid-auto-columns | [grid-auto-flow] grid-auto-rows / grid-template-columns |initial | inherit; """ @@ -1403,64 +1399,8 @@ class CSSObject: ] """writing-mode: horizontal-tb | vertical-rl | vertical-lr;""" - @classmethod - def name(cls) -> str: - """Name string.""" - return cls.__name__.replace("_", "-") - - @classmethod - def properties(cls) -> t.Dict[str, str]: - """Returns properties mapping.""" - properties = {} - for base in cls.__bases__: - if hasattr(base, "properties"): - properties.update(base.properties()) - for prop, val in cls.__dict__.items(): - if isinstance(val, str) and not prop.startswith("__"): - properties[css_attr[prop]] = val - return properties - - @classmethod - def subclasses(cls) -> t.List["CSSObject"]: - """Returns the list of available subclasses.""" - cs = [] - for c in cls.__dict__.values(): - if hasattr(c, "properties"): - cs.append(c) - return cs - def __str__(self) -> str: - """String represenstation.""" - return render(self, minify=False) + """String representation.""" + return to_css(self) __repr__ = __str__ - - -def _render( - obj: t.Union[CSSObject, t.Type[CSSObject]], parent: str, container: t.Dict -) -> None: - """Render CSS object.""" - container[f"{parent} .{obj.name()}"[1:]] = obj.properties() - for subc in obj.subclasses(): - _render( - obj=subc, - parent=f"{parent} .{obj.name()}", - container=container, - ) - - -def render(*objs: t.Union[CSSObject, t.Type[CSSObject]], minify: bool = False) -> str: - """Render CSS stylesheet.""" - sheet = "" - container: t.Dict[str, t.Dict] = {} - separator = "" if minify else " " - newline = "" if minify else "\n" - tab = "" if minify else " " - for obj in objs: - _render(obj=obj, parent="", container=container) - for cls in sorted(container): - sheet += f"{cls}" + "{" + newline - for prop, val in container[cls].items(): - sheet += f"{tab}{prop}:{separator}{val};{newline}" - sheet += "}" + newline - return sheet diff --git a/ph7/formatters.py b/ph7/formatters.py index 2bbf0c0..893735e 100644 --- a/ph7/formatters.py +++ b/ph7/formatters.py @@ -68,7 +68,7 @@ def cformat( if hasattr(class_name, "name"): ctx.static.add(resource=t.cast(CSSObject, class_name)) - return t.cast(CSSObject, class_name).name() + return t.cast(CSSObject, class_name).format() if isinstance(class_name, str): return class_name @@ -77,7 +77,7 @@ def cformat( for cls in class_name: if hasattr(cls, "name"): ctx.static.add(resource=t.cast(CSSObject, cls)) - cls_name += f"{t.cast(CSSObject, cls).name()} " + cls_name += f"{t.cast(CSSObject, cls).format()} " continue cls_name += f"{cls} " return cls_name[:-1] diff --git a/ph7/static.py b/ph7/static.py index e96d2ca..7369ea2 100644 --- a/ph7/static.py +++ b/ph7/static.py @@ -5,8 +5,7 @@ import typing as t from ph7.core.html import HtmlNode -from ph7.css import CSSObject -from ph7.css import render as to_css +from ph7.css import CSSObject, to_css from ph7.html import script, style, unpack from ph7.js import JavaScriptObject, JSCallable from ph7.js.transpile import to_js diff --git a/scripts/render/css.py b/scripts/render/css.py index 140fce1..0266e3b 100644 --- a/scripts/render/css.py +++ b/scripts/render/css.py @@ -26,71 +26,20 @@ # pylint: disable=line-too-long,too-many-lines,redefined-outer-name,redefined-builtin,invalid-name,too-many-locals -import typing as t -from ph7.style import attributes as css_attr from typing_extensions import Literal -class CSSObject: - \"\"\"CSS class representation.\"\"\" +from ph7.core.css import CSSNode, to_css - __css__ = True +class CSSObject(CSSNode): + \"\"\"CSS class representation.\"\"\" {properties} - @classmethod - def name(cls) -> str: - \"\"\"Name string.\"\"\" - return cls.__name__.replace("_", "-") - - @classmethod - def properties(cls) -> t.Dict[str, str]: - \"\"\"Returns properties mapping.\"\"\" - properties = {{}} - for base in cls.__bases__: - if hasattr(base, "properties"): - properties.update(base.properties()) - for prop, val in cls.__dict__.items(): - if isinstance(val, str) and not prop.startswith("__"): - properties[css_attr[prop]] = val - return properties - - @classmethod - def subclasses(cls) -> t.List["CSSObject"]: - \"\"\"Returns the list of available subclasses.\"\"\" - cs = [] - for c in cls.__dict__.values(): - if hasattr(c, "properties"): - cs.append(c) - return cs - - -def _render(obj: t.Union[CSSObject, t.Type[CSSObject]], parent: str, container: t.Dict) -> None: - \"\"\"Render CSS object.\"\"\" - container[f"{{parent}} .{{obj.name()}}"[1:]] = obj.properties() - for subc in obj.subclasses(): - _render( - obj=subc, - parent=f"{{parent}} .{{obj.name()}}", - container=container, - ) - - -def render(*objs: t.Union[CSSObject, t.Type[CSSObject]], minify: bool = False) -> str: - \"\"\"Render CSS stylesheet.\"\"\" - sheet = "" - container: t.Dict[str, t.Dict] = {{}} - separator = "" if minify else " " - newline = "" if minify else "\\n" - tab = "" if minify else " " - for obj in objs: - _render(obj=obj, parent="", container=container) - for cls in sorted(container): - sheet += f"{{cls}}" + "{{" + newline - for prop, val in container[cls].items(): - sheet += f"{{tab}}{{prop}}:{{separator}}{{val}};{{newline}}" - sheet += "}}" + newline - return sheet + def __str__(self) -> str: + \"\"\"String representation.\"\"\" + return to_css(self) + __repr__ = __str__ """ properties = ""