HTML in Python
Experimental Feature
This feature is new and experimental. You may use this, but expect a lot of weirdness. If you find any issue, feel free to report to GitHub Issues
This is an alternative mode to write liku components. While using declarative mode works to define components, it can be very overwhelming real quick, as well as not being very easy to read as the component grows bigger and bigger. This mode fixes that issue by writing your component in HTML, while maintaining templating engine features, much like JSX.
Prerequisite
This feature requires lxml
to be installed. To install the supported version, install the package with
htm
extras:
Visual Studio Code Extension
To aid developers on developing their app with Liku with HTML in Python mode, this extension for Visual Studio Code exist. Currently, there is no ETA to release on marketplace, but you may see the code and compile it yourself here.
Features:
- Completion on component names
- Completion on props
- Completion on
{{ expression }}
directive - Completion on programmatic value props
However, there are a lot of caveats:
- You must use triple backtick inside
html()
for the completion to work. - All completions are taken on all scopes. This means other function's locals will also be included.
- No check for already filled in props. This means you might be suggested already filled props.
- No semantic tokens. (aka syntax highlighting)
- No type checks/linting.
If you are using other editor, you may make use of the LSP inside bundled/tools
directory.
Feature Overview
Below is the code that we'll learn in this article. We'll explain all the features of across the following sections.
In general, all the html code will be done in a html()
call, that can be imported from liku.htm
from liku.htm import html
def Card(name: str, image: str | None):
compact = image is None
if compact:
return html(
"""
<div class="card card-compact">
<p>{{ name }}</p>
</div>
"""
)
return html(
"""
<div class="card">
<img :src="image" class="card-image" />
<p>{{ name }}</p>
</div>
"""
)
def PeopleList(people: list[tuple[str, str | None]]):
children = list(map(lambda p: html("""<Card :name="p[0]" :image="p[1]" />"""), people))
# The following also works
# children = list(map(lambda p: Card(p[0], p[1]), people))
return html(
"""
<div class="grid grid-cols-3 gap-2">
{{ children }}
</div>
"""
)
people = [
("Ren", "https://avatars.githubusercontent.com/u/6541445?v=4"),
("Linus", None)
]
print(html(
"""
<PeopleList :people="people />
"""
))
Writing Components
A component is simply a function returning either a liku HTML element, a string, or None
. You may also
return a list of any of the types mentioned earlier. To accomodate type check, you can import HTMLNode
from liku.elements
.
from liku.htm import html
def Example():
return html("""<p>Hello world!</p>""")
print(html("""<Example />""")) # This will output <p>Hello world!</p>
When parsing your html, Liku will look through all your scoped variables, and call the function accordingly.
Multiword Components
If your component has multiple word in the function name (such as example_component()
), you can use
example-component
as well as example_component
as the tag name. However, you should stick with snake
case.
Using Python Expression
Just like other templating engine, you are able to inject expression to print into the final HTML. In Liku,
this is done using {{ expression }}
directive.
Danger
This feature makes use of eval()
. Please make sure all the expression are safe. Do not run user provided
strings.
from liku.htm import html
def Greet():
name = "Liku"
return html("""<p>Hello, {{ name }}!</p>""")
print(html("""<Greet />""")) # This will output <p>Hello, Liku!</p>
Liku will parse the html, find the directive and run the expression inside the directive. It will then replace the directive with the result of the expression.
Passing Props
All positional and keyword arguments are props in liku, excluding variable arguments. Therefore, all keys must be the variable name of the function, and the value will be passed to the function call.
from liku.htm import html
def Greet(name: str):
return html("""<p>Hello, {{ name }}!</p>""")
print(html("""<Greet name="Liku" />""")) # This will output <p>Hello, Liku!</p>
Info
You do not need type hints, but it would be best if you could.
Internally, liku will convert all props into a dict object, then spread it in the function call. For example,
the previous example will call the function like so: Greet(**{"name": "Liku"})
, which is essentially the same
as calling Greet(name="Liku")
. Therefore, the props order does not matter.
Catch-all Props
Sometimes you might need extra props that you don't care to list on. Maybe because there is too much, or you simply just want to pass it to child or have special handling. You can do so using variable keyword arguments.
from liku.htm import html
import liku as e
def ExampleForward(name: str, **kwargs: object):
return e.p(
props=kwargs,
children=[
f"Hello, {name}!"
]
)
print(html("""<ExampleForward name="Liku" class="font-bold" />""")) # This will output <p class="font-bold">Hello, Liku!</p>
Programmatic Value
Danger
This feature makes use of eval()
. Please make sure all the expression are safe. Do not run user provided
strings.
There are times where you might want the value of a prop be based on a variable or other data. You may do so by
prepending the key of the prop with a colon (:
), and let the value be a Python expression.
from liku.htm import html
def Greet(name: str):
return html("""<p>Hello, {{ name }}!</p>""")
user_name = "Liku"
print(html("""<Greet :name="user_name" />""")) # This will output <p>Hello, Liku!</p>
Internally, Liku will parse through the props and run eval()
on all of the value in such props.
Control Flow
Outputting conditionally and looping through a list is a common problem during generation. You are
encouraged to split this logic outside of the html()
call, as Liku itself does not have any logic
of conditional and recursion.
One way to achieve this is as follows:
from dataclasses import dataclass
from liku.htm import html
@dataclass
class GroceryItem:
name: str
finished: bool
def Grocery(item: GroceryItem):
# Run looping outside of html, saving the result...
if item.finished:
suffix = "(OK)"
else:
suffix = html("""<button>Finish</button>""")
# ... then embed the result
return html("""<li>{{ item.name }} {{ suffix }}</li>""")
def GroceriesList(items: list[GroceryItem]):
# Run looping outside of html, saving the result...
items_html = list(map(Grocery, items))
return html(
# ... then embed the result
"""
<ul>{{ items_html }}</ul>
"""
)
print(
GroceriesList(
[
GroceryItem("Sample", True),
GroceryItem("Sample 2", False),
]
)
)