Hypertag is a modern language for front-end development that allows writing markup documents in a way similar to writing Python scripts, where indentation determines relationships between nested elements and removes the need for explicit closing tags. Hypertag provides advanced control of page rendering with native control blocks; high level of modularity thanks to Python-like imports and DOM manipulation; unprecedented support for code reuse with native custom tags (hypertags), and more. Compatible with HTML 5. See the Quick Start below, or the Language Reference for details.
Authored by Marcin Wojnarski from Paperity.
Install in Python 3:
pip install hypertag-lang # watch out the name, it is "hypertag-lang"
Usage:
from hypertag import HyperHTML
html = HyperHTML().render(script) # rendering of a Hypertag `script` to HTML
A typical Hypertag script consists of nested blocks with tags:
ul
li
| This is the first item of a "ul" list.
| Pipe (|) marks a plain-text block. HTML is auto-escaped: & < >
li
/ This is the second item.
Slash (/) marks a <b>markup block</b> (no HTML escaping).
Text blocks may consist of multiple lines, like here.
Here, ul
is a "tag", which renders to its HTML equivalent <ul>...</ul>
. Indentation of blocks gets preserved in the output during rendering:
<ul>
<li>
This is the first item of a "ul" list.
Pipe (|) marks a plain-text block. HTML is auto-escaped: & < >
</li>
<li>
This is the second item.
Slash (/) marks a <b>markup block</b> (no HTML escaping).
Text blocks may consist of multiple lines, like here.
</li>
</ul>
There are three types of text blocks: plain-text (|), markup (/), verbatim (!).
| Plain-text block may contain {'em'+'bedded'} expressions & its output is HTML-escaped.
/ Markup block may contain expressions; output is not escaped, so <b>raw tags</b> can be used.
! In a verbatim $block$ {expressions} are left unparsed, no <escaping> is done.
output:
Plain-text block may contain embedded expressions & its output is HTML-escaped.
Markup block may contain expressions; output is not escaped, so <b>raw tags</b> can be used.
In a verbatim $block$ {expressions} are left unparsed, no <escaping> is done.
Content of a tagged block can be arranged as inline, outline ("out of the line"), or mixed inline+outline. Inline content starts right after the tag in the headline and is rendered to a more compact form.
h1 | This is inline text, no surrounding newlines are printed in the output.
p
/ These are sub-blocks of an outline content...
| ...of the parent paragraph block.
output:
<h1>This is inline text, no surrounding newlines are printed in the output.</h1>
<p>
These are sub-blocks of an outline content...
...of the parent paragraph block.
</p>
Mixed inline+outline content is allowed if a colon :
is additionally put in the headline:
div: | This inline text is followed by a sub-block "p".
p
i | Line 1
b | Line 2
output:
<div>This inline text is followed by a sub-block "p".
<p>
<i>Line 1</i>
<b>Line 2</b>
</p></div>
Without a colon, all content is interpreted as multiline text:
div |
Line 1
Line 2
output:
<div>
Line 1
Line 2
</div>
A special null tag (.
) can be used to better align tagged and untagged blocks in the code:
p
i | This line is in italics ...
. | ... and this one is not, but both are vertically aligned in the script.
. | The null tag helps with code alignment when a tag is missing.
output:
<p>
<i>This line is in italics ...</i>
... and this one is not, but both are vertically aligned in the script.
The null tag helps with code alignment when a tag is missing.
</p>
Tags may have attributes and can be chained together using a colon :
, like here:
h1 class='big-title' : b : a href="http://hypertag.io" style="color:DarkBlue"
| Tags can be chained together using a colon ":".
| Each tag in a chain can have its own attributes.
| Attributes are passed in a space-separated list, no parentheses.
output:
<h1 class="big-title"><b><a href="http://hypertag.io" style="color:DarkBlue">
Tags can be chained together using a colon ":".
Each tag in a chain can have its own attributes.
Attributes are passed in a space-separated list, no parentheses.
</a></b></h1>
Shortcuts are available for the two most common HTML attributes:
.CLASS
is equivalent to class=CLASS
, and #ID
means id=ID
.
p #main-content .wide-paragraph | text...
output:
<p id="main-content" class="wide-paragraph">text...</p>
A Hypertag script may define variables to be used in expressions
inside plain-text and markup blocks, or inside attribute lists.
A variable is created by an assignment block ($).
Expressions are embedded in text blocks using {...}
or $...
syntax:
$ k = 3
$ name = "Ala"
| The name repeated $k times is: {name * k}
| The third character of the name is: "$name[2]"
output:
The name repeated 3 times is: AlaAlaAla
The third character of the name is: "a"
Assignment blocks support augmented assignments:
$ a, (b, c) = [1, (2, 3)]
Each variable points to a Python object and can be used with all the standard operators known from Python:
** * / // %
+ - unary minus
<< >>
& ^ |
== != >= <= < > in is "not in" "is not"
not and or
X if TEST else Y - the "else" clause is optional and defaults to "else None"
A:B:C - slice operator inside [...]
. - member access
[] - indexing
() - function call
Python collections: lists, tuples, sets, dictionaries, can be created in a standard way:
| this is a list: { [1,2,3] }
| this is a tuple: { (1,2,3) }
| this is a set: { {1,2,1,2} }
| this is a dict: { {'a': 1, 'b': 2} }
output:
this is a list: [1, 2, 3]
this is a tuple: (1, 2, 3)
this is a set: {1, 2}
this is a dict: {'a': 1, 'b': 2}
Variables can be imported from other Hypertag scripts and Python modules using an import block:
from python_module import $x, $y as z, $fun as my_function, $T as MyClass
from hypertag_script import $name
| fun(x) is equal $my_function(x)
$ obj = MyClass(z)
Wildcard import is supported:
from PATH import *
If your script needs to accept external data to work on, it can use a context block
to declare a list of input variables (dynamic context) that must be passed
by the caller to the render()
method:
context $width # width [px] of the page
context $height # height [px] of the page
| Page dimensions imported from context are $width x $height
This script can be rendered in the following way:
html = HyperHTML().render(script, width = 500, height = 1000)
print(html)
and the output is:
Page dimensions imported from context are 500 x 1000
Context blocks, if present, constitute a public interface of the script, and so they must be placed at its beginning.
Custom tags (hypertags) can be defined directly in a Hypertag script using hypertag definition blocks (%):
% tableRow name price='UNKNOWN'
tr
td | $name
td | $price
A hypertag may declare attributes, possibly with default values. In places of occurrence, hypertags accept positional (unnamed) and/or keyword (named) attributes:
table
tableRow 'Porsche' '200,000'
tableRow 'Jaguar' '150,000'
tableRow name='Cybertruck'
output:
<table>
<tr>
<td>Porsche</td>
<td>200,000</td>
</tr>
<tr>
<td>Jaguar</td>
<td>150,000</td>
</tr>
<tr>
<td>Cybertruck</td>
<td>UNKNOWN</td>
</tr>
</table>
If you want to pass structured (rich-text) data to a hypertag, you can declare a body attribute (@) in the hypertag definition block, and then paste its contents in any place you wish:
% tableRow @info name price='UNKNOWN'
tr
td | $name
td | $price
td
@ info # inline form can be used as well: td @ info
This special attribute will hold the actual body of hypertag's occurrence, represented as a tree of nodes of Hypertag's native Document Object Model (DOM), so that all rich contents and formatting are preserved:
table
tableRow 'Porsche' '200,000'
img src="porsche.jpg"
/ If you insist on <s>air conditioning</s>, 🤔
/ you can always hit the track and roll down the window at <u>160 mph</u>. 😎
tableRow 'Jaguar' '150,000'
img src="jaguar.jpg"
b | Money may not buy happiness, but I'd rather cry in a Jaguar than on a bus.
tableRow 'Cybertruck'
| If you liked Minecraft you will like this one, too.
/ (Honestly, I did it for the memes. <i>Elon Musk</i>)
output:
<table>
<tr>
<td>Porsche</td>
<td>200,000</td>
<td>
<img src="porsche.jpg" />
If you insist on <s>air conditioning</s>, 🤔
you can always hit the track and roll down the window at <u>160 mph</u>. 😎
</td>
</tr>
<tr>
<td>Jaguar</td>
<td>150,000</td>
<td>
<img src="jaguar.jpg" />
<b>Money may not buy happiness, but I'd rather cry in a Jaguar than on a bus.</b>
</td>
</tr>
<tr>
<td>Cybertruck</td>
<td>UNKNOWN</td>
<td>
If you liked Minecraft you will like this one, too.
(Honestly, I did it for the memes. <i>Elon Musk</i>)
</td>
</tr>
</table>
Like variables, tags can also be imported from Hypertag scripts and Python modules.
Due to separation of namespaces (variables vs. tags), all symbols must be
prepended with either $
(denotes a variable) or %
(a tag):
from my.utils import $variable
from my.utils import %tag
Hypertag defines a colon :
as a pipeline operator that allows functions (and all callables)
to be used in expressions as chained filters. A pipeline expression of the form:
EXPR : FUN(*args, **kwargs)
gets translated internally to:
FUN(EXPR, *args, **kwarg)
For example, the expression:
'Hypertag' : str.upper : list : sorted(reverse=True)
evaluates to:
['Y', 'T', 'R', 'P', 'H', 'G', 'E', 'A']
Functions do not have to be explicitly registered as filters before use, unlike in popular templating languages (Jinja, Django templates etc.).
Hypertag seamlessly integrates all of Django's template filters.
They can be imported from hypertag.django.filters
and either called as regular functions
or used inside pipelines. The extra filters from django.contrib.humanize
(the "human touch" to data) are also available. Django must be installed on the system.
from hypertag.django.filters import $slugify, $upper
from hypertag.django.filters import $truncatechars, $floatformat
from hypertag.django.filters import $apnumber, $ordinal
| { 'Hypertag rocks' : slugify : upper }
| { 'Hypertag rocks' : truncatechars(6) }
| { '123.45' : floatformat(4) }
# from django.contrib.humanize:
| "5" spelled out is "{ 5:apnumber }"
| example ordinals {1:ordinal}, {2:ordinal}, {5:ordinal}
output:
HYPERTAG-ROCKS
Hyper…
123.4500
"5" spelled out is "five"
example ordinals 1st, 2nd, 5th
There are control blocks in Hypertag: "if", "try", "for", "while". For example:
$size = 5
if size > 10
| large size
elif size > 3:
| medium size
else
| small size
output:
medium size
Clauses may have inline body; notice the parentheses around expressions:
$size = 5
if (size > 10) | large size
elif (size > 3) | medium size
else | small size
Examples of loops:
for i in [1,2,3] | $i
for i in [1,2,3]:
li | item no. $i
$s = 'abc'
while len(s) > 0 -- Python built-ins ("len") can be used
| letter "$s[0]"
$s = s[1:] -- assignments can occur inside loops
output:
123
<li>item no. 1</li>
<li>item no. 2</li>
<li>item no. 3</li>
letter "a"
letter "b"
letter "c"
The "try" block consists of a single try
clause plus any number (possibly none) of else
clauses.
The first clause that does not raise an exception is returned.
All exceptions that inherit from Python's Exception are caught (this cannot be changed). Example:
$cars = {'ford': 60000, 'audi': 80000}
try
| Price of Opel is $cars['opel'].
else
| Price of Opel is unknown.
output (the 'opel' key is missing in the dictionary):
Price of Opel is unknown.
There is a shortcut version "?" of the "try" syntax, which has no "else" clauses, so its only function is to suppress exceptions:
? | Price of Opel is $cars['opel'].
Importantly, unlike the basic form of "try", the shortcut "?" can prepend a tagged block. The code below renders empty string instead of raising an exception:
? b : a href=$cars.url | the "a" tag fails because "cars" has no "url" member
The "try" block is particularly useful when combined with qualifiers:
"optional" (?
) and "obligatory" (!
), placed at the end of (sub)expressions to mark
that a given piece of calculation either:
- can be ignored (replaced with
''
) if it fails with an exception (?
); or - must be non-empty (not false), otherwise an exception will be raised (
!
).
Together, these language constructs enable fine-grained control over data post-processing, sanitization and display. They can be used to verify the availability of particular elements of data (keys in dictionaries, attributes of objects) and create alternative paths of calculation that will handle multiple edge cases at once:
| Price of Opel is {cars['opel']? or cars['audi'] * 0.8}
In the above code, the price of Opel is not present in the dictionary, but thanks
to the "optional" qualifier ?
, a KeyError is caught early, and a fallback is used
to approximate the price from another entry:
Price of Opel is 64000.0
With the "obligatory" qualifier !
one can verify that a variable has a non-default
(non-empty) value, and adapt the displayed message if not:
% display name='' price=0
try | Product "$name!" costs {price}!.
else | Product "$name!" is available, but the price is unknown yet.
else | There is a product priced at {price!}.
else | Sorry, we're closed.
display 'Pen' 100
display 'Pencil'
display price=25
output:
Product "Pen" costs 100.
Product "Pencil" is available, but the price is unknown yet.
There is a product priced at 25.
Qualifiers can also be used in loops to test for non-emptiness of the collections to be iterated over:
try
for p in products!
| $p.name costs $p.price
else
| No products currently available.
When initialized with $products=[]
, the above code outputs:
No products currently available.
All Python built-ins, including the common types and functions:
list
, set
, dict
, int
, min
, max
, enumerate
, sorted
etc.,
are automatically imported and can be used in a Hypertag script:
| $len('cat'), $list('cat')
| $int('123'), $min(4,5,6)
for i, c in enumerate(sorted('cat')):
| $i, $c
output:
3, ['c', 'a', 't']
123, 4
0, a
1, c
2, t
There are still many Hypertag features that have not been mentioned
in this Quick Start:
block layout modifiers (dedent, append);
comments (#
and --
);
built-in tags and functions;
raw (r-) vs. formatted (f-) strings;
pass keyword; concatenation operator;
DOM manipulation; and more.
See the Language Reference for details.