A DSL for representing HTML/XML in Python using an expression-like syntax. Why? You get to use the Python syntax you already know.
This example from the Jinja2 website can be represented in BMX like so:
mydoc = (
DOCTYPE.html
+html(lang='en')
+head
+title +"My Webpage" -title
-head
+body
+ul('#navigation')
+(
+li
+a(href=item.href) +item.caption -a
-li
for item in navigation)
-ul
+h1 +"My Webpage" -h1
+a_variable
# a comment
-body
-html)
Note: Just as with ordinary Python expressions, multi-line BMX expressions must be surrounded by parentheses.
bmx
is tested on CPython versions 3.6-3.9. It has 2 dependencies: singledispatchmethod (backported from 3.8) and MarkupSafe - to escape html in strings.
pip install bmx
An example using Flask (available in the top-level source directory):
# flask_greeter.py
from bmx.htmltags import (
html,
head,
title,
body,
p
)
from flask import Flask
app = Flask(__name__)
@app.route('/<name>')
def greeter(name: str):
return str(
# fmt: off
+html
+head
+title +"Flask Greeter" -title
-head
+body
+p +f"Hello {name}" -p
-body
-html
# fmt: on
)
Install Flask then run it as:
FLASK_APP=flask_greeter.py flask run
Go to https://127.0.0.1:5000/<your_name>
in your browser (eg. https://127.0.0.1:5000/Stuart
) and you will see the message.
Type | HTML | BMX | Comment/Mnemonic |
---|---|---|---|
Opening tag | <div> |
+div |
Mnemonic: Adding content |
Closing tag | </div> |
-div |
Mnemonic: opposite of adding content |
Self-closing tag | <input/> |
+input |
Self-closing tag are pre-defined |
Attributes | <a href="/">Home</a> |
+a(href="/") +"Home" -a |
Mnemonic: attributes are keyword arguments. |
Attributes | <button aria-label="Close">X</button> |
+button(aria_label="Close") +"X" -button |
Note: Underscores in keyword arguments are replaced with dashes |
Attributes | <input type="text"> |
+input_(type_="text") |
Note: Append an underscore to avoid conflicts with Python keywords |
Attributes: shorthand for id and class |
<div id="userinput" class="credentials" > |
+div('#userinput.credentials') |
#id .classname |
Attributes: shorthand for class |
<div class="col-sm-8 col-md-7 py-4"> |
+div .col_sm_8 .col_md_7 .py_4 |
.classname Underscores are replaced with dashes |
Composing tags and content | <h1>The Title</h1> |
+h1 +"The Title" -h1 |
Mnemonic: think string concatenation ie. "Hello " + "World!" |
We define a Tag
class which overrides the unary +/- and binary +/- operators to model the opening and closing tags of HTML. We provide a __call__
method to model HTML attributes as keyword arguments and a __getattr__
method to provide a shorthand for HTML classes (see above). A Tag
is instantiated for every HTML tag and is available with a from bmx.htmltags import html, head, body, span
.
bmx
uses MarkupSafe to escape HTML from strings. If you are sure that you don't want to escape the HTML in a string, you can wrap it in a Markup object and the string will be included as-is.
To use the Black uncompromising autoformatter, surround your BMX markup with #fmt: off
and #fmt: on
comments like this:
result = (
# fmt: off
+html
+body
+h1 +"My Page" -h1
-body
-html
# fmt: on
)
To use autopep8, you can use the #fmt: off
and #fmt: on
comments as above or turn off 2 fixes:
- E225 - Fix missing whitespace around operator.
- E131 - Fix hanging indent for unaligned continuation line.
whereever you put your autopep8 configuration
ignore = E225,E131
Fixes for:
- Class list can only be created once #4
- Keyword arguments in snake_case should be translated to kebab-case #3
- README improvements/fixes
- default to using MarkupSafe for strings
- include DOCTYPE in htmltags module
- README improvements/fixes
- Initial release